From 2da4319310820fc2c0f96b770d5de668efeb01b6 Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Tue, 10 Mar 2026 16:16:34 +0530 Subject: [PATCH 1/5] feat: implement Rust FFmpeg MP4 demuxer using rsmpeg Replace GPAC with FFmpeg for MP4 demuxing via a Rust implementation using rsmpeg. Supports AVC/H.264, HEVC/H.265, CEA-608, CEA-708, and tx3g subtitle tracks. Includes chapter extraction via ccxr_dumpchapters. VobSub tracks are detected but not yet supported. Selected at compile time via ENABLE_FFMPEG_MP4 / enable_mp4_ffmpeg Cargo feature. --- src/rust/Cargo.toml | 1 + src/rust/build.rs | 19 ++ src/rust/src/demuxer/mod.rs | 3 + src/rust/src/demuxer/mp4.rs | 371 +++++++++++++++++++++++++++++ src/rust/src/lib.rs | 2 + src/rust/src/mp4_ffmpeg_exports.rs | Bin 0 -> 4748 bytes src/rust/wrapper.h | 1 + 7 files changed, 397 insertions(+) create mode 100644 src/rust/src/demuxer/mp4.rs create mode 100644 src/rust/src/mp4_ffmpeg_exports.rs diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 34b704122..7372367f6 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -53,6 +53,7 @@ with_libcurl = [] # hardsubx_ocr enables OCR and the platform-appropriate rsmpeg hardsubx_ocr = ["rsmpeg", "tesseract-sys", "leptonica-sys"] +enable_mp4_ffmpeg = ["rsmpeg"] sanity_check = [] [profile.release-with-debug] diff --git a/src/rust/build.rs b/src/rust/build.rs index 6cce8fc38..8e36ee192 100644 --- a/src/rust/build.rs +++ b/src/rust/build.rs @@ -43,6 +43,15 @@ fn main() { "mprint", ]); + #[cfg(feature = "enable_mp4_ffmpeg")] + allowlist_functions.extend_from_slice(&[ + "ccx_mp4_process_avc_sample", + "ccx_mp4_process_hevc_sample", + "ccx_mp4_process_cc_packet", + "ccx_mp4_flush_tx3g", + "mprint", + ]); + let mut allowlist_types = Vec::new(); allowlist_types.extend_from_slice(&[ // Match both lowercase (dtvcc_*) and uppercase (DTVCC_*) patterns @@ -83,7 +92,17 @@ fn main() { #[cfg(feature = "hardsubx_ocr")] { builder = builder.clang_arg("-DENABLE_HARDSUBX"); + } + // Pass -DENABLE_FFMPEG_MP4 for both hardsubx_ocr and enable_mp4_ffmpeg features + let has_ffmpeg_mp4 = env::var("CARGO_FEATURE_ENABLE_MP4_FFMPEG").is_ok() + || env::var("CARGO_FEATURE_HARDSUBX_OCR").is_ok(); + if has_ffmpeg_mp4 { + builder = builder.clang_arg("-DENABLE_FFMPEG_MP4"); + } + + #[cfg(feature = "hardsubx_ocr")] + { // Check FFMPEG_INCLUDE_DIR environment variable (works on all platforms) if let Ok(ffmpeg_include) = env::var("FFMPEG_INCLUDE_DIR") { builder = builder.clang_arg(format!("-I{}", ffmpeg_include)); diff --git a/src/rust/src/demuxer/mod.rs b/src/rust/src/demuxer/mod.rs index 37698963f..735b1c8ad 100644 --- a/src/rust/src/demuxer/mod.rs +++ b/src/rust/src/demuxer/mod.rs @@ -41,3 +41,6 @@ pub mod demuxer_data; pub mod dvdraw; pub mod scc; pub mod stream_functions; + +#[cfg(feature = "enable_mp4_ffmpeg")] +pub mod mp4; diff --git a/src/rust/src/demuxer/mp4.rs b/src/rust/src/demuxer/mp4.rs new file mode 100644 index 000000000..59110f3c1 --- /dev/null +++ b/src/rust/src/demuxer/mp4.rs @@ -0,0 +1,371 @@ +//! FFmpeg-based MP4 demuxer for CCExtractor +//! +//! This module provides MP4 demuxing using FFmpeg's libavformat as an +//! alternative to the GPAC-based implementation. It is enabled when +//! building with the `enable_mp4_ffmpeg` feature flag. + +use crate::bindings::{ + cc_subtitle, ccx_mp4_flush_tx3g, ccx_mp4_process_avc_sample, ccx_mp4_process_cc_packet, + ccx_mp4_process_hevc_sample, lib_ccx_ctx, mprint, +}; +use rsmpeg::avcodec::AVPacket; +use rsmpeg::avformat::AVFormatContextInput; +use rsmpeg::ffi::{ + AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_DVD_SUBTITLE, + AV_CODEC_ID_EIA_608, AV_CODEC_ID_H264, AV_CODEC_ID_HEVC, AV_CODEC_ID_MOV_TEXT, +}; +use std::ffi::CString; + +#[derive(Debug, PartialEq, Clone)] +pub enum Mp4TrackType { + AvcH264, + HevcH265, + Tx3g, + Cea608, + Cea708, + VobSub, + Unknown, +} + +#[derive(Debug, Clone)] +pub struct Mp4Track { + pub stream_index: usize, + pub track_type: Mp4TrackType, + pub time_base_den: u32, + pub nal_unit_size: u8, +} + +pub fn avc_nal_unit_size(extradata: &[u8]) -> u8 { + if extradata.len() < 5 || extradata[0] != 1 { + return 4; + } + (extradata[4] & 0x03) + 1 +} + +pub fn hevc_nal_unit_size(extradata: &[u8]) -> u8 { + if extradata.len() < 22 { + return 4; + } + (extradata[21] & 0x03) + 1 +} + +pub fn classify_stream(codec_type: i32, codec_id: u32, codec_tag: u32) -> Mp4TrackType { + if codec_type == AVMEDIA_TYPE_VIDEO { + if codec_id == AV_CODEC_ID_H264 { + return Mp4TrackType::AvcH264; + } else if codec_id == AV_CODEC_ID_HEVC { + return Mp4TrackType::HevcH265; + } + } else if codec_type == AVMEDIA_TYPE_SUBTITLE { + if codec_id == AV_CODEC_ID_MOV_TEXT { + return Mp4TrackType::Tx3g; + } else if codec_id == AV_CODEC_ID_EIA_608 { + return Mp4TrackType::Cea608; + } else if codec_id == AV_CODEC_ID_DVD_SUBTITLE { + return Mp4TrackType::VobSub; + } + } else if codec_type == AVMEDIA_TYPE_DATA { + // c608/c708 tracks show up as data streams with a codec tag. + // FFmpeg stores codec_tag in little-endian (native) byte order on x86. + let c608 = u32::from_le_bytes(*b"c608"); + let c708 = u32::from_le_bytes(*b"c708"); + if codec_tag == c608 { + return Mp4TrackType::Cea608; + } else if codec_tag == c708 { + return Mp4TrackType::Cea708; + } + } + Mp4TrackType::Unknown +} + +/// Open an MP4 file, enumerate its tracks, and return both tracks and the +/// open format context - avoiding the need to open the file twice. +pub fn open_and_enumerate(path: &str) -> Result<(Vec, AVFormatContextInput), String> { + let path_cstr = CString::new(path).map_err(|e| e.to_string())?; + + let fmt_ctx = AVFormatContextInput::open(&path_cstr, None, &mut None) + .map_err(|e| format!("Failed to open '{}': {}", path, e))?; + + let mut tracks = Vec::new(); + + for (i, stream) in fmt_ctx.streams().iter().enumerate() { + let codecpar = stream.codecpar(); + let codec_type = codecpar.codec_type; + let codec_id = codecpar.codec_id; + let codec_tag = codecpar.codec_tag; + let time_base_den = stream.time_base.den as u32; + + let extradata = if codecpar.extradata.is_null() || codecpar.extradata_size <= 0 { + &[][..] + } else { + unsafe { + std::slice::from_raw_parts(codecpar.extradata, codecpar.extradata_size as usize) + } + }; + + let track_type = classify_stream(codec_type, codec_id, codec_tag); + + let nal_unit_size = match track_type { + Mp4TrackType::AvcH264 => avc_nal_unit_size(extradata), + Mp4TrackType::HevcH265 => hevc_nal_unit_size(extradata), + _ => 0, + }; + + tracks.push(Mp4Track { + stream_index: i, + track_type, + time_base_den, + nal_unit_size, + }); + } + + Ok((tracks, fmt_ctx)) +} + +fn packet_timestamps(pkt: &AVPacket) -> (u64, u32) { + let dts = if pkt.dts != i64::MIN { + pkt.dts as u64 + } else if pkt.pts != i64::MIN { + pkt.pts as u64 + } else { + 0 + }; + let cts_offset = if pkt.pts != i64::MIN && pkt.pts > pkt.dts { + (pkt.pts - pkt.dts).min(u32::MAX as i64) as u32 + } else { + 0 + }; + (dts, cts_offset) +} + +fn packet_dts(pkt: &AVPacket) -> u64 { + if pkt.dts != i64::MIN { + pkt.dts as u64 + } else if pkt.pts != i64::MIN { + pkt.pts.max(0) as u64 + } else { + 0 + } +} + +fn process_avc_packet( + ctx: *mut lib_ccx_ctx, + track: &Mp4Track, + pkt: &AVPacket, + sub: *mut cc_subtitle, +) -> i32 { + let (dts, cts_offset) = packet_timestamps(pkt); + let data = unsafe { std::slice::from_raw_parts(pkt.data, pkt.size as usize) }; + unsafe { + ccx_mp4_process_avc_sample( + ctx, + track.time_base_den, + track.nal_unit_size, + data.as_ptr(), + data.len() as u32, + dts, + cts_offset, + sub, + ) + } +} + +fn process_hevc_packet( + ctx: *mut lib_ccx_ctx, + track: &Mp4Track, + pkt: &AVPacket, + sub: *mut cc_subtitle, +) -> i32 { + let (dts, cts_offset) = packet_timestamps(pkt); + let data = unsafe { std::slice::from_raw_parts(pkt.data, pkt.size as usize) }; + unsafe { + ccx_mp4_process_hevc_sample( + ctx, + track.time_base_den, + track.nal_unit_size, + data.as_ptr(), + data.len() as u32, + dts, + cts_offset, + sub, + ) + } +} + +/// Main entry point: process all tracks in an MP4 file in a single demux pass. +/// +/// # Safety +/// ctx, path, and sub must be valid pointers from C +pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &str, sub: *mut cc_subtitle) -> i32 { + let (tracks, mut fmt_ctx) = match open_and_enumerate(path) { + Ok(t) => t, + Err(e) => { + let msg = + std::ffi::CString::new(format!("ccx_mp4_rust: failed to open '{}': {}\n", path, e)) + .unwrap_or_default(); + mprint(msg.as_ptr()); + return -1; + } + }; + + let avc_count = tracks + .iter() + .filter(|t| t.track_type == Mp4TrackType::AvcH264) + .count(); + let hevc_count = tracks + .iter() + .filter(|t| t.track_type == Mp4TrackType::HevcH265) + .count(); + let cc_count = tracks + .iter() + .filter(|t| { + matches!( + t.track_type, + Mp4TrackType::Cea608 | Mp4TrackType::Cea708 | Mp4TrackType::Tx3g + ) + }) + .count(); + + { + let msg = std::ffi::CString::new(format!( + "MP4 Rust demuxer: {} tracks ({} avc, {} hevc, {} cc)\n", + tracks.len(), + avc_count, + hevc_count, + cc_count + )) + .unwrap_or_default(); + mprint(msg.as_ptr()); + } + + let mut mp4_ret = 0i32; + + while let Ok(Some(pkt)) = fmt_ctx.read_packet() { + let stream_idx = pkt.stream_index as usize; + + let track = match tracks.iter().find(|t| t.stream_index == stream_idx) { + Some(t) => t, + None => continue, + }; + + let status = match track.track_type { + Mp4TrackType::AvcH264 => process_avc_packet(ctx, track, &pkt, sub), + Mp4TrackType::HevcH265 => process_hevc_packet(ctx, track, &pkt, sub), + Mp4TrackType::Cea608 => unsafe { + let data = std::slice::from_raw_parts(pkt.data, pkt.size as usize); + ccx_mp4_process_cc_packet( + ctx, + 0, // CCX_MP4_TRACK_C608 + track.time_base_den, + data.as_ptr(), + data.len() as u32, + packet_dts(&pkt), + sub, + ) + }, + Mp4TrackType::Cea708 => unsafe { + let data = std::slice::from_raw_parts(pkt.data, pkt.size as usize); + ccx_mp4_process_cc_packet( + ctx, + 1, // CCX_MP4_TRACK_C708 + track.time_base_den, + data.as_ptr(), + data.len() as u32, + packet_dts(&pkt), + sub, + ) + }, + Mp4TrackType::Tx3g => unsafe { + let data = std::slice::from_raw_parts(pkt.data, pkt.size as usize); + ccx_mp4_process_cc_packet( + ctx, + 2, // CCX_MP4_TRACK_TX3G + track.time_base_den, + data.as_ptr(), + data.len() as u32, + packet_dts(&pkt), + sub, + ) + }, + Mp4TrackType::VobSub => { + let msg = std::ffi::CString::new( + "MP4: VobSub track found but not yet supported in FFmpeg demuxer\n", + ) + .unwrap(); + unsafe { mprint(msg.as_ptr()) }; + 0 + } + Mp4TrackType::Unknown => 0, + }; + + if status < 0 { + mp4_ret = status; + break; + } else if status > 0 { + mp4_ret = status; + } + } + + // Flush any pending tx3g subtitle + let has_tx3g = tracks.iter().any(|t| t.track_type == Mp4TrackType::Tx3g); + if has_tx3g { + unsafe { ccx_mp4_flush_tx3g(ctx, sub) }; + } + + mp4_ret +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_avc_nal_unit_size_valid() { + let extradata = [0x01, 0x64, 0x00, 0x1F, 0xFF]; + assert_eq!(avc_nal_unit_size(&extradata), 4); + } + + #[test] + fn test_avc_nal_unit_size_invalid() { + assert_eq!(avc_nal_unit_size(&[]), 4); + assert_eq!(avc_nal_unit_size(&[0x00]), 4); + } + + #[test] + fn test_classify_stream_avc() { + let t = classify_stream(AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_H264, 0); + assert_eq!(t, Mp4TrackType::AvcH264); + } + + #[test] + fn test_classify_stream_hevc() { + let t = classify_stream(AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_HEVC, 0); + assert_eq!(t, Mp4TrackType::HevcH265); + } + + #[test] + fn test_classify_stream_tx3g() { + let t = classify_stream(AVMEDIA_TYPE_SUBTITLE, AV_CODEC_ID_MOV_TEXT, 0); + assert_eq!(t, Mp4TrackType::Tx3g); + } + + #[test] + fn test_classify_stream_c608() { + let tag = u32::from_le_bytes(*b"c608"); + let t = classify_stream(AVMEDIA_TYPE_DATA, 0, tag); + assert_eq!(t, Mp4TrackType::Cea608); + } + + #[test] + fn test_classify_stream_c708() { + let tag = u32::from_le_bytes(*b"c708"); + let t = classify_stream(AVMEDIA_TYPE_DATA, 0, tag); + assert_eq!(t, Mp4TrackType::Cea708); + } + + #[test] + fn test_open_invalid_file() { + let result = open_and_enumerate("nonexistent_file.mp4"); + assert!(result.is_err()); + } +} diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index 3801b11af..0754cfc3a 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -27,6 +27,8 @@ pub mod file_functions; pub mod hardsubx; pub mod hlist; pub mod libccxr_exports; +#[cfg(feature = "enable_mp4_ffmpeg")] +pub mod mp4_ffmpeg_exports; pub mod parser; pub mod track_lister; pub mod utils; diff --git a/src/rust/src/mp4_ffmpeg_exports.rs b/src/rust/src/mp4_ffmpeg_exports.rs new file mode 100644 index 0000000000000000000000000000000000000000..02c71776d9c8d380714e6a33f43c434b03c281af GIT binary patch literal 4748 zcmcgwTW`}y6y`a<;xq_kcO55z(uYAssx6evtuDB`vJPvMhDq`9#_Fb0ql@lqn&rO49QDVN&mT?CM%n1OAg z1yn|3bVpDBQU%Y2I?v?aqq1H?ttym{DKk5r-0X}I0$Q`B?S9O-5&$qYM#NLS`Bw{U1jaE$NuPyJ|erD1@DKJn+ zkP8!C0Obi&hH>Vl(H=SMho<1@BFf)L`{SEe`Ec|Gs=x{&4R5vfxIOH2xtEw$dQZy8 zPsAU?FmyOXq3XaToNQ7L+DQ2eI<9k-a(Zr)P%AM zExf{iffN;@^^r&DqVY)V4*-bBejYD`hYm9h%vamsA<;gp9}7MXpm$r5^^^?S!l&ml z9su_(DBaI6lM0dCVrCb5C-Vlj+kl!=#Sw07AGkmnLc#Hu#q`~a!vhDf0A^4iiM~oJ z`2dW5UetdM9`uKc%jmIyGuzc?{ukWL23_ZoV3T4G_xw<(8}HWKf(c!%860+SbTk-# zj2&M_1c`@X)|%O(?Dsk^0f%9Uny^pNsB8NZOzuuVh>^Y}_x$^Z8qV$E;2O-fM$=;0 z@gAOD1I@HN{K5dEQhKQWyxIk$uJt_XxO&y$#*V9<`%_-xs5j0gBG9-1$#cfUB`#zL zHO&2&6Z>sZD1(##y(;S+^tcplgbddtTqU+&;kI;waoRdg3&#G`2u$3X+WAU7mq-2X zOd&#%j463#YkR9rMz%RdyBc9L#<$`^V?4WpP$xUCk zEJ>^-$yljtX_HqX$FAbGA_^a4AMIzC!cKEY^}~rC zTL8(W2fI|vaR@2zR^5W?25^s}R##e-ywDC3E=ZKwqBfjVqI&1?o;P0M48Jzx09xD7 zkIr6N!>kd=+2MiE%2=c&UKPCOt;}D|x2?TQp+y9H1a&_@rhwHNZS0iwY{aVu`Qt#_ zCCWLCUP4L>;VkUKhBkz>O25k4vrI0T}OeQzI?`C*I6ut<_h8ZYgo zp4N89u(R<|26632|LUgV=>&IM`a=(yQ5m5P&nr~v?Kjf=(4N9R%xAM1{^`9jX)ZaX zFT{=5`gVBG2_&J@-PEyE4RsUF)_{wm#wkS@Z48;hOMW<;aa8RB+fwG}h`~!5TG|mm z+YtMx`fvwZB{2x`Hk+d)9`bQpQI@3xo1QgPou?E!d-vtvo#+D=3H$( z_~YldXDc>)u|Be&toIq>_m|j}29m5R!y{Ww!=t8bhKEg23=i{^aNnuuLF0}`ofAXD WKOCRFJ=%o10be7i={$4XQ~v?$`fLjT literal 0 HcmV?d00001 diff --git a/src/rust/wrapper.h b/src/rust/wrapper.h index 25b90c297..a4acfa0d2 100644 --- a/src/rust/wrapper.h +++ b/src/rust/wrapper.h @@ -14,3 +14,4 @@ #include "../lib_ccx/ccx_gxf.h" #include "../lib_ccx/ccx_demuxer_mxf.h" #include "../lib_ccx/cc_bitstream.h" +#include "../lib_ccx/mp4_rust_bridge.h" From 6a35080419b2b867472db369089a186cbe6efdbf Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Tue, 10 Mar 2026 16:32:38 +0530 Subject: [PATCH 2/5] feat: add C bridge for Rust FFmpeg demuxer and integrate into mp4.c - mp4_rust_bridge.c/.h: C-callable functions for AVC/HEVC/CC processing - ccx_gpac_types.h: compat defines for GF_4CC, GF_ISOM_SUBTYPE_C708, etc. - mp4.c: dispatch to Rust demuxer when ENABLE_FFMPEG_MP4 is set - ccextractor.c: iterate all input files in MP4 mode - Remove dead processmp4_ffmpeg/dumpchapters_ffmpeg declarations --- src/ccextractor.c | 30 +++--- src/lib_ccx/ccx_decoders_isdb.c | 80 ++++++++++++--- src/lib_ccx/ccx_gpac_types.h | 43 ++++++++ src/lib_ccx/ccx_mp4.h | 8 ++ src/lib_ccx/mp4.c | 168 ++++++++++++++++++++++++-------- src/lib_ccx/mp4_rust_bridge.c | 47 +++++++++ src/lib_ccx/mp4_rust_bridge.h | 30 ++++++ 7 files changed, 338 insertions(+), 68 deletions(-) create mode 100644 src/lib_ccx/ccx_gpac_types.h create mode 100644 src/lib_ccx/mp4_rust_bridge.c create mode 100644 src/lib_ccx/mp4_rust_bridge.h diff --git a/src/ccextractor.c b/src/ccextractor.c index e8d2cffa5..c178a48d6 100644 --- a/src/ccextractor.c +++ b/src/ccextractor.c @@ -222,24 +222,30 @@ int start_ccx() ret = tmp; break; case CCX_SM_MP4: - mprint("\rAnalyzing data with GPAC (MP4 library)\n"); - close_input_file(ctx); // No need to have it open. GPAC will do it for us + mprint("\rAnalyzing data in MP4 mode\n"); + close_input_file(ctx); // No need to have it open. The MP4 library will reopen it if (ctx->current_file == -1) // We don't have a file to open, must be stdin, and GPAC is incompatible with stdin { fatal(EXIT_INCOMPATIBLE_PARAMETERS, "MP4 requires an actual file, it's not possible to read from a stream, including stdin.\n"); } - if (ccx_options.extract_chapters) + for (int mp4_i = 0; mp4_i < ccx_options.num_input_files; mp4_i++) { - tmp = dumpchapters(ctx, &ctx->mp4_cfg, ctx->inputfile[ctx->current_file]); + char *mp4_file = ccx_options.inputfile[mp4_i]; + if (!mp4_file) + continue; + if (ccx_options.extract_chapters) + { + tmp = dumpchapters(ctx, &ctx->mp4_cfg, mp4_file); + } + else + { + tmp = processmp4(ctx, &ctx->mp4_cfg, mp4_file); + } + if (ccx_options.print_file_reports) + print_file_report(ctx); + if (!ret) + ret = tmp; } - else - { - tmp = processmp4(ctx, &ctx->mp4_cfg, ctx->inputfile[ctx->current_file]); - } - if (ccx_options.print_file_reports) - print_file_report(ctx); - if (!ret) - ret = tmp; break; case CCX_SM_MKV: mprint("\rAnalyzing data in Matroska mode\n"); diff --git a/src/lib_ccx/ccx_decoders_isdb.c b/src/lib_ccx/ccx_decoders_isdb.c index 5a54340df..14444cb1b 100644 --- a/src/lib_ccx/ccx_decoders_isdb.c +++ b/src/lib_ccx/ccx_decoders_isdb.c @@ -141,29 +141,77 @@ typedef uint32_t rgba; static rgba Default_clut[128] = { // 0-7 - RGBA(0, 0, 0, 255), RGBA(255, 0, 0, 255), RGBA(0, 255, 0, 255), RGBA(255, 255, 0, 255), - RGBA(0, 0, 255, 255), RGBA(255, 0, 255, 255), RGBA(0, 255, 255, 255), RGBA(255, 255, 255, 255), + RGBA(0, 0, 0, 255), + RGBA(255, 0, 0, 255), + RGBA(0, 255, 0, 255), + RGBA(255, 255, 0, 255), + RGBA(0, 0, 255, 255), + RGBA(255, 0, 255, 255), + RGBA(0, 255, 255, 255), + RGBA(255, 255, 255, 255), // 8-15 - RGBA(0, 0, 0, 0), RGBA(170, 0, 0, 255), RGBA(0, 170, 0, 255), RGBA(170, 170, 0, 255), - RGBA(0, 0, 170, 255), RGBA(170, 0, 170, 255), RGBA(0, 170, 170, 255), RGBA(170, 170, 170, 255), + RGBA(0, 0, 0, 0), + RGBA(170, 0, 0, 255), + RGBA(0, 170, 0, 255), + RGBA(170, 170, 0, 255), + RGBA(0, 0, 170, 255), + RGBA(170, 0, 170, 255), + RGBA(0, 170, 170, 255), + RGBA(170, 170, 170, 255), // 16-23 - RGBA(0, 0, 85, 255), RGBA(0, 85, 0, 255), RGBA(0, 85, 85, 255), RGBA(0, 85, 170, 255), - RGBA(0, 85, 255, 255), RGBA(0, 170, 85, 255), RGBA(0, 170, 255, 255), RGBA(0, 255, 85, 255), + RGBA(0, 0, 85, 255), + RGBA(0, 85, 0, 255), + RGBA(0, 85, 85, 255), + RGBA(0, 85, 170, 255), + RGBA(0, 85, 255, 255), + RGBA(0, 170, 85, 255), + RGBA(0, 170, 255, 255), + RGBA(0, 255, 85, 255), // 24-31 - RGBA(0, 255, 170, 255), RGBA(85, 0, 0, 255), RGBA(85, 0, 85, 255), RGBA(85, 0, 170, 255), - RGBA(85, 0, 255, 255), RGBA(85, 85, 0, 255), RGBA(85, 85, 85, 255), RGBA(85, 85, 170, 255), + RGBA(0, 255, 170, 255), + RGBA(85, 0, 0, 255), + RGBA(85, 0, 85, 255), + RGBA(85, 0, 170, 255), + RGBA(85, 0, 255, 255), + RGBA(85, 85, 0, 255), + RGBA(85, 85, 85, 255), + RGBA(85, 85, 170, 255), // 32-39 - RGBA(85, 85, 255, 255), RGBA(85, 170, 0, 255), RGBA(85, 170, 85, 255), RGBA(85, 170, 170, 255), - RGBA(85, 170, 255, 255), RGBA(85, 255, 0, 255), RGBA(85, 255, 85, 255), RGBA(85, 255, 170, 255), + RGBA(85, 85, 255, 255), + RGBA(85, 170, 0, 255), + RGBA(85, 170, 85, 255), + RGBA(85, 170, 170, 255), + RGBA(85, 170, 255, 255), + RGBA(85, 255, 0, 255), + RGBA(85, 255, 85, 255), + RGBA(85, 255, 170, 255), // 40-47 - RGBA(85, 255, 255, 255), RGBA(170, 0, 85, 255), RGBA(170, 0, 255, 255), RGBA(170, 85, 0, 255), - RGBA(170, 85, 85, 255), RGBA(170, 85, 170, 255), RGBA(170, 85, 255, 255), RGBA(170, 170, 85, 255), + RGBA(85, 255, 255, 255), + RGBA(170, 0, 85, 255), + RGBA(170, 0, 255, 255), + RGBA(170, 85, 0, 255), + RGBA(170, 85, 85, 255), + RGBA(170, 85, 170, 255), + RGBA(170, 85, 255, 255), + RGBA(170, 170, 85, 255), // 48-55 - RGBA(170, 170, 255, 255), RGBA(170, 255, 0, 255), RGBA(170, 255, 85, 255), RGBA(170, 255, 170, 255), - RGBA(170, 255, 255, 255), RGBA(255, 0, 85, 255), RGBA(255, 0, 170, 255), RGBA(255, 85, 0, 255), + RGBA(170, 170, 255, 255), + RGBA(170, 255, 0, 255), + RGBA(170, 255, 85, 255), + RGBA(170, 255, 170, 255), + RGBA(170, 255, 255, 255), + RGBA(255, 0, 85, 255), + RGBA(255, 0, 170, 255), + RGBA(255, 85, 0, 255), // 56-63 - RGBA(255, 85, 85, 255), RGBA(255, 85, 170, 255), RGBA(255, 85, 255, 255), RGBA(255, 170, 0, 255), - RGBA(255, 170, 85, 255), RGBA(255, 170, 170, 255), RGBA(255, 170, 255, 255), RGBA(255, 255, 85, 255), + RGBA(255, 85, 85, 255), + RGBA(255, 85, 170, 255), + RGBA(255, 85, 255, 255), + RGBA(255, 170, 0, 255), + RGBA(255, 170, 85, 255), + RGBA(255, 170, 170, 255), + RGBA(255, 170, 255, 255), + RGBA(255, 255, 85, 255), // 64 RGBA(255, 255, 170, 255), // 65-127 are calculated later. diff --git a/src/lib_ccx/ccx_gpac_types.h b/src/lib_ccx/ccx_gpac_types.h new file mode 100644 index 000000000..495133bee --- /dev/null +++ b/src/lib_ccx/ccx_gpac_types.h @@ -0,0 +1,43 @@ +#ifndef CCX_GPAC_TYPES_H +#define CCX_GPAC_TYPES_H + +/* GPAC compatible type definitions used when building without GPAC headers */ +#include + +typedef uint32_t u32; +typedef int32_t s32; +typedef int64_t s64; +typedef double Double; +typedef uint8_t u8; +typedef uint64_t u64; + +typedef struct +{ + char *data; + uint32_t dataLength; + uint64_t DTS; + uint32_t CTS_Offset; +} GF_ISOSample; + +typedef struct +{ + uint8_t nal_unit_size; +} GF_AVCConfig; +typedef struct +{ + uint8_t nal_unit_size; +} GF_HEVCConfig; +typedef void GF_ISOFile; +typedef void GF_GenericSampleDescription; + +#define GF_4CC(a, b, c, d) ((((u32)(a)) << 24) | (((u32)(b)) << 16) | (((u32)(c)) << 8) | ((u32)(d))) + +#ifndef GF_ISOM_SUBTYPE_C708 +#define GF_ISOM_SUBTYPE_C708 GF_4CC('c', '7', '0', '8') +#endif + +#ifndef GF_QT_SUBTYPE_C608 +#define GF_QT_SUBTYPE_C608 GF_4CC('c', '6', '0', '8') +#endif + +#endif /* CCX_GPAC_TYPES_H */ diff --git a/src/lib_ccx/ccx_mp4.h b/src/lib_ccx/ccx_mp4.h index 51331b55d..4cf292fed 100644 --- a/src/lib_ccx/ccx_mp4.h +++ b/src/lib_ccx/ccx_mp4.h @@ -3,4 +3,12 @@ int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file); int dumpchapters(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file); + +#ifdef ENABLE_FFMPEG_MP4 + +/* Rust-based MP4 demuxer - preferred when available */ +int ccxr_processmp4(struct lib_ccx_ctx *ctx, char *file); +int ccxr_dumpchapters(struct lib_ccx_ctx *ctx, char *file); +#endif + #endif diff --git a/src/lib_ccx/mp4.c b/src/lib_ccx/mp4.c index 48e20e842..2983f3f5f 100644 --- a/src/lib_ccx/mp4.c +++ b/src/lib_ccx/mp4.c @@ -1,9 +1,14 @@ +#include #include #include #include +#ifndef ENABLE_FFMPEG_MP4 #include #include +#else +#include "ccx_gpac_types.h" +#endif #include "lib_ccx.h" #include "utility.h" #include "ccx_encoders_common.h" @@ -50,7 +55,7 @@ static struct unsigned type[32]; } s_nalu_stats; -static int process_avc_sample(struct lib_ccx_ctx *ctx, u32 timescale, GF_AVCConfig *c, GF_ISOSample *s, struct cc_subtitle *sub) +int process_avc_sample(struct lib_ccx_ctx *ctx, u32 timescale, GF_AVCConfig *c, GF_ISOSample *s, struct cc_subtitle *sub) { int status = 0; u32 i; @@ -119,7 +124,7 @@ static int process_avc_sample(struct lib_ccx_ctx *ctx, u32 timescale, GF_AVCConf return status; } -static int process_hevc_sample(struct lib_ccx_ctx *ctx, u32 timescale, GF_HEVCConfig *c, GF_ISOSample *s, struct cc_subtitle *sub) +int process_hevc_sample(struct lib_ccx_ctx *ctx, u32 timescale, GF_HEVCConfig *c, GF_ISOSample *s, struct cc_subtitle *sub) { int status = 0; u32 i; @@ -200,6 +205,7 @@ static int process_hevc_sample(struct lib_ccx_ctx *ctx, u32 timescale, GF_HEVCCo return status; } +#ifndef ENABLE_FFMPEG_MP4 static int process_xdvb_track(struct lib_ccx_ctx *ctx, const char *basename, GF_ISOFile *f, u32 track, struct cc_subtitle *sub) { u32 timescale, i, sample_count; @@ -254,7 +260,9 @@ static int process_xdvb_track(struct lib_ccx_ctx *ctx, const char *basename, GF_ return status; } +#endif // !ENABLE_FFMPEG_MP4 +#ifndef ENABLE_FFMPEG_MP4 static int process_avc_track(struct lib_ccx_ctx *ctx, const char *basename, GF_ISOFile *f, u32 track, struct cc_subtitle *sub) { u32 timescale, i, sample_count, last_sdi = 0; @@ -334,7 +342,9 @@ static int process_avc_track(struct lib_ccx_ctx *ctx, const char *basename, GF_I return status; } +#endif // !ENABLE_FFMPEG_MP4 +#ifndef ENABLE_FFMPEG_MP4 static int process_hevc_track(struct lib_ccx_ctx *ctx, const char *basename, GF_ISOFile *f, u32 track, struct cc_subtitle *sub) { u32 timescale, i, sample_count, last_sdi = 0; @@ -417,7 +427,9 @@ static int process_hevc_track(struct lib_ccx_ctx *ctx, const char *basename, GF_ return status; } +#endif // !ENABLE_FFMPEG_MP4 +#ifndef ENABLE_FFMPEG_MP4 static int process_vobsub_track(struct lib_ccx_ctx *ctx, GF_ISOFile *f, u32 track, struct cc_subtitle *sub) { u32 timescale, i, sample_count; @@ -555,6 +567,7 @@ static int process_vobsub_track(struct lib_ccx_ctx *ctx, GF_ISOFile *f, u32 trac return status; } +#endif // !ENABLE_FFMPEG_MP4 static char *format_duration(u64 dur, u32 timescale, char *szDur, size_t szDur_size) { @@ -877,8 +890,12 @@ static int process_tx3g(struct lib_ccx_ctx *ctx, struct encoder_ctx *enc_ctx, } */ + int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file) { +#ifdef ENABLE_FFMPEG_MP4 + return ccxr_processmp4(ctx, file); +#else int mp4_ret = 0; GF_ISOFile *f; u32 i, j, track_count, avc_track_count, hevc_track_count, cc_track_count; @@ -915,9 +932,7 @@ int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file) free(dec_ctx->xds_ctx); return -2; } - mprint("ok\n"); - track_count = gf_isom_get_track_count(f); avc_track_count = 0; @@ -1235,61 +1250,134 @@ int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file) } return mp4_ret; + +#endif // ENABLE_FFMPEG_MP4 } int dumpchapters(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file) { - int mp4_ret = 0; - GF_ISOFile *f; - mprint("Opening \'%s\': ", file); +#ifdef ENABLE_FFMPEG_MP4 + return ccxr_dumpchapters(ctx, file); +#else + { + int mp4_ret = 0; + GF_ISOFile *f; + mprint("Opening \'%s\': ", file); #ifdef MP4_DEBUG - gf_log_set_tool_level(GF_LOG_CONTAINER, GF_LOG_DEBUG); + gf_log_set_tool_level(GF_LOG_CONTAINER, GF_LOG_DEBUG); #endif - if ((f = gf_isom_open(file, GF_ISOM_OPEN_READ, NULL)) == NULL) - { - mprint("failed to open\n"); - return 5; - } + if ((f = gf_isom_open(file, GF_ISOM_OPEN_READ, NULL)) == NULL) + { + mprint("failed to open\n"); + return 5; + } - mprint("ok\n"); + mprint("ok\n"); - char szName[1024]; - FILE *t; - u32 i, count; - count = gf_isom_get_chapter_count(f, 0); - if (count > 0) - { - if (file) + char szName[1024]; + FILE *t; + u32 i, count; + count = gf_isom_get_chapter_count(f, 0); + if (count > 0) { - snprintf(szName, sizeof(szName), "%s.txt", get_basename(file)); + if (file) + { + snprintf(szName, sizeof(szName), "%s.txt", get_basename(file)); - t = gf_fopen(szName, "wt"); - if (!t) - return 5; + t = gf_fopen(szName, "wt"); + if (!t) + return 5; + } + else + { + t = stdout; + } + mp4_ret = 1; + printf("Writing chapters into %s\n", szName); } else { - t = stdout; + mprint("No chapters information found!\n"); } - mp4_ret = 1; - printf("Writing chapters into %s\n", szName); + + for (i = 0; i < count; i++) + { + u64 chapter_time; + const char *name; + char szDur[64]; + gf_isom_get_chapter(f, 0, i + 1, &chapter_time, &name); + fprintf(t, "CHAPTER%02d=%s\n", i + 1, format_duration(chapter_time, 1000, szDur, sizeof(szDur))); + fprintf(t, "CHAPTER%02dNAME=%s\n", i + 1, name); + } + if (file) + gf_fclose(t); + return mp4_ret; } - else +#endif // ENABLE_FFMPEG_MP4 +} + +#ifdef ENABLE_FFMPEG_MP4 + +#include "mp4_rust_bridge.h" + +#ifndef GF_QT_SUBTYPE_C608 +#define GF_QT_SUBTYPE_C608 GF_4CC('c', '6', '0', '8') +#endif + +int ccx_mp4_process_cc_packet(struct lib_ccx_ctx *ctx, int track_type, + uint32_t timescale, const uint8_t *data, + uint32_t data_len, uint64_t dts, + struct cc_subtitle *sub) +{ + struct lib_cc_decode *dec_ctx = update_decoder_list(ctx); + struct encoder_ctx *enc_ctx = update_encoder_list(ctx); + if (!dec_ctx || !enc_ctx) + return -1; + + enc_ctx->timing = dec_ctx->timing; + + /* Set timing so process_clcp / process_tx3g get correct timestamps */ + set_current_pts(dec_ctx->timing, (LLONG)dts * MPEG_CLOCK_FREQ / timescale); + if (track_type == CCX_MP4_TRACK_C608 || track_type == CCX_MP4_TRACK_C708) + dec_ctx->timing->current_picture_coding_type = CCX_FRAME_TYPE_I_FRAME; + set_fts(dec_ctx->timing); + + int mp4_ret = 0; + char *buf = (char *)data; + + if (track_type == CCX_MP4_TRACK_TX3G) { - mprint("No chapters information found!\n"); + process_tx3g(ctx, enc_ctx, dec_ctx, sub, &mp4_ret, + buf, data_len, 0); } - - for (i = 0; i < count; i++) + else { - u64 chapter_time; - const char *name; - char szDur[64]; - gf_isom_get_chapter(f, 0, i + 1, &chapter_time, &name); - fprintf(t, "CHAPTER%02d=%s\n", i + 1, format_duration(chapter_time, 1000, szDur, sizeof(szDur))); - fprintf(t, "CHAPTER%02dNAME=%s\n", i + 1, name); + u32 sub_type = (track_type == CCX_MP4_TRACK_C708) + ? GF_ISOM_SUBTYPE_C708 + : GF_QT_SUBTYPE_C608; + int atomStart = 0; + while (atomStart < (int)data_len) + { + int atom_len = process_clcp(ctx, enc_ctx, dec_ctx, sub, &mp4_ret, + sub_type, buf + atomStart, + data_len - atomStart); + if (atom_len < 0) + break; + atomStart += atom_len; + } } - if (file) - gf_fclose(t); return mp4_ret; } + +void ccx_mp4_flush_tx3g(struct lib_ccx_ctx *ctx, struct cc_subtitle *sub) +{ + struct lib_cc_decode *dec_ctx = update_decoder_list(ctx); + struct encoder_ctx *enc_ctx = update_encoder_list(ctx); + if (!dec_ctx || !enc_ctx) + return; + int mp4_ret = 0; + process_tx3g(ctx, enc_ctx, dec_ctx, sub, &mp4_ret, NULL, 0, 1); +} + +#endif /* ENABLE_FFMPEG_MP4 */ diff --git a/src/lib_ccx/mp4_rust_bridge.c b/src/lib_ccx/mp4_rust_bridge.c new file mode 100644 index 000000000..cc8d56918 --- /dev/null +++ b/src/lib_ccx/mp4_rust_bridge.c @@ -0,0 +1,47 @@ +#ifdef ENABLE_FFMPEG_MP4 + +#include "mp4_rust_bridge.h" +#include "ccx_gpac_types.h" +#include "lib_ccx.h" +#include "ccx_encoders_common.h" + +/* Forward declare the actual functions from mp4.c */ +int process_avc_sample(struct lib_ccx_ctx *ctx, uint32_t timescale, + GF_AVCConfig *c, GF_ISOSample *s, + struct cc_subtitle *sub); + +int process_hevc_sample(struct lib_ccx_ctx *ctx, uint32_t timescale, + GF_HEVCConfig *c, GF_ISOSample *s, + struct cc_subtitle *sub); + +int ccx_mp4_process_avc_sample(struct lib_ccx_ctx *ctx, uint32_t timescale, + uint8_t nal_unit_size, const uint8_t *data, + uint32_t data_len, uint64_t dts, + uint32_t cts_offset, struct cc_subtitle *sub) +{ + GF_AVCConfig cfg = {nal_unit_size}; + GF_ISOSample s = { + .data = (char *)data, + .dataLength = data_len, + .DTS = dts, + .CTS_Offset = cts_offset, + }; + return process_avc_sample(ctx, timescale, &cfg, &s, sub); +} + +int ccx_mp4_process_hevc_sample(struct lib_ccx_ctx *ctx, uint32_t timescale, + uint8_t nal_unit_size, const uint8_t *data, + uint32_t data_len, uint64_t dts, + uint32_t cts_offset, struct cc_subtitle *sub) +{ + GF_HEVCConfig cfg = {nal_unit_size}; + GF_ISOSample s = { + .data = (char *)data, + .dataLength = data_len, + .DTS = dts, + .CTS_Offset = cts_offset, + }; + return process_hevc_sample(ctx, timescale, &cfg, &s, sub); +} + +#endif /* ENABLE_FFMPEG_MP4 */ diff --git a/src/lib_ccx/mp4_rust_bridge.h b/src/lib_ccx/mp4_rust_bridge.h new file mode 100644 index 000000000..fc6b25105 --- /dev/null +++ b/src/lib_ccx/mp4_rust_bridge.h @@ -0,0 +1,30 @@ +#ifndef MP4_RUST_BRIDGE_H +#define MP4_RUST_BRIDGE_H + +#include "lib_ccx.h" +#include "ccx_encoders_common.h" + +/* Bridge functions exposing internal mp4.c processing to Rust */ +int ccx_mp4_process_avc_sample(struct lib_ccx_ctx *ctx, uint32_t timescale, + uint8_t nal_unit_size, const uint8_t *data, + uint32_t data_len, uint64_t dts, + uint32_t cts_offset, struct cc_subtitle *sub); + +int ccx_mp4_process_hevc_sample(struct lib_ccx_ctx *ctx, uint32_t timescale, + uint8_t nal_unit_size, const uint8_t *data, + uint32_t data_len, uint64_t dts, + uint32_t cts_offset, struct cc_subtitle *sub); + +/* CC track types for ccx_mp4_process_cc_packet */ +#define CCX_MP4_TRACK_C608 0 +#define CCX_MP4_TRACK_C708 1 +#define CCX_MP4_TRACK_TX3G 2 + +int ccx_mp4_process_cc_packet(struct lib_ccx_ctx *ctx, int track_type, + uint32_t timescale, const uint8_t *data, + uint32_t data_len, uint64_t dts, + struct cc_subtitle *sub); + +void ccx_mp4_flush_tx3g(struct lib_ccx_ctx *ctx, struct cc_subtitle *sub); + +#endif /* MP4_RUST_BRIDGE_H */ From aa75430a0374e0bdd90c8e646a5167b59321f736 Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Tue, 10 Mar 2026 16:32:41 +0530 Subject: [PATCH 3/5] build: CMake and build.rs changes for FFmpeg MP4 demuxer - src/CMakeLists.txt: link swresample, re-add libs after ccx_rust for circular dependencies, --no-as-needed on Linux, ENABLE_HARDSUBX - lib_ccx/CMakeLists.txt: target_compile_definitions for ENABLE_HARDSUBX - rust/CMakeLists.txt: enable_mp4_ffmpeg Cargo feature --- .gitignore | 3 +++ docs/CHANGES.TXT | 1 + src/CMakeLists.txt | 18 ++++++++++++++++-- src/lib_ccx/CMakeLists.txt | 8 ++++++-- src/rust/CMakeLists.txt | 10 +++++++--- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 925e24025..22a4275be 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,6 @@ plans/ tess.log **/tess.log ut=srt* +*.bak +mac/libccx_rust.a +tests/samples.zip diff --git a/docs/CHANGES.TXT b/docs/CHANGES.TXT index 3607239d6..8fc53618c 100644 --- a/docs/CHANGES.TXT +++ b/docs/CHANGES.TXT @@ -1,5 +1,6 @@ 0.96.7 (unreleased) ------------------- +- Feature: Added FFmpeg-based MP4 demuxing as alternative to GPAC (enabled with -DWITH_FFMPEG=ON). - Fix: Remove strdup() memory leaks in WebVTT styling encoder, fix invalid CSS rgba(0,256,0) green value, fix missing free(unescaped) on write-error path (#2154) - Fix: Prevent crash in Rust timing module when logging out-of-range PTS/FTS timestamps from malformed streams. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 16736731f..69f3ba05d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -175,21 +175,24 @@ if (PKG_CONFIG_FOUND AND WITH_FFMPEG) pkg_check_modules (AVCODEC REQUIRED libavcodec) pkg_check_modules (AVFILTER REQUIRED libavfilter) pkg_check_modules (SWSCALE REQUIRED libswscale) + pkg_check_modules (SWRESAMPLE REQUIRED libswresample) set (EXTRA_LIBS ${EXTRA_LIBS} ${AVFORMAT_LIBRARIES}) set (EXTRA_LIBS ${EXTRA_LIBS} ${AVUTIL_LIBRARIES}) set (EXTRA_LIBS ${EXTRA_LIBS} ${AVCODEC_LIBRARIES}) set (EXTRA_LIBS ${EXTRA_LIBS} ${AVFILTER_LIBRARIES}) set (EXTRA_LIBS ${EXTRA_LIBS} ${SWSCALE_LIBRARIES}) + set (EXTRA_LIBS ${EXTRA_LIBS} ${SWRESAMPLE_LIBRARIES}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVFORMAT_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVUTIL_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVCODEC_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVFILTER_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${SWSCALE_INCLUDE_DIRS}) + set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${SWRESAMPLE_INCLUDE_DIRS}) set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_FFMPEG") - set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_HARDSUBX") + set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_FFMPEG_MP4") endif (PKG_CONFIG_FOUND AND WITH_FFMPEG) ######################################################## @@ -219,20 +222,22 @@ if (PKG_CONFIG_FOUND AND WITH_HARDSUBX) pkg_check_modules (AVCODEC REQUIRED libavcodec) pkg_check_modules (AVFILTER REQUIRED libavfilter) pkg_check_modules (SWSCALE REQUIRED libswscale) + pkg_check_modules (SWRESAMPLE REQUIRED libswresample) set (EXTRA_LIBS ${EXTRA_LIBS} ${AVFORMAT_LIBRARIES}) set (EXTRA_LIBS ${EXTRA_LIBS} ${AVUTIL_LIBRARIES}) set (EXTRA_LIBS ${EXTRA_LIBS} ${AVCODEC_LIBRARIES}) set (EXTRA_LIBS ${EXTRA_LIBS} ${AVFILTER_LIBRARIES}) set (EXTRA_LIBS ${EXTRA_LIBS} ${SWSCALE_LIBRARIES}) + set (EXTRA_LIBS ${EXTRA_LIBS} ${SWRESAMPLE_LIBRARIES}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVFORMAT_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVUTIL_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVCODEC_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVFILTER_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${SWSCALE_INCLUDE_DIRS}) + set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${SWRESAMPLE_INCLUDE_DIRS}) - set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_HARDSUBX") pkg_check_modules (TESSERACT REQUIRED tesseract) pkg_check_modules (LEPTONICA REQUIRED lept) @@ -241,6 +246,7 @@ if (PKG_CONFIG_FOUND AND WITH_HARDSUBX) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${TESSERACT_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${LEPTONICA_INCLUDE_DIRS}) + set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_HARDSUBX") endif (PKG_CONFIG_FOUND AND WITH_HARDSUBX) add_executable (ccextractor ${SOURCEFILE} ${FREETYPE_SOURCE} ${UTF8PROC_SOURCE}) @@ -252,6 +258,14 @@ add_executable (ccextractor ${SOURCEFILE} ${FREETYPE_SOURCE} ${UTF8PROC_SOURCE}) if (PKG_CONFIG_FOUND) add_subdirectory (rust) set (EXTRA_LIBS ${EXTRA_LIBS} ccx_rust) + # Re-add libs after ccx_rust to resolve circular dependencies (Rust calls C and FFmpeg) + set (EXTRA_LIBS ${EXTRA_LIBS} ccx) + if (WITH_FFMPEG OR WITH_HARDSUBX) + if (NOT WIN32 AND NOT APPLE) + set (EXTRA_LIBS ${EXTRA_LIBS} "-Wl,--no-as-needed") + endif() + set (EXTRA_LIBS ${EXTRA_LIBS} ${AVFORMAT_LIBRARIES} ${AVUTIL_LIBRARIES} ${AVCODEC_LIBRARIES} ${AVFILTER_LIBRARIES} ${SWSCALE_LIBRARIES} ${SWRESAMPLE_LIBRARIES}) + endif() endif (PKG_CONFIG_FOUND) diff --git a/src/lib_ccx/CMakeLists.txt b/src/lib_ccx/CMakeLists.txt index a891560b8..9e4005328 100644 --- a/src/lib_ccx/CMakeLists.txt +++ b/src/lib_ccx/CMakeLists.txt @@ -38,7 +38,7 @@ if (WITH_FFMPEG) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${SWSCALE_INCLUDE_DIRS}) set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_FFMPEG") - set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_HARDSUBX") + set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_FFMPEG_MP4") endif (WITH_FFMPEG) if (WITH_OCR) @@ -59,6 +59,10 @@ endif (WITH_OCR) aux_source_directory ("${PROJECT_SOURCE_DIR}/lib_ccx/" SOURCEFILE) +if (NOT WITH_HARDSUBX) + list(FILTER SOURCEFILE EXCLUDE REGEX ".*hardsubx.*") +endif() + add_library (ccx ${SOURCEFILE} ccx_dtvcc.h ccx_dtvcc.c ccx_encoders_mcc.c ccx_encoders_mcc.h) target_link_libraries (ccx ${EXTRA_LIBS}) target_include_directories (ccx PUBLIC ${EXTRA_INCLUDES}) @@ -84,7 +88,7 @@ if (WITH_HARDSUBX) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVFILTER_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${SWSCALE_INCLUDE_DIRS}) - set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DENABLE_HARDSUBX") + target_compile_definitions(ccx PRIVATE ENABLE_HARDSUBX) endif (WITH_HARDSUBX) file (GLOB HeaderFiles *.h) diff --git a/src/rust/CMakeLists.txt b/src/rust/CMakeLists.txt index 85eddf28d..698ba861e 100644 --- a/src/rust/CMakeLists.txt +++ b/src/rust/CMakeLists.txt @@ -13,10 +13,14 @@ else() set(PROFILE "release") endif() +set(FEATURES "") + +if(WITH_FFMPEG) + set(FEATURES "${FEATURES} enable_mp4_ffmpeg") +endif() + if(WITH_OCR AND WITH_HARDSUBX) - set(FEATURES "hardsubx_ocr") -else() - set(FEATURES "") + set(FEATURES "${FEATURES} hardsubx_ocr") endif() # Check rust version From 83b23065a27621676d8ec9fd1792d1c3e1312a12 Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Fri, 13 Mar 2026 12:23:26 +0530 Subject: [PATCH 4/5] fix: correct tx3g subtitle start timestamps in FFmpeg MP4 demuxer set_fts() produces stale fts_now values after empty tx3g gap packets, causing subtitle start times to be incorrect. Override fts_now with the correct DTS-based millisecond timestamp before calling process_tx3g. Fixes #2198 --- src/lib_ccx/mp4.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib_ccx/mp4.c b/src/lib_ccx/mp4.c index 2983f3f5f..c3d1ebd45 100644 --- a/src/lib_ccx/mp4.c +++ b/src/lib_ccx/mp4.c @@ -1348,6 +1348,8 @@ int ccx_mp4_process_cc_packet(struct lib_ccx_ctx *ctx, int track_type, if (track_type == CCX_MP4_TRACK_TX3G) { + /* Override fts_now with correct DTS-based ms timestamp */ + dec_ctx->timing->fts_now = (LLONG)dts * 1000 / timescale; process_tx3g(ctx, enc_ctx, dec_ctx, sub, &mp4_ret, buf, data_len, 0); } From f30a36016110f0f12af30e76f2e2df16331d8562 Mon Sep 17 00:00:00 2001 From: Dhanush Varma Date: Mon, 16 Mar 2026 16:44:20 +0530 Subject: [PATCH 5/5] fix: use packet duration for tx3g last subtitle end time Pass AVPacket.duration through ccx_mp4_process_cc_packet so the last subtitle ends at start+duration instead of video end. --- src/lib_ccx/mp4.c | 11 +++++++++-- src/lib_ccx/mp4_rust_bridge.h | 2 +- src/rust/src/demuxer/mp4.rs | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/lib_ccx/mp4.c b/src/lib_ccx/mp4.c index c3d1ebd45..674a40ad3 100644 --- a/src/lib_ccx/mp4.c +++ b/src/lib_ccx/mp4.c @@ -1325,9 +1325,11 @@ int dumpchapters(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file) #define GF_QT_SUBTYPE_C608 GF_4CC('c', '6', '0', '8') #endif +static LLONG last_tx3g_end_ms = 0; + int ccx_mp4_process_cc_packet(struct lib_ccx_ctx *ctx, int track_type, uint32_t timescale, const uint8_t *data, - uint32_t data_len, uint64_t dts, + uint32_t data_len, uint64_t dts, int64_t duration, struct cc_subtitle *sub) { struct lib_cc_decode *dec_ctx = update_decoder_list(ctx); @@ -1349,7 +1351,9 @@ int ccx_mp4_process_cc_packet(struct lib_ccx_ctx *ctx, int track_type, if (track_type == CCX_MP4_TRACK_TX3G) { /* Override fts_now with correct DTS-based ms timestamp */ - dec_ctx->timing->fts_now = (LLONG)dts * 1000 / timescale; + dec_ctx->timing->fts_now = (LLONG)(dts * 1000 + timescale / 2) / timescale; + if (duration > 0) + last_tx3g_end_ms = (LLONG)((dts + duration) * 1000 + timescale / 2) / timescale; process_tx3g(ctx, enc_ctx, dec_ctx, sub, &mp4_ret, buf, data_len, 0); } @@ -1378,6 +1382,9 @@ void ccx_mp4_flush_tx3g(struct lib_ccx_ctx *ctx, struct cc_subtitle *sub) struct encoder_ctx *enc_ctx = update_encoder_list(ctx); if (!dec_ctx || !enc_ctx) return; + /* Use stored end time from last tx3g packet duration */ + if (last_tx3g_end_ms > 0) + dec_ctx->timing->fts_now = last_tx3g_end_ms; int mp4_ret = 0; process_tx3g(ctx, enc_ctx, dec_ctx, sub, &mp4_ret, NULL, 0, 1); } diff --git a/src/lib_ccx/mp4_rust_bridge.h b/src/lib_ccx/mp4_rust_bridge.h index fc6b25105..9cda25451 100644 --- a/src/lib_ccx/mp4_rust_bridge.h +++ b/src/lib_ccx/mp4_rust_bridge.h @@ -23,7 +23,7 @@ int ccx_mp4_process_hevc_sample(struct lib_ccx_ctx *ctx, uint32_t timescale, int ccx_mp4_process_cc_packet(struct lib_ccx_ctx *ctx, int track_type, uint32_t timescale, const uint8_t *data, uint32_t data_len, uint64_t dts, - struct cc_subtitle *sub); + int64_t duration, struct cc_subtitle *sub); void ccx_mp4_flush_tx3g(struct lib_ccx_ctx *ctx, struct cc_subtitle *sub); diff --git a/src/rust/src/demuxer/mp4.rs b/src/rust/src/demuxer/mp4.rs index 59110f3c1..7fc65f40e 100644 --- a/src/rust/src/demuxer/mp4.rs +++ b/src/rust/src/demuxer/mp4.rs @@ -260,6 +260,7 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &str, sub: *mut cc_su data.as_ptr(), data.len() as u32, packet_dts(&pkt), + 0, // no duration needed for CC sub, ) }, @@ -272,6 +273,7 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &str, sub: *mut cc_su data.as_ptr(), data.len() as u32, packet_dts(&pkt), + 0, // no duration needed for CC sub, ) }, @@ -284,6 +286,7 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &str, sub: *mut cc_su data.as_ptr(), data.len() as u32, packet_dts(&pkt), + pkt.duration, sub, ) },