diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 64cd62c43..aadef346c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -307,6 +307,9 @@ if (NOT WIN32 AND NOT APPLE) -Wl,--undefined=ccx_mp4_process_tx3g_packet -Wl,--undefined=ccx_mp4_flush_tx3g -Wl,--undefined=ccx_mp4_report_progress + -Wl,--undefined=ccx_mp4_vobsub_init + -Wl,--undefined=ccx_mp4_vobsub_process + -Wl,--undefined=ccx_mp4_vobsub_free -Wl,--undefined=mprint -Wl,--undefined=update_decoder_list -Wl,--undefined=update_encoder_list) diff --git a/src/lib_ccx/mp4_rust_bridge.c b/src/lib_ccx/mp4_rust_bridge.c index af4e6d58d..681ad31fc 100644 --- a/src/lib_ccx/mp4_rust_bridge.c +++ b/src/lib_ccx/mp4_rust_bridge.c @@ -15,6 +15,7 @@ #include "ccx_encoders_mcc.h" #include "ccx_mp4.h" #include "mp4_rust_bridge.h" +#include "vobsub_decoder.h" /* Walk a length-prefixed AVCC/HVCC sample, invoking do_NAL() per NAL unit. * AVC and HEVC share the iteration; is_hevc only flips the decoder state and @@ -280,4 +281,70 @@ void ccx_mp4_report_progress(struct lib_ccx_ctx *ctx, unsigned int cur, unsigned } } +/* ── VobSub / DVD subtitle bridge ───────────────────────────────── */ + +void *ccx_mp4_vobsub_init(void) +{ + if (!vobsub_ocr_available()) + { + mprint("VOBSUB to text conversion requires OCR support.\n" + "Please rebuild CCExtractor with -DWITH_OCR=ON\n"); + return NULL; + } + return init_vobsub_decoder(); +} + +int ccx_mp4_vobsub_process(void *vob_opaque, struct lib_ccx_ctx *ctx, + unsigned char *data, unsigned int data_length, + long long start_ms, long long end_ms, + struct cc_subtitle *sub) +{ + struct vobsub_ctx *vob_ctx = (struct vobsub_ctx *)vob_opaque; + 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, start_ms * MPEG_CLOCK_FREQ / 1000); + set_fts(dec_ctx->timing); + + struct cc_subtitle vob_sub; + memset(&vob_sub, 0, sizeof(vob_sub)); + + int ret = vobsub_decode_spu(vob_ctx, data, (size_t)data_length, + start_ms, end_ms, &vob_sub); + + if (ret == 0 && vob_sub.got_output) + { + encode_sub(enc_ctx, &vob_sub); + sub->got_output = 1; + + if (vob_sub.data) + { + struct cc_bitmap *rect = (struct cc_bitmap *)vob_sub.data; + for (int j = 0; j < vob_sub.nb_data; j++) + { + if (rect[j].data0) + free(rect[j].data0); + if (rect[j].data1) + free(rect[j].data1); +#ifdef ENABLE_OCR + if (rect[j].ocr_text) + free(rect[j].ocr_text); +#endif + } + free(vob_sub.data); + } + } + + return ret; +} + +void ccx_mp4_vobsub_free(void *vob_opaque) +{ + struct vobsub_ctx *vob_ctx = (struct vobsub_ctx *)vob_opaque; + if (vob_ctx) + { + delete_vobsub_decoder(&vob_ctx); + } +} + #endif /* ENABLE_FFMPEG_MP4 */ diff --git a/src/lib_ccx/mp4_rust_bridge.h b/src/lib_ccx/mp4_rust_bridge.h index 85619a024..01d4460c2 100644 --- a/src/lib_ccx/mp4_rust_bridge.h +++ b/src/lib_ccx/mp4_rust_bridge.h @@ -91,6 +91,17 @@ extern "C" */ void ccx_mp4_report_progress(struct lib_ccx_ctx *ctx, unsigned int cur, unsigned int total); + /* + * VobSub (DVD subtitle) bridge. + * Wraps vobsub_decoder.c for use from the Rust FFmpeg path. + */ + void *ccx_mp4_vobsub_init(void); + int ccx_mp4_vobsub_process(void *vob_ctx, struct lib_ccx_ctx *ctx, + unsigned char *data, unsigned int data_length, + long long start_ms, long long end_ms, + struct cc_subtitle *sub); + void ccx_mp4_vobsub_free(void *vob_ctx); + #ifdef __cplusplus } #endif diff --git a/src/rust/src/demuxer/mp4.rs b/src/rust/src/demuxer/mp4.rs index b5d81ddea..55167faad 100644 --- a/src/rust/src/demuxer/mp4.rs +++ b/src/rust/src/demuxer/mp4.rs @@ -11,12 +11,7 @@ //! - `c708` subtitle (CEA-708 via ccdp) //! - `tx3g` / `mov_text` timed-text subtitles //! -//! # Known limitations -//! - **dvdsub / bitmap subtitles in MP4** are not supported. Samples such as -//! `1f3e951d516b.mp4` contain `subp` tracks with DVD-style bitmap subtitles, -//! which neither the GPAC backend nor this FFmpeg backend currently decodes. -//! Extracting these requires rendering bitmaps and running OCR, which is out -//! of scope for the MP4 demuxer itself; track it separately if needed. +//! - `dvdsub` (DVD bitmap subtitles) via OCR through vobsub_decoder #[cfg(feature = "enable_mp4_ffmpeg")] use rsmpeg::avformat::AVFormatContextInput; @@ -67,6 +62,18 @@ extern "C" { fn ccx_mp4_report_progress(ctx: *mut lib_ccx_ctx, cur: c_uint, total: c_uint); + fn ccx_mp4_vobsub_init() -> *mut std::ffi::c_void; + fn ccx_mp4_vobsub_process( + vob_ctx: *mut std::ffi::c_void, + ctx: *mut lib_ccx_ctx, + data: *mut u8, + data_length: c_uint, + start_ms: i64, + end_ms: i64, + sub: *mut cc_subtitle, + ) -> c_int; + fn ccx_mp4_vobsub_free(vob_ctx: *mut std::ffi::c_void); + fn update_decoder_list(ctx: *mut lib_ccx_ctx) -> *mut lib_cc_decode; fn update_encoder_list(ctx: *mut lib_ccx_ctx) -> *mut encoder_ctx; @@ -83,6 +90,7 @@ enum TrackType { Cea608, Cea708, Tx3g, + DvdSub, } /// Information about a track we want to process @@ -195,6 +203,8 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_s Some(TrackType::Cea708) } else if codec_tag == FOURCC_TX3G || codec_id == ffi::AV_CODEC_ID_MOV_TEXT { Some(TrackType::Tx3g) + } else if codec_id == ffi::AV_CODEC_ID_DVD_SUBTITLE { + Some(TrackType::DvdSub) } else { None } @@ -209,6 +219,7 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_s TrackType::Cea608 => "CEA-608", TrackType::Cea708 => "CEA-708", TrackType::Tx3g => "tx3g", + TrackType::DvdSub => "dvdsub", }; let msg = format!( "Track {}, type={} timescale={}\n\0", @@ -238,7 +249,7 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_s .filter(|t| { matches!( t.track_type, - TrackType::Cea608 | TrackType::Cea708 | TrackType::Tx3g + TrackType::Cea608 | TrackType::Cea708 | TrackType::Tx3g | TrackType::DvdSub ) }) .count(); @@ -279,6 +290,14 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_s } } + // Init vobsub decoder if we have dvdsub tracks + let has_dvdsub = tracks.iter().any(|t| t.track_type == TrackType::DvdSub); + let vob_ctx = if has_dvdsub { + ccx_mp4_vobsub_init() + } else { + std::ptr::null_mut() + }; + // Read packets and dispatch let mut mp4_ret: c_int = 0; let mut pkt: ffi::AVPacket = std::mem::zeroed(); @@ -286,6 +305,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_dvdsub_pts: i64 = -1; + let mut prev_dvdsub_data: Vec = Vec::new(); loop { let ret = ffi::av_read_frame(fmt_ctx.as_ptr() as *mut _, &mut pkt); @@ -386,6 +407,37 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_s } } } + TrackType::DvdSub => { + if pkt.size > 0 && !pkt.data.is_null() && !vob_ctx.is_null() { + let stream = *(*fmt_ctx.as_ptr()).streams.add(track.stream_index); + let tb = (*stream).time_base; + let cur_pts_ms = if pts != AV_NOPTS_VALUE && tb.den > 0 { + pts * 1000 * tb.num as i64 / tb.den as i64 + } else { + 0 + }; + + // Flush previous dvdsub packet now that we know its end time + if !prev_dvdsub_data.is_empty() && prev_dvdsub_pts >= 0 { + let end_ms = cur_pts_ms; + ccx_mp4_vobsub_process( + vob_ctx, + ctx, + prev_dvdsub_data.as_mut_ptr(), + prev_dvdsub_data.len() as c_uint, + prev_dvdsub_pts, + end_ms, + sub, + ); + mp4_ret = 1; + } + + // Buffer current packet + let data_slice = std::slice::from_raw_parts(pkt.data, pkt.size as usize); + prev_dvdsub_data = data_slice.to_vec(); + prev_dvdsub_pts = cur_pts_ms; + } + } } } @@ -402,6 +454,25 @@ pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_s ccx_mp4_flush_tx3g(ctx, sub); } + // Flush last dvdsub packet (use 5s default duration) + if !prev_dvdsub_data.is_empty() && prev_dvdsub_pts >= 0 && !vob_ctx.is_null() { + let end_ms = prev_dvdsub_pts + 5000; + ccx_mp4_vobsub_process( + vob_ctx, + ctx, + prev_dvdsub_data.as_mut_ptr(), + prev_dvdsub_data.len() as c_uint, + prev_dvdsub_pts, + end_ms, + sub, + ); + } + + // Free vobsub decoder + if !vob_ctx.is_null() { + ccx_mp4_vobsub_free(vob_ctx); + } + // End-of-stream: encode any caption that finished on the last processed // sample but hasn't been flushed by slice_header yet. GPAC's mp4.c does // the equivalent via encode_sub after its per-track loop returns.