diff --git a/src/lib_ccx/ccx_common_timing.c b/src/lib_ccx/ccx_common_timing.c index df2b1ff26..3513b5fb8 100644 --- a/src/lib_ccx/ccx_common_timing.c +++ b/src/lib_ccx/ccx_common_timing.c @@ -19,7 +19,7 @@ unsigned pts_big_change; // PTS timing related stuff -double current_fps = (double)30000.0 / 1001; /* 29.97 */ // TODO: Get from framerates_values[] instead +double current_fps = (double)30000.0 / 1001; /* 29.97 default, updated at runtime from stream data */ int frames_since_ref_time = 0; unsigned total_frames_count; @@ -122,7 +122,7 @@ size_t print_scc_time(struct ccx_boundary_time time, char *buf) // Format produces "HH:MM:SS;FF" = 11 chars + null, use 32 for safety const size_t max_time_len = 32; - frame = ((double)(time.time_in_ms - 1000 * (time.ss + 60 * (time.mm + 60 * time.hh))) * 29.97 / 1000); + frame = ((double)(time.time_in_ms - 1000 * (time.ss + 60 * (time.mm + 60 * time.hh))) * current_fps / 1000); return (size_t)snprintf(buf + time.set, max_time_len, fmt, time.hh, time.mm, time.ss, (unsigned)frame); } diff --git a/src/rust/lib_ccxr/src/time/units.rs b/src/rust/lib_ccxr/src/time/units.rs index 8676813c4..922b063a7 100644 --- a/src/rust/lib_ccxr/src/time/units.rs +++ b/src/rust/lib_ccxr/src/time/units.rs @@ -389,12 +389,21 @@ impl Timestamp { Ok(s) } - /// SCC time formatting - pub fn to_scc_time(&self) -> Result { + /// SCC time formatting using `fps` to compute the frame number. + /// + /// # Examples + /// ```rust + /// # use lib_ccxr::time::units::Timestamp; + /// let timestamp = Timestamp::from_millis(500); + /// assert_eq!(timestamp.to_scc_time(25.0).unwrap(), "00:00:00;12"); + /// ``` + pub fn to_scc_time(&self, fps: f64) -> Result { let mut result = String::new(); let (h, m, s, _) = self.as_hms_millis()?; - let frame = (self.millis - 1000 * (s + 60 * (m + 60 * h)) as i64) as f64 * 29.97 / 1000.0; + let secs = s as i64 + 60 * (m as i64 + 60 * h as i64); + let frame = (self.millis - 1000 * secs) as f64 * fps / 1000.0; + let frame = frame as u32; write!(result, "{h:02}:{m:02}:{s:02};{frame:02}")?; @@ -742,6 +751,24 @@ mod tests { assert!(ts.to_vtt_time().is_err()); } + #[test] + fn test_timestamp_to_scc_time() { + let ts = Timestamp::from_millis(5445500); + assert_eq!(ts.to_scc_time(29.97).unwrap(), "01:30:45;14"); + + // The frame number follows the given fps. + let ts = Timestamp::from_millis(960); + assert_eq!(ts.to_scc_time(25.0).unwrap(), "00:00:00;24"); + assert_eq!(ts.to_scc_time(29.97).unwrap(), "00:00:00;28"); + + // Frame numbers are truncated, never fractional. + let ts = Timestamp::from_millis(500); + assert_eq!(ts.to_scc_time(25.0).unwrap(), "00:00:00;12"); + + let ts = Timestamp::from_millis(-1); + assert!(ts.to_scc_time(29.97).is_err()); + } + #[test] fn test_timestamp_to_hms_millis_time() { let ts = Timestamp::from_millis(5445500); diff --git a/src/rust/src/decoder/timing.rs b/src/rust/src/decoder/timing.rs index 48a2766a4..cf63fc928 100644 --- a/src/rust/src/decoder/timing.rs +++ b/src/rust/src/decoder/timing.rs @@ -1,6 +1,6 @@ //! Utilty functions to get timing for captions -use crate::{bindings::*, cb_708, cb_field1, cb_field2}; +use crate::{bindings::*, cb_708, cb_field1, cb_field2, current_fps}; use log::{debug, error}; @@ -88,10 +88,11 @@ impl PartialEq for ccx_boundary_time { /// Returns a hh:mm:ss;frame string of time for SCC format pub fn get_scc_time_str(time: ccx_boundary_time) -> String { // Feel sorry for formatting:( + let fps = unsafe { current_fps }; let frame: u8 = (((time.time_in_ms - 1000 * ((time.ss as i64) + 60 * ((time.mm as i64) + 60 * (time.hh as i64)))) as f64) - * 29.97 + * fps / 1000.0) as u8; format!("{:02}:{:02}:{:02};{:02}", time.hh, time.mm, time.ss, frame) } @@ -246,6 +247,23 @@ mod test { ); } + #[test] + fn test_get_scc_time_str_frame_number() { + // In test builds current_fps is mocked to 30.0 (see crate::lib.rs), + // so 500 ms into a second is frame 15. The old hardcoded 29.97 would + // truncate to frame 14. + assert_eq!( + get_scc_time_str(ccx_boundary_time { + hh: 0, + mm: 0, + ss: 0, + time_in_ms: 500, + set: Default::default() + }), + "00:00:00;15" + ); + } + fn set_tmp_cb_values() { unsafe { cb_708 = 10; diff --git a/src/rust/src/decoder/tv_screen.rs b/src/rust/src/decoder/tv_screen.rs index d8c82b37b..1158d1c00 100644 --- a/src/rust/src/decoder/tv_screen.rs +++ b/src/rust/src/decoder/tv_screen.rs @@ -18,6 +18,7 @@ use super::timing::{get_scc_time_str, get_time_str}; use super::{CCX_DTVCC_SCREENGRID_COLUMNS, CCX_DTVCC_SCREENGRID_ROWS}; use crate::{ bindings::*, + current_fps, utils::{is_false, is_true}, }; use encoding_rs::Encoding; @@ -492,12 +493,14 @@ impl dtvcc_tv_screen { let mut buf = String::new(); let mut time_show = ccx_boundary_time::get_time(self.time_ms_show); let time_end = ccx_boundary_time::get_time(self.time_ms_hide); + // One frame duration in milliseconds (truncated like the C path) + let one_frame_ms = unsafe { (1000.0 / current_fps) as i64 }; // Caption overlapping situation match writer.old_cc_time_end.cmp(&(time_show.time_in_ms as i32)) { Ordering::Greater => { // Correct the frame delay - time_show.time_in_ms -= 1000 / 29.97 as i64; + time_show.time_in_ms -= one_frame_ms; buf.push_str(&(get_scc_time_str(time_show) + "\t942c 942c ").to_owned()); // Clear the buffer and start pop on caption buf.push_str("94ae 94ae 9420 9420"); @@ -507,12 +510,12 @@ impl dtvcc_tv_screen { let time_to_display = ccx_boundary_time::get_time(writer.old_cc_time_end as i64); buf.push_str(&(get_scc_time_str(time_to_display) + "\t942c 942c \n\n").to_owned()); // Correct the frame delay - time_show.time_in_ms -= 1000 / 29.97 as i64; + time_show.time_in_ms -= one_frame_ms; // Clear the buffer and start pop on caption in new time buf.push_str(&(get_scc_time_str(time_show) + "\t94ae 94ae 9420 9420").to_owned()); } Ordering::Equal => { - time_show.time_in_ms -= 1000 / 29.97 as i64; + time_show.time_in_ms -= one_frame_ms; buf.push_str( &(get_scc_time_str(time_show) + "\t942c 942c 94ae 94ae 9420 9420").to_owned(), ); @@ -781,6 +784,54 @@ mod test { assert!(screen.is_row_empty(1)); } + #[test] + #[cfg(unix)] + fn test_write_scc_timecode_uses_current_fps() { + // In test builds current_fps is mocked to 30.0 (see crate::lib.rs). + use std::io::{Read, Seek, SeekFrom}; + use std::os::unix::io::AsRawFd; + use tempfile::tempfile; + + let mut screen = get_zero_allocated_obj::(); + screen.time_ms_show = 500; + screen.time_ms_hide = 2000; + screen.chars[0][0] = dtvcc_symbol::new(0x41); + + let mut writer_ctx = get_zero_allocated_obj::(); + let transcript_settings = get_zero_allocated_obj::(); + let mut temp_file = tempfile().expect("Failed to create temp file"); + writer_ctx.fd = temp_file.as_raw_fd(); + writer_ctx.filename = std::ptr::null_mut(); + + let mut counter = 0u32; + let mut writer = Writer::new( + &mut counter, + 0, + ccx_output_format::CCX_OF_SCC, + &mut writer_ctx, + 0, + &transcript_settings, + 0, + b"\r\n", + ); + + screen + .write_scc(&mut writer) + .expect("write_scc should succeed"); + + temp_file.seek(SeekFrom::Start(0)).unwrap(); + let mut output = String::new(); + temp_file.read_to_string(&mut output).unwrap(); + + // 500 ms minus one frame (33 ms at 30 fps) leaves 467 ms => frame 14. + // The old hardcoded 29.97 math would produce frame 13 for both steps. + assert!( + output.starts_with("00:00:00;14"), + "unexpected SCC timecode: {:?}", + output + ); + } + #[test] #[cfg(unix)] fn test_writer_output_with_valid_fd() {