use crate::error::EdlResult;
use crate::event::EdlEvent;
use crate::timecode::{EdlFrameRate, EdlTimecode};
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct CameraAngleId(pub String);
impl CameraAngleId {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for CameraAngleId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct CameraAngle {
pub id: CameraAngleId,
pub reel: String,
pub label: Option<String>,
}
impl CameraAngle {
#[must_use]
pub fn new(id: impl Into<String>, reel: impl Into<String>) -> Self {
Self {
id: CameraAngleId::new(id),
reel: reel.into(),
label: None,
}
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
#[derive(Debug, Clone)]
pub struct MulticamGroup {
pub name: String,
pub angles: Vec<CameraAngle>,
pub sync_timecode: Option<EdlTimecode>,
}
impl MulticamGroup {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
angles: Vec::new(),
sync_timecode: None,
}
}
pub fn add_angle(&mut self, angle: CameraAngle) {
self.angles.push(angle);
}
pub fn set_sync_timecode(&mut self, tc: EdlTimecode) {
self.sync_timecode = Some(tc);
}
#[must_use]
pub fn find_angle(&self, id: &CameraAngleId) -> Option<&CameraAngle> {
self.angles.iter().find(|a| &a.id == id)
}
}
#[derive(Debug, Clone)]
pub struct MulticamCut {
pub record_in: EdlTimecode,
pub angle_id: CameraAngleId,
}
impl MulticamCut {
#[must_use]
pub fn new(record_in: EdlTimecode, angle_id: CameraAngleId) -> Self {
Self {
record_in,
angle_id,
}
}
}
#[derive(Debug, Clone)]
pub struct MulticamSequence {
pub group: MulticamGroup,
pub cuts: Vec<MulticamCut>,
}
impl MulticamSequence {
#[must_use]
pub fn new(group: MulticamGroup) -> Self {
Self {
group,
cuts: Vec::new(),
}
}
pub fn add_cut(&mut self, cut: MulticamCut) {
self.cuts.push(cut);
self.cuts.sort_by_key(|c| c.record_in.to_frames());
}
#[must_use]
pub fn active_angle_at(&self, frame: u64) -> Option<&CameraAngleId> {
self.cuts
.iter()
.rev()
.find(|c| c.record_in.to_frames() <= frame)
.map(|c| &c.angle_id)
}
pub fn flatten(&self, end_frame: u64, fps: EdlFrameRate) -> EdlResult<Vec<EdlEvent>> {
use crate::audio::AudioChannel;
use crate::event::{EditType, TrackType};
if self.cuts.is_empty() {
return Ok(Vec::new());
}
let mut events: Vec<EdlEvent> = Vec::new();
let mut event_number: u32 = 1;
for (i, cut) in self.cuts.iter().enumerate() {
let rec_in = cut.record_in;
let rec_out_frame = self
.cuts
.get(i + 1)
.map(|next| next.record_in.to_frames())
.unwrap_or(end_frame);
if rec_out_frame <= rec_in.to_frames() {
continue;
}
let angle = self
.group
.find_angle(&cut.angle_id)
.ok_or_else(|| crate::error::EdlError::InvalidTrackType(cut.angle_id.0.clone()))?;
let rec_out = EdlTimecode::from_frames(rec_out_frame, fps)?;
let src_in = rec_in;
let src_out = rec_out;
let event = EdlEvent::new(
event_number,
angle.reel.clone(),
TrackType::Audio(AudioChannel::A1),
EditType::Cut,
src_in,
src_out,
rec_in,
rec_out,
);
events.push(event);
event_number += 1;
}
Ok(events)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::timecode::{EdlFrameRate, EdlTimecode};
fn tc(h: u8, m: u8, s: u8, f: u8) -> EdlTimecode {
EdlTimecode::new(h, m, s, f, EdlFrameRate::Fps25).expect("valid timecode")
}
#[test]
fn test_camera_angle_id_display() {
let id = CameraAngleId::new("CAM_A");
assert_eq!(id.to_string(), "CAM_A");
}
#[test]
fn test_multicam_group_find_angle() {
let mut group = MulticamGroup::new("Scene1");
group.add_angle(CameraAngle::new("CAM_A", "REEL_A"));
group.add_angle(CameraAngle::new("CAM_B", "REEL_B"));
let id = CameraAngleId::new("CAM_A");
let found = group.find_angle(&id).expect("should find angle");
assert_eq!(found.reel, "REEL_A");
let missing = CameraAngleId::new("CAM_Z");
assert!(group.find_angle(&missing).is_none());
}
#[test]
fn test_multicam_sequence_active_angle() {
let mut group = MulticamGroup::new("Scene1");
group.add_angle(CameraAngle::new("CAM_A", "REEL_A"));
group.add_angle(CameraAngle::new("CAM_B", "REEL_B"));
let mut seq = MulticamSequence::new(group);
seq.add_cut(MulticamCut::new(
tc(1, 0, 0, 0),
CameraAngleId::new("CAM_A"),
));
seq.add_cut(MulticamCut::new(
tc(1, 0, 5, 0),
CameraAngleId::new("CAM_B"),
));
let frame_in_a = tc(1, 0, 2, 0).to_frames();
let frame_in_b = tc(1, 0, 7, 0).to_frames();
assert_eq!(
seq.active_angle_at(frame_in_a).map(|a| a.as_str()),
Some("CAM_A")
);
assert_eq!(
seq.active_angle_at(frame_in_b).map(|a| a.as_str()),
Some("CAM_B")
);
}
#[test]
fn test_multicam_sequence_flatten() {
let mut group = MulticamGroup::new("Scene1");
group.add_angle(CameraAngle::new("CAM_A", "REEL_A"));
group.add_angle(CameraAngle::new("CAM_B", "REEL_B"));
let mut seq = MulticamSequence::new(group);
seq.add_cut(MulticamCut::new(
tc(1, 0, 0, 0),
CameraAngleId::new("CAM_A"),
));
seq.add_cut(MulticamCut::new(
tc(1, 0, 5, 0),
CameraAngleId::new("CAM_B"),
));
let end = tc(1, 0, 10, 0).to_frames();
let events = seq
.flatten(end, EdlFrameRate::Fps25)
.expect("flatten should succeed");
assert_eq!(events.len(), 2);
assert_eq!(events[0].reel, "REEL_A");
assert_eq!(events[1].reel, "REEL_B");
}
}