diff --git a/src/lib_ccx/ccx_common_timing.c b/src/lib_ccx/ccx_common_timing.c index 1fd123cb..b02151bd 100644 --- a/src/lib_ccx/ccx_common_timing.c +++ b/src/lib_ccx/ccx_common_timing.c @@ -30,6 +30,18 @@ int gop_rollover = 0; struct ccx_common_timing_settings_t ccx_common_timing_settings; +#ifndef DISABLE_RUST +void ccxr_add_current_pts(struct ccx_common_timing_ctx *ctx, LLONG pts); +void ccxr_set_current_pts(struct ccx_common_timing_ctx *ctx, LLONG pts); +int ccxr_set_fts(struct ccx_common_timing_ctx *ctx); +LLONG ccxr_get_fts(struct ccx_common_timing_ctx *ctx, int current_field); +LLONG ccxr_get_fts_max(struct ccx_common_timing_ctx *ctx); +char *ccxr_print_mstime_static(LLONG mstime, char *buf); +void ccxr_print_debug_timing(struct ccx_common_timing_ctx *ctx); +void ccxr_calculate_ms_gop_time(struct gop_time_code *g); +int ccxr_gop_accepted(struct gop_time_code *g); +#endif + void ccx_common_timing_init(LLONG *file_position, int no_sync) { ccx_common_timing_settings.disable_sync_check = 0; @@ -73,11 +85,18 @@ struct ccx_common_timing_ctx *init_timing_ctx(struct ccx_common_timing_settings_ void add_current_pts(struct ccx_common_timing_ctx *ctx, LLONG pts) { +#ifndef DISABLE_RUST + return ccxr_add_current_pts(ctx, pts); +#endif + set_current_pts(ctx, ctx->current_pts + pts); } void set_current_pts(struct ccx_common_timing_ctx *ctx, LLONG pts) { +#ifndef DISABLE_RUST + return ccxr_set_current_pts(ctx, pts); +#endif LLONG prev_pts = ctx->current_pts; ctx->current_pts = pts; if (ctx->pts_set == 0) @@ -95,6 +114,9 @@ void set_current_pts(struct ccx_common_timing_ctx *ctx, LLONG pts) int set_fts(struct ccx_common_timing_ctx *ctx) { +#ifndef DISABLE_RUST + return ccxr_set_fts(ctx); +#endif int pts_jump = 0; // ES don't have PTS unless GOP timing is used @@ -266,6 +288,10 @@ int set_fts(struct ccx_common_timing_ctx *ctx) LLONG get_fts(struct ccx_common_timing_ctx *ctx, int current_field) { +#ifndef DISABLE_RUST + return ccxr_get_fts(ctx, current_field); +#endif + LLONG fts; switch (current_field) @@ -290,6 +316,10 @@ LLONG get_fts(struct ccx_common_timing_ctx *ctx, int current_field) LLONG get_fts_max(struct ccx_common_timing_ctx *ctx) { +#ifndef DISABLE_RUST + return ccxr_get_fts_max(ctx); +#endif + // This returns the maximum FTS that belonged to a frame. Caption block // counters are not applicable. return ctx->fts_max + ctx->fts_global; @@ -350,6 +380,11 @@ size_t print_mstime_buff(LLONG mstime, char *fmt, char *buf) char *print_mstime_static(LLONG mstime) { static char buf[15]; // 14 should be long enough + +#ifndef DISABLE_RUST + return ccxr_print_mstime_static(mstime, buf); +#endif + print_mstime_buff(mstime, "%02u:%02u:%02u:%03u", buf); return buf; } @@ -357,6 +392,10 @@ char *print_mstime_static(LLONG mstime) /* Helper function for to display debug timing info. */ void print_debug_timing(struct ccx_common_timing_ctx *ctx) { +#ifndef DISABLE_RUST + return ccxr_print_debug_timing(ctx); +#endif + // Avoid wrong "Calc. difference" and "Asynchronous by" numbers // for uninitialized min_pts LLONG tempmin_pts = (ctx->min_pts == 0x01FFFFFFFFLL ? ctx->sync_pts : ctx->min_pts); @@ -383,6 +422,10 @@ void print_debug_timing(struct ccx_common_timing_ctx *ctx) void calculate_ms_gop_time(struct gop_time_code *g) { +#ifndef DISABLE_RUST + return ccxr_calculate_ms_gop_time(g); +#endif + int seconds = (g->time_code_hours * 3600) + (g->time_code_minutes * 60) + g->time_code_seconds; g->ms = (LLONG)(1000 * (seconds + g->time_code_pictures / current_fps)); if (gop_rollover) @@ -391,6 +434,10 @@ void calculate_ms_gop_time(struct gop_time_code *g) int gop_accepted(struct gop_time_code *g) { +#ifndef DISABLE_RUST + return ccxr_gop_accepted(g); +#endif + if (!((g->time_code_hours <= 23) && (g->time_code_minutes <= 59) && (g->time_code_seconds <= 59) && (g->time_code_pictures <= 59))) return 0; diff --git a/src/rust/build.rs b/src/rust/build.rs index 9429cdb1..85c40174 100644 --- a/src/rust/build.rs +++ b/src/rust/build.rs @@ -31,6 +31,7 @@ fn main() { "ccx_output_format", "ccx_boundary_time", "gop_time_code", + "ccx_common_timing_settings_t", "ccx_s_options", "ccx_s_teletext_config", "ccx_output_date_format", diff --git a/src/rust/lib_ccxr/src/common/options.rs b/src/rust/lib_ccxr/src/common/options.rs index 63fbcbb9..489605c4 100644 --- a/src/rust/lib_ccxr/src/common/options.rs +++ b/src/rust/lib_ccxr/src/common/options.rs @@ -38,7 +38,7 @@ impl Default for EncodersTranscriptFormat { } } -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone, PartialEq)] pub enum FrameType { #[default] ResetOrUnknown = 0, diff --git a/src/rust/lib_ccxr/src/time/c_functions.rs b/src/rust/lib_ccxr/src/time/c_functions.rs new file mode 100644 index 00000000..1f3180ee --- /dev/null +++ b/src/rust/lib_ccxr/src/time/c_functions.rs @@ -0,0 +1,61 @@ +//! Provides Rust equivalent for functions in C. Uses Rust-native types as input and output. + +use super::*; + +/// Rust equivalent for `add_current_pts` function in C. Uses Rust-native types as input and output. +pub fn add_current_pts(ctx: &mut TimingContext, pts: MpegClockTick) { + ctx.add_current_pts(pts) +} + +/// Rust equivalent for `set_current_pts` function in C. Uses Rust-native types as input and output. +pub fn set_current_pts(ctx: &mut TimingContext, pts: MpegClockTick) { + ctx.set_current_pts(pts) +} + +/// Rust equivalent for `set_fts` function in C. Uses Rust-native types as input and output. +pub fn set_fts(ctx: &mut TimingContext) -> bool { + ctx.set_fts() +} + +/// Rust equivalent for `get_fts` function in C. Uses Rust-native types as input and output. +pub fn get_fts(ctx: &mut TimingContext, current_field: CaptionField) -> Timestamp { + ctx.get_fts(current_field) +} + +/// Rust equivalent for `get_fts_max` function in C. Uses Rust-native types as input and output. +pub fn get_fts_max(ctx: &mut TimingContext) -> Timestamp { + ctx.get_fts_max() +} + +/// Rust equivalent for `print_mstime_static` function in C. Uses Rust-native types as input and output. +pub fn print_mstime_static(mstime: Timestamp, sep: char) -> String { + mstime.to_hms_millis_time(sep).unwrap() +} + +/// Rust equivalent for `print_debug_timing` function in C. Uses Rust-native types as input and output. +pub fn print_debug_timing(ctx: &mut TimingContext) { + ctx.print_debug_timing() +} + +/// Rust equivalent for `calculate_ms_gop_time` function in C. Uses Rust-native types as input and output. +pub fn calculate_ms_gop_time(g: GopTimeCode) -> Timestamp { + g.timestamp() +} + +/// Rust equivalent for `gop_accepted` function in C. Uses Rust-native types as input and output. +pub fn gop_accepted(g: GopTimeCode) -> bool { + let mut timing_info = GLOBAL_TIMING_INFO.write().unwrap(); + + let gop_time = if let Some(gt) = timing_info.gop_time { + gt + } else { + return true; + }; + + if g.did_rollover(&gop_time) { + timing_info.gop_rollover = true; + true + } else { + gop_time.timestamp() <= g.timestamp() + } +} diff --git a/src/rust/lib_ccxr/src/time/mod.rs b/src/rust/lib_ccxr/src/time/mod.rs index 9cd41dac..1906271d 100644 --- a/src/rust/lib_ccxr/src/time/mod.rs +++ b/src/rust/lib_ccxr/src/time/mod.rs @@ -1,4 +1,4 @@ -//! Provide types for storing time in different formats +//! Provide types for storing time in different formats and manage timing information while decoding. //! //! Time can be represented in one of following formats: //! - [`Timestamp`] as number of milliseconds @@ -6,6 +6,9 @@ //! - [`FrameCount`] as number of frames //! - [`GopTimeCode`] as a GOP time code (as defined in the MPEG standard) //! +//! [`GLOBAL_TIMING_INFO`] and [`TimingContext`] are used for managing time-related information +//! during the deocoding process. +//! //! # Conversion Guide //! //! | From | To | @@ -16,8 +19,27 @@ //! | any pts | [`MpegClockTick`] | //! | any frame count | [`FrameCount`] | //! | `gop_time_code` | [`GopTimeCode`] | +//! | `current_field` | [`CaptionField`] | +//! | `ccx_common_timing_ctx.pts_set` | [`PtsSet`] | +//! | `ccx_common_timing_settings_t` | [`TimingSettings`] | +//! | `ccx_common_timing_ctx` | [`TimingContext`] | +//! | `init_timing_ctx` | [`TimingContext::new`] | +//! | `add_current_pts` | [`TimingContext::add_current_pts`] | +//! | `set_current_pts` | [`TimingContext::set_current_pts`] | +//! | `set_fts` | [`TimingContext::set_fts`] | +//! | `get_fts` | [`TimingContext::get_fts`] | +//! | `get_fts_max` | [`TimingContext::get_fts_max`] | +//! | `print_mstime_buff` | [`Timestamp::write_hms_millis_time`] | //! | `print_mstime_static` | [`Timestamp::to_hms_millis_time`] | +//! | `print_scc_time` | [`Timestamp::to_scc_time`] | +//! | `print_debug_timing` | [`TimingContext::print_debug_timing`] | //! | `gop_accepted` | [`GopTimeCode::did_rollover`] + some additional logic | //! | `calculate_ms_gop_time` | [`GopTimeCode::new`], [`GopTimeCode::timestamp`] | +//! | `cb_708`, `cb_field1`, `cb_field2`, `pts_big_change`, `current_fps`, `frames_since_ref_time`, `total_frames_count`, `gop_time`, `first_gop_time`, `fts_at_gop_start`, `gop_rollover`, `ccx_common_timing_settings` | [`GlobalTimingInfo`], [`GLOBAL_TIMING_INFO`] | +pub mod c_functions; +pub mod timing; pub mod units; + +pub use timing::*; +pub use units::*; diff --git a/src/rust/lib_ccxr/src/time/timing.rs b/src/rust/lib_ccxr/src/time/timing.rs new file mode 100644 index 00000000..564b6163 --- /dev/null +++ b/src/rust/lib_ccxr/src/time/timing.rs @@ -0,0 +1,559 @@ +use crate::common::{FrameType, FRAMERATES_VALUES}; +use crate::time::units::{FrameCount, GopTimeCode, MpegClockTick, Timestamp}; +use crate::util::log::{debug, info, DebugMessageFlag}; +use std::sync::RwLock; + +/// The maximum allowed difference between [`TimingContext::current_pts`] and [`TimingContext::sync_pts`] in seconds. +/// +/// If the difference crosses this value, a PTS jump has occured and is handled accordingly. +const MAX_DIF: i64 = 5; + +/// A unique global instance of [`GlobalTimingInfo`] to be used throughout the program. +pub static GLOBAL_TIMING_INFO: RwLock = RwLock::new(GlobalTimingInfo::new()); + +pub const DEFAULT_FRAME_RATE: f64 = 30000.0 / 1001.0; + +/// Represents the status of [`TimingContext::current_pts`] and [`TimingContext::min_pts`] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PtsSet { + No = 0, + Received = 1, + MinPtsSet = 2, +} + +/// Represent the field of 608 or 708 caption. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum CaptionField { + Field1, + Field2, + Cea708, +} + +/// A collective struct for storing timing-related information when decoding a file. +/// +/// [`GlobalTimingInfo`] serves a similar purpose. The only difference is that its lifetime is +/// global. +#[derive(Debug)] +pub struct TimingContext { + pub pts_set: PtsSet, + /// if true then don't adjust again. + min_pts_adjusted: bool, + pub current_pts: MpegClockTick, + pub current_picture_coding_type: FrameType, + /// Store temporal reference of current frame. + pub current_tref: FrameCount, + pub min_pts: MpegClockTick, + pub sync_pts: MpegClockTick, + /// No screen should start before this FTS + pub minimum_fts: Timestamp, + /// Time stamp of current file (w/ fts_offset, w/o fts_global). + pub fts_now: Timestamp, + /// Time before first sync_pts. + pub fts_offset: Timestamp, + /// Time before first GOP. + pub fts_fc_offset: Timestamp, + /// Remember the maximum fts that we saw in current file. + pub fts_max: Timestamp, + /// Duration of previous files (-ve mode). + pub fts_global: Timestamp, + pub sync_pts2fts_set: bool, + pub sync_pts2fts_fts: Timestamp, + pub sync_pts2fts_pts: MpegClockTick, + /// PTS resets when current_pts is lower than prev. + pts_reset: bool, +} + +/// Settings for overall timing functionality in [`TimingContext`]. +#[derive(Debug)] +pub struct TimingSettings { + /// If true, timeline jumps will be ignored. This is important in several input formats that are assumed to have correct timing, no matter what. + pub disable_sync_check: bool, + + /// If true, there will be no sync at all. Mostly useful for debugging. + pub no_sync: bool, + + // Needs to be set, as it's used in set_fts. + pub is_elementary_stream: bool, +} + +/// A collective struct to store global timing-related information while decoding a file. +/// +/// [`TimingContext`] serves a similar purpose. The only difference is that its lifetime is not +/// global and its information could be reset while execution of program. +#[derive(Debug)] +pub struct GlobalTimingInfo { + // Count 608 (per field) and 708 blocks since last set_fts() call + pub cb_field1: u64, + pub cb_field2: u64, + pub cb_708: u64, + pub pts_big_change: bool, + pub current_fps: f64, + pub frames_since_ref_time: FrameCount, + pub total_frames_count: FrameCount, + pub gop_time: Option, + pub first_gop_time: Option, + pub fts_at_gop_start: Timestamp, + pub gop_rollover: bool, + pub timing_settings: TimingSettings, +} + +impl TimingContext { + /// Create a new [`TimingContext`]. + pub fn new() -> TimingContext { + TimingContext { + pts_set: PtsSet::No, + min_pts_adjusted: false, + current_pts: MpegClockTick::new(0), + current_picture_coding_type: FrameType::ResetOrUnknown, + current_tref: FrameCount::new(0), + min_pts: MpegClockTick::new(0x01FFFFFFFF), + sync_pts: MpegClockTick::new(0), + minimum_fts: Timestamp::from_millis(0), + fts_now: Timestamp::from_millis(0), + fts_offset: Timestamp::from_millis(0), + fts_fc_offset: Timestamp::from_millis(0), + fts_max: Timestamp::from_millis(0), + fts_global: Timestamp::from_millis(0), + sync_pts2fts_set: false, + sync_pts2fts_fts: Timestamp::from_millis(0), + sync_pts2fts_pts: MpegClockTick::new(0), + pts_reset: false, + } + } + + /// Add `pts` to `TimingContext::current_pts`. + /// + /// It also checks for PTS resets. + pub fn add_current_pts(&mut self, pts: MpegClockTick) { + self.set_current_pts(self.current_pts + pts) + } + + /// Set `TimingContext::current_pts` to `pts`. + /// + /// It also checks for PTS resets. + pub fn set_current_pts(&mut self, pts: MpegClockTick) { + let prev_pts = self.current_pts; + self.current_pts = pts; + if self.pts_set == PtsSet::No { + self.pts_set = PtsSet::Received + } + debug!(msg_type = DebugMessageFlag::VIDEO_STREAM; "PTS: {} ({:8})", self.current_pts.as_timestamp().to_hms_millis_time(':').unwrap(), self.current_pts.as_i64()); + debug!(msg_type = DebugMessageFlag::VIDEO_STREAM; " FTS: {} \n", self.fts_now.to_hms_millis_time(':').unwrap()); + + // Check if PTS reset + if self.current_pts < prev_pts { + self.pts_reset = true; + } + } + + pub fn set_fts(&mut self) -> bool { + let mut timing_info = GLOBAL_TIMING_INFO.write().unwrap(); + + let mut pts_jump = false; + + // ES don't have PTS unless GOP timing is used + if self.pts_set == PtsSet::No && timing_info.timing_settings.is_elementary_stream { + return true; + } + + // First check for timeline jump (only when min_pts was set (implies sync_pts)). + if self.pts_set == PtsSet::MinPtsSet { + let dif = if timing_info.timing_settings.disable_sync_check { + // Disables sync check. Used for several input formats. + 0 + } else { + (self.current_pts - self.sync_pts).as_timestamp().seconds() + }; + + // Previously in C equivalent code, its -0.2 + if !(0..=MAX_DIF).contains(&dif) { + // ATSC specs: More than 3501 ms means missing component + info!("\nWarning: Reference clock has changed abruptly ({} seconds filepos={}), attempting to synchronize\n", dif, "Unable to get file position"); // TODO: get the file position somehow + info!("Last sync PTS value: {}\n", self.sync_pts.as_i64()); + info!("Current PTS value: {}\n", self.current_pts.as_i64()); + info!("Note: You can disable this behavior by adding -ignoreptsjumps to the parameters.\n"); + + pts_jump = true; + timing_info.pts_big_change = true; + + // Discard the gap if it is not on an I-frame or temporal reference zero. + if self.current_tref.as_u64() != 0 + && self.current_picture_coding_type != FrameType::IFrame + { + self.fts_now = self.fts_max; + info!("Change did not occur on first frame - probably a broken GOP\n"); + return true; + } + } + } + + // If min_pts was set just before a rollover we compensate by "roll-oving" it too + if self.pts_set == PtsSet::MinPtsSet && !self.min_pts_adjusted { + // We want to be aware of the upcoming rollover, not after it happened, so we don't take + // the 3 most significant bits but the 3 next ones + let min_pts_big_bits = (self.min_pts.as_i64() >> 30) & 7; + let cur_pts_big_bits = (self.current_pts.as_i64() >> 30) & 7; + if cur_pts_big_bits == 7 && min_pts_big_bits == 0 { + // Huge difference possibly means the first min_pts was actually just over the boundary + // Take the current_pts (smaller, accounting for the rollover) instead + self.min_pts = self.current_pts; + self.min_pts_adjusted = true; + } else if (1..=6).contains(&cur_pts_big_bits) { + // Far enough from the boundary + // Prevent the eventual difference with min_pts to make a bad adjustment + self.min_pts_adjusted = true; + } + } + + // Set min_pts, fts_offset + if self.pts_set != PtsSet::No { + self.pts_set = PtsSet::MinPtsSet; + + // Use this part only the first time min_pts is set. Later treat + // it as a reference clock change + if self.current_pts < self.min_pts && !pts_jump { + // If this is the first GOP, and seq 0 was not encountered yet + // we might reset min_pts/fts_offset again + + self.min_pts = self.current_pts; + + // Avoid next async test + self.sync_pts = self.current_pts + - self + .current_tref + .as_mpeg_clock_tick(timing_info.current_fps); + + if self.current_tref.as_u64() == 0 + || (timing_info.total_frames_count - timing_info.frames_since_ref_time).as_u64() + == 0 + { + // Earliest time in GOP. + // OR + // If this is the first frame (PES) there cannot be an offset. + // This part is also reached for dvr-ms/NTSC (RAW) as + // total_frames_count = frames_since_ref_time = 0 when + // this is called for the first time. + self.fts_offset = Timestamp::from_millis(0); + } else { + // It needs to be "+1" because the current frame is + // not yet counted. + self.fts_offset = (timing_info.total_frames_count + - timing_info.frames_since_ref_time + + FrameCount::new(1)) + .as_timestamp(timing_info.current_fps); + } + debug!( + msg_type = DebugMessageFlag::TIME; + "\nFirst sync time PTS: {} {:+}ms (time before this PTS)\n", + self.min_pts.as_timestamp().to_hms_millis_time(':').unwrap(), + self.fts_offset.millis() + ); + debug!( + msg_type = DebugMessageFlag::TIME; + "Total_frames_count {} frames_since_ref_time {}\n", + timing_info.total_frames_count.as_u64(), + timing_info.frames_since_ref_time.as_u64() + ); + } + + // -nosync disables syncing + if pts_jump && !timing_info.timing_settings.no_sync { + // The current time in the old time base is calculated using + // sync_pts (set at the beginning of the last GOP) plus the + // time of the frames since then. + self.fts_offset = self.fts_offset + + (self.sync_pts - self.min_pts).as_timestamp() + + timing_info + .frames_since_ref_time + .as_timestamp(timing_info.current_fps); + self.fts_max = self.fts_offset; + + // Start counting again from here + self.pts_set = PtsSet::Received; // Force min to be set again + self.sync_pts2fts_set = false; // Make note of the new conversion values + + // Avoid next async test - the gap might have occured on + // current_tref != 0. + self.sync_pts = self.current_pts + - self + .current_tref + .as_mpeg_clock_tick(timing_info.current_fps); + // Set min_pts = sync_pts as this is used for fts_now + self.min_pts = self.sync_pts; + + debug!( + msg_type = DebugMessageFlag::TIME; + "\nNew min PTS time: {} {:+}ms (time before this PTS)\n", + self.min_pts.as_timestamp().to_hms_millis_time(':').unwrap(), + self.fts_offset.millis() + ); + } + } + + // Set sync_pts, fts_offset + if self.current_tref.as_u64() == 0 { + self.sync_pts = self.current_pts; + } + + // Reset counters + timing_info.cb_field1 = 0; + timing_info.cb_field2 = 0; + timing_info.cb_708 = 0; + + // Avoid wrong "Calc. difference" and "Asynchronous by" numbers + // for uninitialized min_pts + // CFS: Remove or think decent condition + if self.pts_set != PtsSet::No { + // If pts_set is TRUE we have min_pts + self.fts_now = (self.current_pts - self.min_pts).as_timestamp() + self.fts_offset; + if !self.sync_pts2fts_set { + self.sync_pts2fts_pts = self.current_pts; + self.sync_pts2fts_fts = self.fts_now; + self.sync_pts2fts_set = true; + } + } else { + // No PTS info at all!! + info!("Set PTS called without any global timestamp set\n"); + return false; + } + + if self.fts_now > self.fts_max { + self.fts_max = self.fts_now; + } + + // If PTS resets, then fix minimum_fts and fts_max + if self.pts_reset { + self.minimum_fts = Timestamp::from_millis(0); + self.fts_max = self.fts_now; + self.pts_reset = false; + } + + true + } + + /// Returns the total FTS. + /// + /// Caption block counters are included. + pub fn get_fts(&self, current_field: CaptionField) -> Timestamp { + let timing_info = GLOBAL_TIMING_INFO.read().unwrap(); + let count = match current_field { + CaptionField::Field1 => timing_info.cb_field1, + CaptionField::Field2 => timing_info.cb_field2, + CaptionField::Cea708 => timing_info.cb_708, + }; + self.fts_now + self.fts_global + Timestamp::from_millis((count as i64) * 1001 / 30) + } + + /// This returns the maximum FTS that belonged to a frame. + /// + /// Caption block counters are not applicable. + pub fn get_fts_max(&self) -> Timestamp { + self.fts_max + self.fts_global + } + + /// Log FTS and PTS information for debugging purpose. + pub fn print_debug_timing(&self) { + let zero = Timestamp::from_millis(0); + let timing_info = GLOBAL_TIMING_INFO.read().unwrap(); + + let gop_time = timing_info.gop_time.map(|x| x.timestamp()).unwrap_or(zero); + let first_gop_time = timing_info + .first_gop_time + .map(|x| x.timestamp()) + .unwrap_or(zero); + + // Avoid wrong "Calc. difference" and "Asynchronous by" numbers + // for uninitialized min_pts + let tempmin_pts = if self.min_pts.as_i64() == 0x01FFFFFFFF { + self.sync_pts + } else { + self.min_pts + }; + + info!( + "Sync time stamps: PTS: {} ", + self.sync_pts + .as_timestamp() + .to_hms_millis_time(':') + .unwrap() + ); + info!( + " GOP start FTS: {}\n", + gop_time.to_hms_millis_time(':').unwrap() + ); + + // Length first GOP to last GOP + let goplenms = gop_time - first_gop_time; + // Length at last sync point + let ptslenms = (self.sync_pts - tempmin_pts).as_timestamp() + self.fts_offset; + + info!( + "Last FTS: {}", + self.get_fts_max().to_hms_millis_time(':').unwrap() + ); + info!( + " GOP start FTS: {}\n", + timing_info + .fts_at_gop_start + .to_hms_millis_time(':') + .unwrap() + ); + + let one_frame = FrameCount::new(1).as_timestamp(timing_info.current_fps); + + // Times are based on last GOP and/or sync time + info!( + "Max FTS diff. to PTS: {:6}ms GOP: {:6}ms\n\n", + (self.get_fts_max() + one_frame - ptslenms) + .to_hms_millis_time(':') + .unwrap(), + (self.get_fts_max() + one_frame - goplenms) + .to_hms_millis_time(':') + .unwrap() + ); + } + + /// Constructs a [`TimingContext`] from its individual fields. + /// + /// # Safety + /// + /// Make sure that [`TimingContext`] is in a valid state. + #[allow(clippy::too_many_arguments)] + pub unsafe fn from_raw_parts( + pts_set: PtsSet, + min_pts_adjusted: bool, + current_pts: MpegClockTick, + current_picture_coding_type: FrameType, + current_tref: FrameCount, + min_pts: MpegClockTick, + sync_pts: MpegClockTick, + minimum_fts: Timestamp, + fts_now: Timestamp, + fts_offset: Timestamp, + fts_fc_offset: Timestamp, + fts_max: Timestamp, + fts_global: Timestamp, + sync_pts2fts_set: bool, + sync_pts2fts_fts: Timestamp, + sync_pts2fts_pts: MpegClockTick, + pts_reset: bool, + ) -> TimingContext { + TimingContext { + pts_set, + min_pts_adjusted, + current_pts, + current_picture_coding_type, + current_tref, + min_pts, + sync_pts, + minimum_fts, + fts_now, + fts_offset, + fts_fc_offset, + fts_max, + fts_global, + sync_pts2fts_set, + sync_pts2fts_fts, + sync_pts2fts_pts, + pts_reset, + } + } + + /// Returns the individual fields of a [`TimingContext`]. + /// + /// # Safety + /// + /// Certain fields are supposed to be private. + #[allow(clippy::type_complexity)] + pub unsafe fn as_raw_parts( + &self, + ) -> ( + PtsSet, + bool, + MpegClockTick, + FrameType, + FrameCount, + MpegClockTick, + MpegClockTick, + Timestamp, + Timestamp, + Timestamp, + Timestamp, + Timestamp, + Timestamp, + bool, + Timestamp, + MpegClockTick, + bool, + ) { + let TimingContext { + pts_set, + min_pts_adjusted, + current_pts, + current_picture_coding_type, + current_tref, + min_pts, + sync_pts, + minimum_fts, + fts_now, + fts_offset, + fts_fc_offset, + fts_max, + fts_global, + sync_pts2fts_set, + sync_pts2fts_fts, + sync_pts2fts_pts, + pts_reset, + } = *self; + + ( + pts_set, + min_pts_adjusted, + current_pts, + current_picture_coding_type, + current_tref, + min_pts, + sync_pts, + minimum_fts, + fts_now, + fts_offset, + fts_fc_offset, + fts_max, + fts_global, + sync_pts2fts_set, + sync_pts2fts_fts, + sync_pts2fts_pts, + pts_reset, + ) + } +} + +impl GlobalTimingInfo { + /// Create a new instance of [`GlobalTimingInfo`]. + const fn new() -> GlobalTimingInfo { + GlobalTimingInfo { + cb_field1: 0, + cb_field2: 0, + cb_708: 0, + pts_big_change: false, + current_fps: FRAMERATES_VALUES[4], // 29.97 + frames_since_ref_time: FrameCount::new(0), + total_frames_count: FrameCount::new(0), + gop_time: None, + first_gop_time: None, + fts_at_gop_start: Timestamp::from_millis(0), + gop_rollover: false, + timing_settings: TimingSettings { + disable_sync_check: false, + no_sync: false, + is_elementary_stream: false, + }, + } + } +} + +impl Default for TimingContext { + fn default() -> Self { + Self::new() + } +} diff --git a/src/rust/lib_ccxr/src/time/units.rs b/src/rust/lib_ccxr/src/time/units.rs index 99d9e3cc..0bc546c6 100644 --- a/src/rust/lib_ccxr/src/time/units.rs +++ b/src/rust/lib_ccxr/src/time/units.rs @@ -1,7 +1,7 @@ use std::convert::TryInto; +use std::ffi::c_int; use std::fmt::Write; use std::num::TryFromIntError; -use std::os::raw::c_int; use std::time::{SystemTime, UNIX_EPOCH}; use derive_more::{Add, Neg, Sub}; @@ -397,6 +397,18 @@ impl Timestamp { Ok(s) } + /// SCC time formatting + pub fn to_scc_time(&self) -> 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; + + write!(result, "{:02}:{:02}:{:02};{:02}", h, m, s, frame)?; + + Ok(result) + } + /// Returns a formatted [`Timestamp`] using ctime's format. /// /// # Examples diff --git a/src/rust/src/decoder/mod.rs b/src/rust/src/decoder/mod.rs index 3f8a37bf..7430415d 100644 --- a/src/rust/src/decoder/mod.rs +++ b/src/rust/src/decoder/mod.rs @@ -289,7 +289,7 @@ mod test { assert_eq!(decoder.packet, vec![0xC2, 0x23, 0x45, 0x67, 0x01, 0x02]); assert_eq!(decoder.packet_length, 6); - assert_eq!(decoder.is_header_parsed, true); + assert!(decoder.is_header_parsed); } #[test] diff --git a/src/rust/src/decoder/service_decoder.rs b/src/rust/src/decoder/service_decoder.rs index 39d0a05d..65243a8a 100644 --- a/src/rust/src/decoder/service_decoder.rs +++ b/src/rust/src/decoder/service_decoder.rs @@ -1385,7 +1385,7 @@ mod test { decoder.windows[0].col_count = 4; decoder.windows[0].attribs.print_direction = dtvcc_window_pd::DTVCC_WINDOW_PD_LEFT_RIGHT as i32; - let block = ['a' as u8, 'b' as u8] as [c_uchar; 2]; + let block = [b'a', b'b'] as [c_uchar; 2]; decoder.process_p16(&block); diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index a65eec8a..8b014cd1 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -24,7 +24,6 @@ pub mod utils; #[cfg(windows)] use std::os::windows::io::{FromRawHandle, RawHandle}; -use std::{io::Write, os::raw::c_char, os::raw::c_int}; use args::Args; use bindings::*; @@ -37,7 +36,11 @@ use utils::is_true; use env_logger::{builder, Target}; use log::{warn, LevelFilter}; -use std::ffi::CStr; +use std::{ + ffi::CStr, + io::Write, + os::raw::{c_char, c_double, c_int, c_long, c_uint}, +}; #[cfg(test)] static mut cb_708: c_int = 0; @@ -60,6 +63,15 @@ extern "C" { static mut MPEG_CLOCK_FREQ: c_int; static mut tlt_config: ccx_s_teletext_config; static mut ccx_options: ccx_s_options; + static mut pts_big_change: c_uint; + static mut current_fps: c_double; + static mut frames_since_ref_time: c_int; + static mut total_frames_count: c_uint; + static mut gop_time: gop_time_code; + static mut first_gop_time: gop_time_code; + static mut fts_at_gop_start: c_long; + static mut gop_rollover: c_int; + static mut ccx_common_timing_settings: ccx_common_timing_settings_t; static mut capitalization_list: word_list; static mut profane: word_list; } diff --git a/src/rust/src/libccxr_exports/mod.rs b/src/rust/src/libccxr_exports/mod.rs index 62a0d1d4..c2f64a25 100644 --- a/src/rust/src/libccxr_exports/mod.rs +++ b/src/rust/src/libccxr_exports/mod.rs @@ -6,7 +6,7 @@ use lib_ccxr::util::log::*; use lib_ccxr::util::{bits::*, levenshtein::*}; use std::convert::TryInto; -use std::os::raw::{c_char, c_int, c_uint}; +use std::ffi::{c_char, c_int, c_uint}; /// Initializes the logger at the rust side. /// diff --git a/src/rust/src/libccxr_exports/time.rs b/src/rust/src/libccxr_exports/time.rs index 9b3cdb16..915c7e5b 100644 --- a/src/rust/src/libccxr_exports/time.rs +++ b/src/rust/src/libccxr_exports/time.rs @@ -1,9 +1,16 @@ -use crate::bindings::*; +#![allow(clippy::useless_conversion)] -use std::ffi::CStr; -use std::os::raw::{c_char, c_int}; +use std::convert::TryInto; +use std::ffi::{c_char, c_int, c_long, CStr}; -use lib_ccxr::time::units::*; +use crate::{ + bindings::*, cb_708, cb_field1, cb_field2, ccx_common_timing_settings as timing_settings, + current_fps, first_gop_time, frames_since_ref_time, fts_at_gop_start, gop_rollover, gop_time, + pts_big_change, total_frames_count, +}; + +use lib_ccxr::common::FrameType; +use lib_ccxr::time::{c_functions as c, *}; use lib_ccxr::util::{time::*, write_string_into_pointer}; /// Rust equivalent for `timestamp_to_srttime` function in C. Uses C-native types as input and @@ -122,3 +129,420 @@ pub unsafe extern "C" fn ccxr_millis_to_time( *seconds = seconds_value; *ms = ms_value; } + +/// Construct a [`TimingContext`] from a pointer of `ccx_common_timing_ctx`. +/// +/// It is used to move data of [`TimingContext`] from C to Rust. +/// +/// # Safety +/// +/// `ctx` should not be null. +unsafe fn generate_timing_context(ctx: *const ccx_common_timing_ctx) -> TimingContext { + let pts_set = match (*ctx).pts_set { + 0 => PtsSet::No, + 1 => PtsSet::Received, + 2 => PtsSet::MinPtsSet, + _ => panic!("incorrect value for pts_set"), + }; + + let min_pts_adjusted = (*ctx).min_pts_adjusted != 0; + let current_pts = MpegClockTick::new((*ctx).current_pts); + + let current_picture_coding_type = match (*ctx).current_picture_coding_type { + ccx_frame_type_CCX_FRAME_TYPE_RESET_OR_UNKNOWN => FrameType::ResetOrUnknown, + ccx_frame_type_CCX_FRAME_TYPE_I_FRAME => FrameType::IFrame, + ccx_frame_type_CCX_FRAME_TYPE_P_FRAME => FrameType::PFrame, + ccx_frame_type_CCX_FRAME_TYPE_B_FRAME => FrameType::BFrame, + ccx_frame_type_CCX_FRAME_TYPE_D_FRAME => FrameType::DFrame, + _ => panic!("Incorrect value for current_picture_coding_type"), + }; + + let current_tref = FrameCount::new((*ctx).current_tref.try_into().unwrap()); + let min_pts = MpegClockTick::new((*ctx).min_pts); + let sync_pts = MpegClockTick::new((*ctx).sync_pts); + let minimum_fts = Timestamp::from_millis((*ctx).minimum_fts); + let fts_now = Timestamp::from_millis((*ctx).fts_now); + let fts_offset = Timestamp::from_millis((*ctx).fts_offset); + let fts_fc_offset = Timestamp::from_millis((*ctx).fts_fc_offset); + let fts_max = Timestamp::from_millis((*ctx).fts_max); + let fts_global = Timestamp::from_millis((*ctx).fts_global); + let sync_pts2fts_set = (*ctx).sync_pts2fts_set != 0; + let sync_pts2fts_fts = Timestamp::from_millis((*ctx).sync_pts2fts_fts); + let sync_pts2fts_pts = MpegClockTick::new((*ctx).sync_pts2fts_pts); + let pts_reset = (*ctx).pts_reset != 0; + + TimingContext::from_raw_parts( + pts_set, + min_pts_adjusted, + current_pts, + current_picture_coding_type, + current_tref, + min_pts, + sync_pts, + minimum_fts, + fts_now, + fts_offset, + fts_fc_offset, + fts_max, + fts_global, + sync_pts2fts_set, + sync_pts2fts_fts, + sync_pts2fts_pts, + pts_reset, + ) +} + +/// Copy the contents [`TimingContext`] to a `ccx_common_timing_ctx`. +/// +/// It is used to move data of [`TimingContext`] from Rust to C. +/// +/// # Safety +/// +/// `ctx` should not be null. +unsafe fn write_back_to_common_timing_ctx( + ctx: *mut ccx_common_timing_ctx, + timing_ctx: &TimingContext, +) { + let ( + pts_set, + min_pts_adjusted, + current_pts, + current_picture_coding_type, + current_tref, + min_pts, + sync_pts, + minimum_fts, + fts_now, + fts_offset, + fts_fc_offset, + fts_max, + fts_global, + sync_pts2fts_set, + sync_pts2fts_fts, + sync_pts2fts_pts, + pts_reset, + ) = timing_ctx.as_raw_parts(); + + (*ctx).pts_set = match pts_set { + PtsSet::No => 0, + PtsSet::Received => 1, + PtsSet::MinPtsSet => 2, + }; + + (*ctx).min_pts_adjusted = if min_pts_adjusted { 1 } else { 0 }; + (*ctx).current_pts = current_pts.as_i64(); + + (*ctx).current_picture_coding_type = match current_picture_coding_type { + FrameType::ResetOrUnknown => ccx_frame_type_CCX_FRAME_TYPE_RESET_OR_UNKNOWN, + FrameType::IFrame => ccx_frame_type_CCX_FRAME_TYPE_I_FRAME, + FrameType::PFrame => ccx_frame_type_CCX_FRAME_TYPE_P_FRAME, + FrameType::BFrame => ccx_frame_type_CCX_FRAME_TYPE_B_FRAME, + FrameType::DFrame => ccx_frame_type_CCX_FRAME_TYPE_D_FRAME, + }; + + (*ctx).current_tref = current_tref.as_u64().try_into().unwrap(); + (*ctx).min_pts = min_pts.as_i64(); + (*ctx).sync_pts = sync_pts.as_i64(); + (*ctx).minimum_fts = minimum_fts.millis(); + (*ctx).fts_now = fts_now.millis(); + (*ctx).fts_offset = fts_offset.millis(); + (*ctx).fts_fc_offset = fts_fc_offset.millis(); + (*ctx).fts_max = fts_max.millis(); + (*ctx).fts_global = fts_global.millis(); + (*ctx).sync_pts2fts_set = if sync_pts2fts_set { 1 } else { 0 }; + (*ctx).sync_pts2fts_fts = sync_pts2fts_fts.millis(); + (*ctx).sync_pts2fts_pts = sync_pts2fts_pts.as_i64(); + (*ctx).pts_reset = if pts_reset { 1 } else { 0 }; +} + +/// Write to [`GLOBAL_TIMING_INFO`] from the equivalent static variables in C. +/// +/// It is used to move data of [`GLOBAL_TIMING_INFO`] from C to Rust. +/// +/// # Safety +/// +/// All the static variables should be initialized and in valid state. +unsafe fn apply_timing_info() { + let mut timing_info = GLOBAL_TIMING_INFO.write().unwrap(); + + timing_info.cb_field1 = cb_field1.try_into().unwrap(); + timing_info.cb_field2 = cb_field2.try_into().unwrap(); + timing_info.cb_708 = cb_708.try_into().unwrap(); + timing_info.pts_big_change = pts_big_change != 0; + timing_info.current_fps = current_fps; + timing_info.frames_since_ref_time = FrameCount::new(frames_since_ref_time.try_into().unwrap()); + timing_info.total_frames_count = FrameCount::new(total_frames_count.into()); + timing_info.gop_time = generate_gop_time_code(gop_time); + timing_info.first_gop_time = generate_gop_time_code(first_gop_time); + timing_info.fts_at_gop_start = Timestamp::from_millis(fts_at_gop_start.into()); + timing_info.gop_rollover = gop_rollover != 0; + timing_info.timing_settings.disable_sync_check = timing_settings.disable_sync_check != 0; + timing_info.timing_settings.no_sync = timing_settings.no_sync != 0; + timing_info.timing_settings.is_elementary_stream = timing_settings.is_elementary_stream != 0; +} + +/// Write from [`GLOBAL_TIMING_INFO`] to the equivalent static variables in C. +/// +/// It is used to move data of [`GLOBAL_TIMING_INFO`] from Rust to C. +/// +/// # Safety +/// +/// All the static variables should be initialized and in valid state. +unsafe fn write_back_from_timing_info() { + let timing_info = GLOBAL_TIMING_INFO.read().unwrap(); + + cb_field1 = timing_info.cb_field1.try_into().unwrap(); + cb_field2 = timing_info.cb_field2.try_into().unwrap(); + cb_708 = timing_info.cb_708.try_into().unwrap(); + pts_big_change = if timing_info.pts_big_change { 1 } else { 0 }; + current_fps = timing_info.current_fps; + frames_since_ref_time = timing_info + .frames_since_ref_time + .as_u64() + .try_into() + .unwrap(); + total_frames_count = timing_info.total_frames_count.as_u64().try_into().unwrap(); + gop_time = write_gop_time_code(timing_info.gop_time); + first_gop_time = write_gop_time_code(timing_info.first_gop_time); + fts_at_gop_start = timing_info.fts_at_gop_start.millis() as c_long; + gop_rollover = if timing_info.gop_rollover { 1 } else { 0 }; + timing_settings.disable_sync_check = if timing_info.timing_settings.disable_sync_check { + 1 + } else { + 0 + }; + timing_settings.no_sync = if timing_info.timing_settings.no_sync { + 1 + } else { + 0 + }; + timing_settings.is_elementary_stream = if timing_info.timing_settings.is_elementary_stream { + 1 + } else { + 0 + }; +} + +/// Construct a [`GopTimeCode`] from `gop_time_code`. +unsafe fn generate_gop_time_code(g: gop_time_code) -> Option { + if g.inited == 0 { + None + } else { + Some(GopTimeCode::from_raw_parts( + g.drop_frame_flag != 0, + g.time_code_hours.try_into().unwrap(), + g.time_code_minutes.try_into().unwrap(), + g.time_code_seconds.try_into().unwrap(), + g.time_code_pictures.try_into().unwrap(), + Timestamp::from_millis(g.ms), + )) + } +} + +/// Construct a `gop_time_code` from [`GopTimeCode`]. +unsafe fn write_gop_time_code(g: Option) -> gop_time_code { + if let Some(gop) = g { + let ( + drop_frame, + time_code_hours, + time_code_minutes, + time_code_seconds, + time_code_pictures, + timestamp, + ) = gop.as_raw_parts(); + + gop_time_code { + drop_frame_flag: if drop_frame { 1 } else { 0 }, + time_code_hours: time_code_hours.into(), + time_code_minutes: time_code_minutes.into(), + marker_bit: 0, + time_code_seconds: time_code_seconds.into(), + time_code_pictures: time_code_pictures.into(), + inited: 1, + ms: timestamp.millis(), + } + } else { + gop_time_code { + drop_frame_flag: 0, + time_code_hours: 0, + time_code_minutes: 0, + marker_bit: 0, + time_code_seconds: 0, + time_code_pictures: 0, + inited: 0, + ms: 0, + } + } +} + +/// Rust equivalent for `add_current_pts` function in C. Uses C-native types as input and output. +/// +/// # Safety +/// +/// `ctx` must not be null. +#[no_mangle] +pub unsafe extern "C" fn ccxr_add_current_pts(ctx: *mut ccx_common_timing_ctx, pts: c_long) { + apply_timing_info(); + let mut context = generate_timing_context(ctx); + + c::add_current_pts(&mut context, MpegClockTick::new(pts.into())); + + write_back_to_common_timing_ctx(ctx, &context); + write_back_from_timing_info(); +} + +/// Rust equivalent for `set_current_pts` function in C. Uses C-native types as input and output. +/// +/// # Safety +/// +/// `ctx` must not be null. +#[no_mangle] +pub unsafe extern "C" fn ccxr_set_current_pts(ctx: *mut ccx_common_timing_ctx, pts: c_long) { + apply_timing_info(); + let mut context = generate_timing_context(ctx); + + c::set_current_pts(&mut context, MpegClockTick::new(pts.into())); + + write_back_to_common_timing_ctx(ctx, &context); + write_back_from_timing_info(); +} + +/// Rust equivalent for `set_fts` function in C. Uses C-native types as input and output. +/// +/// # Safety +/// +/// `ctx` must not be null. +#[no_mangle] +pub unsafe extern "C" fn ccxr_set_fts(ctx: *mut ccx_common_timing_ctx) -> c_int { + apply_timing_info(); + let mut context = generate_timing_context(ctx); + + let ans = c::set_fts(&mut context); + + write_back_to_common_timing_ctx(ctx, &context); + write_back_from_timing_info(); + + if ans { + 1 + } else { + 0 + } +} + +/// Rust equivalent for `get_fts` function in C. Uses C-native types as input and output. +/// +/// # Safety +/// +/// `ctx` must not be null. `current_field` must be 1, 2 or 3. +#[no_mangle] +pub unsafe extern "C" fn ccxr_get_fts( + ctx: *mut ccx_common_timing_ctx, + current_field: c_int, +) -> c_long { + apply_timing_info(); + let mut context = generate_timing_context(ctx); + + let caption_field = match current_field { + 1 => CaptionField::Field1, + 2 => CaptionField::Field2, + 3 => CaptionField::Cea708, + _ => panic!("incorrect value for caption field"), + }; + + let ans = c::get_fts(&mut context, caption_field); + + write_back_to_common_timing_ctx(ctx, &context); + write_back_from_timing_info(); + + ans.millis().try_into().unwrap() +} + +/// Rust equivalent for `get_fts_max` function in C. Uses C-native types as input and output. +/// +/// # Safety +/// +/// `ctx` must not be null. +#[no_mangle] +pub unsafe extern "C" fn ccxr_get_fts_max(ctx: *mut ccx_common_timing_ctx) -> c_long { + apply_timing_info(); + let mut context = generate_timing_context(ctx); + + let ans = c::get_fts_max(&mut context); + + write_back_to_common_timing_ctx(ctx, &context); + write_back_from_timing_info(); + + ans.millis().try_into().unwrap() +} + +/// Rust equivalent for `print_mstime_static` function in C. Uses C-native types as input and output. +/// +/// # Safety +/// +/// `buf` must not be null. It must have sufficient length to hold the time in string form. +#[no_mangle] +pub unsafe extern "C" fn ccxr_print_mstime_static(mstime: c_long, buf: *mut c_char) -> *mut c_char { + let time = Timestamp::from_millis(mstime.into()); + let ans = c::print_mstime_static(time, ':'); + write_string_into_pointer(buf, &ans); + buf +} + +/// Rust equivalent for `print_debug_timing` function in C. Uses C-native types as input and output. +/// +/// # Safety +/// +/// `ctx` must not be null. +#[no_mangle] +pub unsafe extern "C" fn ccxr_print_debug_timing(ctx: *mut ccx_common_timing_ctx) { + apply_timing_info(); + let mut context = generate_timing_context(ctx); + + c::print_debug_timing(&mut context); + + write_back_to_common_timing_ctx(ctx, &context); + write_back_from_timing_info(); +} + +/// Rust equivalent for `calculate_ms_gop_time` function in C. Uses C-native types as input and output. +/// +/// # Safety +/// +/// `g` must not be null. +#[no_mangle] +pub unsafe extern "C" fn ccxr_calculate_ms_gop_time(g: *mut gop_time_code) { + apply_timing_info(); + let timing_info = GLOBAL_TIMING_INFO.read().unwrap(); + + (*g).ms = GopTimeCode::new( + (*g).drop_frame_flag != 0, + (*g).time_code_hours.try_into().unwrap(), + (*g).time_code_minutes.try_into().unwrap(), + (*g).time_code_seconds.try_into().unwrap(), + (*g).time_code_pictures.try_into().unwrap(), + timing_info.current_fps, + timing_info.gop_rollover, + ) + .unwrap() + .timestamp() + .millis() +} + +/// Rust equivalent for `gop_accepted` function in C. Uses C-native types as input and output. +/// +/// # Safety +/// +/// `g` must not be null. +#[no_mangle] +pub unsafe extern "C" fn ccxr_gop_accepted(g: *mut gop_time_code) -> c_int { + if let Some(gop) = generate_gop_time_code(*g) { + let ans = c::gop_accepted(gop); + if ans { + 1 + } else { + 0 + } + } else { + 0 + } +}