summaryrefslogtreecommitdiff
path: root/rust/crates/time/src/time.rs
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-07 01:09:13 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-07 01:09:13 +0200
commite4b67094e757917588fb624a30aa046667403add (patch)
tree55452692eac69a5a63b58f2a5cdb701dd7023d68 /rust/crates/time/src/time.rs
parent79df73643185c02493364a077402726d1fa7b9ec (diff)
downloadopencut-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.rs439
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,
+};