Spaces:
Sleeping
Sleeping
| import os | |
| import time | |
| import shutil | |
| import asyncio | |
| import logging | |
| from aiohttp import web | |
| logger = logging.getLogger(__name__) | |
| HLS_DIR = "/dev/shm/hls" | |
| if not os.path.exists("/dev/shm"): | |
| HLS_DIR = "/tmp/hls" | |
| SESSIONS = {} | |
| async def _ffmpeg_worker(token, bot_app, target_media, pyrogram_sem): | |
| out_dir = os.path.join(HLS_DIR, token) | |
| if os.path.exists(out_dir): | |
| shutil.rmtree(out_dir, ignore_errors=True) | |
| os.makedirs(out_dir, exist_ok=True) | |
| m3u8_path = os.path.join(out_dir, "stream.m3u8") | |
| cmd = [ | |
| "ffmpeg", "-y", | |
| "-i", "pipe:0", | |
| "-c:v", "copy", | |
| "-c:a", "aac", "-b:a", "192k", | |
| "-f", "hls", | |
| "-hls_time", "4", | |
| "-hls_playlist_type", "event", | |
| "-hls_flags", "delete_segments+append_list", | |
| "-hls_segment_type", "fmp4", | |
| "-hls_segment_filename", os.path.join(out_dir, "seg%d.m4s"), | |
| m3u8_path | |
| ] | |
| process = await asyncio.create_subprocess_exec( | |
| *cmd, | |
| stdin=asyncio.subprocess.PIPE, | |
| stdout=asyncio.subprocess.DEVNULL, | |
| stderr=asyncio.subprocess.DEVNULL | |
| ) | |
| if token in SESSIONS: | |
| SESSIONS[token]['process'] = process | |
| generator = bot_app.stream_media(target_media) | |
| try: | |
| async with pyrogram_sem: | |
| async for chunk in generator: | |
| if process.returncode is not None: | |
| break | |
| process.stdin.write(chunk) | |
| await process.stdin.drain() | |
| except asyncio.CancelledError: | |
| logger.debug(f"FFmpeg worker cancelled for {token}") | |
| except Exception as e: | |
| logger.error(f"Error piping to FFmpeg for {token}: {e}") | |
| finally: | |
| try: | |
| process.stdin.close() | |
| except: | |
| pass | |
| try: | |
| await generator.aclose() | |
| except: | |
| pass | |
| try: | |
| await asyncio.wait_for(process.wait(), timeout=5) | |
| except asyncio.TimeoutError: | |
| process.kill() | |
| async def start_hls_session(token, bot_app, target_media, pyrogram_sem): | |
| if token in SESSIONS: | |
| SESSIONS[token]['last_accessed'] = time.time() | |
| return | |
| SESSIONS[token] = {'last_accessed': time.time(), 'dir': os.path.join(HLS_DIR, token)} | |
| task = asyncio.create_task(_ffmpeg_worker(token, bot_app, target_media, pyrogram_sem)) | |
| if token in SESSIONS: | |
| SESSIONS[token]['task'] = task | |
| async def serve_hls_playlist(request): | |
| token = request.match_info.get('token') | |
| if token not in SESSIONS: | |
| return web.Response(status=404, text="HLS session not found") | |
| SESSIONS[token]['last_accessed'] = time.time() | |
| m3u8_path = os.path.join(SESSIONS[token]['dir'], "stream.m3u8") | |
| # Poll until m3u8 exists (user wait for first segment) | |
| for _ in range(20): | |
| if os.path.exists(m3u8_path): | |
| break | |
| await asyncio.sleep(0.5) | |
| if not os.path.exists(m3u8_path): | |
| return web.Response(status=500, text="HLS playlist generation timeout") | |
| return web.FileResponse(m3u8_path, headers={ | |
| "Cache-Control": "no-cache", | |
| "Access-Control-Allow-Origin": "*" | |
| }) | |
| async def serve_hls_segment(request): | |
| token = request.match_info.get('token') | |
| segment = request.match_info.get('segment') | |
| if token not in SESSIONS: | |
| return web.Response(status=404, text="HLS session not found") | |
| SESSIONS[token]['last_accessed'] = time.time() | |
| seg_path = os.path.join(SESSIONS[token]['dir'], segment) | |
| if not os.path.exists(seg_path): | |
| return web.Response(status=404, text="Segment not found") | |
| return web.FileResponse(seg_path, headers={ | |
| "Cache-Control": "public, max-age=3600", | |
| "Access-Control-Allow-Origin": "*" | |
| }) | |
| async def cleanup_worker(): | |
| while True: | |
| await asyncio.sleep(15) | |
| now = time.time() | |
| to_delete = [] | |
| for token, data in SESSIONS.items(): | |
| if now - data['last_accessed'] > 30: # 30 seconds idle | |
| to_delete.append(token) | |
| for token in to_delete: | |
| logger.info(f"Cleaning up idle HLS session {token}") | |
| data = SESSIONS.pop(token) | |
| if 'task' in data: | |
| data['task'].cancel() | |
| if 'process' in data: | |
| try: | |
| data['process'].kill() | |
| except: | |
| pass | |
| shutil.rmtree(data['dir'], ignore_errors=True) | |
| def setup_hls_routes(app): | |
| app.router.add_get('/hls/{token}/stream.m3u8', serve_hls_playlist) | |
| app.router.add_get('/hls/{token}/{segment}', serve_hls_segment) | |
| asyncio.create_task(cleanup_worker()) |