From dcf54c1250431f5a4a2848f7c07781ef5eebacaf Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 31 Mar 2026 04:03:52 +0200 Subject: feat: migrate time utilities from typescript to rust WASM --- rust/crates/time/Cargo.toml | 4 +- rust/crates/time/src/time.rs | 298 ++++++++++++++++++++++++++++++++----------- 2 files changed, 226 insertions(+), 76 deletions(-) (limited to 'rust/crates') diff --git a/rust/crates/time/Cargo.toml b/rust/crates/time/Cargo.toml index 08a93224..bf66e4e2 100644 --- a/rust/crates/time/Cargo.toml +++ b/rust/crates/time/Cargo.toml @@ -9,7 +9,9 @@ crate-type = ["rlib", "cdylib"] [dependencies] bridge = { version = "0.1.0", path = "../bridge" } +serde = { version = "1", features = ["derive"] } +tsify-next = { version = "0.5", optional = true } wasm-bindgen = { version = "0.2.115", optional = true } [features] -wasm = ["dep:wasm-bindgen"] \ No newline at end of file +wasm = ["dep:wasm-bindgen", "dep:tsify-next", "tsify-next/js"] \ No newline at end of file diff --git a/rust/crates/time/src/time.rs b/rust/crates/time/src/time.rs index 222bb712..84b69327 100644 --- a/rust/crates/time/src/time.rs +++ b/rust/crates/time/src/time.rs @@ -1,52 +1,129 @@ use bridge::export; +use serde::{Deserialize, Serialize}; -const DEFAULT_TIME_CODE_FORMAT: &str = "HH:MM:SS:CS"; +const SECONDS_PER_HOUR: f64 = 3600.0; +const SECONDS_PER_MINUTE: f64 = 60.0; +const CENTISECONDS_PER_SECOND: f64 = 100.0; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum TimeCodeFormat { +#[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, } -impl TimeCodeFormat { - fn parse(format: Option<&str>) -> Option { - match format.unwrap_or(DEFAULT_TIME_CODE_FORMAT) { - "MM:SS" => Some(Self::MmSs), - "HH:MM:SS" => Some(Self::HhMmSs), - "HH:MM:SS:CS" => Some(Self::HhMmSsCs), - "HH:MM:SS:FF" => Some(Self::HhMmSsFf), - _ => None, - } - } +#[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, +} - fn as_str(self) -> &'static str { - match self { - Self::MmSs => "MM:SS", - Self::HhMmSs => "HH:MM:SS", - Self::HhMmSsCs => "HH:MM:SS:CS", - Self::HhMmSsFf => "HH:MM:SS:FF", - } - } +#[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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fps: Option, +} + +#[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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fps: Option, +} + +#[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(time: f64, fps: f64) -> f64 { +pub fn round_to_frame(RoundToFrameOptions { time, fps }: RoundToFrameOptions) -> f64 { (time * fps).round() / fps } #[export] pub fn format_time_code( - time_in_seconds: f64, - format: Option, - fps: Option, + FormatTimeCodeOptions { + time_in_seconds, + format, + fps, + }: FormatTimeCodeOptions, ) -> Option { - let format = TimeCodeFormat::parse(format.as_deref())?; - let hours = (time_in_seconds / 3600.0).floor() as u64; - let minutes = ((time_in_seconds % 3600.0) / 60.0).floor() as u64; - let seconds = (time_in_seconds % 60.0).floor() as u64; - let centiseconds = ((time_in_seconds % 1.0) * 100.0).floor() as u64; + 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}")), @@ -69,12 +146,18 @@ pub fn format_time_code( } #[export] -pub fn parse_time_code(time_code: &str, format: Option, fps: Option) -> Option { +pub fn parse_time_code( + ParseTimeCodeOptions { + time_code, + format, + fps, + }: ParseTimeCodeOptions, +) -> Option { if time_code.trim().is_empty() { return None; } - let format = TimeCodeFormat::parse(format.as_deref())?; + let format = format.unwrap_or(TimeCodeFormat::HhMmSsCs); let parts = time_code .trim() .split(':') @@ -86,35 +169,42 @@ pub fn parse_time_code(time_code: &str, format: Option, fps: Option let [minutes, seconds] = parts.as_slice() else { return None; }; - if *seconds >= 60 { + if *seconds >= SECONDS_PER_MINUTE as u32 { return None; } - Some((*minutes as f64 * 60.0) + *seconds as f64) + Some((*minutes as f64 * SECONDS_PER_MINUTE) + *seconds as f64) } TimeCodeFormat::HhMmSs => { let [hours, minutes, seconds] = parts.as_slice() else { return None; }; - if *minutes >= 60 || *seconds >= 60 { + if *minutes >= SECONDS_PER_MINUTE as u32 || *seconds >= SECONDS_PER_MINUTE as u32 { return None; } - Some((*hours as f64 * 3600.0) + (*minutes as f64 * 60.0) + *seconds as f64) + 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 >= 60 || *seconds >= 60 || *centiseconds >= 100 { + 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 * 3600.0) - + (*minutes as f64 * 60.0) + (*hours as f64 * SECONDS_PER_HOUR) + + (*minutes as f64 * SECONDS_PER_MINUTE) + *seconds as f64 - + (*centiseconds as f64 / 100.0), + + (*centiseconds as f64 / CENTISECONDS_PER_SECOND), ) } TimeCodeFormat::HhMmSsFf => { @@ -126,13 +216,16 @@ pub fn parse_time_code(time_code: &str, format: Option, fps: Option let [hours, minutes, seconds, frames] = parts.as_slice() else { return None; }; - if *minutes >= 60 || *seconds >= 60 || *frames as f64 >= fps { + if *minutes >= SECONDS_PER_MINUTE as u32 + || *seconds >= SECONDS_PER_MINUTE as u32 + || *frames as f64 >= fps + { return None; } Some( - (*hours as f64 * 3600.0) - + (*minutes as f64 * 60.0) + (*hours as f64 * SECONDS_PER_HOUR) + + (*minutes as f64 * SECONDS_PER_MINUTE) + *seconds as f64 + (*frames as f64 / fps), ) @@ -141,7 +234,9 @@ pub fn parse_time_code(time_code: &str, format: Option, fps: Option } #[export] -pub fn guess_time_code_format(time_code: &str) -> Option { +pub fn guess_time_code_format( + GuessTimeCodeFormatOptions { time_code }: GuessTimeCodeFormatOptions, +) -> Option { if time_code.trim().is_empty() { return None; } @@ -153,41 +248,52 @@ pub fn guess_time_code_format(time_code: &str) -> Option { })?; match part_count { - 2 => Some(TimeCodeFormat::MmSs.as_str().to_string()), - 3 => Some(TimeCodeFormat::HhMmSs.as_str().to_string()), - 4 => Some(TimeCodeFormat::HhMmSsFf.as_str().to_string()), + 2 => Some(TimeCodeFormat::MmSs), + 3 => Some(TimeCodeFormat::HhMmSs), + 4 => Some(TimeCodeFormat::HhMmSsFf), _ => None, } } #[export] -pub fn time_to_frame(time: f64, fps: f64) -> f64 { +pub fn time_to_frame(TimeToFrameOptions { time, fps }: TimeToFrameOptions) -> f64 { (time * fps).round() } #[export] -pub fn frame_to_time(frame: f64, fps: f64) -> f64 { +pub fn frame_to_time(FrameToTimeOptions { frame, fps }: FrameToTimeOptions) -> f64 { frame / fps } #[export] -pub fn snap_time_to_frame(time: f64, fps: f64) -> f64 { +pub fn snap_time_to_frame(SnapTimeToFrameOptions { time, fps }: SnapTimeToFrameOptions) -> f64 { if fps <= 0.0 { return time; } - frame_to_time(time_to_frame(time, fps), fps) + frame_to_time(FrameToTimeOptions { + frame: time_to_frame(TimeToFrameOptions { time, fps }), + fps, + }) } #[export] -pub fn get_snapped_seek_time(raw_time: f64, duration: f64, fps: f64) -> f64 { - let snapped_time = snap_time_to_frame(raw_time, fps); - let last_frame = get_last_frame_time(duration, fps); +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(duration: f64, fps: f64) -> f64 { +pub fn get_last_frame_time( + GetLastFrameTimeOptions { duration, fps }: GetLastFrameTimeOptions, +) -> f64 { if duration <= 0.0 { return 0.0; } @@ -206,18 +312,26 @@ mod tests { #[test] fn rounds_to_the_nearest_frame() { - assert_eq!(round_to_frame(1.24, 10.0), 1.2); - assert_eq!(round_to_frame(1.26, 10.0), 1.3); + 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(3723.45, None, None), - Some("01:02:03:44".to_string()), + 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(65.0, Some("MM:SS".to_string()), None), + format_time_code(FormatTimeCodeOptions { + time_in_seconds: 65.0, + format: Some(TimeCodeFormat::MmSs), + fps: None, + }), Some("01:05".to_string()), ); } @@ -225,11 +339,19 @@ mod tests { #[test] fn formats_frame_based_time_codes() { assert_eq!( - format_time_code(1.5, Some("HH:MM:SS:FF".to_string()), Some(30.0)), + 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(1.5, Some("HH:MM:SS:FF".to_string()), None), + format_time_code(FormatTimeCodeOptions { + time_in_seconds: 1.5, + format: Some(TimeCodeFormat::HhMmSsFf), + fps: None, + }), None, ); } @@ -237,38 +359,64 @@ mod tests { #[test] fn parses_time_codes() { assert_eq!( - parse_time_code("01:05", Some("MM:SS".to_string()), None), + parse_time_code(ParseTimeCodeOptions { + time_code: "01:05".to_string(), + format: Some(TimeCodeFormat::MmSs), + fps: None, + }), Some(65.0), ); assert_eq!( - parse_time_code("00:00:01:15", Some("HH:MM:SS:FF".to_string()), Some(30.0)), + 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("00:00:01:30", Some("HH:MM:SS:FF".to_string()), Some(30.0)), + 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("01:05"), Some("MM:SS".to_string())); assert_eq!( - guess_time_code_format("00:00:01"), - Some("HH:MM:SS".to_string()), + 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("00:00:01:15"), - Some("HH:MM:SS:FF".to_string()), + 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(1.26, 10.0), 13.0); - assert_eq!(frame_to_time(13.0, 10.0), 1.3); - assert_eq!(snap_time_to_frame(1.26, 10.0), 1.3); - assert_eq!(get_last_frame_time(10.0, 5.0), 9.8); - assert_eq!(get_snapped_seek_time(10.0, 10.0, 5.0), 9.8); + 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, + ); } } -- cgit v1.3.1