import streamlit as st import json import os import subprocess import threading import time import requests from datetime import datetime from streamlit_autorefresh import st_autorefresh # Configuración de la página st.set_page_config( page_title="TubeScript - Panel de Control", page_icon="📺", layout="wide", initial_sidebar_state="expanded" ) # Archivo para guardar configuraciones CONFIG_FILE = "stream_config.json" STREAMS_STATE_FILE = "streams_state.json" PROCESS_STATE_FILE = "process_state.json" # Diccionario de procesos activos if 'active_processes' not in st.session_state: st.session_state.active_processes = {} if 'search_results' not in st.session_state: st.session_state.search_results = [] # ==================== FUNCIONES DE CONFIGURACIÓN ==================== def save_process_state(): """Guardar estado de procesos activos en archivo para persistencia""" state = {} for key, info in st.session_state.active_processes.items(): state[key] = { 'pid': info.get('pid'), 'platform': info.get('platform'), 'start_time': info.get('start_time'), 'status': info.get('status'), 'rtmp_url': info.get('rtmp_url'), 'enabled': info.get('enabled', True) } with open(PROCESS_STATE_FILE, 'w') as f: json.dump(state, f, indent=2) def load_process_state(): """Cargar estado de procesos desde archivo""" if os.path.exists(PROCESS_STATE_FILE): try: with open(PROCESS_STATE_FILE, 'r') as f: return json.load(f) except: return {} return {} def check_process_alive(pid): """Verificar si un proceso está corriendo usando su PID""" try: # En Unix/Linux/Mac, enviar señal 0 para verificar si existe os.kill(pid, 0) return True except OSError: return False except Exception: return False def load_config(): """Cargar configuración desde archivo JSON""" if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, 'r') as f: return json.load(f) return { "platforms": { "YouTube": {"rtmp_url": "", "stream_key": "", "enabled": False}, "Facebook": {"rtmp_url": "", "stream_key": "", "enabled": False}, "Twitch": {"rtmp_url": "", "stream_key": "", "enabled": False}, "X (Twitter)": {"rtmp_url": "", "stream_key": "", "enabled": False}, "Instagram": {"rtmp_url": "", "stream_key": "", "enabled": False}, "TikTok": {"rtmp_url": "", "stream_key": "", "enabled": False}, } } def save_config(config): """Guardar configuración en archivo JSON""" with open(CONFIG_FILE, 'w') as f: json.dump(config, f, indent=2) def load_streams_state(): """Cargar estado de las transmisiones""" if os.path.exists(STREAMS_STATE_FILE): with open(STREAMS_STATE_FILE, 'r') as f: return json.load(f) return {} def save_streams_state(state): """Guardar estado de las transmisiones""" with open(STREAMS_STATE_FILE, 'w') as f: json.dump(state, f, indent=2) # ==================== FUNCIONES DE YOUTUBE ==================== def search_youtube_live(query): """Buscar videos en vivo de YouTube usando yt-dlp""" try: command = [ "yt-dlp", "--flat-playlist", "--dump-json", f"ytsearch10:{query} live" ] result = subprocess.run(command, capture_output=True, text=True, timeout=30) if result.returncode != 0: return [] videos = [] for line in result.stdout.strip().split('\n'): if line: try: video_data = json.loads(line) if video_data.get('is_live'): videos.append({ 'id': video_data.get('id'), 'title': video_data.get('title'), 'channel': video_data.get('channel'), 'url': f"https://www.youtube.com/watch?v={video_data.get('id')}" }) except: continue return videos except Exception as e: st.error(f"Error al buscar videos: {str(e)}") return [] def get_video_info(video_url): """Obtener información del video de YouTube""" try: command = [ "yt-dlp", "--dump-json", "--skip-download", video_url ] result = subprocess.run(command, capture_output=True, text=True, timeout=30) if result.returncode == 0: video_data = json.loads(result.stdout) return { 'id': video_data.get('id'), 'title': video_data.get('title'), 'channel': video_data.get('channel'), 'is_live': video_data.get('is_live', False), 'thumbnail': video_data.get('thumbnail'), 'url': video_url } except Exception as e: st.error(f"Error al obtener información del video: {str(e)}") return None def get_stream_url(video_url): """Obtener la URL del stream m3u8 de YouTube para transmisión con estrategia de fallback""" cookies_path = "cookies.txt" # Lista de formatos a intentar en orden de prioridad format_strategies = [ ("best", "Mejor calidad disponible"), ("best[ext=mp4]", "Mejor calidad MP4"), ("bestvideo+bestaudio", "Mejor video y audio separados"), ("worst", "Menor calidad (más compatible)"), ] for format_spec, description in format_strategies: try: st.info(f"🔄 Intentando: {description}...") command = [ "yt-dlp", "-g", # Obtener solo la URL "-f", format_spec, "--no-warnings", "--no-check-certificate", "--extractor-args", "youtube:player_client=android", # Usar cliente Android (más compatible) ] if os.path.exists(cookies_path): command.extend(["--cookies", cookies_path]) command.append(video_url) result = subprocess.run( command, capture_output=True, text=True, timeout=60, env={**os.environ, "PYTHONIOENCODING": "utf-8"} ) if result.returncode == 0 and result.stdout.strip(): # Obtener todas las URLs urls = [url.strip() for url in result.stdout.strip().split('\n') if url.strip()] if urls: # Buscar la mejor URL stream_url = None # Prioridad 1: URLs con m3u8 for url in urls: if 'm3u8' in url.lower(): stream_url = url break # Prioridad 2: URLs de googlevideo if not stream_url: for url in urls: if 'googlevideo.com' in url: stream_url = url break # Prioridad 3: Cualquier URL HTTP válida if not stream_url: for url in urls: if url.startswith('http'): stream_url = url break if stream_url: st.success(f"✅ URL obtenida con: {description}") return stream_url # Si llegamos aquí, este formato falló # Mostrar error completo para debugging if result.stderr: error_detail = result.stderr.strip() # Mostrar solo las primeras líneas para no saturar la UI error_lines = error_detail.split('\n')[:10] error_preview = '\n'.join(error_lines) st.warning(f"⚠️ Formato {description} falló:") with st.expander("Ver error detallado", expanded=False): st.code(error_preview, language=None) if len(error_detail.split('\n')) > 10: st.caption("... (error truncado, ver logs completos)") except subprocess.TimeoutExpired: st.warning(f"⏱️ Timeout con formato: {description}") continue except Exception as e: st.error(f"❌ Error con formato {description}: {str(e)}") continue # Si todos los formatos fallaron st.error("❌ No se pudo obtener la URL del stream con ningún formato") with st.expander("🔍 Ver detalles del error", expanded=False): st.warning("⚠️ Posibles causas:") st.markdown(""" 1. **El video no está EN VIVO** 🔴 - Verifica que el video tenga el indicador rojo "EN VIVO" - Los videos pregrabados no funcionan 2. **Video con restricciones** - Restricciones geográficas - Restricciones de edad - Video privado o no listado 3. **Problema con yt-dlp** - yt-dlp puede estar desactualizado o corrupto - YouTube cambió su API 4. **Problema de red** - Conexión lenta o inestable - Firewall bloqueando la conexión """) st.info("💡 Soluciones:") st.markdown(""" 1. **Actualizar yt-dlp en el contenedor:** ```bash docker exec streamlit_panel pip install --force-reinstall yt-dlp ``` 2. **Intenta con un canal de noticias 24/7:** - CNN, BBC News, DW News (siempre en vivo) 3. **Agrega cookies de YouTube:** - Exporta cookies con extensión de navegador - Guarda como `cookies.txt` en el directorio raíz 4. **Reconstruir contenedor:** ```bash docker-compose down docker-compose build --no-cache docker-compose up -d ``` """) return None # ==================== FUNCIONES DE STREAMING ==================== def start_ffmpeg_stream(source_url, rtmp_url, stream_key, platform_name): """Iniciar transmisión con FFmpeg usando la URL m3u8 directamente y guardar PID""" full_rtmp = f"{rtmp_url}/{stream_key}" if stream_key else rtmp_url # Comando FFmpeg optimizado para streaming desde m3u8 command = [ "ffmpeg", "-re", # Leer input a velocidad nativa (importante para streaming en vivo) "-i", source_url, # URL m3u8 de YouTube "-c", "copy", # Copiar codec sin recodificar (video y audio) "-f", "flv", # Formato FLV para RTMP/RTMPS "-loglevel", "error", # Solo mostrar errores full_rtmp # URL RTMP de destino ] try: process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) # Obtener PID del proceso pid = process.pid # Guardar el proceso en el diccionario con toda la información process_key = f"{platform_name}" st.session_state.active_processes[process_key] = { 'process': process, 'pid': pid, 'platform': platform_name, 'start_time': datetime.now().isoformat(), 'status': 'running', 'command': ' '.join(command), 'rtmp_url': rtmp_url, 'enabled': True } # Guardar estado en archivo JSON para persistencia save_process_state() return True, f"Transmisión iniciada (PID: {pid})" except Exception as e: return False, f"Error al iniciar transmisión: {str(e)}" def stop_stream(platform_name): """Detener transmisión de una plataforma usando su PID""" process_key = f"{platform_name}" if process_key in st.session_state.active_processes: process_info = st.session_state.active_processes[process_key] process = process_info['process'] pid = process_info.get('pid') try: # Intentar terminar gracefully process.terminate() process.wait(timeout=5) except: # Forzar si no responde try: process.kill() except: # Si el proceso ya no existe, intentar con PID if pid and check_process_alive(pid): try: os.kill(pid, 15) # SIGTERM except: pass # Eliminar del diccionario del st.session_state.active_processes[process_key] # Actualizar estado persistido save_process_state() return True, "Transmisión detenida" return False, "No hay transmisión activa" def check_stream_health(platform_name): """Verificar el estado de salud de una transmisión usando PID""" process_key = f"{platform_name}" if process_key not in st.session_state.active_processes: return "stopped", "Detenido" process_info = st.session_state.active_processes[process_key] process = process_info.get('process') pid = process_info.get('pid') # Verificar usando PID primero if pid and not check_process_alive(pid): # El proceso no está vivo según PID st.session_state.active_processes[process_key]['status'] = 'error' return "error", f"Error: Proceso detenido (PID: {pid})" # Verificar si el proceso sigue corriendo if process and process.poll() is None: # Proceso activo return "running", f"Transmitiendo (PID: {pid})" else: # Proceso terminó st.session_state.active_processes[process_key]['status'] = 'error' return "error", f"Error: Proceso terminado (PID: {pid})" # ==================== INTERFAZ DE USUARIO ==================== def render_sidebar(): """Renderizar barra lateral con configuración mejorada""" with st.sidebar: st.title("⚙️ Configuración") config = load_config() # URLs RTMP por defecto para cada plataforma default_rtmp_urls = { "YouTube": "rtmp://a.rtmp.youtube.com/live2", "Facebook": "rtmps://live-api-s.facebook.com:443/rtmp/", "Twitch": "rtmp://live.twitch.tv/app", "X (Twitter)": "rtmps://fa.contribute.live-video.net/app", "Instagram": "rtmps://live-upload.instagram.com:443/rtmp/", "TikTok": "rtmp://push.live.tiktok.com/live/" } st.subheader("Plataformas de Streaming") # Contador de plataformas configuradas configured_count = sum( 1 for p in config["platforms"].values() if p.get("stream_key") and p.get("rtmp_url") ) st.caption(f"✅ {configured_count} de {len(config['platforms'])} configuradas") st.write("") # Espaciador for platform_name, platform_config in config["platforms"].items(): with st.expander(f"🎥 {platform_name}", expanded=False): # Switch para habilitar/deshabilitar la plataforma is_enabled = platform_config.get("enabled", False) enabled = st.toggle( "Habilitar esta plataforma", value=is_enabled, key=f"enabled_{platform_name}", help="Activa para poder usar esta plataforma en transmisiones" ) st.write("") # Espaciador # Stream Key (campo principal) st.markdown("**🔑 Stream Key** (Requerido)") stream_key = st.text_input( "Ingresa tu Stream Key", value=platform_config.get("stream_key", ""), type="password", key=f"key_{platform_name}", placeholder="Pega aquí tu Stream Key...", help=f"Obtén tu Stream Key desde el panel de {platform_name}", label_visibility="collapsed" ) st.write("") # Espaciador # RTMP URL con valor por defecto st.markdown("**🌐 RTMP URL** (Opcional)") # Usar URL por defecto si no hay ninguna configurada current_rtmp = platform_config.get("rtmp_url", "") if not current_rtmp and platform_name in default_rtmp_urls: current_rtmp = default_rtmp_urls[platform_name] # Checkbox para usar URL personalizada use_custom_rtmp = st.checkbox( "Usar URL RTMP personalizada", value=bool(current_rtmp and current_rtmp != default_rtmp_urls.get(platform_name)), key=f"custom_rtmp_{platform_name}", help="Marca si necesitas usar una URL RTMP diferente a la por defecto" ) if use_custom_rtmp: rtmp_url = st.text_input( "URL RTMP personalizada", value=current_rtmp, key=f"rtmp_{platform_name}", placeholder=f"Ejemplo: {default_rtmp_urls.get(platform_name, 'rtmp://...')}", help="Ingresa la URL RTMP completa", label_visibility="collapsed" ) else: # Usar URL por defecto rtmp_url = default_rtmp_urls.get(platform_name, "") st.info(f"📍 Usando URL por defecto:\n`{rtmp_url}`") st.write("") # Espaciador # Indicador de estado is_configured = bool(stream_key and rtmp_url) if is_configured and enabled: st.success("✅ Plataforma lista para usar") elif is_configured and not enabled: st.warning("⚠️ Configurada pero deshabilitada") elif not stream_key: st.error("❌ Falta Stream Key") elif not rtmp_url: st.error("❌ Falta RTMP URL") # Guardar configuración config["platforms"][platform_name]["rtmp_url"] = rtmp_url config["platforms"][platform_name]["stream_key"] = stream_key config["platforms"][platform_name]["enabled"] = enabled st.write("") # Espaciador # Botón de guardar if st.button("💾 Guardar Configuración", use_container_width=True, type="primary"): save_config(config) st.success("✅ Configuración guardada correctamente") time.sleep(1) st.rerun() st.divider() # Guía rápida with st.expander("❓ ¿Cómo obtener mi Stream Key?", expanded=False): st.markdown(""" ### YouTube 1. Ve a [YouTube Studio](https://studio.youtube.com) 2. Click en "Emisión en Vivo" → "Stream" 3. Copia la "Clave de transmisión" ### Facebook 1. Ve a [Creator Studio](https://business.facebook.com/creatorstudio) 2. Click en "Emisión en vivo" 3. Copia la "Clave de stream" ### Twitch 1. Ve a [Dashboard](https://dashboard.twitch.tv/settings/stream) 2. En "Configuración del canal" 3. Copia la "Clave de transmisión principal" ### X (Twitter) 1. Ve a [Media Studio](https://studio.twitter.com) 2. Click en "Crear" → "En vivo" 3. Copia la "Stream Key" """) # Información de URLs por defecto with st.expander("📋 URLs RTMP por Defecto", expanded=False): for platform, url in default_rtmp_urls.items(): st.code(f"{platform}:\n{url}", language=None) def render_search_panel(): """Renderizar panel de búsqueda""" st.header("🔍 Buscar Video en Vivo") col1, col2 = st.columns([3, 1]) with col1: search_query = st.text_input( "Buscar transmisión en vivo de YouTube", placeholder="Ej: noticias, deportes, gaming...", key="search_input" ) with col2: st.write("") # Espaciador search_button = st.button("🔍 Buscar", use_container_width=True) # Opción para URL directa video_url = st.text_input( "O ingresa la URL directa del video", placeholder="https://www.youtube.com/watch?v=...", key="url_input" ) if search_button and search_query: with st.spinner("Buscando videos en vivo..."): results = search_youtube_live(search_query) st.session_state.search_results = results # Mostrar resultados de búsqueda if st.session_state.search_results: st.subheader("📺 Resultados") for video in st.session_state.search_results: col1, col2 = st.columns([4, 1]) with col1: st.markdown(f"**{video['title']}**") st.caption(f"Canal: {video['channel']}") with col2: if st.button("Seleccionar", key=f"select_{video['id']}"): st.session_state.selected_video_url = video['url'] # Guardar info básica del video st.session_state.selected_video_info = { 'title': video['title'], 'channel': video['channel'], 'url': video['url'], 'id': video['id'], 'is_live': True } st.rerun() # Video seleccionado por URL directa if video_url: st.session_state.selected_video_url = video_url # Resetear stream_url para forzar nueva obtención if 'stream_url' in st.session_state: del st.session_state.stream_url if 'selected_video_url' in st.session_state and st.session_state.selected_video_url: st.divider() st.subheader("✅ Video Seleccionado") video_info = get_video_info(st.session_state.selected_video_url) if video_info: # Guardar info completa del video en session_state st.session_state.selected_video_info = video_info col1, col2 = st.columns([1, 3]) with col1: if video_info.get('thumbnail'): st.image(video_info['thumbnail'], use_container_width=True) with col2: st.markdown(f"**{video_info['title']}**") st.caption(f"Canal: {video_info['channel']}") if video_info['is_live']: st.success("🔴 EN VIVO") else: st.warning("⚠️ Este video no está en vivo") st.code(video_info['url'], language=None) def render_streaming_control(): """Renderizar panel de control de transmisión""" st.header("🎛️ Control de Transmisión") if 'selected_video_url' not in st.session_state: st.info("ℹ️ Selecciona un video primero para comenzar a transmitir") return config = load_config() # Obtener información del video para preview if 'selected_video_info' in st.session_state: video_info = st.session_state.selected_video_info # Mostrar preview del video st.subheader("📺 Video Seleccionado") col1, col2 = st.columns([1, 2]) with col1: if video_info.get('thumbnail'): st.image(video_info['thumbnail'], use_container_width=True) with col2: st.markdown(f"**{video_info.get('title', 'Sin título')}**") st.caption(f"Canal: {video_info.get('channel', 'Desconocido')}") if video_info.get('is_live'): st.success("🔴 EN VIVO") else: st.warning("⚠️ Este video no está en vivo") st.caption(f"🔗 {st.session_state.selected_video_url}") st.divider() # Obtener URL del stream if 'stream_url' not in st.session_state: with st.spinner("Obteniendo URL del stream m3u8..."): stream_url = get_stream_url(st.session_state.selected_video_url) if stream_url: st.session_state.stream_url = stream_url else: st.error("❌ No se pudo obtener la URL del stream") return st.success("✅ Stream listo para transmitir") # Mostrar la URL m3u8 obtenida with st.expander("🔗 Ver URL m3u8 del Stream", expanded=False): st.code(st.session_state.stream_url, language=None) st.caption("Esta es la URL HLS/m3u8 que se usará para la transmisión") # Mostrar ejemplo del comando FFmpeg que se usará st.markdown("**Ejemplo de comando FFmpeg:**") example_rtmp = "rtmps://live-api-s.facebook.com:443/rtmp/TU-STREAM-KEY" example_cmd = f'ffmpeg -re -i "{st.session_state.stream_url}" -c copy -f flv {example_rtmp}' st.code(example_cmd, language="bash") st.divider() # Filtrar solo plataformas configuradas Y habilitadas configured_platforms = { name: conf for name, conf in config["platforms"].items() if conf["rtmp_url"] and conf["stream_key"] and conf.get("enabled", False) } if not configured_platforms: st.warning("⚠️ No hay plataformas habilitadas y configuradas") st.info("💡 Ve a la barra lateral y:") st.markdown(""" 1. Activa el switch "Habilitar esta plataforma" 2. Ingresa tu Stream Key 3. (Opcional) Personaliza la RTMP URL 4. Guarda la configuración """) return # Mostrar controles para plataformas configuradas st.subheader(f"🎯 Plataformas Configuradas ({len(configured_platforms)})") # Botón para iniciar todas las transmisiones col1, col2, col3 = st.columns([2, 2, 2]) with col1: # Contar cuántas están transmitiendo transmitting_count = sum( 1 for name in configured_platforms.keys() if check_stream_health(name)[0] == "running" ) if transmitting_count == 0: if st.button("▶️ Iniciar Todas las Transmisiones", type="primary", use_container_width=True): for platform_name in configured_platforms.keys(): start_ffmpeg_stream( st.session_state.stream_url, configured_platforms[platform_name]["rtmp_url"], configured_platforms[platform_name]["stream_key"], platform_name ) st.success(f"✅ Iniciando transmisión en {len(configured_platforms)} plataformas...") st.rerun() with col2: if transmitting_count > 0: if st.button("⏹️ Detener Todas las Transmisiones", type="secondary", use_container_width=True): for platform_name in configured_platforms.keys(): stop_stream(platform_name) st.info("🛑 Deteniendo todas las transmisiones...") st.rerun() with col3: st.metric("Transmitiendo", f"{transmitting_count}/{len(configured_platforms)}") st.divider() # LISTA DE REDES PREPARADAS Y LISTAS st.subheader("📋 Redes Habilitadas y Listas para Transmitir") # Crear tabla de resumen ready_platforms = [] for platform_name, platform_config in configured_platforms.items(): status, _ = check_stream_health(platform_name) process_info = st.session_state.active_processes.get(platform_name, {}) pid = process_info.get('pid', '-') # Determinar si está lista y habilitada is_ready = platform_config["rtmp_url"] and platform_config["stream_key"] is_enabled = platform_config.get("enabled", False) ready_platforms.append({ "Red Social": platform_name, "Estado": "🟢 Activo" if status == "running" else "⚪ Listo" if status == "stopped" else "🔴 Error", "PID": pid if pid != '-' else '-', "Habilitada": "✅ Sí" if is_enabled else "❌ No", "Configurada": "✅ Sí" if is_ready else "❌ No" }) # Mostrar tabla if ready_platforms: st.dataframe( ready_platforms, use_container_width=True, hide_index=True ) # Resumen rápido col_summary1, col_summary2, col_summary3, col_summary4 = st.columns(4) total = len(ready_platforms) running = sum(1 for p in ready_platforms if "🟢" in p["Estado"]) ready = sum(1 for p in ready_platforms if "✅" in p["Configurada"]) errors = sum(1 for p in ready_platforms if "🔴" in p["Estado"]) with col_summary1: st.metric("Total Plataformas", total) with col_summary2: st.metric("Transmitiendo", running, delta=None if running == 0 else "Activas") with col_summary3: st.metric("Listas", ready) with col_summary4: st.metric("Errores", errors, delta="Revisar" if errors > 0 else None, delta_color="inverse") st.divider() # Mostrar tarjetas de plataformas en columnas st.subheader("🎛️ Control Individual por Plataforma") cols = st.columns(2) for idx, (platform_name, platform_config) in enumerate(configured_platforms.items()): with cols[idx % 2]: render_platform_card(platform_name, platform_config) def render_platform_card(platform_name, platform_config): """Renderizar tarjeta de control con switch para cada plataforma""" # Verificar si la plataforma está habilitada en la configuración is_platform_enabled = platform_config.get("enabled", False) with st.container(border=True): # Verificar estado status, status_msg = check_stream_health(platform_name) # Obtener información del proceso si existe process_info = st.session_state.active_processes.get(platform_name, {}) pid = process_info.get('pid', 'N/A') start_time_str = process_info.get('start_time') # Determinar color del semáforo y estilo if status == "running": status_emoji = "🟢" status_text = "TRANSMITIENDO" is_switch_on = True elif status == "error": status_emoji = "🔴" status_text = "ERROR" is_switch_on = False else: status_emoji = "⚪" status_text = "LISTO" is_switch_on = False # Encabezado con icono de plataforma col1, col2, col3 = st.columns([2, 1, 1]) with col1: # Mostrar si está deshabilitada if not is_platform_enabled: st.markdown(f"### 🎥 {platform_name} ⚠️") else: st.markdown(f"### 🎥 {platform_name}") with col2: st.markdown(f"

