wpms/w
0
1import os2import uuid3import asyncio4import subprocess5import shutil6from fastapi import FastAPI, Query, HTTPException7from fastapi.responses import FileResponse8from fastapi.middleware.cors import CORSMiddleware9import logging10 11logging.basicConfig(level=logging.INFO)12logger = logging.getLogger(__name__)13 14app = FastAPI(title="M3U8 to MP4 Converter", version="1.0.0")15 16app.add_middleware(17 CORSMiddleware,18 allow_origins=["*"],19 allow_credentials=True,20 allow_methods=["*"],21 allow_headers=["*"],22)23 24TEMP_DIR = "/tmp/m3u8_conversions"25os.makedirs(TEMP_DIR, exist_ok=True)26 27active_conversions = {}28 29 30def cleanup_files(file_paths):31 for path in file_paths:32 try:33 if os.path.exists(path):34 if os.path.isdir(path):35 shutil.rmtree(path)36 else:37 os.remove(path)38 logger.info(f"Cleaned up: {path}")39 except Exception as e:40 logger.error(f"Cleanup error for {path}: {e}")41 42 43def get_ffmpeg_path():44 ffmpeg_path = shutil.which("ffmpeg")45 if ffmpeg_path:46 return ffmpeg_path47 for path in ["/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/app/ffmpeg", "./ffmpeg"]:48 if os.path.exists(path):49 return path50 return "ffmpeg"51 52 53async def convert_m3u8_to_mp4(m3u8_url: str, output_path: str, task_id: str):54 ffmpeg = get_ffmpeg_path()55 56 cmd = [57 ffmpeg,58 "-i", m3u8_url,59 "-c", "copy",60 "-bsf:a", "aac_adtstoasc",61 "-movflags", "+faststart",62 "-y",63 output_path64 ]65 66 cmd_fallback = [67 ffmpeg,68 "-i", m3u8_url,69 "-c:v", "libx264",70 "-preset", "fast",71 "-crf", "23",72 "-c:a", "aac",73 "-b:a", "128k",74 "-movflags", "+faststart",75 "-y",76 output_path77 ]78 79 logger.info(f"Starting conversion: {task_id}")80 logger.info(f"Command: {' '.join(cmd)}")81 82 active_conversions[task_id] = {"status": "converting", "progress": 0}83 84 try:85 process = await asyncio.create_subprocess_exec(86 *cmd,87 stdout=asyncio.subprocess.PIPE,88 stderr=asyncio.subprocess.PIPE89 )90 91 stdout, stderr = await process.communicate()92 93 if process.returncode != 0:94 logger.warning(f"Copy mode failed, trying re-encode mode: {stderr.decode()[:500]}")95 active_conversions[task_id]["status"] = "re-encoding"96 97 process = await asyncio.create_subprocess_exec(98 *cmd_fallback,99 stdout=asyncio.subprocess.PIPE,100 stderr=asyncio.subprocess.PIPE101 )102 stdout, stderr = await process.communicate()103 104 if process.returncode != 0:105 raise Exception(f"Conversion failed: {stderr.decode()[:1000]}")106 107 active_conversions[task_id]["status"] = "completed"108 logger.info(f"Conversion completed: {task_id}")109 110 except Exception as e:111 active_conversions[task_id]["status"] = "failed"112 active_conversions[task_id]["error"] = str(e)113 logger.error(f"Conversion error: {e}")114 raise115 116 117@app.get("/")118def root():119 return {120 "message": "M3U8 to MP4 Converter API",121 "usage": "/video?url=<m3u8-url>&format=mp4",122 "endpoints": {123 "convert": "/video?url=<m3u8-url>&format=mp4",124 "status": "/status/<task_id>"125 }126 }127 128 129@app.get("/video")130async def convert_video(131 url: str = Query(..., description="M3U8 URL to convert"),132 format: str = Query("mp4", description="Output format (mp4 only)")133):134 if not url:135 raise HTTPException(status_code=400, detail="URL parameter is required")136 137 if not url.startswith(("http://", "https://")):138 raise HTTPException(status_code=400, detail="Invalid URL format")139 140 task_id = str(uuid.uuid4())141 output_filename = f"video_{task_id}.mp4"142 output_path = os.path.join(TEMP_DIR, output_filename)143 144 try:145 await convert_m3u8_to_mp4(url, output_path, task_id)146 147 if not os.path.exists(output_path):148 raise HTTPException(status_code=500, detail="Conversion failed - output file not created")149 150 file_size = os.path.getsize(output_path)151 logger.info(f"File size: {file_size} bytes")152 153 response = FileResponse(154 path=output_path,155 media_type="video/mp4",156 filename=f"converted_video_{task_id}.mp4",157 headers={158 "Content-Disposition": f'attachment; filename="converted_video_{task_id}.mp4"'159 }160 )161 162 @response.background163 def cleanup():164 cleanup_files([output_path])165 166 return response167 168 except HTTPException:169 cleanup_files([output_path])170 raise171 except Exception as e:172 cleanup_files([output_path])173 logger.error(f"Error: {e}")174 raise HTTPException(status_code=500, detail=f"Conversion error: {str(e)}")175 176 177@app.get("/status/{task_id}")178def get_status(task_id: str):179 if task_id not in active_conversions:180 return {"status": "not_found"}181 return active_conversions[task_id]182 183 184@app.get("/health")185def health_check():186 ffmpeg = get_ffmpeg_path()187 ffmpeg_available = os.path.exists(ffmpeg) or shutil.which("ffmpeg") is not None188 return {189 "status": "healthy",190 "ffmpeg_available": ffmpeg_available,191 "ffmpeg_path": ffmpeg192 }193 194 195if __name__ == "__main__":196 import uvicorn197 port = int(os.environ.get("PORT", 7860))198 uvicorn.run(app, host="0.0.0.0", port=port)199 