1132 lines
40 KiB
Python
1132 lines
40 KiB
Python
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()
|