mirror of
https://github.com/CCExtractor/ccextractor.git
synced 2026-07-08 18:06:30 +00:00
feat(mp4): add dvdsub/VobSub support to FFmpeg MP4 demuxer (#2269)
* feat(mp4): add dvdsub/VobSub support to FFmpeg MP4 demuxer Wire the existing vobsub_decoder (OCR-based bitmap subtitle decoder) into the FFmpeg MP4 demuxer path. Previously, dvdsub tracks in MP4 containers were documented as unsupported — GPAC could extract them but the FFmpeg path could not. Changes: - Add DvdSub track type detection (AV_CODEC_ID_DVD_SUBTITLE) in mp4.rs - Add C bridge functions (ccx_mp4_vobsub_init/process/free) that call the existing vobsub_decoder module - Buffer dvdsub packets to compute end times from next-packet PTS - Add --undefined linker flags for bridge symbols Tested on sample 1f3e951d516b.mp4 (dvdsub in MP4): - GPAC: 5405 bytes output - FFmpeg + this patch: 5405 bytes, byte-identical to GPAC With this change, the FFmpeg path extracts captions from every sample that GPAC can, plus one additional sample (ad9f9e03240e.m4v) that GPAC cannot handle. * style: apply clang-format to vobsub bridge --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
e4443a74ba
commit
283bfd9896
@@ -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)
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<u8> = 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.
|
||||
|
||||
Reference in New Issue
Block a user