From 45a491c8967acb9bf9738e949b8c1b19f39c817b Mon Sep 17 00:00:00 2001 From: Pratham Date: Fri, 16 Oct 2026 15:16:38 +0530 Subject: [PATCH] [FIX] Use packet timestamps for tx3g subtitle times on the FFmpeg MP4 path The tx3g start/end times came from the global timing pointer, which every track shares: it can already be past the subtitle being processed, and on the very first subtitle it is still at zero. Result on a tx3g MP4 with subtitles at 1s-3s and 4s-6s: 1: 00:00:00,000 --> 00:00:03,999 2: 00:00:04,000 --> 00:00:09,959 Take the times from the packet timestamps directly instead: the start is the packet PTS in milliseconds and the end is the sample duration when present, with the previous behaviour as fallback (next subtitle's start, or the usual 5 second default for the last sample). The timing pointer is still set from the explicit start, it is just no longer used to derive the output times. Output now matches the GPAC path exactly: 1: 00:00:01,000 --> 00:00:02,999 2: 00:00:04,000 --> 00:00:05,999 --- src/lib_ccx/mp4_rust_bridge.c | 13 ++++++------ src/lib_ccx/mp4_rust_bridge.h | 18 +++++++++------- src/rust/src/demuxer/mp4.rs | 40 +++++++++++++++++++++++++++-------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/lib_ccx/mp4_rust_bridge.c b/src/lib_ccx/mp4_rust_bridge.c index 681ad31fc..5558d15e5 100644 --- a/src/lib_ccx/mp4_rust_bridge.c +++ b/src/lib_ccx/mp4_rust_bridge.c @@ -222,14 +222,14 @@ int ccx_mp4_process_cc_packet(struct lib_ccx_ctx *ctx, int sub_type_c608, int ccx_mp4_process_tx3g_packet(struct lib_ccx_ctx *ctx, unsigned char *data, unsigned int data_length, - long long dts, int cts_offset, - unsigned int timescale, + long long start_ms, struct cc_subtitle *sub) { struct lib_cc_decode *dec_ctx = update_decoder_list(ctx); struct encoder_ctx *enc_ctx = update_encoder_list(ctx); - set_current_pts(dec_ctx->timing, (dts + cts_offset) * MPEG_CLOCK_FREQ / timescale); + /* Keep the global timing pointer roughly in sync with this track. */ + set_current_pts(dec_ctx->timing, start_ms * MPEG_CLOCK_FREQ / 1000); set_fts(dec_ctx->timing); /* tx3g format: 16-bit text length + UTF-8 text */ @@ -242,7 +242,7 @@ int ccx_mp4_process_tx3g_packet(struct lib_ccx_ctx *ctx, /* Set up subtitle for deferred encoding */ sub->type = CC_TEXT; - sub->start_time = dec_ctx->timing->fts_now; + sub->start_time = start_ms; if (sub->data != NULL) free(sub->data); sub->data = malloc(text_length + 1); @@ -255,14 +255,13 @@ int ccx_mp4_process_tx3g_packet(struct lib_ccx_ctx *ctx, return 0; } -void ccx_mp4_flush_tx3g(struct lib_ccx_ctx *ctx, struct cc_subtitle *sub) +void ccx_mp4_flush_tx3g(struct lib_ccx_ctx *ctx, struct cc_subtitle *sub, long long end_ms) { - struct lib_cc_decode *dec_ctx = update_decoder_list(ctx); struct encoder_ctx *enc_ctx = update_encoder_list(ctx); if (sub->data != NULL) { - sub->end_time = dec_ctx->timing->fts_now; + sub->end_time = end_ms; encode_sub(enc_ctx, sub); } } diff --git a/src/lib_ccx/mp4_rust_bridge.h b/src/lib_ccx/mp4_rust_bridge.h index 01d4460c2..7345e4ef7 100644 --- a/src/lib_ccx/mp4_rust_bridge.h +++ b/src/lib_ccx/mp4_rust_bridge.h @@ -59,28 +59,30 @@ extern "C" /* * Process a tx3g (3GPP timed text) subtitle sample. * + * Subtitle times are taken explicitly from the packet timestamps instead of + * the global timing pointer, which can be far ahead of this track because + * packets from every track are interleaved. + * * @param ctx CCExtractor library context * @param data tx3g sample data * @param data_length Length of the data - * @param dts Decoding timestamp - * @param cts_offset Composition time offset - * @param timescale Media timescale + * @param start_ms Subtitle start time in milliseconds * @param sub Output subtitle structure * @return 0 on success, non-zero on error */ int ccx_mp4_process_tx3g_packet(struct lib_ccx_ctx *ctx, unsigned char *data, unsigned int data_length, - long long dts, int cts_offset, - unsigned int timescale, + long long start_ms, struct cc_subtitle *sub); /* * Flush pending tx3g subtitle (encode the last one). * - * @param ctx CCExtractor library context - * @param sub Output subtitle structure + * @param ctx CCExtractor library context + * @param sub Output subtitle structure + * @param end_ms Subtitle end time in milliseconds */ - void ccx_mp4_flush_tx3g(struct lib_ccx_ctx *ctx, struct cc_subtitle *sub); + void ccx_mp4_flush_tx3g(struct lib_ccx_ctx *ctx, struct cc_subtitle *sub, long long end_ms); /* * Report progress for the MP4 demuxer. diff --git a/src/rust/src/demuxer/mp4.rs b/src/rust/src/demuxer/mp4.rs index cf0cd4548..fc3151892 100644 --- a/src/rust/src/demuxer/mp4.rs +++ b/src/rust/src/demuxer/mp4.rs @@ -54,13 +54,11 @@ extern "C" { ctx: *mut lib_ccx_ctx, data: *mut u8, data_length: c_uint, - dts: i64, - cts_offset: c_int, - timescale: c_uint, + start_ms: i64, sub: *mut cc_subtitle, ) -> c_int; - fn ccx_mp4_flush_tx3g(ctx: *mut lib_ccx_ctx, sub: *mut cc_subtitle); + fn ccx_mp4_flush_tx3g(ctx: *mut lib_ccx_ctx, sub: *mut cc_subtitle, end_ms: i64); fn ccx_mp4_report_progress(ctx: *mut lib_ccx_ctx, cur: c_uint, total: c_uint); @@ -313,6 +311,8 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_s let mut packet_count: u32 = 0; let mut has_tx3g = false; + let mut prev_tx3g_start_ms: i64 = -1; + let mut prev_tx3g_end_ms: i64 = -1; let mut prev_dvdsub_pts: i64 = -1; let mut prev_dvdsub_data: Vec = Vec::new(); @@ -397,20 +397,36 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_s } TrackType::Tx3g => { if pkt.size > 0 && !pkt.data.is_null() { + // Subtitle times are taken from the packet timestamps directly: + // the global timing pointer may already be past this subtitle + // because packets from every track are interleaved. + let start_ms = pts * 1000 / timescale as i64; + let end_ms = if pkt.duration > 0 { + start_ms + pkt.duration * 1000 / timescale as i64 + } else { + -1 + }; if has_tx3g { - ccx_mp4_flush_tx3g(ctx, sub); + // Fall back to the new subtitle's start when the previous + // sample had no duration information. + let flush_end_ms = if prev_tx3g_end_ms >= 0 { + prev_tx3g_end_ms + } else { + start_ms + }; + ccx_mp4_flush_tx3g(ctx, sub, flush_end_ms); } let r = ccx_mp4_process_tx3g_packet( ctx, pkt.data, pkt.size as c_uint, - dts, - cts_offset, - timescale, + start_ms, sub, ); if r == 0 { has_tx3g = true; + prev_tx3g_start_ms = start_ms; + prev_tx3g_end_ms = end_ms; mp4_ret = 1; } } @@ -459,7 +475,13 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_s // Flush last tx3g subtitle if has_tx3g { - ccx_mp4_flush_tx3g(ctx, sub); + let end_ms = if prev_tx3g_end_ms >= 0 { + prev_tx3g_end_ms + } else { + // Match the 5 second default used for the other subtitle paths. + prev_tx3g_start_ms + 5000 + }; + ccx_mp4_flush_tx3g(ctx, sub, end_ms); } // Flush last dvdsub packet (use 5s default duration)