summaryrefslogtreecommitdiff
path: root/rust/crates/time/src/time.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rust/crates/time/src/time.rs')
-rw-r--r--rust/crates/time/src/time.rs298
1 files changed, 223 insertions, 75 deletions
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<Self> {
- 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<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(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<String>,
- fps: Option<f64>,
+ FormatTimeCodeOptions {
+ time_in_seconds,
+ format,
+ fps,
+ }: FormatTimeCodeOptions,
) -> Option<String> {
- 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<String>, fps: Option<f64>) -> Option<f64> {
+pub fn parse_time_code(
+ ParseTimeCodeOptions {
+ time_code,
+ format,
+ fps,
+ }: ParseTimeCodeOptions,
+) -> Option<f64> {
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<String>, fps: Option<f64>
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<String>, fps: Option<f64>
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<String>, fps: Option<f64>
}
#[export]
-pub fn guess_time_code_format(time_code: &str) -> Option<String> {
+pub fn guess_time_code_format(
+ GuessTimeCodeFormatOptions { time_code }: GuessTimeCodeFormatOptions,
+) -> Option<TimeCodeFormat> {
if time_code.trim().is_empty() {
return None;
}
@@ -153,41 +248,52 @@ pub fn guess_time_code_format(time_code: &str) -> Option<String> {
})?;
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,
+ );
}
}