diff --git a/crates/rendering/src/decoder/ffmpeg.rs b/crates/rendering/src/decoder/ffmpeg.rs index 5a13503e976..1a06901233d 100644 --- a/crates/rendering/src/decoder/ffmpeg.rs +++ b/crates/rendering/src/decoder/ffmpeg.rs @@ -81,7 +81,6 @@ struct PendingRequest { reply: oneshot::Sender, } -const MAX_FRAME_LOOKBACK_TOLERANCE: u32 = 2; const MAX_FRAME_CACHE_BYTES: usize = 128 * 1024 * 1024; /// Cache weight of a frame still on the GPU (it pins an NVDEC surface). #[cfg(target_os = "linux")] @@ -765,24 +764,28 @@ impl FfmpegDecoder { continue; } - if is_backward_seek { - let best_cached_frame = sw_cache - .range(..=requested_frame) - .next_back() - .filter(|(k, _)| { - requested_frame.saturating_sub(**k) <= MAX_FRAME_LOOKBACK_TOLERANCE - }) - .map(|(k, _)| *k); - - if let Some(frame_num) = best_cached_frame - && let Some(cached) = sw_cache.get_mut(&frame_num) - { - let data = cached.produce(&mut sw_converter); + // Requests inside a KNOWN pts hole are answered with the + // hole's start frame — the true VFR hold — without + // touching the decoder; see the hardware path below. + if let Some(hole_start) = sw_pts_holes + .range(..=requested_frame) + .next_back() + .filter(|&(_, &end)| requested_frame < end) + .map(|(&start, _)| start) + { + let data = sw_cache + .get_mut(&hole_start) + .map(|c| c.produce(&mut sw_converter)) + .or_else(|| sw_gap_hold.clone().filter(|h| h.number == hole_start)); + if let Some(data) = data { + sw_gap_hold = Some(data.clone()); *sw_last_sent_frame.borrow_mut() = Some(data.clone()); let _ = reply.send(data.frame); continue; } + } + if is_backward_seek { if let Err(error) = sw_this.reset(requested_time) && let Some(control) = &managed { @@ -799,28 +802,6 @@ impl FfmpegDecoder { sw_reader_at = None; } - // Requests inside a KNOWN pts hole are answered with the - // hole's start frame — the true VFR hold — without - // touching the decoder; see the hardware path above. - if !is_backward_seek - && let Some(hole_start) = sw_pts_holes - .range(..=requested_frame) - .next_back() - .filter(|&(_, &end)| requested_frame < end) - .map(|(&start, _)| start) - { - let data = sw_cache - .get_mut(&hole_start) - .map(|c| c.produce(&mut sw_converter)) - .or_else(|| sw_gap_hold.clone().filter(|h| h.number == hole_start)); - if let Some(data) = data { - sw_gap_hold = Some(data.clone()); - *sw_last_sent_frame.borrow_mut() = Some(data.clone()); - let _ = reply.send(data.frame); - continue; - } - } - if reply.is_closed() { continue; } @@ -1348,24 +1329,33 @@ impl FfmpegDecoder { continue; } - if is_backward_seek { - let best_cached_frame = cache - .range(..=requested_frame) - .next_back() - .filter(|(k, _)| { - requested_frame.saturating_sub(**k) <= MAX_FRAME_LOOKBACK_TOLERANCE - }) - .map(|(k, _)| *k); - - if let Some(frame_num) = best_cached_frame - && let Some(cached) = cache.get_mut(&frame_num) - { - let data = cached.produce(&mut converter); + // Requests inside a KNOWN pts hole are answered with the + // hole's start frame — the true VFR hold (the frame simply + // stayed on screen) — without touching the decoder. This + // keeps the post-hole frames cached and the reader parked, + // however long the hole runs. Only recorded holes qualify: a + // bare "some cached frame lies beyond the request" test would + // also match disjoint cache islands left by seeks, and + // serving stale content there would freeze playback. + if let Some(hole_start) = pts_holes + .range(..=requested_frame) + .next_back() + .filter(|&(_, &end)| requested_frame < end) + .map(|(&start, _)| start) + { + let data = cache + .get_mut(&hole_start) + .map(|c| c.produce(&mut converter)) + .or_else(|| gap_hold.clone().filter(|h| h.number == hole_start)); + if let Some(data) = data { + gap_hold = Some(data.clone()); *last_sent_frame.borrow_mut() = Some(data.clone()); let _ = reply.send(data.frame); continue; } + } + if is_backward_seek { if let Err(error) = this.reset(requested_time) && let Some(control) = &managed { @@ -1383,33 +1373,6 @@ impl FfmpegDecoder { reader_at = None; } - // Requests inside a KNOWN pts hole are answered with the - // hole's start frame — the true VFR hold (the frame simply - // stayed on screen) — without touching the decoder. This - // keeps the post-hole frames cached and the reader parked, - // however long the hole runs. Only recorded holes qualify: a - // bare "some cached frame lies beyond the request" test would - // also match disjoint cache islands left by seeks, and - // serving stale content there would freeze playback. - if !is_backward_seek - && let Some(hole_start) = pts_holes - .range(..=requested_frame) - .next_back() - .filter(|&(_, &end)| requested_frame < end) - .map(|(&start, _)| start) - { - let data = cache - .get_mut(&hole_start) - .map(|c| c.produce(&mut converter)) - .or_else(|| gap_hold.clone().filter(|h| h.number == hole_start)); - if let Some(data) = data { - gap_hold = Some(data.clone()); - *last_sent_frame.borrow_mut() = Some(data.clone()); - let _ = reply.send(data.frame); - continue; - } - } - if reply.is_closed() { continue; } diff --git a/crates/rendering/src/decoder/model/expectations.toml b/crates/rendering/src/decoder/model/expectations.toml index a4e0bc344fa..ac2eb15e2c7 100644 --- a/crates/rendering/src/decoder/model/expectations.toml +++ b/crates/rendering/src/decoder/model/expectations.toml @@ -43,223 +43,83 @@ flaky = true reason = "Seeds 28, 53 and 59 fail in every run; which wrong answer comes first depends on the wall clock. Without a stall the first wrong answer is a future one at a seek (seed 28 request 61, seed 53 request 8, seed 59 request 9: the next sample, e.g. sample 68 for 67): pool position capping, the reader's pool position is capped to the newest request (avassetreader.rs update_decoder_position(decoder_idx, capped)) so a reader that already decoded past the sample is reused without a reset. When the test thread stalls for 250 ms or more, the decoder's idle timeout (IDLE_READER_TIMEOUT, avassetreader.rs) clears the cache and reader state, the later requests take another path and the first wrong answer is a stale one from the wrong-GOP seek (reader started on the keyframe time rounded to 10 ms). Measured on Apple silicon: future 3 of 3 per seed without a stall, stale for all three seeds with a 300 ms stall before every request, and for seed 53 with a single 300 ms stall before request 6 (a 240 ms stall keeps future). macos-15: future in 6 of 7 runs, stale in 1 (38743345280). With #2595's decoder changes seed 28 passes and seeds 53 and 59 keep only the stale (wrong-GOP) answer" link = "https://github.com/CapSoftware/Cap/pull/2595 https://github.com/CapSoftware/Cap/pull/2603" -[[xfail]] -case = "decoder-model/ffmpeg/camera-fast/random-seeks/playback" -class = "future" -seeds = "7" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/camera-fast/random-seeks/playback" class = "grid-rounding" -seeds = "2 11-13 20 23 26 33" +seeds = "2 11-13 20 26" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/cfr/keyframe-seeks/playback" -class = "future" -seeds = "0-1 4 6 17-18 20 23 38 41-42 46 48 50 53 56 62-63 65 69 83 90 93 101 104 108 110 115 118-119 122 124 126 130 133 136 140 142 145 147 149-151 154 161 167 169 181 183 186 188 192 199-201 203 205 207 209 212 215-216 219 223 235 237 239 245 248 250 253 256 260-261 263 268 270-271 275 285 288 290-291 299" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/cfr/keyframe-seeks/playback" class = "grid-rounding" -seeds = "0 4 6 8-9 12 15 17 20 23 28 30-31 39 41-42 46 48 50 52-54 56 61-63 65 75 79 83-84 86 90 93 96 102 107-108 110-111 115 117 119 121-122 124-125 129 136 138 140-143 145 147-151 154 156 161 167 169 172 177 181 183 186 188-192 194-195 197-201 203 205-207 209 211-212 216 219 222-223 226 234-235 237 239 241 245 248 250 253-256 263 265-266 268 270 273-275 283-285 290-291 294 299" +seeds = "8 12 23 39 46 56 62 65 79 90 107 115 117 119 122 129 140-141 147-149 154 161 169 181 183 188 192 194-195 197 199-201 203 205-207 209 216 222-223 234-235 237 239 241 245 248 250 253 256 266 268 275 283 291 294" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/cfr/keyframe-seeks/playback" -class = "stale" -seeds = "5 8 96 161" -reason = "NEW: the FFmpeg decoder answers a backward request whose frame is not cached with a cached frame up to 2 grid frames older (MAX_FRAME_LOOKBACK_TOLERANCE)" -link = "" - -[[xfail]] -case = "decoder-model/ffmpeg/cfr/random-seeks/playback" -class = "future" -seeds = "60" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/cfr/random-seeks/playback" class = "grid-rounding" -seeds = "31 40 52 57 61 63 69 74 80 93 117 120 126 153 166 180 182 188 196 251 276 279 297 299" +seeds = "31 52 57 61 63 69 74 80 93 117 120 126 153 166 180 182 196 251 279 297" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/cfr/random-seeks/playback" -class = "stale" -seeds = "12 67 129 198 233 281" -reason = "NEW: the FFmpeg decoder answers a backward request whose frame is not cached with a cached frame up to 2 grid frames older (MAX_FRAME_LOOKBACK_TOLERANCE)" -link = "" - -[[xfail]] -case = "decoder-model/ffmpeg/cfr/scrub/playback" -class = "future" -seeds = "88" -reason = "Found once each sequence keeps running after its first wrong answer (full failure sets, runs 38851482697 and 38851482675): a later request is answered with a sample ahead of the right one (seed index 88 request 220). FFmpeg decoder request handling, the mechanisms of this decoder's other entries (backward lookback tolerance, reset decisions)" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/cfr/scrub/playback" class = "grid-rounding" -seeds = "13 30 50 62-63 93 107 119 121-122 126 143-144 149 176 187 199" +seeds = "30 62-63 107 122 126 143 149 187 199" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/cfr/scrub/playback" -class = "stale" -seeds = "7 11 15 26 37-39 46 49-50 52-53 55 63 70-72 80 83 93-95 105-107 109 113 118 124 127 132-134 141-142 150 155 173 175 188 191 193 199" -reason = "NEW: the FFmpeg decoder answers a backward request whose frame is not cached with a cached frame up to 2 grid frames older (MAX_FRAME_LOOKBACK_TOLERANCE)" -link = "" - -[[xfail]] -case = "decoder-model/ffmpeg/cfr/tail/export" -class = "grid-rounding" -seeds = "26" -reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" -link = "" - -[[xfail]] -case = "decoder-model/ffmpeg/cfr/timeline/export" -class = "future" -seeds = "29 50" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/cfr/timeline/export" class = "grid-rounding" -seeds = "16 26 32 50 65 75 95 102 133 145" +seeds = "16 26 32 75 95 102 133 145" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/cfr/timeline/export" -class = "stale" -seeds = "35" -reason = "NEW: the FFmpeg decoder answers a backward request whose frame is not cached with a cached frame up to 2 grid frames older (MAX_FRAME_LOOKBACK_TOLERANCE)" -link = "" - -[[xfail]] -case = "decoder-model/ffmpeg/cfr/transition/export" -class = "future" -seeds = "6 66 73" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/cfr/transition/export" class = "grid-rounding" -seeds = "0 5-7 15 22 23 25 29 38 41 45 53 56 61 62 66 72 73" +seeds = "6 41 56 61 66 72-73" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/cfr/transition/export" -class = "stale" -seeds = "0-2 4-5 7 15 22-26 29 33-36 38 40-41 52 60 62 67 73-75 79" -reason = "NEW: the FFmpeg decoder answers a backward request whose frame is not cached with a cached frame up to 2 grid frames older (MAX_FRAME_LOOKBACK_TOLERANCE)" -link = "" - -[[xfail]] -case = "decoder-model/ffmpeg/cfr/transition/playback" -class = "future" -seeds = "69" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/cfr/transition/playback" class = "grid-rounding" -seeds = "9 14 19-21 25 29 31 42 51 53 54 57 58 61 64-66" +seeds = "19 29 42 51 65-66" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/cfr/transition/playback" -class = "stale" -seeds = "3 6 7 9 14 20 21 25 27 31 36 37 39 43-45 50 53 54 57 59 61 64-66 70 77" -reason = "NEW: the FFmpeg decoder answers a backward request whose frame is not cached with a cached frame up to 2 grid frames older (MAX_FRAME_LOOKBACK_TOLERANCE)" -link = "" - -[[xfail]] -case = "decoder-model/ffmpeg/dense/random-seeks/playback" -class = "future" -seeds = "90 121" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/dense/random-seeks/playback" class = "grid-rounding" -seeds = "1 7 15 17 21-22 25 32-33 35-36 45 47-48 55 64 70 82 86 90-91 100 103 114-115 121 126 144 147" +seeds = "1 7 15 17 21-22 25 33 35-36 45 47-48 55 64 70 82 86 90 100 103 114-115 121 126 144" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/dense/random-seeks/playback" -class = "stale" -seeds = "39 57 61" -reason = "NEW: the FFmpeg decoder answers a backward request whose frame is not cached with a cached frame up to 2 grid frames older (MAX_FRAME_LOOKBACK_TOLERANCE)" -link = "" - -[[xfail]] -case = "decoder-model/ffmpeg/vfr/keyframe-seeks/playback" -class = "future" -seeds = "2-5 8-11 14 20-22 24-27 29 31 32 34 36 37 39-41 46 48 50 53-56 58 59 61-65 67-71 73 76 79 82 84 86 88-91 93-99" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/vfr/keyframe-seeks/playback" class = "grid-rounding" -seeds = "1-5 9-11 13 16-18 21-25 27-32 34 36-37 40-44 46-48 50-51 53-56 58-66 68-74 76 79-93 95-97" +seeds = "2 4-5 9-11 13 18 22-23 27 29 31-32 36 41 48 50 59 62-63 69-73 76 81-82 84 88-91 95" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/vfr/random-seeks/playback" -class = "future" -seeds = "62" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/vfr/random-seeks/playback" class = "grid-rounding" -seeds = "3 8 10 12 16 20 34 46 52 58 59 64 77 78 86 95 117 137 144 160 168 185 196" +seeds = "3 12 16 20 34 46 58-59 64 77-78 86 117 137 144 160 168 196" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/vfr/scrub/playback" -class = "future" -seeds = "114" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/vfr/scrub/playback" class = "grid-rounding" -seeds = "23 30 46 48 78 87 111 114 119" +seeds = "23 30 46 114" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/vfr/scrub/playback" -class = "stale" -seeds = "0 7-8 12 17-18 21 23 46 59 66 75 86 103 108" -reason = "NEW: the FFmpeg decoder answers a backward request whose frame is not cached with a cached frame up to 2 grid frames older (MAX_FRAME_LOOKBACK_TOLERANCE)" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/vfr/sequential/playback" class = "grid-rounding" @@ -267,38 +127,16 @@ seeds = "25" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/vfr/timeline/export" -class = "future" -seeds = "25" -reason = "NEW: the FFmpeg decoder seeks with the request time rounded to the stream tick, so a request a fraction of a tick before a keyframe lands on that keyframe and is answered with a later frame" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/vfr/timeline/export" class = "grid-rounding" -seeds = "20 77 110" +seeds = "77" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" -[[xfail]] -case = "decoder-model/ffmpeg/vfr/transition/playback" -class = "future" -seeds = "66 75 77" -reason = "Found once each sequence keeps running after its first wrong answer (full failure sets, runs 38851482697 and 38851482675): a later request is answered with a sample ahead of the right one (seed index 66 request 206). FFmpeg decoder request handling, the mechanisms of this decoder's other entries (backward lookback tolerance, reset decisions)" -link = "" - [[xfail]] case = "decoder-model/ffmpeg/vfr/transition/playback" class = "grid-rounding" -seeds = "10 12 13 15 16 19 20 28 33 34 43 47 48 56 59 62 66 68 70 71 75 77" +seeds = "13 16 20 34 48 62 70 75 77" reason = "NEW: the FFmpeg decoder seeks to the exact request time, so after a reset it answers with the frame on screen at that time even when its rounded grid label is after the request; sequential playback shows the previous frame for the same request" link = "" - -[[xfail]] -case = "decoder-model/ffmpeg/vfr/transition/playback" -class = "stale" -seeds = "4-6 8 10 12 13 18 19 21 28 31 33 38 39 41 45-47 49 50 52 55 56 58 59 61 66 68 71 75-77 79" -reason = "NEW: the FFmpeg decoder answers a backward request whose frame is not cached with a cached frame up to 2 grid frames older (MAX_FRAME_LOOKBACK_TOLERANCE)" -link = "" - diff --git a/crates/verify/expectations.toml b/crates/verify/expectations.toml index 97d0aa7f8e8..32dedfe7a42 100644 --- a/crates/verify/expectations.toml +++ b/crates/verify/expectations.toml @@ -63,30 +63,6 @@ bound = { camera-frames = 178, delta = 60, frame = [1, 178] } reason = "NEW: FFmpeg decoder, same as the AVAssetReader case: a camera file whose pts start 2 s in runs ahead from the first output frame (178 of 179 frames, up to 60 source frames ahead, every platform)" link = "" -[[xfail]] -case = "seek/seek-gop/storm30/ffmpeg" -class = "stale" -flaky = true -bound = { screen-frames = 4, camera-frames = 4, delta = 2, frame = [1, 900] } -reason = "FFmpeg decoder backward-seek lookback tolerance (MAX_FRAME_LOOKBACK_TOLERANCE in crates/rendering/src/decoder/ffmpeg.rs): a backward seek whose frame is not cached is answered with a cached frame up to 2 source frames older. The fast tier passes; the full tier's 3000 seeks hit it once (macOS, 1 frame stale). Bound: a few seeks, never more than the 2 frames the tolerance reaches" -link = "" - -[[xfail]] -case = "seek/seek-native/storm30/ffmpeg" -class = "stale" -flaky = true -bound = { screen-frames = 4, camera-frames = 4, delta = 2, frame = [1, 900] } -reason = "Same FFmpeg lookback tolerance as seek/seek-gop/storm30/ffmpeg on the native fixture: the fast tier passes, the full tier hits it once (macOS, 1 frame stale)" -link = "" - -[[xfail]] -case = "seek/vfr-holds/storm60/ffmpeg" -class = "stale" -flaky = true -bound = { screen-frames = 4, camera-frames = 4, delta = 2, frame = [1, 1800] } -reason = "Same FFmpeg lookback tolerance as seek/seek-gop/storm30/ffmpeg over long VFR holds: the fast tier passes, the full tier hits it on 2 camera seeks (macOS, 2 frames stale)" -link = "" - # Decode deadline under machine starvation. These apply only to a case the # starvation sentinel saw stall (starved = true) and only with the signature # of the mechanism; the same symptom on a healthy machine is a failure. @@ -189,14 +165,6 @@ bound = { screen-frames = 1, delta = 1, frame = [149, 149] } reason = "During a cross-fade both clips read the same AVAssetReader decoder at two positions; a reset for one position drops the other position's last vended frame from the cache, so at 60 fps the incoming side of output frame 149 (2.483 s) shows source frame 115 instead of 116. Not the IOSurface source lifetime bug: it stays with that fix (sol-avf proof run 38762654200). Same frame in export and in offline playback. Fires in almost every run but depends on the decoder's cache state, which its 250 ms idle timeout (IDLE_READER_TIMEOUT) clears: a 300 ms pause in the request stream at 2.0, 2.42 or 2.48 s makes offline playback correct (measured on Apple silicon), and the export passed once on macos-15 (push run 38763177849). Bound: that one screen frame, one source frame stale" link = "https://github.com/CapSoftware/Cap/pull/2638" -[[xfail]] -case = "seek/seek-native/keyframes60/ffmpeg" -class = "stale" -platforms = ["linux", "windows"] -bound = { screen-frames = 2, camera-frames = 2, delta = 61, frame = [124, 490] } -reason = "NEW: FFmpeg decoder. Off macOS this fixture is libx264 (the AVFoundation encoder is unavailable), so its keyframe set and shuffled request order differ from macOS. Request 124 shows source frame 0 for 61, the first-frame shortcut bug of seek/seek-gop/home-then-play30/ffmpeg; request 490 shows 243 for 244: the 2-frame backward lookback tolerance. Identical on Linux and Windows" -link = "" - [[xfail]] case = "editing/mapping/duration-covered" class = "coverage" diff --git a/crates/video-decode/src/ffmpeg.rs b/crates/video-decode/src/ffmpeg.rs index 75481f6292c..0a4408efec5 100644 --- a/crates/video-decode/src/ffmpeg.rs +++ b/crates/video-decode/src/ffmpeg.rs @@ -406,7 +406,12 @@ impl FFmpegDecoder { pub fn reset(&mut self, requested_time: f32) -> Result<(), ffmpeg::Error> { use ffmpeg::rescale; - let timestamp_us = (requested_time * 1_000_000.0) as i64; + // avformat rounds the target to the stream's tick, so a request just + // before a keyframe landed on that keyframe, past the request. The + // reader decodes forward to the request from wherever the seek lands. + const SEEK_MARGIN_US: i64 = 2_000; + let timestamp_us = + ((f64::from(requested_time) * 1_000_000.0) as i64 - SEEK_MARGIN_US).max(0); let position = rescale::Rescale::rescale(×tamp_us, (1, 1_000_000), rescale::TIME_BASE); self.decoder.flush();