Repository navigation
Stream a video conversion while it runs, then swap to the cached file - #419
Merged
Merged
Conversation
A video that needs a full re-encode used to show a progress panel for the whole conversion. The same ffmpeg run now also writes a fragmented MP4 through the tee muxer, and a new /streaming_video route tails that growing file. The player starts playing it within seconds, keeps polling /prepare_video, and when the cached file is ready swaps to it at the same timestamp, which is when the scrubber starts working. Remuxes, replays and cache hits are unchanged. If the stream errors or produces no metadata within 15s, the player falls back to the progress panel for the rest of that open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every iOS/iPadOS browser and Safari refuse a growing MP4 of unknown length, so the iPad waited out the whole conversion. The same ffmpeg run now also writes an HLS EVENT playlist (fMP4 segments, a third tee output), served by /streaming_hls. Browsers that report native HLS try it first, then the growing MP4, then the progress panel. Measured with Chrome's native HLS player, and built in: - segments are served as video/iso.segment; as video/mp4 Chrome parses a fragment as a whole file and fails the stream - the playlist is offered only once it lists 3 segments, and is served with EXT-X-START:TIME-OFFSET=0; shorter live playlists fail outright, and a live playlist otherwise starts near its newest segment From adversarial review: - the HLS output is named relative to the cache dir (ffmpeg runs there), so a % in the user's path cannot become a segment-name pattern - URLs carry the conversion run's token, so cached segments never splice across runs of an edited video - the server reports when HLS is not coming (by media time, so a slow encode is not mistaken for a dead one) and the player then uses the MP4 - a stream that dies mid-play resumes the finished file at the same spot - the HLS dir lingers 120 s after the run for a player that has not swapped yet; timer start failure cannot turn a success into a failure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Desktop Chrome reports native HLS support, so it was taking the HLS path, but on a playlist that is still growing its player behaves like a live one: it skipped over large stretches (user report) or froze (reproduced with a fast-converting clip), while it plays the growing MP4 perfectly. HLS is now chosen only for Apple's WebKit (navigator.vendor), which is the player that needs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From adversarial review of the last two commits: - The HLS give-up compared ffmpeg's out_time against 10 s. out_time follows the audio, and x264's lookahead holds the video back by dozens of frames, which at a low frame rate is seconds of media. A 10 fps or 5 fps clip with audio (screen recordings, dashcams) was given up on while its playlist was healthy, stranding an iPad on the progress panel. The rule now counts completed fragments of the streamed MP4, which gets the same packets and, with scene-cut keyframes off while streaming, cuts at the same 2 s keyframes as the HLS segments. - WebKit's Linux ports (GNOME Web, WPE) report Apple's vendor string and play HLS through GStreamer; Apple's player is now also required to be on an Apple platform (iPadOS desktop mode reports MacIntel). - Jest: canPlayType is spied rather than assigned and deleted, which removed jsdom's own method for the rest of the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows CI: the route inserted EXT-X-START with a bare LF, so a CRLF playlist came out with mixed line endings, and the test fixture's write_text produced CRLF there. ffmpeg itself writes LF, but the tag now follows whatever the playlist uses, and the fixture writes bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows/3.10 CI: request_transcode reports "ready" as soon as the converted file is published, while the worker is still cleaning up the streamed copies in its finally. The end-to-end test read its results mid-clean-up (KeyError: 'copy'). _await_conversion now also drains the single worker before returning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Opening a video the browser can't play (DivX/XviD
.avi, MPEG-2, HEVC, 10-bit H.264…) used to mean watching a progress panel until ffmpeg finished the whole conversion. Now a full re-encode starts playing within a few seconds. When the conversion finishes, the player switches to the cached file at the same position, and from then on the scrubber works.The single ffmpeg run uses the
teemuxer to write three outputs from one encode:GET /streaming_video/…for Chrome and Firefox.GET /streaming_hls/…/hls/{token}/{name}for Apple's player. Every browser on iOS and iPadOS, and Safari, refuses a growing MP4 of unknown length. (I measured it: the iPad sent the first range probe and then nothing more.)Remuxes are never streamed, since they finish in seconds.
Player (
video-player.js).navigator.vendor === "Apple Computer, Inc."(every WebKit browser, iOS Chrome included) on an Applenavigator.platform, plus native HLS support. The platform test leaves out WebKit's Linux ports, which report the same vendor; iPadOS desktop mode reportsMacInteland still matches. Every other browser goes straight to the growing MP4, including desktop Chrome, though it also claims native HLS (see below).readyit swaps to the cached file and restores the position; a paused clip stays paused.Server.
uvicorn.runnow setstimeout_graceful_shutdown=5. A stream lasts as long as its conversion, and without the limit Ctrl+C waited for ffmpeg to finish.Browser behaviour this was built against (measured)
200.Range: bytes=0-with a206of what exists so far makes both fail.200; closed ranges get a206capped at 8 MB; a mid-file resume gets a416.video/mp4, a segment fails withDEMUXER_ERROR_COULD_NOT_PARSEbecause Chrome parses the fragment as a whole file. They are served asvideo/iso.segment.#EXT-X-START:TIME-OFFSET=0(ffmpeg has no option for it).Traps worth knowing about
tee. Copied AAC from an.avior.tskeeps its source tag, which the MP4 outputs reject, so the whole job fails. When streaming, audio is always re-encoded (cheap).flush_packets=1on the fMP4 output. Without it, only the 28-byteftypreaches disk for a long time.%danywhere in the cache path would break them. ffmpeg therefore runs from the cache directory with the HLS output named relative to it, and the executable, source and target are made absolute.index0and the segments are cacheable, so without the token an edited video could be spliced from two runs.onfail=ignore. The server clearshls_pendingonce the streamed MP4 holds 5 complete fragments and the playlist still isn't playable; the player then uses the MP4 instead of waiting. Both outputs get the same packets, and while streaming scene-cut keyframes are off (-sc_threshold 0), so MP4 fragment N closes when HLS segment N does. It doesn't use wall-clock time, because a slow encode would trip it. It doesn't use ffmpeg'sout_timeeither: that follows the audio, which x264's lookahead leaves seconds ahead of the video at low frame rates, and a 5–10 fps clip with sound got given up on while its playlist was healthy..tmpfiles and directories are reaped bysweep_transcode_cache. Nothing new ends in.mp4, whichpruneand the byte budget act on.Test plan
out_time;'[]|%05d;.avi;Known limitations (not addressed)
.tmp, the fMP4 and the HLS directory), and the byte budget doesn't count them.C:photos) on Python 3.10/3.11 aren't made absolute.🤖 Generated with Claude Code