diff --git a/server.py b/server.py index 023b976..09fa2df 100644 --- a/server.py +++ b/server.py @@ -364,6 +364,67 @@ async def get_cached(content_hash: str): return FileResponse(cache_path) +@app.get("/cache/{content_hash}/mp4") +async def get_cached_mp4(content_hash: str): + """Get cached content as MP4 (transcodes MKV on first request, caches result).""" + cache_path = CACHE_DIR / content_hash + mp4_path = CACHE_DIR / f"{content_hash}.mp4" + + if not cache_path.exists(): + raise HTTPException(404, f"Content {content_hash} not in cache") + + # If MP4 already cached, serve it + if mp4_path.exists(): + return FileResponse(mp4_path, media_type="video/mp4") + + # Check if source is already MP4 + media_type = detect_media_type(cache_path) + if media_type != "video": + raise HTTPException(400, "Content is not a video") + + # Check if already MP4 format + import subprocess + try: + result = subprocess.run( + ["ffprobe", "-v", "error", "-select_streams", "v:0", + "-show_entries", "format=format_name", "-of", "csv=p=0", str(cache_path)], + capture_output=True, text=True, timeout=10 + ) + if "mp4" in result.stdout.lower() or "mov" in result.stdout.lower(): + # Already MP4-compatible, just serve original + return FileResponse(cache_path, media_type="video/mp4") + except Exception: + pass # Continue with transcoding + + # Transcode to MP4 (H.264 + AAC) + transcode_path = CACHE_DIR / f"{content_hash}.transcoding.mp4" + try: + result = subprocess.run( + ["ffmpeg", "-y", "-i", str(cache_path), + "-c:v", "libx264", "-preset", "fast", "-crf", "23", + "-c:a", "aac", "-b:a", "128k", + "-movflags", "+faststart", + str(transcode_path)], + capture_output=True, text=True, timeout=600 # 10 min timeout + ) + if result.returncode != 0: + raise HTTPException(500, f"Transcoding failed: {result.stderr[:200]}") + + # Move to final location + transcode_path.rename(mp4_path) + + except subprocess.TimeoutExpired: + if transcode_path.exists(): + transcode_path.unlink() + raise HTTPException(500, "Transcoding timed out") + except Exception as e: + if transcode_path.exists(): + transcode_path.unlink() + raise HTTPException(500, f"Transcoding failed: {e}") + + return FileResponse(mp4_path, media_type="video/mp4") + + @app.get("/ui/cache/{content_hash}", response_class=HTMLResponse) async def ui_cache_view(content_hash: str, request: Request): """View cached content with appropriate display.""" @@ -432,7 +493,8 @@ async def ui_cache_view(content_hash: str, request: Request): """ if media_type == "video": - html += f'' + video_src = video_src_for_request(content_hash, request) + html += f'' elif media_type == "image": html += f'{content_hash}' else: @@ -962,6 +1024,19 @@ async def delete_collection(name: str, username: str = Depends(get_required_user raise HTTPException(404, "Collection not found") +def is_ios_request(request: Request) -> bool: + """Check if request is from iOS device.""" + ua = request.headers.get("user-agent", "").lower() + return "iphone" in ua or "ipad" in ua + + +def video_src_for_request(content_hash: str, request: Request) -> str: + """Get video src URL, using MP4 endpoint for iOS.""" + if is_ios_request(request): + return f"/cache/{content_hash}/mp4" + return f"/cache/{content_hash}" + + def detect_media_type(cache_path: Path) -> str: """Detect if file is image or video based on magic bytes.""" with open(cache_path, "rb") as f: @@ -1627,7 +1702,8 @@ async def ui_detail_page(run_id: str, request: Request):
''' if input_media_type == "video": - html += f'' + input_video_src = video_src_for_request(input_hash, request) + html += f'' elif input_media_type == "image": html += f'input' html += '
' @@ -1641,7 +1717,8 @@ async def ui_detail_page(run_id: str, request: Request):
''' if output_media_type == "video": - html += f'' + output_video_src = video_src_for_request(output_hash, request) + html += f'' elif output_media_type == "image": html += f'output' html += '
'