use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::decode::DecodedFrameSet;
use alpha2_engine::PassthroughSegment;
use alpha2_media::YuvEncoding;
use alpha2_model::{MediaKind, Project};
use alpha2_native::NativeVideoFrame;
use alpha2_time::TimeRange;
pub(crate) struct TranscodePlan {
segments: Vec<PassthroughSegment>,
}
impl TranscodePlan {
pub(crate) fn picture(
&self,
decoded: DecodedFrameSet,
) -> Result<Arc<NativeVideoFrame>, String> {
let time = decoded.time;
let segment = self
.segments
.iter()
.find(|segment| {
segment.timeline_range.start() <= time && time < segment.timeline_range.end()
})
.ok_or_else(|| format!("no transcoded clip covers {time:?}"))?;
let source_time = segment.source_time(time);
decoded
.frames
.into_iter()
.find(|frame| {
frame.kind == MediaKind::Video
&& frame.source == segment.source_id
&& frame.requested == source_time
})
.map(|frame| frame.frame)
.ok_or_else(|| {
format!(
"the decoded frame set for {time:?} lacks clip {}'s picture at {source_time:?}",
segment.clip_id
)
})
}
}
pub(crate) fn yuv_transcode_plan(
project: &Project,
assets: &HashMap<String, PathBuf>,
range: TimeRange,
encoding: YuvEncoding,
) -> Result<TranscodePlan, String> {
plan_with(project, assets, range, |source| {
alpha2_native::check_yuv_transcode(
source,
project.settings.width,
project.settings.height,
encoding,
)
})
}
fn plan_with(
project: &Project,
assets: &HashMap<String, PathBuf>,
range: TimeRange,
mut check: impl FnMut(&Path) -> Result<(), String>,
) -> Result<TranscodePlan, String> {
crate::config::configured_yuv_transcode()?;
let plan = alpha2_engine::passthrough_plan(project, range, false)?;
let mut qualified = HashSet::new();
for segment in &plan.segments {
let source = assets
.get(segment.source_id.as_str())
.ok_or_else(|| format!("source {} has no file", segment.source_id))?;
if qualified.contains(source) {
continue;
}
check(source).map_err(|reason| format!("clip {}: {reason}", segment.clip_id))?;
qualified.insert(source.clone());
}
Ok(TranscodePlan {
segments: plan.segments,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn rough_cut() -> (Project, HashMap<String, PathBuf>) {
let second = alpha2_time::TICKS_PER_SECOND;
let clip = |id: &str, source: &str, start: i64, from: i64| {
json!({
"id": id, "source_id": source,
"timeline_range": {"start": start, "end": start + second},
"source_range": {"start": from, "end": from + second}
})
};
let project = json!({
"schema_version": 1, "id": "cut", "name": "Cut",
"settings": {"width": 128, "height": 96, "frame_rate": {"num": 30, "den": 1}, "sample_rate": 48000},
"sources": [
{"id": "camera", "locator": "fixture://camera", "kind": "video"},
{"id": "phone", "locator": "fixture://phone", "kind": "video"}
],
"timeline": {"tracks": [{"id": "v", "name": "V", "kind": "video", "clips": [
clip("a", "camera", 0, 0),
clip("b", "camera", second, 3 * second),
clip("c", "camera", 2 * second, second),
clip("d", "phone", 3 * second, 0)
]}]}
});
let project = Project::from_json(&project.to_string()).expect("project");
let assets = HashMap::from([
("camera".to_owned(), PathBuf::from("camera.mp4")),
("phone".to_owned(), PathBuf::from("phone.mp4")),
]);
(project, assets)
}
#[test]
fn a_file_many_clips_cut_from_is_checked_once() {
let (project, assets) = rough_cut();
let range = TimeRange::new(
alpha2_time::MediaTime::ZERO,
alpha2_time::MediaTime::from_ticks(4 * alpha2_time::TICKS_PER_SECOND),
)
.expect("range");
let mut checked = Vec::new();
let plan = plan_with(&project, &assets, range, |source| {
checked.push(source.to_owned());
Ok(())
})
.expect("the cut qualifies");
assert_eq!(plan.segments.len(), 4);
assert_eq!(
checked,
[PathBuf::from("camera.mp4"), PathBuf::from("phone.mp4")]
);
}
#[test]
fn a_file_that_refuses_names_the_first_clip_cut_from_it() {
let (project, assets) = rough_cut();
let range = TimeRange::new(
alpha2_time::MediaTime::ZERO,
alpha2_time::MediaTime::from_ticks(4 * alpha2_time::TICKS_PER_SECOND),
)
.expect("range");
let error = plan_with(&project, &assets, range, |source| {
if source == Path::new("phone.mp4") {
Err("the source's chroma sits Center".to_owned())
} else {
Ok(())
}
})
.err()
.expect("the phone clip refuses");
assert_eq!(error, "clip d: the source's chroma sits Center");
}
}