Skip to main content

rivet/job/
splice.rs

1use bytes::Bytes;
2
3use super::audio::PreparedAudio;
4
5/// One clip of a [splice](super::run_splice_job): an input plus an optional
6/// `[start, end)` trim window in seconds (either bound `None` = open).
7#[derive(Clone)]
8pub struct Clip {
9    pub input: Bytes,
10    pub start: Option<f64>,
11    pub end: Option<f64>,
12}
13
14impl Clip {
15    /// A whole clip, no trim.
16    pub fn new(input: impl Into<Bytes>) -> Self {
17        Self { input: input.into(), start: None, end: None }
18    }
19
20    /// A clip trimmed to `[start, end)` seconds (either bound `None` = open).
21    pub fn trimmed(input: impl Into<Bytes>, start: Option<f64>, end: Option<f64>) -> Self {
22        Self { input: input.into(), start, end }
23    }
24}
25
26/// Convert a trim time (seconds) to a half-open source frame index at `fps`
27/// (`ceil`, so `[start,end)` is exact for non-integer fps). `None` → `None`.
28pub(super) fn trim_frame(sec: Option<f64>, fps: f64) -> Option<u64> {
29    sec.map(|s| (s.max(0.0) * fps).ceil() as u64)
30}
31
32/// Trim a prepared audio track to the window `[start, end)` seconds, dropping
33/// packets outside it. Kept packets retain their explicit durations, so the
34/// muxer re-times them from zero — aligning with the trimmed, rebased video.
35/// Cut points land on packet boundaries (≤ ~20 ms), which is fine for A/V sync.
36/// `None`/`None` returns the track unchanged.
37pub(super) fn trim_audio(
38    audio: Option<&PreparedAudio>,
39    start: Option<f64>,
40    end: Option<f64>,
41) -> Option<PreparedAudio> {
42    let a = audio?;
43    if start.is_none() && end.is_none() {
44        return Some(a.clone());
45    }
46    let ticks_per_sec = a.info.timescale.max(1) as f64;
47    let start_tick = (start.unwrap_or(0.0).max(0.0) * ticks_per_sec) as u64;
48    let end_tick = end.map(|e| (e.max(0.0) * ticks_per_sec) as u64);
49    let mut acc: u64 = 0;
50    let mut kept = Vec::new();
51    for (payload, dur) in &a.samples {
52        let sample_start = acc;
53        acc += *dur as u64;
54        if sample_start < start_tick {
55            continue;
56        }
57        if end_tick.is_some_and(|et| sample_start >= et) {
58            break;
59        }
60        kept.push((payload.clone(), *dur));
61    }
62    Some(PreparedAudio { info: a.info.clone(), samples: kept, handling: a.handling.clone() })
63}