Skip to main content

vtome/
video_source.rs

1//! Video sources — input files with demuxing, decoding, and frame queuing.
2//!
3//! A source turns a file into pictures in display order. *When* each one is
4//! shown is not its business: that is the timeline the compositor gives every
5//! film (see [`crate::clock`]).
6
7use std::collections::VecDeque;
8use std::path::Path;
9use std::time::Duration;
10
11use crate::demux::Demuxer;
12use crate::decode::Decoder;
13use crate::error::{Error, Result};
14use crate::frame::Frame;
15use crate::plugin::Plugin;
16
17/// How many decoded pictures a source keeps ready.
18const QUEUE: usize = 30;
19
20/// A video source: an input file with its demuxer, decoder, and frame queue.
21///
22/// VideoSource manages:
23/// - Opening and parsing a media file
24/// - Extracting the video track
25/// - Decoding frames ahead into a bounded queue
26/// - Seeking to an exact frame: the keyframe before it, then decoding forward
27///   and discarding what comes first
28/// - Serving ranges of frames from a [`FrameCache`] instead of the decoder
29/// - Applying plugins to frames
30pub struct VideoSource {
31    /// Persistent, unique identifier for this source (used in layers and compositor).
32    pub persistent_id: String,
33
34    /// The demuxer (format + container handling).
35    demuxer: Box<dyn Demuxer>,
36
37    /// The decoder (codec handling).
38    decoder: Box<dyn Decoder>,
39
40    /// Track ID for the video stream.
41    video_track_id: u32,
42
43    /// How long one frame lasts, where the file says.
44    frame_duration: Option<Duration>,
45
46    /// Bounded frame queue, in display order.
47    frame_queue: VecDeque<Frame>,
48
49    /// Plugins to apply to each frame (in order).
50    plugins: Vec<Box<dyn Plugin>>,
51
52    /// Whether EOF has been reached.
53    eof_reached: bool,
54
55    /// After a seek: pictures due before this are decoded only to be thrown
56    /// away, because decoding has to start at a keyframe.
57    discard_before: Option<Duration>,
58
59    /// The first picture's timestamp — frame zero. Not always zero: an MP4
60    /// with B-frames starts its first picture a frame or two in.
61    origin: Option<Duration>,
62
63    /// Frames to serve from memory, with where the source is in them.
64    cache: Option<Cached>,
65}
66
67/// A [`FrameCache`] in use.
68struct Cached {
69    cache: FrameCache,
70    /// The next frame, by number, that the queue needs.
71    next: u64,
72    /// Whether the decoder is positioned to produce `next`. False after
73    /// serving frames from memory: the decoder is still wherever it stopped.
74    decoder_in_step: bool,
75}
76
77impl VideoSource {
78    /// Opens a file: its container, its video track, and a decoder for it.
79    ///
80    /// # Errors
81    ///
82    /// Whatever opening the container refuses; [`Error::Unsupported`] for a
83    /// file with no video; [`Error::NoDecoder`] where nothing here decodes it.
84    #[cfg(feature = "demux")]
85    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
86        let path = path.as_ref();
87        Self::from_demuxer(crate::open_media(path)?, path)
88    }
89
90    /// [`from_file`](Self::from_file) on media read from `reader` — an entry
91    /// of an archive, bytes in memory. `name` is what errors call it, and
92    /// what the source's id is made from.
93    ///
94    /// # Errors
95    ///
96    /// As [`from_file`](Self::from_file).
97    #[cfg(feature = "demux")]
98    pub fn from_reader(reader: Box<dyn crate::ReadSeek>, name: &str) -> Result<Self> {
99        Self::from_demuxer(crate::open_media_reader(reader, name)?, Path::new(name))
100    }
101
102    #[cfg(feature = "demux")]
103    fn from_demuxer(demuxer: Box<dyn crate::Demuxer>, path: &Path) -> Result<Self> {
104        // Find video track
105        let media_info = demuxer.info();
106        let video_track = media_info
107            .video()
108            .ok_or_else(|| crate::Error::Unsupported {
109                what: format!("{}: no video track in it", path.display()),
110            })?;
111        let video_track_id = video_track.id;
112        let frame_duration = video_track
113            .frame_rate
114            .map(|rate| rate.frame_duration())
115            .filter(|duration| !duration.is_zero());
116
117        // No decoder is an error, not a source that quietly produces no
118        // frames: a black window with no message is the failure this crate
119        // exists to avoid, and `open` already says which feature or platform
120        // would have handled the file.
121        let config = crate::decode::DecoderConfig::from_track(video_track)?;
122        let decoder = crate::decode::open(&config)?;
123
124        let persistent_id = format!("{:x}", {
125            use std::collections::hash_map::DefaultHasher;
126            use std::hash::{Hash, Hasher};
127            let mut hasher = DefaultHasher::new();
128            path.to_string_lossy().hash(&mut hasher);
129            hasher.finish()
130        });
131
132        Ok(VideoSource {
133            persistent_id,
134            demuxer,
135            decoder,
136            video_track_id,
137            frame_duration,
138            frame_queue: VecDeque::with_capacity(QUEUE),
139            plugins: Vec::new(),
140            eof_reached: false,
141            discard_before: None,
142            origin: None,
143            cache: None,
144        })
145    }
146
147    /// Add a plugin to this source (applied in order).
148    pub fn add_plugin(&mut self, plugin: Box<dyn Plugin>) {
149        self.plugins.push(plugin);
150    }
151
152    /// Get the persistent ID (for use in layers).
153    pub fn persistent_id(&self) -> &str {
154        &self.persistent_id
155    }
156
157    /// What the file contains: its tracks, duration, and container.
158    pub fn info(&self) -> &crate::media::MediaInfo {
159        self.demuxer.info()
160    }
161
162    /// The decoder turning this source's packets into frames — worth asking
163    /// whether it [is hardware](Decoder::is_hardware).
164    pub fn decoder(&self) -> &dyn Decoder {
165        self.decoder.as_ref()
166    }
167
168    /// How long one frame lasts, where the file states a frame rate.
169    pub fn frame_duration(&self) -> Option<Duration> {
170        self.frame_duration
171    }
172
173    /// Serves `cache`'s ranges from memory from now on, decoding only outside
174    /// them, and goes back to the start.
175    ///
176    /// # Errors
177    ///
178    /// [`Error::Unsupported`] for a file that states no frame rate — a cache
179    /// is addressed by frame number, which needs one — and whatever rewinding
180    /// refuses.
181    pub fn set_cache(&mut self, cache: FrameCache) -> Result<()> {
182        if self.frame_duration.is_none() {
183            return Err(Error::unsupported(
184                "this file states no frame rate, and a frame cache is addressed by frame number",
185            ));
186        }
187
188        self.origin()?;
189
190        self.cache = Some(Cached {
191            cache,
192            next: 0,
193            decoder_in_step: false,
194        });
195
196        self.rewind()
197    }
198
199    /// Advance playback by decoding frames and filling the queue.
200    ///
201    /// Pulls packets from the demuxer, decodes them, applies plugins, and
202    /// queues frames (up to 30) — or takes them from the cache, where one
203    /// covers them. When EOF is reached, flushes remaining frames from the
204    /// decoder.
205    pub fn advance(&mut self, _delta: Duration) -> Result<()> {
206        while self.frame_queue.len() < QUEUE && !self.eof_reached {
207            if self.serve_from_cache()? {
208                continue;
209            }
210
211            // Get next packet from demuxer
212            match self.demuxer.next_packet()? {
213                Some(packet) => {
214                    // Only process packets from the video track
215                    if packet.track_id == self.video_track_id {
216                        if let Some(frame) = self.decoder.decode(&packet)? {
217                            self.take(frame)?;
218                        }
219                    }
220                }
221                None => {
222                    // EOF reached, flush remaining frames from decoder
223                    for frame in self.decoder.flush()? {
224                        self.take(frame)?;
225                    }
226
227                    // The end of the file is the end of the film only if the
228                    // decoder got there in step. If the flush ran into a
229                    // cached range, that range is served next and decoding
230                    // picks up after it; and a cache can hold the last frames
231                    // outright.
232                    let in_step = self
233                        .cache
234                        .as_ref()
235                        .is_none_or(|cached| cached.decoder_in_step);
236
237                    self.eof_reached = in_step && !self.cache_has_next();
238                }
239            }
240        }
241
242        Ok(())
243    }
244
245    /// The first picture's timestamp: where frame zero is, and what frame
246    /// numbers count from.
247    ///
248    /// Usually zero, but not always — an MP4 with B-frames shows its first
249    /// picture a frame or two in. Found by decoding the start of the film the
250    /// first time it is asked, and remembered; the source is left at the
251    /// start.
252    ///
253    /// # Errors
254    ///
255    /// Whatever seeking or decoding refuses.
256    pub fn origin(&mut self) -> Result<Duration> {
257        if let Some(origin) = self.origin {
258            return Ok(origin);
259        }
260
261        self.frame_queue.clear();
262        self.reposition(Duration::ZERO)?;
263
264        // The decoder hands pictures out in display order, so the first one
265        // out is the earliest.
266        let origin = loop {
267            match self.demuxer.next_packet()? {
268                Some(packet) if packet.track_id == self.video_track_id => {
269                    if let Some(frame) = self.decoder.decode(&packet)? {
270                        break frame.pts();
271                    }
272                }
273                Some(_) => {}
274                None => {
275                    break self
276                        .decoder
277                        .flush()?
278                        .first()
279                        .map_or(Duration::ZERO, Frame::pts)
280                }
281            }
282        };
283
284        self.origin = Some(origin);
285        self.reposition(Duration::ZERO)?;
286
287        Ok(origin)
288    }
289
290    /// Moves to `position` — a timestamp in the file's own terms, as frames
291    /// carry them: the demuxer to the keyframe at or before it, the decoder
292    /// and queue emptied, and every picture due before `position` decoded
293    /// only to be thrown away — so the next frame out is the one due there,
294    /// not the keyframe a second or two earlier. Frame `n` is at
295    /// [`origin`](VideoSource::origin) plus `n` frame durations.
296    ///
297    /// # Errors
298    ///
299    /// Whatever the demuxer's seek or the decoder's reset refuses.
300    pub fn seek(&mut self, position: Duration) -> Result<()> {
301        self.frame_queue.clear();
302        self.reposition(position)?;
303
304        if let (Some(cached), Some(duration)) = (self.cache.as_mut(), self.frame_duration) {
305            let origin = self.origin.unwrap_or_default();
306            cached.next = frame_number(position.saturating_sub(origin), duration);
307            cached.decoder_in_step = true;
308        }
309
310        Ok(())
311    }
312
313    /// The decoder to `position`, leaving the queue alone: what moving from a
314    /// cached range back to decoding needs, with the cached frames still
315    /// waiting to be shown.
316    fn reposition(&mut self, position: Duration) -> Result<()> {
317        self.demuxer.seek(position)?;
318        self.decoder.reset()?;
319        self.eof_reached = false;
320        // Half a frame of slack: a picture due a hair before the target, by
321        // rounding, is the target.
322        let slack = self.frame_duration.unwrap_or_default() / 2;
323        self.discard_before = Some(position.saturating_sub(slack)).filter(|at| !at.is_zero());
324
325        Ok(())
326    }
327
328    /// Back to the start: the demuxer to the first keyframe, the decoder and
329    /// the queue emptied. What looping a film needs, without reopening it.
330    pub fn rewind(&mut self) -> Result<()> {
331        self.seek(Duration::ZERO)
332    }
333
334    /// Get the next frame if available (without removing it from the queue).
335    pub fn current_frame(&self) -> Option<&Frame> {
336        self.frame_queue.front()
337    }
338
339    /// Pop the next frame from the queue (for rendering).
340    pub fn pop_frame(&mut self) -> Option<Frame> {
341        self.frame_queue.pop_front()
342    }
343
344    /// Number of frames in the queue.
345    pub fn queued_frames(&self) -> usize {
346        self.frame_queue.len()
347    }
348
349    /// Whether EOF has been reached and all frames consumed.
350    pub fn is_finished(&self) -> bool {
351        self.eof_reached && self.frame_queue.is_empty()
352    }
353
354    /// One decoded picture: dropped if a seek or the cache says so, otherwise
355    /// through the plugins and into the queue.
356    fn take(&mut self, frame: Frame) -> Result<()> {
357        if let Some(before) = self.discard_before {
358            if frame.pts() < before {
359                return Ok(());
360            }
361
362            self.discard_before = None;
363        }
364
365        if let (Some(cached), Some(duration)) = (self.cache.as_mut(), self.frame_duration) {
366            let origin = self.origin.unwrap_or_default();
367            let number = frame_number(frame.pts().saturating_sub(origin), duration);
368
369            // Already served, from memory or before a seek.
370            if number < cached.next {
371                return Ok(());
372            }
373
374            // Decoding has run into a cached range: memory takes over, and
375            // the decoder will have to be put back in step after it.
376            if cached.cache.get(number).is_some() {
377                cached.decoder_in_step = false;
378                return Ok(());
379            }
380
381            cached.next = number + 1;
382        }
383
384        let mut frame = frame;
385        for plugin in &mut self.plugins {
386            frame = plugin.process(frame)?;
387        }
388
389        self.frame_queue.push_back(frame);
390        Ok(())
391    }
392
393    /// Queues the next frame from memory if the cache has it. Where it does
394    /// not, puts the decoder in step for that frame if it has wandered, and
395    /// returns false so the caller decodes.
396    fn serve_from_cache(&mut self) -> Result<bool> {
397        let (Some(cached), Some(duration)) = (self.cache.as_mut(), self.frame_duration) else {
398            return Ok(false);
399        };
400        let origin = self.origin.unwrap_or_default();
401
402        if let Some(frame) = cached.cache.get(cached.next) {
403            // Retimed to where it belongs in the stream, whatever it was
404            // stamped with when it was made.
405            let frame = frame.with_pts(origin + frame_time(cached.next, duration));
406            cached.next += 1;
407            cached.decoder_in_step = false;
408
409            let mut frame = frame;
410            for plugin in &mut self.plugins {
411                frame = plugin.process(frame)?;
412            }
413
414            self.frame_queue.push_back(frame);
415            return Ok(true);
416        }
417
418        if !cached.decoder_in_step {
419            cached.decoder_in_step = true;
420            let position = origin + frame_time(cached.next, duration);
421
422            self.reposition(position)?;
423        }
424
425        Ok(false)
426    }
427
428    /// Whether the cache holds the next frame the queue needs.
429    fn cache_has_next(&self) -> bool {
430        self.cache
431            .as_ref()
432            .is_some_and(|cached| cached.cache.get(cached.next).is_some())
433    }
434}
435
436/// The frame due at `position`, counting from zero at the frame rate.
437fn frame_number(position: Duration, frame_duration: Duration) -> u64 {
438    let frame = frame_duration.as_nanos().max(1);
439    // Rounded, not floored: a timestamp a nanosecond short of a frame
440    // boundary, from a timescale that does not divide evenly, is that frame.
441    ((position.as_nanos() + frame / 2) / frame) as u64
442}
443
444/// When frame `number` is due, counting from zero at the frame rate.
445fn frame_time(number: u64, frame_duration: Duration) -> Duration {
446    let nanos = u128::from(number) * frame_duration.as_nanos();
447    Duration::from_nanos(nanos.min(u128::from(u64::MAX)) as u64)
448}
449
450/// Decoded frames held in memory for ranges of a film, so those ranges play
451/// without the decoder — an instant start, a loop that never waits on a
452/// keyframe.
453///
454/// Ranges are frame numbers, `start` included and `end` not, counted from the
455/// first frame at the file's frame rate. The frames are the ranges' frames,
456/// in order, end to end.
457#[derive(Clone, Debug)]
458pub struct FrameCache {
459    ranges: Vec<(u64, u64)>,
460    /// Where each range's first frame sits in `frames`.
461    offsets: Vec<usize>,
462    frames: Vec<Frame>,
463}
464
465impl FrameCache {
466    /// A cache of `frames` for `ranges`.
467    ///
468    /// # Errors
469    ///
470    /// [`Error::Unsupported`] for an empty or backwards range, ranges that
471    /// overlap, or a number of frames that is not what the ranges add up to —
472    /// each of which would serve the wrong picture rather than fail later.
473    pub fn new(mut ranges: Vec<(u64, u64)>, frames: Vec<Frame>) -> Result<Self> {
474        ranges.sort_unstable();
475
476        let mut offsets = Vec::with_capacity(ranges.len());
477        let mut total = 0_usize;
478        let mut previous_end = 0_u64;
479
480        for (index, &(start, end)) in ranges.iter().enumerate() {
481            if start >= end {
482                return Err(Error::unsupported(format!(
483                    "frame range {start}..{end} holds no frames"
484                )));
485            }
486
487            if index > 0 && start < previous_end {
488                return Err(Error::unsupported(format!(
489                    "frame range {start}..{end} overlaps the one before it, which ends at {previous_end}"
490                )));
491            }
492
493            offsets.push(total);
494            total += usize::try_from(end - start).map_err(|_| {
495                Error::unsupported(format!("frame range {start}..{end} is too long to hold"))
496            })?;
497            previous_end = end;
498        }
499
500        if total != frames.len() {
501            return Err(Error::unsupported(format!(
502                "the ranges cover {total} frames and {} were given",
503                frames.len()
504            )));
505        }
506
507        Ok(FrameCache {
508            ranges,
509            offsets,
510            frames,
511        })
512    }
513
514    /// The frame numbered `number`, if a range covers it.
515    pub fn get(&self, number: u64) -> Option<&Frame> {
516        let index = self
517            .ranges
518            .partition_point(|&(start, _)| start <= number)
519            .checked_sub(1)?;
520        let (start, end) = self.ranges[index];
521
522        if number >= end {
523            return None;
524        }
525
526        self.frames
527            .get(self.offsets[index] + (number - start) as usize)
528    }
529
530    /// The ranges, sorted.
531    pub fn ranges(&self) -> &[(u64, u64)] {
532        &self.ranges
533    }
534
535    /// How many frames it holds.
536    pub fn len(&self) -> usize {
537        self.frames.len()
538    }
539
540    /// Whether it holds none.
541    pub fn is_empty(&self) -> bool {
542        self.frames.is_empty()
543    }
544
545    /// The memory its pictures take, for holding a show to a budget.
546    pub fn byte_len(&self) -> usize {
547        self.frames.iter().map(Frame::byte_len).sum()
548    }
549}
550
551/// Decodes `ranges` of the film at `path` into the frames a [`FrameCache`]
552/// for those ranges wants — what [`Controls::add_cached`](crate::Controls)
553/// takes.
554///
555/// Each range costs a seek to the keyframe before it and decoding forward;
556/// files `import` writes have a keyframe every two seconds, so that is short.
557///
558/// # Errors
559///
560/// Whatever opening or decoding the file refuses, [`Error::Unsupported`] for a
561/// file with no frame rate, and [`Error::Decode`] for a range that runs past
562/// the end of the film.
563#[cfg(feature = "demux")]
564pub fn cache_frames(path: impl AsRef<Path>, ranges: &[(u64, u64)]) -> Result<Vec<Frame>> {
565    let mut source = VideoSource::from_file(path)?;
566    let duration = source.frame_duration().ok_or_else(|| {
567        Error::unsupported("this file states no frame rate, so its frames cannot be numbered")
568    })?;
569
570    let origin = source.origin()?;
571
572    let mut sorted = ranges.to_vec();
573    sorted.sort_unstable();
574
575    let mut frames = Vec::new();
576
577    for (start, end) in sorted {
578        source.seek(origin + frame_time(start, duration))?;
579        let mut number = start;
580
581        while number < end {
582            source.advance(Duration::ZERO)?;
583
584            let Some(frame) = source.pop_frame() else {
585                return Err(Error::Decode {
586                    encoding: source.decoder().encoding(),
587                    reason: format!(
588                        "frame range {start}..{end} runs past the end of the film, at frame {number}"
589                    ),
590                });
591            };
592
593            frames.push(frame);
594            number += 1;
595        }
596    }
597
598    Ok(frames)
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604    use crate::color::ColorSpace;
605    use crate::frame::PixelFormat;
606
607    fn frame(tag: u8) -> Frame {
608        Frame::packed(
609            2,
610            2,
611            PixelFormat::Rgba8,
612            ColorSpace::srgb(),
613            Duration::ZERO,
614            vec![tag; 16],
615        )
616        .unwrap()
617    }
618
619    fn tag(frame: &Frame) -> u8 {
620        frame.data()[0]
621    }
622
623    #[test]
624    fn a_cache_finds_each_frame_by_number() {
625        let cache = FrameCache::new(
626            vec![(10, 12), (0, 2)],
627            vec![frame(0), frame(1), frame(10), frame(11)],
628        )
629        .unwrap();
630
631        assert_eq!(cache.get(0).map(tag), Some(0));
632        assert_eq!(cache.get(1).map(tag), Some(1));
633        assert!(cache.get(2).is_none());
634        assert!(cache.get(9).is_none());
635        assert_eq!(cache.get(10).map(tag), Some(10));
636        assert_eq!(cache.get(11).map(tag), Some(11));
637        assert!(cache.get(12).is_none());
638        assert_eq!(cache.len(), 4);
639    }
640
641    /// Each of these would serve the wrong picture later rather than fail now.
642    #[test]
643    fn a_cache_that_does_not_add_up_is_refused() {
644        assert!(FrameCache::new(vec![(0, 3)], vec![frame(0), frame(1)]).is_err());
645        assert!(FrameCache::new(vec![(5, 5)], vec![]).is_err());
646        assert!(FrameCache::new(vec![(0, 4), (2, 6)], vec![frame(0); 8]).is_err());
647    }
648
649    #[test]
650    fn frame_numbers_round_rather_than_floor() {
651        let frame = Duration::from_nanos(41_708_333);
652
653        assert_eq!(frame_number(Duration::ZERO, frame), 0);
654        assert_eq!(frame_number(frame * 24 - Duration::from_nanos(3), frame), 24);
655        assert_eq!(frame_number(frame * 24 + Duration::from_nanos(3), frame), 24);
656    }
657}