Repository navigation
fix(frontend): use container duration for WebM decoding #15904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -239,10 +239,17 @@ fn decode_video(config: &VideoDecoder, bytes: Vec<u8>) -> Result<DecodedMediaDat | |||||||||||||
| frame_rate.denominator() > 0, | ||||||||||||||
| "Cannot determine the video frame rate" | ||||||||||||||
| ); | ||||||||||||||
| let source_duration = if input_stream.duration() > 0 { | ||||||||||||||
| Time::new(Some(input_stream.duration()), stream_time_base).as_secs() as f64 | ||||||||||||||
| } else { | ||||||||||||||
| // WebM may expose duration only at container level, in AV_TIME_BASE units. | ||||||||||||||
| // Clamp unknown (AV_NOPTS_VALUE) or nonpositive durations to zero. | ||||||||||||||
| input.duration().max(0) as f64 / f64::from(ffmpeg_next::ffi::AV_TIME_BASE) | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '150,420p' lib/llm/src/preprocessor/media/decoders/video.rs
rg -n 'overflow-checks|\[profile' Cargo.toml lib/llm/Cargo.tomlRepository: ai-dynamo/dynamo Length of output: 10643 🏁 Script executed: set -o pipefail
printf '%s\n' '--- helper definitions and callers ---'
rg -n -F -- 'fn get_num_requested_frames' lib/llm/src/preprocessor/media/decoders/video.rs
rg -n -F -- 'fn get_target_times' lib/llm/src/preprocessor/media/decoders/video.rs
rg -n -F -- 'get_num_requested_frames(' lib/llm/src/preprocessor/media/decoders/video.rs
rg -n -F -- 'get_target_times(' lib/llm/src/preprocessor/media/decoders/video.rs
printf '%s\n' '--- relevant source ---'
sed -n '1,180p' lib/llm/src/preprocessor/media/decoders/video.rs
printf '%s\n' '--- tests ---'
rg -n -F -- 'max_frames' lib/llm/src/preprocessor/media/decoders/video.rs lib/llm/src/preprocessor/media || true
rg -n -F -- 'num_frames' lib/llm/src/preprocessor/media/decoders/video.rs lib/llm/src/preprocessor/media || true
sed -n '420,760p' lib/llm/src/preprocessor/media/decoders/video.rs
printf '%s\n' '--- Cargo profiles ---'
sed -n '200,245p' Cargo.toml
printf '%s\n' '--- pre-change helper and decode diff ---'
git diff dd2a4301ef02b00f64353c1d81f395b0d0bbb707..3ff14375ab926ee0b9aac1b9ea064b5cba4b1a27 -- lib/llm/src/preprocessor/media/decoders/video.rsRepository: ai-dynamo/dynamo Length of output: 31034 Bound container-derived frame counts before constructing target times. When a WebM has no stream duration or frame count, an exaggerated positive container duration can make Apply the allocation bound before 🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] One request can get the frontend OOM-killed. Measured in a running frontend, with steps to reproduce.The input is a 2-frame VP9 WebM of 7 to 11 KB. Its second frame has a timestamp of 1e9 s. The frontend ran in its own container with
#15816 behaves the same, so the fix belongs before
With
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
You are interacting with an AI system. |
||||||||||||||
| }; | ||||||||||||||
| ( | ||||||||||||||
| input_stream.index(), | ||||||||||||||
| stream_time_base, | ||||||||||||||
| Time::new(Some(input_stream.duration()), stream_time_base).as_secs() as f64, | ||||||||||||||
| source_duration, | ||||||||||||||
| (frame_rate.numerator() as f32 / frame_rate.denominator() as f32) as f64, | ||||||||||||||
| input_stream.frames().max(0) as u64, | ||||||||||||||
| input_stream.parameters(), | ||||||||||||||
|
|
@@ -537,6 +544,59 @@ mod tests { | |||||||||||||
| assert!(msg.contains("libvpx-vp9"), "no re-encode hint in: {msg}"); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #[rstest] | ||||||||||||||
| #[case(None, None, 6)] | ||||||||||||||
| #[case(Some(1.0), None, 3)] | ||||||||||||||
| #[case(None, Some(2), 2)] | ||||||||||||||
| fn test_decode_webm_with_container_duration( | ||||||||||||||
| #[case] fps: Option<f64>, | ||||||||||||||
| #[case] num_frames: Option<u64>, | ||||||||||||||
| #[case] expected_frames: usize, | ||||||||||||||
| ) { | ||||||||||||||
| // Finalized six-frame VP9 WebM from the reported data-URL request. | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment mostly repeats the fixture filename and nearby assertions, while the reference to the reported data-URL request is bug-history provenance rather than a lasting constraint needed to understand the test. Remove it or replace it only with non-obvious fixture requirements that are not already asserted below. 🤖 AI FixRemove this comment unless you add a concise note about a fixture property that the test does not already express. |
||||||||||||||
| let path = concat!( | ||||||||||||||
| env!("CARGO_MANIFEST_DIR"), | ||||||||||||||
| "/tests/data/media/224p_6_vp9.webm" | ||||||||||||||
| ); | ||||||||||||||
| let input = ffmpeg_next::format::input(&path).unwrap(); | ||||||||||||||
| let stream = input | ||||||||||||||
| .streams() | ||||||||||||||
| .best(ffmpeg_next::media::Type::Video) | ||||||||||||||
| .unwrap(); | ||||||||||||||
| assert_eq!(stream.duration(), ffmpeg_next::ffi::AV_NOPTS_VALUE); | ||||||||||||||
| assert_eq!(stream.frames(), 0); | ||||||||||||||
| assert_eq!( | ||||||||||||||
| input.duration(), | ||||||||||||||
| 3 * i64::from(ffmpeg_next::ffi::AV_TIME_BASE) | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| let decoder = VideoDecoder { | ||||||||||||||
| fps, | ||||||||||||||
| num_frames, | ||||||||||||||
| strict: true, | ||||||||||||||
| ..Default::default() | ||||||||||||||
| }; | ||||||||||||||
| let decoded = decoder | ||||||||||||||
| .decode(EncodedMediaData::from_bytes(std::fs::read(path).unwrap())) | ||||||||||||||
| .unwrap(); | ||||||||||||||
| assert_eq!( | ||||||||||||||
| decoded.tensor_info.shape, | ||||||||||||||
| vec![expected_frames, 224, 224, 3] | ||||||||||||||
| ); | ||||||||||||||
| let Some(DecodedMediaMetadata::Video(metadata)) = decoded.tensor_info.metadata else { | ||||||||||||||
| panic!("missing video metadata"); | ||||||||||||||
| }; | ||||||||||||||
| assert_eq!(metadata.source_duration, 3.0); | ||||||||||||||
| assert_eq!(metadata.source_fps, 2.0); | ||||||||||||||
| assert_eq!(metadata.sampled_timestamps.len(), expected_frames); | ||||||||||||||
| assert!( | ||||||||||||||
| metadata | ||||||||||||||
| .sampled_timestamps | ||||||||||||||
| .iter() | ||||||||||||||
| .all(|t| (0.0..3.0).contains(t)) | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #[test] | ||||||||||||||
| fn test_decode_video_num_frames() { | ||||||||||||||
| let (encoded_data, width, height, _total_frames) = load_test_video("240p_10.mp4"); | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a stream has unknown video duration/frame count but the container duration is positive because of another stream, such as longer audio, this fallback treats the container duration as the video duration.
get_num_requested_framesthen derives total frame count and target sample times from that longer value; on the default non-strict frontend path the decoder reaches video EOF and returns fewer frames than the requestednum_frames/max_frames, while metadata reports the inflated duration. Fall back only to a duration that is known to describe the video stream, or derive the video duration from decoded video packet/frame timestamps before using it for sampling.🤖 AI Fix
Replace the container-duration fallback with a video-stream duration derived from video packet/frame timestamps, or reject this case until such a video-specific duration is available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed by measurement. The container duration includes the audio, so an ordinary WebM with audio loses samples. A WebM with no container duration still fails before decoding. #15816 decodes all four cases below.
Measured on Linux with the in-tree FFmpeg 9.0.1, default frontend settings (`max_frames` 32, not strict).
num_frames=3fails with "2 of 3"source_duration60.008.num_frames=3returns 1 framesource_duration8.0, timestamps 5.0 to 7.5-live 1)Cannot determine the video frame count