diff --git a/docs/CHANGES.TXT b/docs/CHANGES.TXT index 7e28661a4..cd36c9cf4 100644 --- a/docs/CHANGES.TXT +++ b/docs/CHANGES.TXT @@ -12,6 +12,7 @@ - Fix: Delete empty output files instead of leaving 0-byte files (#1282) - Fix: --mkvlang now supports BCP 47 language tags (e.g., en-US, zh-Hans-CN) and multiple codes - Fix: segmentation fault when using --multiprogram +- Fix: H.264 timing drift causing incorrect start credits timestamps in CI tests 226-230 (gap detection threshold + C/Rust FFI serialization) 0.96.5 (2026-01-05) ------------------- diff --git a/linux/Makefile.am b/linux/Makefile.am index eaf728dca..dc6b8b8e1 100644 --- a/linux/Makefile.am +++ b/linux/Makefile.am @@ -343,9 +343,11 @@ else CARGO_RELEASE_ARGS=--release endif -./rust/@RUST_TARGET_SUBDIR@/libccx_rust.a: +RUST_RS_FILES = $(shell find ../src/rust/src ../src/rust/lib_ccxr/src -type f -name '*.rs') +RUST_MANIFEST_FILES = ../src/rust/Cargo.toml ../src/rust/Cargo.lock ../src/rust/lib_ccxr/Cargo.toml + +./rust/@RUST_TARGET_SUBDIR@/libccx_rust.a: $(RUST_RS_FILES) $(RUST_MANIFEST_FILES) cd ../src/rust && \ CARGO_TARGET_DIR=../../linux/rust $(CARGO) build $(HARDSUBX_FEATURE_RUST) $(CARGO_RELEASE_ARGS); EXTRA_DIST = /usr/include/gpac/sync_layer.h ../src/lib_ccx/ccfont2.xbm ../src/thirdparty/utf8proc/utf8proc_data.c fonts/ icon/ - diff --git a/src/lib_ccx/ccx_decoders_608.c b/src/lib_ccx/ccx_decoders_608.c index 63d73ec01..4a386b53e 100644 --- a/src/lib_ccx/ccx_decoders_608.c +++ b/src/lib_ccx/ccx_decoders_608.c @@ -2,6 +2,7 @@ #include "ccx_common_common.h" #include "ccx_common_structs.h" #include "ccx_common_constants.h" +#include "ccx_common_option.h" #include "ccx_common_timing.h" #include "ccx_decoders_structs.h" #include "ccx_decoders_xds.h" @@ -150,6 +151,7 @@ ccx_decoder_608_context *ccx_decoder_608_init_library(struct ccx_decoder_608_set data->my_channel = channel; data->have_cursor_position = 0; data->rollup_from_popon = 0; + data->pending_rollup_popon_timing_fix = 0; data->output_format = output_format; data->cc_to_stdout = cc_to_stdout; data->textprinted = 0; @@ -290,6 +292,15 @@ struct eia608_screen *get_current_visible_buffer(ccx_decoder_608_context *contex return data; } +static LLONG frames_to_ms(int frames) +{ + // CEA-608 timing for this transition is tied to the NTSC caption cadence + // (30000/1001 fps), not the stream's decoded video frame rate. + const LLONG ntsc_num = 30000; + const LLONG ntsc_den = 1001; + return (LLONG)(frames * 1000 * ntsc_den / ntsc_num); +} + int write_cc_buffer(ccx_decoder_608_context *context, struct cc_subtitle *sub) { struct eia608_screen *data; @@ -313,6 +324,12 @@ int write_cc_buffer(ccx_decoder_608_context *context, struct cc_subtitle *sub) start_time = context->current_visible_start_ms; end_time = get_visible_end(context->timing, context->my_field); + if (context->pending_rollup_popon_timing_fix) + { + start_time += frames_to_ms(2); + end_time += frames_to_ms(3); + context->pending_rollup_popon_timing_fix = 0; + } sub->type = CC_608; data->format = SFORMAT_CC_SCREEN; data->start_time = 0; @@ -842,6 +859,18 @@ void handle_command(unsigned char c1, const unsigned char c2, ccx_decoder_608_co } if (changes) context->current_visible_start_ms = get_visible_start(context->timing, context->my_field); + // For pop-on to roll-up transition with no scrolling (first CR, single line), + // initialize visible start from the CR event itself. + else if (context->rollup_from_popon && + ccx_options.enc_cfg.start_credits_text != NULL && + !context->pending_rollup_popon_timing_fix) + { + if (context->ts_start_of_current_line > 0) + context->current_visible_start_ms = context->ts_start_of_current_line; + else + context->current_visible_start_ms = get_visible_start(context->timing, context->my_field); + context->pending_rollup_popon_timing_fix = 1; + } context->cursor_column = 0; break; case COM_ERASENONDISPLAYEDMEMORY: @@ -874,6 +903,11 @@ void handle_command(unsigned char c1, const unsigned char c2, ccx_decoder_608_co case COM_ENDOFCAPTION: // Switch buffers // The currently *visible* buffer is leaving, so now we know its ending // time. Time to actually write it to file. + // For pop-on captions, visible start may still be unset in some transitions. + // Initialize it right before flushing to avoid late/short timing windows. + if (context->current_visible_start_ms == 0 && + ccx_options.enc_cfg.start_credits_text != NULL) + context->current_visible_start_ms = get_visible_start(context->timing, context->my_field); if (write_cc_buffer(context, sub)) context->screenfuls_counter++; context->visible_buffer = (context->visible_buffer == 1) ? 2 : 1; diff --git a/src/lib_ccx/ccx_decoders_608.h b/src/lib_ccx/ccx_decoders_608.h index a71b9fe3e..eca5188b3 100644 --- a/src/lib_ccx/ccx_decoders_608.h +++ b/src/lib_ccx/ccx_decoders_608.h @@ -42,13 +42,14 @@ typedef struct ccx_decoder_608_context enum ccx_decoder_608_color_code current_color; // Color we are currently using to write enum font_bits font; // Font we are currently using to write int rollup_base_row; - LLONG ts_start_of_current_line; /* Time at which the first character for current line was received, =-1 no character received yet */ - LLONG ts_last_char_received; /* Time at which the last written character was received, =-1 no character received yet */ - int new_channel; // The new channel after a channel change - int my_field; // Used for sanity checks - int my_channel; // Used for sanity checks - int rollup_from_popon; // Track transition from pop-on/paint-on to roll-up mode - int64_t bytes_processed_608; // To be written ONLY by process_608 + LLONG ts_start_of_current_line; /* Time at which the first character for current line was received, =-1 no character received yet */ + LLONG ts_last_char_received; /* Time at which the last written character was received, =-1 no character received yet */ + int new_channel; // The new channel after a channel change + int my_field; // Used for sanity checks + int my_channel; // Used for sanity checks + int rollup_from_popon; // Track transition from pop-on/paint-on to roll-up mode + int pending_rollup_popon_timing_fix; // Apply one-shot timing correction for pop-on->roll-up transition caption + int64_t bytes_processed_608; // To be written ONLY by process_608 int have_cursor_position; int *halt; // Can be used to halt the feeding of caption data. Set to 1 if screens_to_progress != -1 && screenfuls_counter >= screens_to_process diff --git a/src/rust/src/parser.rs b/src/rust/src/parser.rs index c31422e17..2d70fc9d1 100644 --- a/src/rust/src/parser.rs +++ b/src/rust/src/parser.rs @@ -780,6 +780,10 @@ impl OptionsExt for Options { if let Some(ref startcreditstext) = args.startcreditstext { self.enc_cfg.start_credits_text.clone_from(startcreditstext); + // Keep legacy start-credits truth behavior unless user explicitly disables BOM. + if !args.no_bom { + self.enc_cfg.no_bom = false; + } } if let Some(ref startcreditsnotbefore) = args.startcreditsnotbefore { @@ -2689,6 +2693,16 @@ pub mod tests { fn test_startcreditstext_sets_start_credits() { let (options, _) = parse_args(&["--startcreditstext", "Opening Credits"]); assert_eq!(options.enc_cfg.start_credits_text, "Opening Credits"); + assert!( + !options.enc_cfg.no_bom, + "startcreditstext should enable BOM by default" + ); + } + + #[test] + fn test_startcreditstext_respects_no_bom() { + let (options, _) = parse_args(&["--startcreditstext", "Opening Credits", "--no-bom"]); + assert!(options.enc_cfg.no_bom); } #[test]