diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-07 01:09:13 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-07 01:09:13 +0200 |
| commit | e4b67094e757917588fb624a30aa046667403add (patch) | |
| tree | 55452692eac69a5a63b58f2a5cdb701dd7023d68 /rust/crates/time/src/time.rs | |
| parent | 79df73643185c02493364a077402726d1fa7b9ec (diff) | |
| download | opencut-e4b67094e757917588fb624a30aa046667403add.tar.gz | |
refactor: split effects and masks into dedicated rust crates, introduce MediaTime and FrameRate
Diffstat (limited to 'rust/crates/time/src/time.rs')
| -rw-r--r-- | rust/crates/time/src/time.rs | 439 |
1 files changed, 18 insertions, 421 deletions
diff --git a/rust/crates/time/src/time.rs b/rust/crates/time/src/time.rs index 84b69327..3d45c47f 100644 --- a/rust/crates/time/src/time.rs +++ b/rust/crates/time/src/time.rs @@ -1,422 +1,19 @@ -use bridge::export; -use serde::{Deserialize, Serialize}; +mod frame_rate; +mod media_time; +mod timecode; -const SECONDS_PER_HOUR: f64 = 3600.0; -const SECONDS_PER_MINUTE: f64 = 60.0; -const CENTISECONDS_PER_SECOND: f64 = 100.0; - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi, into_wasm_abi))] -#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)] -pub enum TimeCodeFormat { - #[serde(rename = "MM:SS")] - MmSs, - #[serde(rename = "HH:MM:SS")] - HhMmSs, - #[serde(rename = "HH:MM:SS:CS")] - HhMmSsCs, - #[serde(rename = "HH:MM:SS:FF")] - HhMmSsFf, -} - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RoundToFrameOptions { - pub time: f64, - pub fps: f64, -} - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FormatTimeCodeOptions { - pub time_in_seconds: f64, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub format: Option<TimeCodeFormat>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fps: Option<f64>, -} - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ParseTimeCodeOptions { - pub time_code: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub format: Option<TimeCodeFormat>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fps: Option<f64>, -} - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GuessTimeCodeFormatOptions { - pub time_code: String, -} - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TimeToFrameOptions { - pub time: f64, - pub fps: f64, -} - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FrameToTimeOptions { - pub frame: f64, - pub fps: f64, -} - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SnapTimeToFrameOptions { - pub time: f64, - pub fps: f64, -} - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetSnappedSeekTimeOptions { - pub raw_time: f64, - pub duration: f64, - pub fps: f64, -} - -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))] -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetLastFrameTimeOptions { - pub duration: f64, - pub fps: f64, -} - -#[export] -pub fn round_to_frame(RoundToFrameOptions { time, fps }: RoundToFrameOptions) -> f64 { - (time * fps).round() / fps -} - -#[export] -pub fn format_time_code( - FormatTimeCodeOptions { - time_in_seconds, - format, - fps, - }: FormatTimeCodeOptions, -) -> Option<String> { - let format = format.unwrap_or(TimeCodeFormat::HhMmSsCs); - let hours = (time_in_seconds / SECONDS_PER_HOUR).floor() as u64; - let minutes = ((time_in_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE).floor() as u64; - let seconds = (time_in_seconds % SECONDS_PER_MINUTE).floor() as u64; - let centiseconds = ((time_in_seconds % 1.0) * CENTISECONDS_PER_SECOND).floor() as u64; - - match format { - TimeCodeFormat::MmSs => Some(format!("{minutes:02}:{seconds:02}")), - TimeCodeFormat::HhMmSs => Some(format!("{hours:02}:{minutes:02}:{seconds:02}")), - TimeCodeFormat::HhMmSsCs => Some(format!( - "{hours:02}:{minutes:02}:{seconds:02}:{centiseconds:02}", - )), - TimeCodeFormat::HhMmSsFf => { - let fps = fps?; - if fps <= 0.0 { - return None; - } - - let frames = ((time_in_seconds % 1.0) * fps).floor() as u64; - Some(format!( - "{hours:02}:{minutes:02}:{seconds:02}:{frames:02}", - )) - } - } -} - -#[export] -pub fn parse_time_code( - ParseTimeCodeOptions { - time_code, - format, - fps, - }: ParseTimeCodeOptions, -) -> Option<f64> { - if time_code.trim().is_empty() { - return None; - } - - let format = format.unwrap_or(TimeCodeFormat::HhMmSsCs); - let parts = time_code - .trim() - .split(':') - .map(|part| part.parse::<u32>().ok()) - .collect::<Option<Vec<_>>>()?; - - match format { - TimeCodeFormat::MmSs => { - let [minutes, seconds] = parts.as_slice() else { - return None; - }; - if *seconds >= SECONDS_PER_MINUTE as u32 { - return None; - } - - Some((*minutes as f64 * SECONDS_PER_MINUTE) + *seconds as f64) - } - TimeCodeFormat::HhMmSs => { - let [hours, minutes, seconds] = parts.as_slice() else { - return None; - }; - if *minutes >= SECONDS_PER_MINUTE as u32 || *seconds >= SECONDS_PER_MINUTE as u32 { - return None; - } - - Some( - (*hours as f64 * SECONDS_PER_HOUR) - + (*minutes as f64 * SECONDS_PER_MINUTE) - + *seconds as f64, - ) - } - TimeCodeFormat::HhMmSsCs => { - let [hours, minutes, seconds, centiseconds] = parts.as_slice() else { - return None; - }; - if *minutes >= SECONDS_PER_MINUTE as u32 - || *seconds >= SECONDS_PER_MINUTE as u32 - || *centiseconds >= CENTISECONDS_PER_SECOND as u32 - { - return None; - } - - Some( - (*hours as f64 * SECONDS_PER_HOUR) - + (*minutes as f64 * SECONDS_PER_MINUTE) - + *seconds as f64 - + (*centiseconds as f64 / CENTISECONDS_PER_SECOND), - ) - } - TimeCodeFormat::HhMmSsFf => { - let fps = fps?; - if fps <= 0.0 { - return None; - } - - let [hours, minutes, seconds, frames] = parts.as_slice() else { - return None; - }; - if *minutes >= SECONDS_PER_MINUTE as u32 - || *seconds >= SECONDS_PER_MINUTE as u32 - || *frames as f64 >= fps - { - return None; - } - - Some( - (*hours as f64 * SECONDS_PER_HOUR) - + (*minutes as f64 * SECONDS_PER_MINUTE) - + *seconds as f64 - + (*frames as f64 / fps), - ) - } - } -} - -#[export] -pub fn guess_time_code_format( - GuessTimeCodeFormatOptions { time_code }: GuessTimeCodeFormatOptions, -) -> Option<TimeCodeFormat> { - if time_code.trim().is_empty() { - return None; - } - - let part_count = time_code - .split(':') - .try_fold(0usize, |count, part| { - part.parse::<u32>().ok().map(|_| count + 1) - })?; - - match part_count { - 2 => Some(TimeCodeFormat::MmSs), - 3 => Some(TimeCodeFormat::HhMmSs), - 4 => Some(TimeCodeFormat::HhMmSsFf), - _ => None, - } -} - -#[export] -pub fn time_to_frame(TimeToFrameOptions { time, fps }: TimeToFrameOptions) -> f64 { - (time * fps).round() -} - -#[export] -pub fn frame_to_time(FrameToTimeOptions { frame, fps }: FrameToTimeOptions) -> f64 { - frame / fps -} - -#[export] -pub fn snap_time_to_frame(SnapTimeToFrameOptions { time, fps }: SnapTimeToFrameOptions) -> f64 { - if fps <= 0.0 { - return time; - } - - frame_to_time(FrameToTimeOptions { - frame: time_to_frame(TimeToFrameOptions { time, fps }), - fps, - }) -} - -#[export] -pub fn get_snapped_seek_time( - GetSnappedSeekTimeOptions { - raw_time, - duration, - fps, - }: GetSnappedSeekTimeOptions, -) -> f64 { - let snapped_time = snap_time_to_frame(SnapTimeToFrameOptions { time: raw_time, fps }); - let last_frame = get_last_frame_time(GetLastFrameTimeOptions { duration, fps }); - snapped_time.clamp(0.0, last_frame) -} - -#[export] -pub fn get_last_frame_time( - GetLastFrameTimeOptions { duration, fps }: GetLastFrameTimeOptions, -) -> f64 { - if duration <= 0.0 { - return 0.0; - } - - if fps <= 0.0 { - return duration; - } - - let frame_offset = 1.0 / fps; - (duration - frame_offset).max(0.0) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rounds_to_the_nearest_frame() { - assert_eq!(round_to_frame(RoundToFrameOptions { time: 1.24, fps: 10.0 }), 1.2); - assert_eq!(round_to_frame(RoundToFrameOptions { time: 1.26, fps: 10.0 }), 1.3); - } - - #[test] - fn formats_default_time_codes() { - assert_eq!( - format_time_code(FormatTimeCodeOptions { - time_in_seconds: 3723.45, - format: None, - fps: None, - }), - Some("01:02:03:44".to_string()), - ); - assert_eq!( - format_time_code(FormatTimeCodeOptions { - time_in_seconds: 65.0, - format: Some(TimeCodeFormat::MmSs), - fps: None, - }), - Some("01:05".to_string()), - ); - } - - #[test] - fn formats_frame_based_time_codes() { - assert_eq!( - format_time_code(FormatTimeCodeOptions { - time_in_seconds: 1.5, - format: Some(TimeCodeFormat::HhMmSsFf), - fps: Some(30.0), - }), - Some("00:00:01:15".to_string()), - ); - assert_eq!( - format_time_code(FormatTimeCodeOptions { - time_in_seconds: 1.5, - format: Some(TimeCodeFormat::HhMmSsFf), - fps: None, - }), - None, - ); - } - - #[test] - fn parses_time_codes() { - assert_eq!( - parse_time_code(ParseTimeCodeOptions { - time_code: "01:05".to_string(), - format: Some(TimeCodeFormat::MmSs), - fps: None, - }), - Some(65.0), - ); - assert_eq!( - parse_time_code(ParseTimeCodeOptions { - time_code: "00:00:01:15".to_string(), - format: Some(TimeCodeFormat::HhMmSsFf), - fps: Some(30.0), - }), - Some(1.5), - ); - assert_eq!( - parse_time_code(ParseTimeCodeOptions { - time_code: "00:00:01:30".to_string(), - format: Some(TimeCodeFormat::HhMmSsFf), - fps: Some(30.0), - }), - None, - ); - } - - #[test] - fn guesses_time_code_formats() { - assert_eq!( - guess_time_code_format(GuessTimeCodeFormatOptions { time_code: "01:05".to_string() }), - Some(TimeCodeFormat::MmSs), - ); - assert_eq!( - guess_time_code_format(GuessTimeCodeFormatOptions { - time_code: "00:00:01".to_string(), - }), - Some(TimeCodeFormat::HhMmSs), - ); - assert_eq!( - guess_time_code_format(GuessTimeCodeFormatOptions { - time_code: "00:00:01:15".to_string(), - }), - Some(TimeCodeFormat::HhMmSsFf), - ); - } - - #[test] - fn snaps_and_clamps_seek_time() { - assert_eq!(time_to_frame(TimeToFrameOptions { time: 1.26, fps: 10.0 }), 13.0); - assert_eq!(frame_to_time(FrameToTimeOptions { frame: 13.0, fps: 10.0 }), 1.3); - assert_eq!(snap_time_to_frame(SnapTimeToFrameOptions { time: 1.26, fps: 10.0 }), 1.3); - assert_eq!(get_last_frame_time(GetLastFrameTimeOptions { duration: 10.0, fps: 5.0 }), 9.8); - assert_eq!( - get_snapped_seek_time(GetSnappedSeekTimeOptions { - raw_time: 10.0, - duration: 10.0, - fps: 5.0, - }), - 9.8, - ); - } -} +pub use frame_rate::FrameRate; +pub use media_time::{ + FloorToFrameOptions, IsFrameAlignedOptions, LastFrameTimeOptions, MediaTime, + MediaTimeAddOptions, MediaTimeClampOptions, MediaTimeFromFrameOptions, + MediaTimeFromSecondsOptions, MediaTimeMaxOptions, MediaTimeMinOptions, MediaTimeSubOptions, + MediaTimeToFrameOptions, MediaTimeToSecondsOptions, RoundToFrameOptions, + SnappedSeekTimeOptions, TICKS_PER_SECOND, floor_to_frame, is_frame_aligned, last_frame_time, + media_time_add, media_time_clamp, media_time_from_frame, media_time_from_seconds, + media_time_max, media_time_min, media_time_sub, media_time_to_frame, media_time_to_seconds, + round_to_frame, snapped_seek_time, +}; +pub use timecode::{ + FormatTimecodeOptions, GuessTimecodeFormatOptions, ParseTimecodeOptions, TimeCodeFormat, + format_timecode, guess_timecode_format, parse_timecode, +}; |
