feat(transcoding): add sentinel-based transcode guard and frontend retry
- Introduce a `.transcoding` sentinel file to prevent concurrent ffmpeg processes for the same video; return HTTP 202 with `Retry-After: 5` when a transcode is already in progress - Serve cached transcode immediately when the output file exists, avoiding unnecessary re-transcodes on repeat requests - Fix transcode failure response code from 404 to 500 - Add `isTranscoding` state in VideoPlayer that triggers an in-UI message and auto-retries the video source after 5 s when the server responds with 202 - Use `retryKey` to force re-mount of the `<video>` element on retry
This commit is contained in:
parent
89e558207f
commit
a32e95d913
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -128,6 +128,19 @@ const VideoPlayer = ({
|
|||
setSeekToTimestamp,
|
||||
}: VideoPlayerProps) => {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(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' : ''}`}
|
||||
>
|
||||
<div className={embed ? '' : `video-main ${isTheaterMode ? 'theater-mode' : ''}`}>
|
||||
{isTranscoding && (
|
||||
<p className="video-transcoding">Transcoding video for browser playback, retrying in 5 seconds…</p>
|
||||
)}
|
||||
<video
|
||||
ref={videoRef}
|
||||
key={`${getApiUrl()}${videoUrl}`}
|
||||
key={`${getApiUrl()}${videoUrl}-${retryKey}`}
|
||||
poster={`${getApiUrl()}${videoThumbUrl}`}
|
||||
onVolumeChange={(videoTag: VideoTag) => {
|
||||
localStorage.setItem('playerVolume', videoTag.currentTarget.volume.toString());
|
||||
|
|
@ -445,6 +461,16 @@ const VideoPlayer = ({
|
|||
});
|
||||
}}
|
||||
onEnded={handleVideoEnd(videoId, watched)}
|
||||
onError={async () => {
|
||||
try {
|
||||
const res = await fetch(`${getApiUrl()}${videoUrl}`, { method: 'HEAD' });
|
||||
if (res.status === 202) {
|
||||
setIsTranscoding(true);
|
||||
}
|
||||
} catch {
|
||||
// network error — ignore
|
||||
}
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
Loading…
Reference in New Issue