import subprocess import os import uvicorn from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) DIRECTORIO_HLS = "/tmp/hls_vod" os.makedirs(DIRECTORIO_HLS, exist_ok=True) app.mount("/hls", StaticFiles(directory=DIRECTORIO_HLS), name="hls") proceso_ffmpeg = None @app.get("/iniciar-transmision") def iniciar_transmision(url: str): global proceso_ffmpeg if proceso_ffmpeg and proceso_ffmpeg.poll() is None: proceso_ffmpeg.terminate() proceso_ffmpeg.wait() os.system(f"rm -rf {DIRECTORIO_HLS}/*") # Detectar TODAS las pistas de audio probe = subprocess.run( ["ffprobe", "-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0", url], capture_output=True, text=True, timeout=15 ) pistas_audio = [l for l in probe.stdout.strip().splitlines() if l.strip()] num_audios = len(pistas_audio) if num_audios == 0: num_audios = 1 # fallback # Construir -map dinámicamente para todas las pistas mapeos = ["-map", "0:v:0"] for i in range(num_audios): mapeos += ["-map", f"0:a:{i}"] # var_stream_map: video va con audio 0, resto son pistas independientes # Nombres de idioma genéricos si no sabemos cuáles son nombres = ["audio1", "audio2", "audio3", "audio4"] stream_map_parts = ["v:0,a:0,name:" + nombres[0]] for i in range(1, num_audios): stream_map_parts.append(f"a:{i},name:{nombres[i]}") var_stream_map = " ".join(stream_map_parts) comando = [ "ffmpeg", "-y", "-i", url, *mapeos, "-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-profile:v", "baseline", "-level", "3.0", "-g", "48", "-keyint_min", "48", "-sc_threshold", "0", "-c:a", "aac", "-b:a", "128k", "-ac", "2", "-f", "hls", "-hls_time", "6", "-hls_list_size", "0", "-hls_playlist_type", "vod", # VOD real: escribe #EXT-X-ENDLIST al terminar "-hls_flags", "independent_segments", "-hls_segment_type", "mpegts", "-hls_segment_filename", f"{DIRECTORIO_HLS}/seg_%v_%03d.ts", "-master_pl_name", "master.m3u8", "-var_stream_map", var_stream_map, f"{DIRECTORIO_HLS}/playlist_%v.m3u8" ] proceso_ffmpeg = subprocess.Popen( comando, stdout=subprocess.DEVNULL, stderr=open("/tmp/ffmpeg.log", "w") ) return { "estado": "Procesando", "audios_detectados": num_audios, "url_master": "https://cinemax-mp4.hf.space/hls/master.m3u8", "url_directa": "https://cinemax-mp4.hf.space/hls/playlist_0.m3u8", } @app.get("/listo") def listo(): """El PHP hace polling aquí hasta que FFmpeg termina.""" global proceso_ffmpeg if proceso_ffmpeg is None: return {"listo": False, "razon": "Sin proceso"} activo = proceso_ffmpeg.poll() is None segmentos = len([f for f in os.listdir(DIRECTORIO_HLS) if f.endswith(".ts")]) # VOD está listo cuando FFmpeg terminó Y el m3u8 tiene #EXT-X-ENDLIST vod_listo = False playlist = f"{DIRECTORIO_HLS}/playlist_0.m3u8" if not activo and os.path.exists(playlist): with open(playlist) as f: vod_listo = "#EXT-X-ENDLIST" in f.read() return { "listo": vod_listo, "ffmpeg_activo": activo, "segmentos": segmentos } @app.get("/detener") def detener(): global proceso_ffmpeg if proceso_ffmpeg and proceso_ffmpeg.poll() is None: proceso_ffmpeg.terminate() return {"estado": "Detenido"} return {"estado": "No había proceso activo"} if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=7860)