Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/rendering/src/decoder/avassetreader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ impl AVAssetReaderDecoder {
max_fallback_distance,
sender,
) => {
let frame = (requested_time * fps as f32).floor() as u32;
let frame = super::request_frame(requested_time, fps);
if !sender.is_closed() {
pending_requests.push(PendingRequest {
frame,
Expand All @@ -797,7 +797,7 @@ impl AVAssetReaderDecoder {
max_fallback_distance,
sender,
) => {
let frame = (requested_time * fps as f32).floor() as u32;
let frame = super::request_frame(requested_time, fps);
if !sender.is_closed() {
pending_requests.push(PendingRequest {
frame,
Expand Down
4 changes: 2 additions & 2 deletions crates/rendering/src/decoder/ffmpeg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ impl FfmpegDecoder {
}

let requested_time = requested_time.max(0.0);
let requested_frame = (requested_time * fps as f32).floor() as u32;
let requested_frame = super::request_frame(requested_time, fps);

pending_requests.push(PendingRequest {
time: requested_time,
Expand Down Expand Up @@ -1261,7 +1261,7 @@ impl FfmpegDecoder {
}

let requested_time = requested_time.max(0.0);
let requested_frame = (requested_time * fps as f32).floor() as u32;
let requested_frame = super::request_frame(requested_time, fps);

pending_requests.push(PendingRequest {
time: requested_time,
Expand Down
2 changes: 1 addition & 1 deletion crates/rendering/src/decoder/media_foundation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ impl MFDecoder {
if sender.is_closed() {
return;
}
let frame = (requested_time * fps as f32).floor() as u32;
let frame = super::request_frame(requested_time, fps);
pending_requests.push(PendingRequest {
frame,
time: requested_time,
Expand Down
51 changes: 50 additions & 1 deletion crates/rendering/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,27 @@ pub(crate) fn probe_yuv_matrix(path: &std::path::Path) -> YuvMatrix {
.unwrap_or_default()
}

/// f32 steps of slack in [`request_frame`]. The time of frame `n` is the
/// f32 nearest `n / fps`, up to half a step below it (126 / 30 s is
/// 4.1999998 s), plus a step or two from the offsets added on the way; a
/// plain floor then asks for the frame before.
const REQUEST_SLACK_STEPS: f32 = 4.0;
/// Most slack, in frames. Past two hours at 240 fps four f32 steps near a
/// whole frame while half a step is still under a tenth of one, so the cap
/// keeps an aligned request off the next frame.
const REQUEST_SLACK_MAX_FRAMES: f64 = 0.25;

/// The decoder grid index a request for `time` asks for: the frame on screen
/// at `time`, `floor(time * fps)` once `time` is lifted by
/// [`REQUEST_SLACK_STEPS`] f32 steps, at most [`REQUEST_SLACK_MAX_FRAMES`].
pub fn request_frame(time: f32, fps: u32) -> u32 {
let magnitude = time.abs();
let step = f32::from_bits(magnitude.to_bits() + 1) - magnitude;
let fps = f64::from(fps);
let slack = (f64::from(step * REQUEST_SLACK_STEPS) * fps).min(REQUEST_SLACK_MAX_FRAMES);
(f64::from(time) * fps + slack).floor() as u32
}

pub fn pts_to_frame(pts: i64, time_base: Rational, fps: u32) -> u32 {
(fps as f64 * ((pts as f64 * time_base.numerator() as f64) / (time_base.denominator() as f64)))
.round() as u32
Expand Down Expand Up @@ -1906,6 +1927,34 @@ mod tests {
assert!(frame.with_image_buf(|_| ()).is_none());
}

#[test]
fn request_frame_maps_every_frame_time_back_to_its_frame() {
// Frame times as the renderer sends them: n / fps in an f32, for
// three hours. A plain f32 floor maps 126 / 30 s (4.1999998) to frame
// 125; uncapped slack maps frame 1,966,082 at 240 fps (8192.0088 s)
// to the next.
for fps in [24, 25, 30, 50, 60, 120, 144, 240] {
let frames = 3 * 3600 * fps;
for n in 0..frames {
let time = (f64::from(n) / f64::from(fps)) as f32;
assert_eq!(request_frame(time, fps), n, "{n} at {fps} fps");
}
}
}

#[test]
fn request_frame_keeps_times_inside_a_frame_on_that_frame() {
for (time, fps, frame) in [
(0.0, 30, 0),
(0.031, 30, 0),
(0.034, 30, 1),
(4.22, 30, 126),
] {
assert_eq!(request_frame(time, fps), frame, "{time} at {fps} fps");
}
assert_eq!(request_frame(-0.5, 30), 0);
}

#[test]
fn pts_to_frame_maps_real_presentation_time_to_index() {
let tb = Rational::new(1, 600);
Expand Down Expand Up @@ -2689,7 +2738,7 @@ mod managed_worker_tests {
let pts = packet.pts().unwrap();
let frame = pts_to_frame(pts.checked_sub(start_time).unwrap(), time_base, fps);
let requested_time = ((f64::from(frame) + 0.25) / f64::from(fps)) as f32;
assert_eq!((requested_time * fps as f32).floor() as u32, frame);
assert_eq!(request_frame(requested_time, fps), frame);
println!(
"{}",
serde_json::json!({
Expand Down
4 changes: 2 additions & 2 deletions crates/rendering/src/decoder/model/expectations.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ link = ""
[[xfail]]
case = "decoder-model/ffmpeg/cfr/keyframe-seeks/playback"
class = "grid-rounding"
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"
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 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 = ""

Expand Down Expand Up @@ -102,7 +102,7 @@ link = ""
[[xfail]]
case = "decoder-model/ffmpeg/vfr/keyframe-seeks/playback"
class = "grid-rounding"
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"
seeds = "2 4-5 9 11 13 18 23 27 29 31-32 36 41 48 50 59 62-63 70-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 = ""

Expand Down
5 changes: 3 additions & 2 deletions crates/rendering/src/decoder/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
//! Reference rule (the decoder contract documented next to `pts_to_frame`):
//! a sample is labelled `round(fps * pts_seconds)` on the decoder grid, where
//! FFmpeg measures pts from the stream start time and AVAssetReader from
//! zero. A request for time `t` asks for grid index `floor(t * fps)`. The
//! zero. A request for time `t` asks for grid index `request_frame(t, fps)`,
//! `floor(t * fps)` with a few f32 steps of slack for the request time. The
//! correct answer is the newest sample whose label is at or before that index
//! (the frame on screen at `t`); when several samples share that label any of
//! them is accepted. A request before the first sample is answered with the
Expand Down Expand Up @@ -221,7 +222,7 @@ impl Oracle {
Decoder::Avf => time,
Decoder::Ffmpeg => time.max(0.0),
};
(time * self.fps as f32).floor() as u32
super::request_frame(time, self.fps)
}

/// Inclusive range of acceptable sample indices for a request.
Expand Down
2 changes: 1 addition & 1 deletion crates/rendering/src/decoder/multi_position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ impl DecoderPoolManager {
) -> (usize, f32, bool) {
self.total_accesses += 1;

let frame = (requested_time * self.config.fps as f32).floor() as u32;
let frame = super::request_frame(requested_time, self.config.fps);
*self.access_history.entry(frame).or_insert(0) += 1;

if self.access_history.len() > Self::MAX_ACCESS_HISTORY_ENTRIES {
Expand Down
Loading