diff --git a/backend/video/views.py b/backend/video/views.py index 7797056a..7a16faf0 100644 --- a/backend/video/views.py +++ b/backend/video/views.py @@ -335,7 +335,24 @@ class VideoStreamView(ApiBaseView): os.makedirs(cache_dir, exist_ok=True) cache_path = os.path.join(cache_dir, f"{video_id}.mp4") - if not os.path.exists(cache_path): + sentinel_path = cache_path + ".transcoding" + + # Serve cached transcode immediately if available + if os.path.exists(cache_path): + response = FileResponse(open(cache_path, "rb")) + response["Content-Type"] = "video/mp4" + return response + + # Guard against concurrent transcode of the same video + if os.path.exists(sentinel_path): + in_progress = Response({"status": "transcoding"}, status=202) + in_progress["Retry-After"] = "5" + return in_progress + + # Run transcode; sentinel is removed regardless of outcome + try: + with open(sentinel_path, "w"): + pass # create sentinel cmd = [ "ffmpeg", "-y", @@ -358,10 +375,15 @@ class VideoStreamView(ApiBaseView): cache_path, ] subprocess.run(cmd, check=False) + finally: + try: + os.remove(sentinel_path) + except FileNotFoundError: + pass if not os.path.exists(cache_path): error = ErrorResponseSerializer({"error": "transcode failed"}) - return Response(error.data, status=404) + return Response(error.data, status=500) response = FileResponse(open(cache_path, "rb")) response["Content-Type"] = "video/mp4" diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx index 17b73cfa..9cd14a01 100644 --- a/frontend/src/components/VideoPlayer.tsx +++ b/frontend/src/components/VideoPlayer.tsx @@ -128,6 +128,19 @@ const VideoPlayer = ({ setSeekToTimestamp, }: VideoPlayerProps) => { const videoRef = useRef(null); + const [isTranscoding, setIsTranscoding] = useState(false); + const [retryKey, setRetryKey] = useState(0); + + // If the video element errors (e.g. server returned 202 Transcoding), + // show a message and automatically retry after 5 seconds. + useEffect(() => { + if (!isTranscoding) return; + const timer = setTimeout(() => { + setIsTranscoding(false); + setRetryKey((k: number) => k + 1); + }, 5000); + return () => clearTimeout(timer); + }, [isTranscoding]); useEffect(() => { if (seekToTimestamp === undefined || !videoRef.current) { @@ -413,9 +426,12 @@ const VideoPlayer = ({ className={embed ? '' : `player-wrapper ${isTheaterMode ? 'theater-mode' : ''}`} >
+ {isTranscoding && ( +

Transcoding video for browser playback, retrying in 5 seconds…

+ )}