Skip to main content

oxihuman_viewer/
timecode_overlay_view.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Timecode burn-in overlay for video production.
6
7/// Timecode format type.
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub enum TimecodeFormat {
10    Smpte24,
11    Smpte25,
12    Smpte2997,
13    Smpte30,
14    Frames,
15}
16
17/// Timecode overlay view configuration.
18#[derive(Debug, Clone)]
19pub struct TimecodeOverlayView {
20    pub format: TimecodeFormat,
21    pub hours: u32,
22    pub minutes: u32,
23    pub seconds: u32,
24    pub frames: u32,
25    pub position_x: f32,
26    pub position_y: f32,
27    pub text_scale: f32,
28    pub enabled: bool,
29}
30
31impl TimecodeOverlayView {
32    pub fn new() -> Self {
33        Self {
34            format: TimecodeFormat::Smpte24,
35            hours: 0,
36            minutes: 0,
37            seconds: 0,
38            frames: 0,
39            position_x: 0.05,
40            position_y: 0.05,
41            text_scale: 1.0,
42            enabled: true,
43        }
44    }
45}
46
47impl Default for TimecodeOverlayView {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53/// Create a new timecode overlay view.
54pub fn new_timecode_overlay_view() -> TimecodeOverlayView {
55    TimecodeOverlayView::new()
56}
57
58/// Set timecode from component values.
59pub fn tcv_set_timecode(view: &mut TimecodeOverlayView, hh: u32, mm: u32, ss: u32, ff: u32) {
60    view.hours = hh.min(23);
61    view.minutes = mm.min(59);
62    view.seconds = ss.min(59);
63    view.frames = ff;
64}
65
66/// Set timecode format.
67pub fn tcv_set_format(view: &mut TimecodeOverlayView, format: TimecodeFormat) {
68    view.format = format;
69}
70
71/// Set burn-in position in normalized viewport coords.
72pub fn tcv_set_position(view: &mut TimecodeOverlayView, x: f32, y: f32) {
73    view.position_x = x.clamp(0.0, 1.0);
74    view.position_y = y.clamp(0.0, 1.0);
75}
76
77/// Toggle timecode overlay visibility.
78pub fn tcv_set_enabled(view: &mut TimecodeOverlayView, enabled: bool) {
79    view.enabled = enabled;
80}
81
82/// Format timecode as standard HH:MM:SS:FF string.
83pub fn tcv_format_string(view: &TimecodeOverlayView) -> String {
84    format!(
85        "{:02}:{:02}:{:02}:{:02}",
86        view.hours, view.minutes, view.seconds, view.frames
87    )
88}
89
90/// Compute total frame count from timecode components.
91pub fn tcv_total_frames(view: &TimecodeOverlayView) -> u64 {
92    let fps = match view.format {
93        TimecodeFormat::Smpte24 => 24u64,
94        TimecodeFormat::Smpte25 => 25,
95        TimecodeFormat::Smpte2997 => 30,
96        TimecodeFormat::Smpte30 => 30,
97        TimecodeFormat::Frames => 1,
98    };
99    let total_secs =
100        (view.hours as u64) * 3600 + (view.minutes as u64) * 60 + (view.seconds as u64);
101    total_secs * fps + (view.frames as u64)
102}
103
104/// Serialize to JSON-like string.
105pub fn timecode_overlay_view_to_json(view: &TimecodeOverlayView) -> String {
106    format!(
107        r#"{{"timecode":"{}","enabled":{}}}"#,
108        tcv_format_string(view),
109        view.enabled
110    )
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_defaults() {
119        let v = new_timecode_overlay_view();
120        assert_eq!(v.hours, 0);
121        assert!(v.enabled);
122    }
123
124    #[test]
125    fn test_set_timecode() {
126        let mut v = new_timecode_overlay_view();
127        tcv_set_timecode(&mut v, 1, 30, 45, 12);
128        assert_eq!(v.hours, 1);
129        assert_eq!(v.frames, 12);
130    }
131
132    #[test]
133    fn test_minutes_clamp() {
134        let mut v = new_timecode_overlay_view();
135        tcv_set_timecode(&mut v, 0, 100, 0, 0);
136        assert_eq!(v.minutes, 59);
137    }
138
139    #[test]
140    fn test_format_string() {
141        let mut v = new_timecode_overlay_view();
142        tcv_set_timecode(&mut v, 1, 2, 3, 4);
143        assert_eq!(tcv_format_string(&v), "01:02:03:04");
144    }
145
146    #[test]
147    fn test_total_frames_zero() {
148        let v = new_timecode_overlay_view();
149        assert_eq!(tcv_total_frames(&v), 0);
150    }
151
152    #[test]
153    fn test_total_frames_one_second() {
154        let mut v = new_timecode_overlay_view();
155        tcv_set_timecode(&mut v, 0, 0, 1, 0);
156        assert_eq!(tcv_total_frames(&v), 24);
157    }
158
159    #[test]
160    fn test_position_clamp() {
161        let mut v = new_timecode_overlay_view();
162        tcv_set_position(&mut v, -1.0, 2.0);
163        assert_eq!(v.position_x, 0.0);
164        assert_eq!(v.position_y, 1.0);
165    }
166
167    #[test]
168    fn test_json() {
169        let v = new_timecode_overlay_view();
170        let s = timecode_overlay_view_to_json(&v);
171        assert!(s.contains("timecode"));
172    }
173
174    #[test]
175    fn test_clone() {
176        let v = new_timecode_overlay_view();
177        let v2 = v.clone();
178        assert_eq!(v2.format, v.format);
179    }
180}