Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 67 additions & 0 deletions src/lib_ccx/mp4_rust_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 */
11 changes: 11 additions & 0 deletions src/lib_ccx/mp4_rust_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 78 additions & 7 deletions src/rust/src/demuxer/mp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -83,6 +90,7 @@ enum TrackType {
Cea608,
Cea708,
Tx3g,
DvdSub,
}

/// Information about a track we want to process
Expand Down Expand Up @@ -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
}
Expand All @@ -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",
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -279,13 +290,23 @@ 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();
ffi::av_init_packet(&mut pkt);

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);
Expand Down Expand Up @@ -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;
}
}
}
}

Expand All @@ -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.
Expand Down
Loading