{status_emoji}

", unsafe_allow_html=True) with col3: st.markdown(f"

PID: {pid}

", unsafe_allow_html=True) # Estado st.markdown(f"**Estado:** {status_text}") st.caption(status_msg) # Advertencia si está deshabilitada if not is_platform_enabled: st.warning("⚠️ Plataforma deshabilitada en configuración") st.info("💡 Ve a la barra lateral y activa el switch de esta plataforma") return # No mostrar el switch de transmisión si está deshabilitada # Switch principal para habilitar/deshabilitar transmisión st.write("") # Espaciador switch_label = f"🔴 Transmitir a {platform_name}" # El valor del switch depende del estado actual switch_value = st.toggle( switch_label, value=is_switch_on, key=f"switch_{platform_name}", help=f"Activar/Desactivar transmisión a {platform_name}" ) # Detectar cambio en el switch if switch_value != is_switch_on: if switch_value: # Usuario activó el switch - INICIAR transmisión if 'stream_url' in st.session_state: with st.spinner(f"Iniciando transmisión en {platform_name}..."): success, message = start_ffmpeg_stream( st.session_state.stream_url, platform_config["rtmp_url"], platform_config["stream_key"], platform_name ) if success: st.success(f"✅ {message}") else: st.error(f"❌ {message}") time.sleep(0.5) # Pequeña pausa para que el usuario vea el mensaje st.rerun() else: # Usuario desactivó el switch - DETENER transmisión with st.spinner(f"Deteniendo transmisión en {platform_name}..."): success, message = stop_stream(platform_name) if success: st.info(f"🛑 {message}") else: st.error(f"❌ {message}") time.sleep(0.5) st.rerun() # Información adicional cuando está transmitiendo if status == "running" and start_time_str: try: start_time = datetime.fromisoformat(start_time_str) duration = datetime.now() - start_time hours, remainder = divmod(duration.seconds, 3600) minutes, seconds = divmod(remainder, 60) col_time1, col_time2 = st.columns(2) with col_time1: st.metric("⏱️ Tiempo Activo", f"{hours:02d}:{minutes:02d}:{seconds:02d}") with col_time2: # Verificar si el proceso está vivo is_alive = check_process_alive(pid) if isinstance(pid, int) else False health_status = "✅ Activo" if is_alive else "❌ Inactivo" st.metric("🔍 Proceso", health_status) except: pass # Información de configuración (colapsable) with st.expander("ℹ️ Detalles de Configuración", expanded=False): st.text(f"RTMP URL: {platform_config['rtmp_url']}") st.text(f"Stream Key: {'*' * 20}...{platform_config['stream_key'][-4:]}") st.text(f"Habilitada: {'✅ Sí' if is_platform_enabled else '❌ No'}") if status == "running": st.text(f"PID: {pid}") if 'command' in process_info: st.text("Comando FFmpeg:") st.code(process_info['command'], language='bash') def render_monitor(): """Renderizar panel de monitoreo con información de PIDs""" st.header("📊 Monitor de Estado y PIDs") if not st.session_state.active_processes: st.info("ℹ️ No hay transmisiones activas") st.markdown(""" ### Cómo iniciar transmisiones: 1. Ve a la pestaña **🔍 Búsqueda** 2. Selecciona un video en vivo 3. Ve a la pestaña **🎛️ Control** 4. Activa los switches de las plataformas que desees """) return # Auto-refresh cada 5 segundos st_autorefresh(interval=5000, key="monitor_refresh") # Resumen general st.subheader("📈 Resumen General") col1, col2, col3, col4 = st.columns(4) total_processes = len(st.session_state.active_processes) running_count = 0 error_count = 0 for process_key in st.session_state.active_processes: status, _ = check_stream_health(process_key) if status == "running": running_count += 1 elif status == "error": error_count += 1 with col1: st.metric("Total Transmisiones", total_processes) with col2: st.metric("🟢 Activas", running_count) with col3: st.metric("🔴 Con Errores", error_count) with col4: stopped_count = total_processes - running_count - error_count st.metric("⚪ Detenidas", stopped_count) st.divider() # Detalles por plataforma st.subheader("🔍 Detalle por Plataforma") for process_key, process_info in st.session_state.active_processes.items(): platform = process_info['platform'] pid = process_info.get('pid', 'N/A') start_time = datetime.fromisoformat(process_info['start_time']) duration = datetime.now() - start_time # Verificar estado status, status_msg = check_stream_health(platform) # Verificar si el PID está vivo is_process_alive = check_process_alive(pid) if isinstance(pid, int) else False # Color del contenedor según estado if status == "running": status_emoji = "🟢" status_color = "green" elif status == "error": status_emoji = "🔴" status_color = "red" else: status_emoji = "⚪" status_color = "gray" with st.container(border=True): col_name, col_status, col_action = st.columns([3, 2, 1]) with col_name: st.markdown(f"### {status_emoji} {platform}") with col_status: st.markdown(f"**PID:** {pid}") st.caption(status_msg) with col_action: if st.button("⏹️ Detener", key=f"monitor_stop_{platform}"): stop_stream(platform) st.rerun() # Métricas col_time, col_pid_status, col_uptime = st.columns(3) with col_time: hours, remainder = divmod(duration.seconds, 3600) minutes, seconds = divmod(remainder, 60) st.metric("⏱️ Tiempo Activo", f"{hours:02d}:{minutes:02d}:{seconds:02d}") with col_pid_status: process_status = "✅ Vivo" if is_process_alive else "❌ Muerto" st.metric("🔍 Estado del Proceso", process_status) with col_uptime: start_time_formatted = start_time.strftime("%H:%M:%S") st.metric("🕐 Inicio", start_time_formatted) # Información adicional expandible with st.expander("ℹ️ Información Técnica", expanded=False): st.code(f"PID: {pid}", language=None) st.code(f"Plataforma: {platform}", language=None) st.code(f"RTMP: {process_info.get('rtmp_url', 'N/A')}", language=None) if 'command' in process_info: st.markdown("**Comando FFmpeg:**") st.code(process_info['command'], language='bash') # Verificación en tiempo real del proceso if isinstance(pid, int): st.markdown("**Verificación del Proceso:**") try: # Intentar obtener información del proceso os.kill(pid, 0) st.success(f"✅ El proceso {pid} está corriendo") except OSError: st.error(f"❌ El proceso {pid} no está corriendo") except Exception as e: st.warning(f"⚠️ No se pudo verificar: {str(e)}") status, status_msg = check_stream_health(platform) col1, col2, col3, col4 = st.columns([2, 1, 2, 1]) with col1: st.markdown(f"**{platform}**") with col2: if status == "running": st.success("🟢 ACTIVO") else: st.error("🔴 ERROR") with col3: hours, remainder = divmod(duration.seconds, 3600) minutes, seconds = divmod(remainder, 60) st.text(f"⏱️ {hours:02d}:{minutes:02d}:{seconds:02d}") with col4: if st.button("⏹️", key=f"monitor_stop_{platform}"): stop_stream(platform) st.rerun() st.divider() # ==================== APLICACIÓN PRINCIPAL ==================== def main(): st.title("📺 TubeScript - Panel de Control de Retransmisión") # Renderizar sidebar render_sidebar() # Tabs principales tab1, tab2, tab3 = st.tabs(["🔍 Búsqueda", "🎛️ Control", "📊 Monitor"]) with tab1: render_search_panel() with tab2: render_streaming_control() with tab3: render_monitor() # Footer st.divider() st.caption("TubeScript API Pro © 2026 - Panel de Control de Retransmisión Multi-Plataforma") if __name__ == "__main__": main()