use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::media::{Media, Track};
use crate::pipeline::{CodecConfig, Sample};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SplicePoint {
pub track_id: u32,
pub sample_index: usize,
pub presentation_time: u64,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SpliceResult {
pub media: Media,
pub discontinuity_points: Vec<SplicePoint>,
}
fn codec_kind(config: &CodecConfig) -> &'static str {
match config {
CodecConfig::Avc { .. } => "avc",
CodecConfig::Hevc { .. } => "hevc",
CodecConfig::Vvc { .. } => "vvc",
CodecConfig::Av1 { .. } => "av1",
CodecConfig::Vp9 { .. } => "vp9",
CodecConfig::Vp8 { .. } => "vp8",
CodecConfig::Mpeg2Video { .. } => "mpeg2video",
CodecConfig::Aac { .. } => "aac",
CodecConfig::Ac3 { .. } => "ac3",
CodecConfig::Eac3 { .. } => "eac3",
CodecConfig::Ac4 { .. } => "ac4",
CodecConfig::Opus { .. } => "opus",
CodecConfig::Flac { .. } => "flac",
CodecConfig::Dts { .. } => "dts",
CodecConfig::MpegH { .. } => "mpegh",
CodecConfig::MpegAudio { .. } => "mpegaudio",
CodecConfig::Vorbis { .. } => "vorbis",
CodecConfig::Data { .. } => "data",
}
}
fn is_video(config: &CodecConfig) -> bool {
matches!(
codec_kind(config),
"avc" | "hevc" | "vvc" | "av1" | "vp9" | "vp8" | "mpeg2video"
)
}
fn track_end_decode_time(track: &Track) -> u64 {
let span: u64 = track.samples.iter().map(|s| s.duration as u64).sum();
track.start_decode_time.saturating_add(span)
}
fn match_tracks(a: &Media, b: &Media) -> Result<Vec<usize>> {
if a.tracks.len() != b.tracks.len() {
return Err(Error::InvalidInput(
"splice: media have differing track counts",
));
}
let mut mapping = Vec::with_capacity(a.tracks.len());
for (i, at) in a.tracks.iter().enumerate() {
let bj = b
.tracks
.iter()
.position(|bt| bt.spec.track_id == at.spec.track_id)
.unwrap_or(i);
let bt = &b.tracks[bj];
if codec_kind(&at.spec.config) != codec_kind(&bt.spec.config) {
return Err(Error::InvalidInput(
"splice: matched tracks have incompatible codecs",
));
}
if at.spec.timescale != bt.spec.timescale {
return Err(Error::InvalidInput(
"splice: matched tracks have incompatible timescales",
));
}
mapping.push(bj);
}
Ok(mapping)
}
pub fn concat(a: &Media, b: &Media) -> Result<SpliceResult> {
let mapping = match_tracks(a, b)?;
for &bj in &mapping {
if let Some(first) = b.tracks[bj].samples.first() {
if !first.is_sync {
return Err(Error::InvalidInput(
"concat: appended track does not begin on a sync sample",
));
}
}
}
let mut out_tracks = Vec::with_capacity(a.tracks.len());
let mut points = Vec::new();
for (i, at) in a.tracks.iter().enumerate() {
let bt = &b.tracks[mapping[i]];
let join_dts = track_end_decode_time(at);
let mut samples = at.samples.clone();
let join_index = samples.len();
samples.extend(bt.samples.iter().cloned());
if !bt.samples.is_empty() {
points.push(SplicePoint {
track_id: at.spec.track_id,
sample_index: join_index,
presentation_time: join_dts
.saturating_add_signed(bt.samples[0].composition_offset as i64),
});
}
out_tracks.push(Track::new_at(
at.spec.clone(),
samples,
at.start_decode_time,
));
}
Ok(SpliceResult {
media: Media::new(out_tracks, a.movie_timescale),
discontinuity_points: points,
})
}
pub fn snap_to_preceding_sync(track: &Track, at_ticks: u64) -> Option<(u64, usize)> {
if track.samples.is_empty() {
return None;
}
let mut dts = track.start_decode_time;
let mut best: (u64, usize) = (track.start_decode_time, 0);
for (i, s) in track.samples.iter().enumerate() {
if dts > at_ticks {
break;
}
if s.is_sync {
best = (dts, i);
}
dts = dts.saturating_add(s.duration as u64);
}
Some(best)
}
pub fn splice_insert(base: &Media, ad: &Media, at_ticks: u64) -> Result<SpliceResult> {
let mapping = match_tracks(base, ad)?;
for &aj in &mapping {
if let Some(first) = ad.tracks[aj].samples.first() {
if !first.is_sync {
return Err(Error::InvalidInput(
"splice_insert: ad track does not begin on a sync sample",
));
}
}
}
let video_idx = base
.tracks
.iter()
.position(|t| is_video(&t.spec.config))
.ok_or(Error::InvalidInput(
"splice_insert: base has no video track to align the splice on",
))?;
let (snapped_video_dts, video_split) =
snap_to_preceding_sync(&base.tracks[video_idx], at_ticks).ok_or(Error::InvalidInput(
"splice_insert: base video track has no samples",
))?;
let split_offset_ticks =
snapped_video_dts.saturating_sub(base.tracks[video_idx].start_decode_time);
let mut out_tracks = Vec::with_capacity(base.tracks.len());
let mut points = Vec::new();
for (i, bt) in base.tracks.iter().enumerate() {
let adt = &ad.tracks[mapping[i]];
let split_index = if i == video_idx {
video_split
} else {
sample_index_at_offset(bt, split_offset_ticks)
};
let ad_span: u64 = adt.samples.iter().map(|s| s.duration as u64).sum();
let mut samples: Vec<Sample> = Vec::with_capacity(bt.samples.len() + adt.samples.len());
samples.extend(bt.samples[..split_index].iter().cloned());
let ad_index = samples.len();
samples.extend(adt.samples.iter().cloned());
let resume_index = samples.len();
samples.extend(bt.samples[split_index..].iter().cloned());
let boundary_dts: u64 = bt.start_decode_time
+ bt.samples[..split_index]
.iter()
.map(|s| s.duration as u64)
.sum::<u64>();
if !adt.samples.is_empty() {
points.push(SplicePoint {
track_id: bt.spec.track_id,
sample_index: ad_index,
presentation_time: boundary_dts
.saturating_add_signed(adt.samples[0].composition_offset as i64),
});
}
if split_index < bt.samples.len() {
let resume_dts = boundary_dts + ad_span;
points.push(SplicePoint {
track_id: bt.spec.track_id,
sample_index: resume_index,
presentation_time: resume_dts
.saturating_add_signed(bt.samples[split_index].composition_offset as i64),
});
}
out_tracks.push(Track::new_at(
bt.spec.clone(),
samples,
bt.start_decode_time,
));
}
Ok(SpliceResult {
media: Media::new(out_tracks, base.movie_timescale),
discontinuity_points: points,
})
}
fn sample_index_at_offset(track: &Track, offset_ticks: u64) -> usize {
let mut acc = 0u64;
for (i, s) in track.samples.iter().enumerate() {
if acc >= offset_ticks {
return i;
}
acc = acc.saturating_add(s.duration as u64);
}
track.samples.len()
}