TubeScript-API/streamlit_app.py
2026-01-29 22:49:00 -07:00

1132 lines
40 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"<h2 style='text-align: center; margin: 0;'>{status_emoji}</h2>", unsafe_allow_html=True)
with col3:
st.markdown(f"<p style='text-align: center; margin: 0; font-size: 12px;'>PID: {pid}</p>", 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()