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",
CodecConfig::Subtitle { .. } => "subtitle",
}
}
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 {
if let Some(last) = track.samples.last()
&& let Some(dts) = last.dts
{
let dts = u64::try_from(dts).unwrap_or(0);
return dts.saturating_add(last.duration.unwrap_or(0) as u64);
}
let span: u64 = track
.samples
.iter()
.map(|s| s.duration.unwrap_or(0) 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)
}
fn pick_reference_track(a: &[Track], b: &[Track], mapping: &[usize]) -> Option<usize> {
if let Some(i) = a.iter().position(|t| is_video(&t.spec.config)) {
return Some(i);
}
a.iter().enumerate().position(|(i, at)| {
at.samples.iter().any(|s| s.dts.is_some())
|| b[mapping[i]].samples.iter().any(|s| s.dts.is_some())
})
}
fn rescale_ticks(ticks: i64, from_timescale: u32, to_timescale: u32) -> i64 {
if from_timescale == 0 || to_timescale == 0 || from_timescale == to_timescale {
return ticks;
}
((ticks as i128 * to_timescale as i128) / from_timescale as i128) as i64
}
fn shift_samples(samples: &[Sample], offset: i64) -> Vec<Sample> {
samples
.iter()
.cloned()
.map(|mut s| {
if let Some(d) = s.dts {
s.dts = Some(d + offset);
}
if let Some(p) = s.pts {
s.pts = Some(p + offset);
}
s
})
.collect()
}
fn result_start_decode_time(samples: &[Sample], fallback: u64) -> u64 {
samples
.first()
.and_then(|s| s.dts)
.map(|d| u64::try_from(d).unwrap_or(0))
.unwrap_or(fallback)
}
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()
&& !first.flags.is_sync
{
return Err(Error::InvalidInput(
"concat: appended track does not begin on a sync sample",
));
}
}
let ref_shift = pick_reference_track(&a.tracks, &b.tracks, &mapping).map(|ri| {
let ref_timescale = a.tracks[ri].spec.timescale;
let join_dts_ref = track_end_decode_time(&a.tracks[ri]) as i64;
let bt = &b.tracks[mapping[ri]];
let incoming_ref_start = bt
.samples
.first()
.and_then(|s| s.dts)
.unwrap_or(bt.start_decode_time as i64);
(ref_timescale, join_dts_ref, incoming_ref_start)
});
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 shifted = match ref_shift {
Some((ref_ts, join_dts_ref, incoming_ref_start)) => {
let track_ts = at.spec.timescale;
let offset = rescale_ticks(join_dts_ref, ref_ts, track_ts)
- rescale_ticks(incoming_ref_start, ref_ts, track_ts);
shift_samples(&bt.samples, offset)
}
None => bt.samples.clone(),
};
let join_index = at.samples.len();
let mut samples = at.samples.clone();
samples.extend(shifted.iter().cloned());
if !shifted.is_empty() {
let presentation_time = match shifted[0].pts {
Some(p) => u64::try_from(p).unwrap_or(0),
None => track_end_decode_time(at),
};
points.push(SplicePoint {
track_id: at.spec.track_id,
sample_index: join_index,
presentation_time,
});
}
let track_start = result_start_decode_time(&samples, at.start_decode_time);
out_tracks.push(Track::new_at(at.spec.clone(), samples, track_start));
}
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 fallback_dts = track.start_decode_time;
let mut best: (u64, usize) = (track.start_decode_time, 0);
for (i, s) in track.samples.iter().enumerate() {
let dts = s
.dts
.map(|d| u64::try_from(d).unwrap_or(0))
.unwrap_or(fallback_dts);
if dts > at_ticks {
break;
}
if s.flags.is_sync {
best = (dts, i);
}
fallback_dts = dts.saturating_add(s.duration.unwrap_or(0) 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()
&& !first.flags.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 video_timescale = u128::from(base.tracks[video_idx].spec.timescale.max(1));
let ref_timescale = base.tracks[video_idx].spec.timescale;
let join_dts_ref = boundary_decode_time(&base.tracks[video_idx], video_split) as i64;
let ad_video = &ad.tracks[mapping[video_idx]];
let incoming_ref_start = ad_video
.samples
.first()
.and_then(|s| s.dts)
.unwrap_or(ad_video.start_decode_time as i64);
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 track_ts = bt.spec.timescale;
let split_index = if i == video_idx {
video_split
} else {
let track_timescale = u128::from(bt.spec.timescale.max(1));
let offset_in_track_ticks =
(u128::from(split_offset_ticks) * track_timescale / video_timescale) as u64;
sample_index_at_offset(bt, offset_in_track_ticks)
};
let boundary_dts = boundary_decode_time(bt, split_index);
let ad_offset = rescale_ticks(join_dts_ref, ref_timescale, track_ts)
- rescale_ticks(incoming_ref_start, ref_timescale, track_ts);
let shifted_ad = shift_samples(&adt.samples, ad_offset);
let ad_span: u64 = adt
.samples
.iter()
.map(|s| s.duration.unwrap_or(0) as u64)
.sum();
let resume_shift: i64 = match shifted_ad.first().and_then(|s| s.dts) {
Some(new_ad_first_dts) => new_ad_first_dts + ad_span as i64 - boundary_dts as i64,
None => ad_span as i64,
};
let shifted_resume = shift_samples(&bt.samples[split_index..], resume_shift);
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(shifted_ad.iter().cloned());
let resume_index = samples.len();
samples.extend(shifted_resume.iter().cloned());
if !shifted_ad.is_empty() {
let presentation_time = match shifted_ad[0].pts {
Some(p) => u64::try_from(p).unwrap_or(0),
None => boundary_dts,
};
points.push(SplicePoint {
track_id: bt.spec.track_id,
sample_index: ad_index,
presentation_time,
});
}
if split_index < bt.samples.len() {
let presentation_time = match shifted_resume.first().and_then(|s| s.pts) {
Some(p) => u64::try_from(p).unwrap_or(0),
None => boundary_dts.saturating_add(ad_span),
};
points.push(SplicePoint {
track_id: bt.spec.track_id,
sample_index: resume_index,
presentation_time,
});
}
let track_start = result_start_decode_time(&samples, bt.start_decode_time);
out_tracks.push(Track::new_at(bt.spec.clone(), samples, track_start));
}
Ok(SpliceResult {
media: Media::new(out_tracks, base.movie_timescale),
discontinuity_points: points,
})
}
fn boundary_decode_time(track: &Track, split_index: usize) -> u64 {
match track.samples.get(split_index).and_then(|s| s.dts) {
Some(dts) => u64::try_from(dts).unwrap_or(0),
None if split_index >= track.samples.len() => track_end_decode_time(track),
None => {
track.start_decode_time
+ track.samples[..split_index]
.iter()
.map(|s| s.duration.unwrap_or(0) as u64)
.sum::<u64>()
}
}
}
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.unwrap_or(0) as u64);
}
track.samples.len()
}