use std::hash::{Hash, Hasher};
use tellur_core::geometry::Vec2;
use tellur_core::raster::{RasterImage, Resolution};
use tellur_core::render_context::RenderContext;
use tellur_core::time::TimelineTime;
use tellur_core::timeline::Timeline;
use tellur_core::timeline_component::{
resolve, resolve_with_canvas, Arrangement, AudioBuffer, Clock, NodeKind, ResolveError,
ResolvedTimeline, TimelineComponent,
};
#[doc(hidden)]
pub use tellur_core as __core;
pub const ENTRY_SYMBOL: &[u8] = b"tellur_timeline_collection_v4\0";
pub mod abi;
pub use abi::{
validate_plugin_fingerprint, AbiFingerprintFn, AbiMismatchError, ABI_FINGERPRINT,
ABI_FINGERPRINT_SYMBOL,
};
#[doc(hidden)]
#[macro_export]
macro_rules! __tellur_export_abi_fingerprint {
() => {
#[no_mangle]
pub extern "C" fn tellur_abi_fingerprint_v1() -> *const ::std::os::raw::c_char {
$crate::abi::ABI_FINGERPRINT_C.as_ptr().cast()
}
};
}
pub type EntryFn = unsafe extern "Rust" fn() -> Box<dyn TimelineCollection>;
#[derive(Debug, Clone, PartialEq)]
pub struct TimelineInfo {
pub id: String,
pub title: String,
pub duration: f32,
pub error: Option<String>,
}
pub trait TimelineCollection: Send {
fn timelines(&self) -> Vec<TimelineInfo>;
fn build(
&self,
id: &str,
t: TimelineTime,
target: Resolution,
ctx: &mut dyn RenderContext,
) -> Option<RasterImage>;
fn arrangement(&self, _id: &str) -> Option<Arrangement> {
None
}
fn render_audio(&self, _id: &str, _rate: u32, _channels: u16) -> Option<AudioBuffer> {
None
}
fn render_audio_window(
&self,
_id: &str,
_start: f32,
_duration: f32,
_rate: u32,
_channels: u16,
) -> Option<AudioBuffer> {
None
}
}
pub struct SingleTimeline {
id: &'static str,
title: &'static str,
resolved: Result<ResolvedTimeline, ResolveError>,
}
pub fn single_timeline<T: TimelineComponent + Send + 'static>(
id: &'static str,
title: &'static str,
root: T,
) -> SingleTimeline {
SingleTimeline {
id,
title,
resolved: resolve(root),
}
}
pub fn single_timeline_with_canvas<T: TimelineComponent + Send + 'static>(
id: &'static str,
title: &'static str,
root: T,
canvas: Vec2,
) -> SingleTimeline {
SingleTimeline {
id,
title,
resolved: resolve_with_canvas(root, canvas),
}
}
impl SingleTimeline {
fn resolved(&self) -> Option<&ResolvedTimeline> {
self.resolved.as_ref().ok()
}
}
impl TimelineCollection for SingleTimeline {
fn timelines(&self) -> Vec<TimelineInfo> {
let (duration, error) = match &self.resolved {
Ok(resolved) => (resolved.duration(), None),
Err(e) => (0.0, Some(e.to_string())),
};
vec![TimelineInfo {
id: self.id.to_owned(),
title: self.title.to_owned(),
duration,
error,
}]
}
fn build(
&self,
id: &str,
t: TimelineTime,
target: Resolution,
ctx: &mut dyn RenderContext,
) -> Option<RasterImage> {
if id != self.id {
return None;
}
self.resolved()?.frame(t, target, ctx)
}
fn arrangement(&self, id: &str) -> Option<Arrangement> {
if id != self.id {
return None;
}
Some(self.resolved()?.source().arrangement(0.0))
}
fn render_audio(&self, id: &str, rate: u32, channels: u16) -> Option<AudioBuffer> {
if id != self.id {
return None;
}
Some(self.resolved()?.render_audio(rate, channels))
}
fn render_audio_window(
&self,
id: &str,
start: f32,
duration: f32,
rate: u32,
channels: u16,
) -> Option<AudioBuffer> {
if id != self.id {
return None;
}
Some(
self.resolved()?
.render_audio_window(start, duration, rate, channels),
)
}
}
pub struct LegacyTimeline<T: Timeline + Send> {
timeline: T,
}
impl<T: Timeline + Send> LegacyTimeline<T> {
pub fn new(timeline: T) -> Self {
Self { timeline }
}
}
impl<T: Timeline + Send> PartialEq for LegacyTimeline<T> {
fn eq(&self, _other: &Self) -> bool {
true
}
}
impl<T: Timeline + Send> Eq for LegacyTimeline<T> {}
impl<T: Timeline + Send> Hash for LegacyTimeline<T> {
fn hash<H: Hasher>(&self, _state: &mut H) {
}
}
impl<T: Timeline + Send + 'static> TimelineComponent for LegacyTimeline<T> {
fn duration(&self) -> Option<f32> {
Some(self.timeline.duration())
}
fn frame(
&self,
clock: Clock<'_>,
canvas: Vec2,
target: Resolution,
ctx: &mut dyn RenderContext,
) -> Option<RasterImage> {
let _ = canvas;
Some(self.timeline.build(clock.global(), target, ctx))
}
fn arrangement(&self, offset: f32) -> Arrangement {
Arrangement {
kind: NodeKind::Video,
label: String::new(),
name: None,
source: None,
start: offset,
end: offset + self.timeline.duration(),
trim: None,
triggers: Vec::new(),
children: Vec::new(),
}
}
}
#[macro_export]
macro_rules! export_timeline {
($id:expr, $title:expr, $builder:path) => {
$crate::__tellur_export_abi_fingerprint!();
#[no_mangle]
pub extern "Rust" fn tellur_timeline_collection_v4(
) -> ::std::boxed::Box<dyn $crate::TimelineCollection> {
::std::boxed::Box::new($crate::single_timeline($id, $title, $builder()))
}
};
($id:expr, $title:expr, $builder:path, canvas = ($w:expr, $h:expr)) => {
$crate::__tellur_export_abi_fingerprint!();
#[no_mangle]
pub extern "Rust" fn tellur_timeline_collection_v4(
) -> ::std::boxed::Box<dyn $crate::TimelineCollection> {
::std::boxed::Box::new($crate::single_timeline_with_canvas(
$id,
$title,
$builder(),
$crate::__core::geometry::Vec2($w, $h),
))
}
};
}
#[macro_export]
macro_rules! export_legacy_timeline {
($id:expr, $title:expr, $builder:path) => {
$crate::__tellur_export_abi_fingerprint!();
#[no_mangle]
pub extern "Rust" fn tellur_timeline_collection_v4(
) -> ::std::boxed::Box<dyn $crate::TimelineCollection> {
::std::boxed::Box::new($crate::single_timeline(
$id,
$title,
$crate::LegacyTimeline::new($builder()),
))
}
};
}
#[macro_export]
macro_rules! export_timeline_collection {
($builder:path) => {
$crate::__tellur_export_abi_fingerprint!();
#[no_mangle]
pub extern "Rust" fn tellur_timeline_collection_v4(
) -> ::std::boxed::Box<dyn $crate::TimelineCollection> {
::std::boxed::Box::new($builder())
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use tellur_core::geometry::{Constraints, Vec2};
use tellur_core::raster::{PixelFormat, RasterComponent};
use tellur_core::timeline_component::Timed;
use tellur_core::timeline_container::Timeline as TimelineContainer;
#[derive(PartialEq, Hash)]
struct Dot;
impl RasterComponent for Dot {
fn layout(&self, _c: Constraints) -> Vec2 {
Vec2(1.0, 1.0)
}
fn render(&self, _s: Vec2, _t: Resolution, _ctx: &mut dyn RenderContext) -> RasterImage {
RasterImage::cpu(1, 1, PixelFormat::Rgba8, vec![0u8, 0, 0, 0])
}
}
fn build_new_api_timeline() -> TimelineContainer {
TimelineContainer::builder()
.child(Dot.at(0.0..2.0))
.child(Dot.at(0.0..3.0))
.build()
}
#[test]
fn single_timeline_surfaces_resolved_duration() {
let collection = single_timeline("main", "Main", build_new_api_timeline());
let infos = collection.timelines();
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].id, "main");
assert_eq!(infos[0].title, "Main");
assert_eq!(infos[0].duration, 3.0);
assert_eq!(infos[0].error, None);
}
#[test]
fn single_timeline_arrangement_walks_the_resolved_tree() {
let collection = single_timeline("main", "Main", build_new_api_timeline());
assert!(collection.arrangement("other").is_none());
let root = collection
.arrangement("main")
.expect("the resolved tree has an arrangement");
assert_eq!(root.kind, NodeKind::Timeline);
assert_eq!(root.start, 0.0);
assert_eq!(root.end, 3.0);
assert!(root.trim.is_none());
assert_eq!(root.children.len(), 2);
for child in &root.children {
assert_eq!(child.kind, NodeKind::Video);
assert!(child.children.is_empty());
}
}
#[test]
fn legacy_timeline_adapter_arrangement_is_one_video_span() {
let legacy = LegacyTimeline::new(tellur_core::timeline::timeline(
4.0,
|_t, target: Resolution, _ctx| {
RasterImage::cpu(
target.width,
target.height,
PixelFormat::Rgba8,
vec![0u8; (target.width * target.height * 4) as usize],
)
},
));
let collection = single_timeline("main", "Main", legacy);
assert_eq!(collection.timelines()[0].duration, 4.0);
let root = collection.arrangement("main").expect("resolves to a span");
assert_eq!(root.kind, NodeKind::Video);
assert_eq!(root.start, 0.0);
assert_eq!(root.end, 4.0);
assert!(root.children.is_empty());
}
}