Skip to main content

falcon_ffmpeg/
player.rs

1//! The FFmpeg player: two worker threads decode ahead, the owner on the
2//! UI thread takes frames when they are due.
3//!
4//! - The **demux** thread reads packets, decodes and resamples the sound
5//!   itself (it is cheap), stretches it through `atempo` at any speed but
6//!   normal, and queues video packets for the video thread.
7//!   It stops reading when both queues are full enough and performs seeks.
8//! - The **video** thread decodes and converts pictures to RGBA into a
9//!   short queue of frames.
10//! - The sound is pulled by an [`AudioOut`] the caller plays (falcon hands
11//!   it to rodio). What it takes moves the audio clock, which is the master
12//!   clock while sound plays; with no sound (no track, no output, muted, or
13//!   the track ended first) a wall clock is.
14//!
15//! Every seek bumps a generation number; work done for an older generation
16//! is thrown away, so a seek never shows a stale frame.
17
18use std::collections::VecDeque;
19use std::path::{Path, PathBuf};
20use std::sync::atomic::{AtomicU32, Ordering};
21use std::sync::{Arc, Condvar, Mutex, MutexGuard};
22use std::thread::JoinHandle;
23use std::time::Instant;
24
25use crate::decode::{Decoder, Input, OUT_CHANNELS, OUT_RATE, Packet, Resampler, Scaler};
26use crate::ffi;
27use crate::media;
28use crate::sync::{self, AudioClock, FrameQueue, WallClock};
29use crate::tempo::Stretch;
30use crate::{Error, Frame, Info, Result};
31
32/// Sound decoded ahead of the output, in media seconds.
33const AUDIO_AHEAD: f64 = 1.0;
34/// Video packets queued ahead of the video thread.
35const VIDEO_PACKETS: usize = 48;
36/// Bytes of video packets beyond which the demuxer waits whatever the sound needs.
37const MAX_PACKET_BYTES: usize = 64 << 20;
38/// Samples (both channels) the output takes at once: about 10 ms.
39const CHUNK: usize = 1024;
40
41enum VideoMsg {
42    Packet(Packet),
43    /// Start of a new generation: flush the decoder, drop frames before `target`.
44    Flush { generation: u64, target: f64 },
45    End,
46}
47
48struct State {
49    stop: bool,
50    generation: u64,
51    /// A seek waiting for the demux thread.
52    seek: Option<f64>,
53    rate: f64,
54    /// Whether sound is decoded and played (a track, an output, not muted).
55    audio_on: bool,
56    paused: bool,
57    packets: VecDeque<VideoMsg>,
58    packet_bytes: usize,
59    frames: FrameQueue<Frame>,
60    frame_cap: usize,
61    /// The video thread has drained the decoder for this generation.
62    video_done: bool,
63    /// The demuxer reached the end for this generation.
64    demux_done: bool,
65    /// Interleaved samples ready for the output, and the media time of the
66    /// first one.
67    samples: VecDeque<f32>,
68    samples_pts: f64,
69    /// The speed the queued samples were made for: each second of them
70    /// covers this many seconds of the clip.
71    samples_rate: f64,
72    clock: Option<AudioClock>,
73}
74
75impl State {
76    fn audio_ahead(&self) -> f64 {
77        (self.samples.len() / OUT_CHANNELS) as f64 * self.samples_rate / f64::from(OUT_RATE)
78    }
79}
80
81struct Shared {
82    state: Mutex<State>,
83    cv: Condvar,
84    /// The output volume, 0..=1, as `f32` bits.
85    volume: AtomicU32,
86    epoch: Instant,
87}
88
89impl Shared {
90    fn lock(&self) -> MutexGuard<'_, State> {
91        self.state.lock().unwrap_or_else(|p| p.into_inner())
92    }
93
94    fn wait<'a>(&self, g: MutexGuard<'a, State>) -> MutexGuard<'a, State> {
95        self.cv.wait(g).unwrap_or_else(|p| p.into_inner())
96    }
97
98    fn wall(&self) -> f64 {
99        self.epoch.elapsed().as_secs_f64()
100    }
101}
102
103/// The sound of a [`Player`]: interleaved stereo `f32` samples at
104/// [`OUT_RATE`], silence while paused or starved. It ends (returns `None`)
105/// once the player is dropped. Pull it from the audio output's thread.
106pub struct AudioOut {
107    shared: Arc<Shared>,
108    buf: Vec<f32>,
109    pos: usize,
110}
111
112impl AudioOut {
113    pub const CHANNELS: u16 = OUT_CHANNELS as u16;
114    pub const RATE: u32 = OUT_RATE;
115
116    /// The next sample.
117    pub fn next_sample(&mut self) -> Option<f32> {
118        if self.pos >= self.buf.len() {
119            self.refill()?;
120        }
121        let s = self.buf[self.pos];
122        self.pos += 1;
123        Some(s)
124    }
125
126    fn refill(&mut self) -> Option<()> {
127        let mut st = self.shared.lock();
128        if st.stop {
129            return None;
130        }
131        self.buf.clear();
132        self.pos = 0;
133        if st.paused || !st.audio_on || st.samples.is_empty() {
134            self.buf.resize(CHUNK, 0.0);
135            return Some(());
136        }
137        let n = st.samples.len().min(CHUNK) / OUT_CHANNELS * OUT_CHANNELS;
138        let volume = f32::from_bits(self.shared.volume.load(Ordering::Relaxed));
139        self.buf.extend(st.samples.drain(..n).map(|s| s * volume));
140        if self.buf.is_empty() {
141            self.buf.resize(CHUNK, 0.0);
142            return Some(());
143        }
144        let span = (n / OUT_CHANNELS) as f64 * st.samples_rate / f64::from(OUT_RATE);
145        st.clock = Some(AudioClock { pts: st.samples_pts, taken_at: self.shared.wall(), span, rate: st.samples_rate });
146        st.samples_pts += span;
147        drop(st);
148        self.shared.cv.notify_all();
149        Some(())
150    }
151}
152
153/// What the clocks need from the shared state.
154#[derive(Clone, Copy)]
155struct Snapshot {
156    /// Sound is playing (on, and not run out at the end).
157    audio: bool,
158    clock: Option<AudioClock>,
159}
160
161/// A playing file. Lives on the thread that draws it; dropping it stops
162/// the workers and ends its [`AudioOut`].
163pub struct Player {
164    shared: Arc<Shared>,
165    threads: Vec<JoinHandle<()>>,
166    path: PathBuf,
167    info: Info,
168    /// Whether the file has a sound track with a decoder and an output.
169    audio_out: bool,
170    /// Whether dropping it leaves the workers to end by themselves, instead
171    /// of waiting for the picture they are on.
172    detach: bool,
173    wall: WallClock,
174    /// Whether the audio clock was the master last time it was read.
175    on_audio: bool,
176    generation: u64,
177    playing: bool,
178    looping: bool,
179    rate: f32,
180    muted: bool,
181    /// Where the playhead is while paused or waiting for a seek.
182    pos: f64,
183    /// Frames to show regardless of the clock: one after a seek, `n` for a
184    /// step forward.
185    show_next: u32,
186    /// The seek being waited for.
187    seeking: Option<f64>,
188    ready: bool,
189}
190
191impl Player {
192    /// Opens `path` paused at the start. With `audio`, the file's sound is
193    /// decoded and the returned [`AudioOut`] must be played for the clock
194    /// to run; without, it plays silent on the wall clock.
195    pub fn open(path: &Path, audio: bool) -> Result<(Player, Option<AudioOut>)> {
196        Player::open_sized(path, audio, u32::MAX, false)
197    }
198
199    /// As [`Player::open`], for a small preview: pictures fit `max_side` on
200    /// their long side, and dropping it never waits for the workers, which
201    /// stop after the packet they are on. Opening reads and probes the
202    /// file, so it may take a moment; call it off the UI thread (a `Player`
203    /// and its [`AudioOut`] can be sent to the thread that draws).
204    pub fn open_preview(path: &Path, audio: bool, max_side: u32) -> Result<(Player, Option<AudioOut>)> {
205        Player::open_sized(path, audio, max_side, true)
206    }
207
208    fn open_sized(path: &Path, audio: bool, max_side: u32, detach: bool) -> Result<(Player, Option<AudioOut>)> {
209        let input = Input::open(path)?;
210        let api = input.api();
211        let (vi, vcodec) = input.best(ffi::AVMEDIA_TYPE_VIDEO).ok_or_else(|| Error("no video stream FFmpeg can decode".into()))?;
212        let video = Decoder::open(&input, vi, vcodec)?;
213        let info = media::info(&input, &video);
214        let sound = if audio { input.best(ffi::AVMEDIA_TYPE_AUDIO).and_then(|(ai, ac)| Decoder::open(&input, ai, ac).ok()) } else { None };
215        let audio_out = sound.is_some();
216        let shared = Arc::new(Shared {
217            state: Mutex::new(State {
218                stop: false,
219                generation: 0,
220                seek: None,
221                rate: 1.0,
222                audio_on: audio_out,
223                paused: true,
224                packets: VecDeque::new(),
225                packet_bytes: 0,
226                frames: FrameQueue::default(),
227                frame_cap: 4,
228                video_done: false,
229                demux_done: false,
230                samples: VecDeque::new(),
231                samples_pts: 0.0,
232                samples_rate: 1.0,
233                clock: None,
234            }),
235            cv: Condvar::new(),
236            volume: AtomicU32::new(1.0f32.to_bits()),
237            epoch: Instant::now(),
238        });
239        let fps = if info.fps > 0.0 { f64::from(info.fps) } else { 30.0 };
240        let packet = Packet::new(api)?;
241        let demux = {
242            let shared = shared.clone();
243            let resampler = Resampler::new(api);
244            std::thread::Builder::new()
245                .name("falcon-ffmpeg-demux".into())
246                .spawn(move || demux_loop(&shared, input, vi, sound, resampler, packet))
247                .map_err(|e| Error(e.to_string()))?
248        };
249        let decode = {
250            let shared = shared.clone();
251            let scaler = Scaler::new(api);
252            std::thread::Builder::new()
253                .name("falcon-ffmpeg-video".into())
254                .spawn(move || video_loop(&shared, video, scaler, fps, max_side))
255                .map_err(|e| Error(e.to_string()))?
256        };
257        let out = audio_out.then(|| AudioOut { shared: shared.clone(), buf: Vec::new(), pos: 0 });
258        let player = Player {
259            shared,
260            threads: vec![demux, decode],
261            path: path.to_path_buf(),
262            info,
263            audio_out,
264            detach,
265            wall: WallClock::new(0.0),
266            on_audio: false,
267            generation: 0,
268            playing: false,
269            looping: false,
270            rate: 1.0,
271            muted: false,
272            pos: 0.0,
273            show_next: 1,
274            seeking: None,
275            ready: false,
276        };
277        Ok((player, out))
278    }
279
280    pub fn info(&self) -> &Info {
281        &self.info
282    }
283
284    /// The first picture is decoded (or there is none to wait for).
285    pub fn is_ready(&self) -> bool {
286        self.ready
287    }
288
289    fn fps(&self) -> f64 {
290        if self.info.fps > 0.0 { f64::from(self.info.fps) } else { 30.0 }
291    }
292
293    /// Plays at the current speed; from the end, plays from the start.
294    pub fn play(&mut self) {
295        if self.at_end() {
296            self.seek(0.0);
297        }
298        self.playing = true;
299        let wall = self.shared.wall();
300        self.wall.set(self.pos, wall);
301        self.wall.start(wall);
302        self.shared.lock().paused = false;
303        self.shared.cv.notify_all();
304    }
305
306    pub fn pause(&mut self) {
307        if self.playing {
308            self.pos = self.current_time();
309        }
310        self.playing = false;
311        self.wall.pause(self.shared.wall());
312        self.shared.lock().paused = true;
313    }
314
315    pub fn toggle(&mut self) {
316        if self.playing { self.pause() } else { self.play() }
317    }
318
319    pub fn is_playing(&self) -> bool {
320        self.playing
321    }
322
323    /// Jumps to `secs` (clamped to the clip): the keyframe before it is
324    /// found and decoded forward to the frame at `secs`.
325    pub fn seek(&mut self, secs: f64) {
326        let t = secs.clamp(0.0, self.info.duration.max(0.0));
327        self.generation += 1;
328        {
329            let mut st = self.shared.lock();
330            st.generation = self.generation;
331            st.seek = Some(t);
332            st.frames.clear();
333            st.samples.clear();
334            st.clock = None;
335            st.video_done = false;
336            st.demux_done = false;
337        }
338        self.shared.cv.notify_all();
339        self.wall.set(t, self.shared.wall());
340        self.on_audio = false;
341        self.pos = t;
342        self.seeking = Some(t);
343        self.show_next = 1;
344    }
345
346    /// Moves `frames` frames forwards (or backwards when negative) and
347    /// pauses. Forward steps take the next decoded frames; backward steps
348    /// seek to the earlier frame.
349    pub fn step(&mut self, frames: i32) {
350        self.pause();
351        if frames > 0 {
352            self.show_next = self.show_next.max(frames.unsigned_abs());
353        } else if frames < 0 {
354            let t = sync::step_back(self.pos, frames, self.fps());
355            self.seek(t);
356        }
357    }
358
359    pub fn rate(&self) -> f32 {
360        self.rate
361    }
362
363    /// Changes the speed. The sound is stretched to match and keeps its
364    /// pitch, as `preservesPitch` does in Eagle's `<video>`, so the queued
365    /// sound is decoded again from here.
366    pub fn set_rate(&mut self, rate: f32) {
367        let rate = rate.clamp(0.1, 16.0);
368        if (rate - self.rate).abs() < 1e-4 {
369            return;
370        }
371        let now = self.current_time();
372        self.rate = rate;
373        self.wall.set_rate(f64::from(rate), self.shared.wall());
374        let audio = {
375            let mut st = self.shared.lock();
376            st.rate = f64::from(rate);
377            st.audio_on
378        };
379        if audio {
380            self.seek(now);
381        }
382    }
383
384    /// 0..=1.
385    pub fn set_volume(&mut self, volume: f32) {
386        self.shared.volume.store(volume.clamp(0.0, 1.0).to_bits(), Ordering::Relaxed);
387    }
388
389    /// Muting stops decoding the sound and runs the picture on the wall
390    /// clock; unmuting decodes it again from the playhead.
391    pub fn set_muted(&mut self, muted: bool) {
392        if muted == self.muted {
393            return;
394        }
395        let now = self.current_time();
396        self.muted = muted;
397        if !self.audio_out {
398            return;
399        }
400        {
401            let mut st = self.shared.lock();
402            st.audio_on = !muted;
403            st.samples.clear();
404            st.clock = None;
405        }
406        let wall = self.shared.wall();
407        self.wall.set(now, wall);
408        self.on_audio = false;
409        self.pos = now;
410        if !muted {
411            self.seek(now);
412        }
413    }
414
415    pub fn set_looping(&mut self, looping: bool) {
416        self.looping = looping;
417    }
418
419    pub fn looping(&self) -> bool {
420        self.looping
421    }
422
423    /// The playhead in seconds.
424    pub fn current_time(&self) -> f64 {
425        if let Some(t) = self.seeking {
426            return t;
427        }
428        if !self.playing {
429            return self.pos;
430        }
431        self.read_clock(self.snapshot()).clamp(0.0, self.info.duration.max(0.0))
432    }
433
434    fn snapshot(&self) -> Snapshot {
435        let st = self.shared.lock();
436        Snapshot { audio: st.audio_on && !(st.demux_done && st.samples.is_empty()), clock: st.clock }
437    }
438
439    /// The master clock's time: the audio clock while sound plays, else
440    /// the wall clock. Until [`Player::follow_clock`] hands over, a stopped
441    /// audio clock reads where it stopped.
442    fn read_clock(&self, s: Snapshot) -> f64 {
443        let wall = self.shared.wall();
444        if s.audio || self.on_audio {
445            return s.clock.map_or(self.pos, |c| c.now(wall));
446        }
447        self.wall.now(wall)
448    }
449
450    /// Notes which clock is the master; when the sound stops (it ended, or
451    /// was muted) the wall clock carries on from where the audio clock was.
452    fn follow_clock(&mut self, s: Snapshot) {
453        if self.on_audio && !s.audio {
454            let wall = self.shared.wall();
455            self.wall.set(s.clock.map_or(self.pos, |c| c.now(wall)), wall);
456        }
457        self.on_audio = s.audio;
458    }
459
460    fn at_end(&self) -> bool {
461        self.info.duration > 0.0 && self.pos >= self.info.duration - 1.0 / self.fps()
462    }
463
464    /// Call every frame: at the end of the clip, starts again from the top
465    /// when looping, or stops.
466    pub fn tick(&mut self) {
467        if !self.playing || self.seeking.is_some() || self.show_next > 0 {
468            return;
469        }
470        let ended = {
471            let st = self.shared.lock();
472            st.video_done && st.frames.is_empty() && !(st.audio_on && !st.samples.is_empty())
473        };
474        if !ended {
475            return;
476        }
477        if self.looping {
478            self.seek(0.0);
479        } else {
480            self.pause();
481            self.pos = self.info.duration;
482        }
483    }
484
485    /// The frame due now, if it differs from the last one handed out.
486    pub fn frame(&mut self) -> Option<Frame> {
487        let shared = self.shared.clone();
488        let mut st = shared.lock();
489        if self.show_next > 0 {
490            let mut shown = None;
491            while self.show_next > 0 {
492                let Some(f) = st.frames.pop() else { break };
493                self.show_next -= 1;
494                shown = Some(f);
495            }
496            let done = st.video_done && st.frames.is_empty();
497            drop(st);
498            self.shared.cv.notify_all();
499            if let Some((pts, f)) = shown {
500                self.ready = true;
501                if self.seeking.take().is_some() || !self.playing {
502                    self.pos = pts.max(0.0);
503                }
504                let wall = self.shared.wall();
505                self.wall.set(self.pos, wall);
506                return Some(f);
507            }
508            if done {
509                // Nothing left to show (a step past the end).
510                self.ready = true;
511                self.show_next = 0;
512                self.seeking = None;
513            }
514            return None;
515        }
516        if !self.playing {
517            return None;
518        }
519        let snapshot = Snapshot { audio: st.audio_on && !(st.demux_done && st.samples.is_empty()), clock: st.clock };
520        self.follow_clock(snapshot);
521        let now = self.read_clock(snapshot);
522        let due = st.frames.pop_due(now);
523        drop(st);
524        let (_, f) = due?;
525        self.shared.cv.notify_all();
526        Some(f)
527    }
528
529    /// The picture at the playhead at the clip's full size, decoded afresh
530    /// so playback is not disturbed.
531    pub fn current_frame(&self) -> Result<Frame> {
532        let t = self.current_time();
533        media::frame_at(&self.path, t)
534    }
535}
536
537impl Drop for Player {
538    fn drop(&mut self) {
539        self.shared.lock().stop = true;
540        self.shared.cv.notify_all();
541        if self.detach {
542            return;
543        }
544        for t in self.threads.drain(..) {
545            let _ = t.join();
546        }
547    }
548}
549
550/// Reads packets, decodes sound, queues video packets and carries out seeks.
551fn demux_loop(shared: &Shared, mut input: Input, vi: i32, mut sound: Option<Decoder>, mut resampler: Resampler, mut packet: Packet) {
552    let api = input.api();
553    let mut stretch = Stretch::new(api);
554    let mut target = f64::NEG_INFINITY;
555    loop {
556        let mut st = shared.lock();
557        let (generation, rate, audio_on) = loop {
558            if st.stop {
559                return;
560            }
561            if let Some(t) = st.seek.take() {
562                let generation = st.generation;
563                drop(st);
564                let _ = input.seek(t);
565                if let Some(s) = sound.as_mut() {
566                    s.flush();
567                }
568                resampler.reset();
569                stretch.reset();
570                target = t;
571                st = shared.lock();
572                if st.generation == generation {
573                    st.packets.clear();
574                    st.packet_bytes = 0;
575                    st.packets.push_back(VideoMsg::Flush { generation, target: t });
576                    st.samples.clear();
577                    st.demux_done = false;
578                    shared.cv.notify_all();
579                }
580                continue;
581            }
582            let need_audio = st.audio_on && sound.is_some() && st.audio_ahead() < AUDIO_AHEAD;
583            let need_video = st.packets.len() < VIDEO_PACKETS;
584            if !st.demux_done && (need_audio || need_video) && st.packet_bytes < MAX_PACKET_BYTES {
585                break (st.generation, st.rate, st.audio_on);
586            }
587            st = shared.wait(st);
588        };
589        drop(st);
590        packet.unref();
591        let more = input.read(&mut packet).unwrap_or(false);
592        if more && packet.stream_index() == vi {
593            let Ok(next) = Packet::new(api) else { return };
594            let p = std::mem::replace(&mut packet, next);
595            let mut st = shared.lock();
596            if st.generation == generation {
597                st.packet_bytes += p.size();
598                st.packets.push_back(VideoMsg::Packet(p));
599                drop(st);
600                shared.cv.notify_all();
601            }
602            continue;
603        }
604        let Some(s) = sound.as_mut() else {
605            if !more {
606                end(shared, generation);
607            }
608            continue;
609        };
610        if more && (packet.stream_index() != s.info.index || !audio_on) {
611            continue;
612        }
613        let _ = s.send(more.then_some(&packet));
614        let mut out = Vec::new();
615        let mut first_pts = None;
616        let mut tempo = stretch.at(rate);
617        // With the filter changing the speed, the resampler leaves it alone.
618        let resampled_for = if tempo.is_some() { 1.0 } else { rate };
619        while s.receive() {
620            let f = s.frame();
621            let pts = s.pts(f);
622            let Ok(mut samples) = resampler.convert(f, resampled_for) else { continue };
623            let t = pts.unwrap_or(f64::NAN);
624            let mut start = t.is_finite().then_some(t);
625            if t.is_finite() && t < target {
626                // Drop the part before a seek's target.
627                let skip = (((target - t) * f64::from(OUT_RATE) / resampled_for) as usize * OUT_CHANNELS).min(samples.len());
628                samples.drain(..skip);
629                if samples.is_empty() {
630                    continue;
631                }
632                start = Some(target);
633            }
634            if let Some(tempo) = tempo.as_deref_mut() {
635                let _ = tempo.push(&samples, start);
636                start = tempo.pull(&mut out);
637            } else {
638                out.extend_from_slice(&samples);
639            }
640            if let Some(start) = start {
641                first_pts.get_or_insert(start);
642            }
643        }
644        if !more && let Some(tempo) = tempo {
645            let _ = tempo.finish();
646            if let Some(start) = tempo.pull(&mut out) {
647                first_pts.get_or_insert(start);
648            }
649        }
650        let mut st = shared.lock();
651        if st.generation == generation && st.audio_on && !out.is_empty() {
652            if st.samples.is_empty() {
653                st.samples_pts = first_pts.unwrap_or(st.samples_pts).max(0.0);
654                st.samples_rate = rate;
655            }
656            st.samples.extend(out);
657        }
658        drop(st);
659        if !more {
660            end(shared, generation);
661        }
662    }
663}
664
665/// Marks the end of the file for `generation`.
666fn end(shared: &Shared, generation: u64) {
667    let mut st = shared.lock();
668    if st.generation == generation {
669        st.packets.push_back(VideoMsg::End);
670        st.demux_done = true;
671    }
672    drop(st);
673    shared.cv.notify_all();
674}
675
676/// Decodes queued video packets into RGBA frames.
677fn video_loop(shared: &Shared, mut video: Decoder, mut scaler: Scaler, fps: f64, max_side: u32) {
678    let mut generation = 0;
679    let mut target = f64::NEG_INFINITY;
680    let mut delivered = false;
681    let mut last_pts = 0.0;
682    loop {
683        let msg = {
684            let mut st = shared.lock();
685            loop {
686                if st.stop {
687                    return;
688                }
689                let control = matches!(st.packets.front(), Some(VideoMsg::Flush { .. } | VideoMsg::End));
690                if st.packets.front().is_some() && (control || st.frames.len() < st.frame_cap) {
691                    break;
692                }
693                st = shared.wait(st);
694            }
695            let msg = st.packets.pop_front();
696            if let Some(VideoMsg::Packet(p)) = &msg {
697                st.packet_bytes = st.packet_bytes.saturating_sub(p.size());
698            }
699            drop(st);
700            shared.cv.notify_all();
701            msg
702        };
703        let Some(msg) = msg else { continue };
704        let ended = match msg {
705            VideoMsg::Flush { generation: g, target: t } => {
706                video.flush();
707                generation = g;
708                target = t;
709                delivered = false;
710                continue;
711            }
712            VideoMsg::Packet(p) => {
713                let _ = video.send(Some(&p));
714                false
715            }
716            VideoMsg::End => {
717                let _ = video.send(None);
718                true
719            }
720        };
721        while video.receive() {
722            let f = video.frame();
723            let pts = video.pts(f).unwrap_or(last_pts + 1.0 / fps);
724            last_pts = pts;
725            if sync::before_target(pts, target, fps) {
726                video.hold();
727                continue;
728            }
729            let Some(frame) = scaler.convert(f, &video.info, max_side) else { continue };
730            delivered = true;
731            push(shared, generation, pts, frame);
732        }
733        if ended {
734            // A seek past the last frame shows the last frame.
735            if !delivered
736                && let Some(f) = video.held()
737                && let Some(frame) = scaler.convert(f, &video.info, max_side)
738            {
739                let pts = video.pts(f).unwrap_or(target);
740                push(shared, generation, pts, frame);
741            }
742            let mut st = shared.lock();
743            if st.generation == generation {
744                st.video_done = true;
745            }
746            drop(st);
747            shared.cv.notify_all();
748        }
749    }
750}
751
752fn push(shared: &Shared, generation: u64, pts: f64, frame: Frame) {
753    let mut st = shared.lock();
754    if st.generation == generation {
755        st.frame_cap = sync::queue_cap(frame.rgba.len());
756        st.frames.push(pts, frame);
757    }
758    drop(st);
759    shared.cv.notify_all();
760}