#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EffectCategory {
Transition,
Filter,
Compositing,
Motion,
ColorCorrection,
Custom,
}
#[derive(Debug, Clone)]
pub struct EffectTypeDef {
pub auid: String,
pub name: String,
pub description: String,
pub category: EffectCategory,
pub is_time_warp: bool,
pub bypass_override: Option<u32>,
pub num_inputs: usize,
pub parameter_names: Vec<String>,
}
impl EffectTypeDef {
#[must_use]
pub fn new(
auid: impl Into<String>,
name: impl Into<String>,
category: EffectCategory,
num_inputs: usize,
) -> Self {
Self {
auid: auid.into(),
name: name.into(),
description: String::new(),
category,
is_time_warp: false,
bypass_override: None,
num_inputs,
parameter_names: Vec::new(),
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
pub fn as_time_warp(mut self) -> Self {
self.is_time_warp = true;
self
}
pub fn add_parameter(mut self, name: impl Into<String>) -> Self {
self.parameter_names.push(name.into());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct EffectTypeRegistry {
by_auid: HashMap<String, EffectTypeDef>,
by_name: HashMap<String, String>, }
impl EffectTypeRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_defaults() -> Self {
let mut reg = Self::new();
reg.register(
EffectTypeDef::new(
"urn:smpte:ul:060e2b34.04010101.0e040302.00000000",
"VideoDissolve",
EffectCategory::Transition,
2,
)
.with_description("Linear video dissolve transition")
.add_parameter("Level"),
);
reg.register(
EffectTypeDef::new(
"urn:smpte:ul:060e2b34.04010101.0e040303.00000000",
"SMPTEVideoWipe",
EffectCategory::Transition,
2,
)
.with_description("SMPTE wipe transition")
.add_parameter("Level")
.add_parameter("WipeCode"),
);
reg.register(
EffectTypeDef::new(
"urn:smpte:ul:060e2b34.04010101.0e040401.00000000",
"VideoSpeedControl",
EffectCategory::Motion,
1,
)
.with_description("Speed change effect")
.as_time_warp()
.add_parameter("SpeedRatio"),
);
reg.register(
EffectTypeDef::new(
"urn:smpte:ul:060e2b34.04010101.0e040501.00000000",
"VideoColorCorrect",
EffectCategory::ColorCorrection,
1,
)
.with_description("Basic color correction")
.add_parameter("Brightness")
.add_parameter("Contrast")
.add_parameter("Saturation"),
);
reg
}
pub fn register(&mut self, def: EffectTypeDef) {
let auid = def.auid.clone();
self.by_name.insert(def.name.clone(), auid.clone());
self.by_auid.insert(auid, def);
}
#[must_use]
pub fn get_by_auid(&self, auid: &str) -> Option<&EffectTypeDef> {
self.by_auid.get(auid)
}
#[must_use]
pub fn get_by_name(&self, name: &str) -> Option<&EffectTypeDef> {
self.by_name.get(name).and_then(|a| self.by_auid.get(a))
}
#[must_use]
pub fn len(&self) -> usize {
self.by_auid.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_auid.is_empty()
}
#[must_use]
pub fn auids(&self) -> Vec<&str> {
self.by_auid.keys().map(String::as_str).collect()
}
#[must_use]
pub fn by_category(&self, category: EffectCategory) -> Vec<&EffectTypeDef> {
self.by_auid
.values()
.filter(|e| e.category == category)
.collect()
}
}
#[derive(Debug, Clone)]
pub struct EffectSlot {
pub slot_id: u32,
pub segment_kind: String,
pub length: i64,
pub attributes: HashMap<String, String>,
}
impl EffectSlot {
#[must_use]
pub fn new(slot_id: u32, segment_kind: impl Into<String>, length: i64) -> Self {
Self {
slot_id,
segment_kind: segment_kind.into(),
length,
attributes: HashMap::new(),
}
}
pub fn set_attr(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.attributes.insert(key.into(), value.into());
}
}
#[derive(Debug, Clone)]
pub struct OperationGroupDef {
pub effect_auid: String,
pub length: i64,
pub is_time_warp: bool,
pub input_segments: Vec<EffectSlot>,
pub parameters: HashMap<String, String>,
}
impl OperationGroupDef {
#[must_use]
pub fn new(effect_auid: impl Into<String>, length: i64, is_time_warp: bool) -> Self {
Self {
effect_auid: effect_auid.into(),
length,
is_time_warp,
input_segments: Vec::new(),
parameters: HashMap::new(),
}
}
pub fn add_input(&mut self, slot: EffectSlot) {
self.input_segments.push(slot);
}
pub fn set_parameter(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.parameters.insert(name.into(), value.into());
}
#[must_use]
pub fn get_parameter(&self, name: &str) -> Option<&str> {
self.parameters.get(name).map(String::as_str)
}
#[must_use]
pub fn input_count(&self) -> usize {
self.input_segments.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_effect_type_def_new() {
let def = EffectTypeDef::new("auid-1", "Dissolve", EffectCategory::Transition, 2);
assert_eq!(def.name, "Dissolve");
assert_eq!(def.num_inputs, 2);
assert!(!def.is_time_warp);
assert!(def.parameter_names.is_empty());
}
#[test]
fn test_effect_type_def_builder() {
let def = EffectTypeDef::new("auid-2", "SpeedRamp", EffectCategory::Motion, 1)
.with_description("Speed ramp effect")
.as_time_warp()
.add_parameter("SpeedRatio");
assert!(def.is_time_warp);
assert_eq!(def.description, "Speed ramp effect");
assert_eq!(def.parameter_names.len(), 1);
assert_eq!(def.parameter_names[0], "SpeedRatio");
}
#[test]
fn test_registry_register_and_lookup() {
let mut reg = EffectTypeRegistry::new();
let def = EffectTypeDef::new("auid-abc", "MyEffect", EffectCategory::Filter, 1);
reg.register(def);
assert_eq!(reg.len(), 1);
assert!(reg.get_by_auid("auid-abc").is_some());
assert!(reg.get_by_name("MyEffect").is_some());
assert!(reg.get_by_auid("missing").is_none());
}
#[test]
fn test_registry_with_defaults() {
let reg = EffectTypeRegistry::with_defaults();
assert!(reg.len() >= 4);
assert!(reg.get_by_name("VideoDissolve").is_some());
assert!(reg.get_by_name("VideoSpeedControl").is_some());
let speed = reg
.get_by_name("VideoSpeedControl")
.expect("speed should be valid");
assert!(speed.is_time_warp);
}
#[test]
fn test_registry_by_category() {
let reg = EffectTypeRegistry::with_defaults();
let transitions = reg.by_category(EffectCategory::Transition);
assert!(transitions.len() >= 2);
let motions = reg.by_category(EffectCategory::Motion);
assert!(!motions.is_empty());
}
#[test]
fn test_registry_is_empty() {
let reg = EffectTypeRegistry::new();
assert!(reg.is_empty());
}
#[test]
fn test_registry_auids() {
let mut reg = EffectTypeRegistry::new();
reg.register(EffectTypeDef::new("a1", "E1", EffectCategory::Filter, 1));
reg.register(EffectTypeDef::new("a2", "E2", EffectCategory::Filter, 1));
let auids = reg.auids();
assert_eq!(auids.len(), 2);
}
#[test]
fn test_effect_slot_creation() {
let mut slot = EffectSlot::new(1, "SourceClip", 100);
slot.set_attr("role", "foreground");
assert_eq!(slot.slot_id, 1);
assert_eq!(slot.length, 100);
assert_eq!(
slot.attributes.get("role").map(String::as_str),
Some("foreground")
);
}
#[test]
fn test_operation_group_def() {
let mut og = OperationGroupDef::new("auid-dissolve", 50, false);
og.add_input(EffectSlot::new(1, "SourceClip", 50));
og.add_input(EffectSlot::new(2, "SourceClip", 50));
og.set_parameter("Level", "0.5");
assert_eq!(og.input_count(), 2);
assert_eq!(og.get_parameter("Level"), Some("0.5"));
assert!(og.get_parameter("Missing").is_none());
}
#[test]
fn test_operation_group_time_warp() {
let og = OperationGroupDef::new("auid-speed", 100, true);
assert!(og.is_time_warp);
}
#[test]
fn test_effect_category_equality() {
assert_eq!(EffectCategory::Transition, EffectCategory::Transition);
assert_ne!(EffectCategory::Transition, EffectCategory::Filter);
}
#[test]
fn test_color_correct_parameters() {
let reg = EffectTypeRegistry::with_defaults();
let cc = reg
.get_by_name("VideoColorCorrect")
.expect("cc should be valid");
assert!(cc.parameter_names.contains(&"Brightness".to_string()));
assert!(cc.parameter_names.contains(&"Contrast".to_string()));
assert!(cc.parameter_names.contains(&"Saturation".to_string()));
}
#[test]
fn test_wipe_parameters() {
let reg = EffectTypeRegistry::with_defaults();
let wipe = reg
.get_by_name("SMPTEVideoWipe")
.expect("wipe should be valid");
assert_eq!(wipe.num_inputs, 2);
assert!(wipe.parameter_names.contains(&"WipeCode".to_string()));
}
}