diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 290e0a6f0..2f54d4356 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -103,6 +103,26 @@ jobs: working-directory: build - name: Display version information run: ./build/ccextractor --version + cmake_ffmpeg_mp4: + runs-on: ubuntu-latest + steps: + - name: Install dependencies + run: sudo apt update && sudo apt-get install libgpac-dev libtesseract-dev libleptonica-dev libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev libavdevice-dev libswresample-dev libswscale-dev + - uses: actions/checkout@v6 + - name: cmake (default GPAC build) + run: mkdir build && cd build && cmake ../src + - name: build (default GPAC build) + run: make -j$(nproc) + working-directory: build + - name: Display version information (GPAC build) + run: ./build/ccextractor --version + - name: cmake (FFmpeg MP4 build) + run: mkdir build_ffmpeg && cd build_ffmpeg && cmake -DWITH_FFMPEG=ON -DWITH_OCR=ON -DWITH_HARDSUBX=ON ../src + - name: build (FFmpeg MP4 build) + run: make -j$(nproc) + working-directory: build_ffmpeg + - name: Display version information (FFmpeg MP4 build) + run: ./build_ffmpeg/ccextractor --version cmake_ocr_hardsubx: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/build_mac.yml b/.github/workflows/build_mac.yml index 347698cd5..a42ec3ca2 100644 --- a/.github/workflows/build_mac.yml +++ b/.github/workflows/build_mac.yml @@ -107,6 +107,21 @@ jobs: working-directory: build - name: Display version information run: ./build/ccextractor --version + cmake_ffmpeg_mp4: + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + run: brew install pkg-config gpac ffmpeg tesseract leptonica + - name: cmake (FFmpeg MP4 build) + run: | + mkdir build && cd build + cmake -DWITH_FFMPEG=ON -DWITH_OCR=ON -DWITH_HARDSUBX=ON ../src + - name: build + run: make -j$(nproc) + working-directory: build + - name: Display version information + run: ./build/ccextractor --version build_shell_hardsubx: # Test build.command with -hardsubx flag (burned-in subtitle extraction) runs-on: macos-latest diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 088d92359..64cd62c43 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -194,20 +194,25 @@ 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) - - 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}) + pkg_check_modules (SWRESAMPLE REQUIRED libswresample) 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_FFMPEG_MP4") + + # NOTE: FFmpeg libs are added to EXTRA_FFMPEG_LIBS (not EXTRA_LIBS) so we + # can place them AFTER ccx_rust in the link order. GNU ld processes + # left-to-right; ccx_rust (containing rsmpeg) references FFmpeg symbols + # like swr_get_out_samples, so FFmpeg shared libs must come after it. + set (EXTRA_FFMPEG_LIBS ${AVFORMAT_LIBRARIES} ${AVUTIL_LIBRARIES} + ${AVCODEC_LIBRARIES} ${AVFILTER_LIBRARIES} ${SWSCALE_LIBRARIES} + ${SWRESAMPLE_LIBRARIES}) endif (PKG_CONFIG_FOUND AND WITH_FFMPEG) ######################################################## @@ -240,12 +245,6 @@ if (PKG_CONFIG_FOUND AND WITH_HARDSUBX) pkg_check_modules (AVFILTER REQUIRED libavfilter) pkg_check_modules (SWSCALE REQUIRED libswscale) - 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_INCLUDES ${EXTRA_INCLUDES} ${AVFORMAT_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVUTIL_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${AVCODEC_INCLUDE_DIRS}) @@ -263,6 +262,11 @@ if (PKG_CONFIG_FOUND AND WITH_HARDSUBX) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${TESSERACT_INCLUDE_DIRS}) set (EXTRA_INCLUDES ${EXTRA_INCLUDES} ${LEPTONICA_INCLUDE_DIRS}) + + # Collect FFmpeg libs for placement after ccx_rust (same reason as WITH_FFMPEG) + set (EXTRA_FFMPEG_LIBS ${EXTRA_FFMPEG_LIBS} ${AVFORMAT_LIBRARIES} + ${AVUTIL_LIBRARIES} ${AVCODEC_LIBRARIES} ${AVFILTER_LIBRARIES} + ${SWSCALE_LIBRARIES}) endif (PKG_CONFIG_FOUND AND WITH_HARDSUBX) add_executable (ccextractor ${SOURCEFILE} ${FREETYPE_SOURCE} ${UTF8PROC_SOURCE}) @@ -274,12 +278,20 @@ add_executable (ccextractor ${SOURCEFILE} ${FREETYPE_SOURCE} ${UTF8PROC_SOURCE}) if (PKG_CONFIG_FOUND) add_subdirectory (rust) set (EXTRA_LIBS ${EXTRA_LIBS} ccx_rust) -endif (PKG_CONFIG_FOUND) + # Corrosion places ccx_rust at the absolute end of the link line. + # On Linux (GNU ld), ccx_rust contains rsmpeg which references FFmpeg + # symbols like swr_get_out_samples. By adding FFmpeg libs as INTERFACE + # dependencies of ccx_rust, CMake places them right after ccx_rust. + if (EXTRA_FFMPEG_LIBS) + set_property(TARGET ccx_rust APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${EXTRA_FFMPEG_LIBS}) + endif() +endif (PKG_CONFIG_FOUND) -target_link_libraries (ccextractor ${EXTRA_LIBS}) +target_link_libraries (ccextractor PRIVATE ${EXTRA_LIBS}) target_include_directories (ccextractor PUBLIC ${EXTRA_INCLUDES}) + # ccx_rust (Rust) calls C functions from ccx (like decode_vbi). # Force the linker to pull these symbols from ccx before processing ccx_rust. if (NOT WIN32 AND NOT APPLE) @@ -287,6 +299,18 @@ if (NOT WIN32 AND NOT APPLE) -Wl,--undefined=decode_vbi -Wl,--undefined=do_cb -Wl,--undefined=store_hdcc) + if (WITH_FFMPEG) + target_link_options (ccextractor PRIVATE + -Wl,--undefined=ccx_mp4_process_avc_sample + -Wl,--undefined=ccx_mp4_process_hevc_sample + -Wl,--undefined=ccx_mp4_process_cc_packet + -Wl,--undefined=ccx_mp4_process_tx3g_packet + -Wl,--undefined=ccx_mp4_flush_tx3g + -Wl,--undefined=ccx_mp4_report_progress + -Wl,--undefined=mprint + -Wl,--undefined=update_decoder_list + -Wl,--undefined=update_encoder_list) + endif() endif() install (TARGETS ccextractor DESTINATION bin) diff --git a/src/ccextractor.c b/src/ccextractor.c index e8d2cffa5..efed81d32 100644 --- a/src/ccextractor.c +++ b/src/ccextractor.c @@ -222,12 +222,23 @@ 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 - if (ctx->current_file == -1) // We don't have a file to open, must be stdin, and GPAC is incompatible with stdin + close_input_file(ctx); // No need to have it open. The demuxer will do it for us + if (ctx->current_file == -1) // We don't have a file to open, must be stdin { fatal(EXIT_INCOMPATIBLE_PARAMETERS, "MP4 requires an actual file, it's not possible to read from a stream, including stdin.\n"); } +#ifdef ENABLE_FFMPEG_MP4 + mprint("\rAnalyzing data with FFmpeg (MP4 demuxer)\n"); + if (ccx_options.extract_chapters) + { + tmp = ccxr_dumpchapters(ctx, ctx->inputfile[ctx->current_file]); + } + else + { + tmp = ccxr_processmp4(ctx, ctx->inputfile[ctx->current_file]); + } +#else + mprint("\rAnalyzing data with GPAC (MP4 library)\n"); if (ccx_options.extract_chapters) { tmp = dumpchapters(ctx, &ctx->mp4_cfg, ctx->inputfile[ctx->current_file]); @@ -236,6 +247,7 @@ int start_ccx() { tmp = processmp4(ctx, &ctx->mp4_cfg, ctx->inputfile[ctx->current_file]); } +#endif if (ccx_options.print_file_reports) print_file_report(ctx); if (!ret) diff --git a/src/lib_ccx/CMakeLists.txt b/src/lib_ccx/CMakeLists.txt index 86cfd42c7..1c6a8c744 100644 --- a/src/lib_ccx/CMakeLists.txt +++ b/src/lib_ccx/CMakeLists.txt @@ -38,6 +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_FFMPEG_MP4") endif (WITH_FFMPEG) if (WITH_OCR) @@ -59,7 +60,10 @@ endif (WITH_OCR) aux_source_directory ("${PROJECT_SOURCE_DIR}/lib_ccx/" SOURCEFILE) add_library (ccx ${SOURCEFILE} ccx_dtvcc.h ccx_dtvcc.c ccx_encoders_mcc.c ccx_encoders_mcc.h) -target_link_libraries (ccx ${EXTRA_LIBS}) +# PRIVATE prevents transitive propagation to consumers (ccextractor). +# The parent CMakeLists.txt adds all needed libs explicitly, controlling +# link order so FFmpeg libs appear after ccx_rust (required by GNU ld). +target_link_libraries (ccx PRIVATE ${EXTRA_LIBS}) target_include_directories (ccx PUBLIC ${EXTRA_INCLUDES}) if (WITH_HARDSUBX) diff --git a/src/lib_ccx/ccx_mp4.h b/src/lib_ccx/ccx_mp4.h index 51331b55d..e3b6f749a 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); +unsigned char *ccdp_find_data(unsigned char *ccdp_atom_content, unsigned int len, unsigned int *cc_count); + +#ifdef ENABLE_FFMPEG_MP4 +/* Rust FFmpeg MP4 demuxer entry points */ +int ccxr_processmp4(struct lib_ccx_ctx *ctx, const char *file); +int ccxr_dumpchapters(struct lib_ccx_ctx *ctx, const char *file); +#endif + #endif diff --git a/src/lib_ccx/mp4_rust_bridge.c b/src/lib_ccx/mp4_rust_bridge.c new file mode 100644 index 000000000..af4e6d58d --- /dev/null +++ b/src/lib_ccx/mp4_rust_bridge.c @@ -0,0 +1,283 @@ +#ifdef ENABLE_FFMPEG_MP4 + +#include +#include +#include +#include "lib_ccx.h" +#include "utility.h" +#include "ccx_encoders_common.h" +#include "ccx_common_option.h" +#include "ccx_dtvcc.h" +#include "activity.h" +#include "avc_functions.h" +#include "ccx_decoders_608.h" +#include "ccx_decoders_common.h" +#include "ccx_encoders_mcc.h" +#include "ccx_mp4.h" +#include "mp4_rust_bridge.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 + * whether we flush accumulated CC data at the end of the sample. */ +int ccx_mp4_process_nal_sample(struct lib_ccx_ctx *ctx, unsigned int timescale, + unsigned char nal_unit_size, int is_hevc, + unsigned char *data, unsigned int data_length, + long long dts, int cts_offset, + struct cc_subtitle *sub) +{ + struct lib_cc_decode *dec_ctx = update_decoder_list(ctx); + struct encoder_ctx *enc_ctx = update_encoder_list(ctx); + + dec_ctx->in_bufferdatatype = CCX_H264; + dec_ctx->avc_ctx->is_hevc = is_hevc ? 1 : 0; + + set_current_pts(dec_ctx->timing, (dts + cts_offset) * MPEG_CLOCK_FREQ / timescale); + set_fts(dec_ctx->timing); + + for (unsigned int i = 0; i < data_length;) + { + unsigned int nal_length; + + if (i + nal_unit_size > data_length) + { + mprint("Corrupted packet in %s sample: offset %u + nal_unit_size %u > %u. Ignoring.\n", + is_hevc ? "HEVC" : "AVC", i, nal_unit_size, data_length); + return 0; + } + switch (nal_unit_size) + { + case 1: + nal_length = data[i]; + break; + case 2: + nal_length = RB16(&data[i]); + break; + case 4: + nal_length = RB32(&data[i]); + break; + default: + mprint("Unexpected nal_unit_size %u (%s)\n", + nal_unit_size, is_hevc ? "HEVC" : "AVC"); + return -1; + } + unsigned int prev = i; + i += nal_unit_size; + if (i + nal_length <= prev || i + nal_length > data_length) + { + mprint("Corrupted %s sample. Ignoring.\n", is_hevc ? "HEVC" : "AVC"); + return 0; + } + + temp_debug = 0; + if (nal_length > 0) + do_NAL(enc_ctx, dec_ctx, &data[i], nal_length, sub); + i += nal_length; + } + + /* HEVC captions arrive as SEI inside the sample; flush the accumulated + * cc_data buffer now since there's no slice-header path to do it. */ + if (is_hevc && dec_ctx->avc_ctx->cc_count > 0) + { + store_hdcc(enc_ctx, dec_ctx, dec_ctx->avc_ctx->cc_data, + dec_ctx->avc_ctx->cc_count, + dec_ctx->timing->current_tref, + dec_ctx->timing->fts_now, sub); + dec_ctx->avc_ctx->cc_buffer_saved = CCX_TRUE; + dec_ctx->avc_ctx->cc_count = 0; + } + + return 0; +} + +int ccx_mp4_process_cc_packet(struct lib_ccx_ctx *ctx, int sub_type_c608, + unsigned char *data, unsigned int data_length, + long long dts, int cts_offset, + unsigned int timescale, + 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); + dec_ctx->timing->current_picture_coding_type = CCX_FRAME_TYPE_I_FRAME; + set_fts(dec_ctx->timing); + + /* ISO BMFF clcp samples are wrapped: [big-endian u32 length][4cc type][payload]. + * libavformat keeps this wrapper in the AVPacket, so unwrap to match GPAC's + * process_clcp(). Fall through to raw-payload handling if the wrapper is absent. */ + unsigned char *payload = data; + unsigned int payload_len = data_length; + if (data_length >= 8) + { + unsigned int atom_length = ((unsigned int)data[0] << 24) | + ((unsigned int)data[1] << 16) | + ((unsigned int)data[2] << 8) | + (unsigned int)data[3]; + const char *atom_type = (const char *)(data + 4); + int is_cdat = !memcmp(atom_type, "cdat", 4) || !memcmp(atom_type, "cdt2", 4); + int is_ccdp = !memcmp(atom_type, "ccdp", 4); + if (atom_length >= 8 && atom_length <= data_length && (is_cdat || is_ccdp)) + { + payload = data + 8; + payload_len = atom_length - 8; + } + } + + /* Two in-the-wild payload shapes for c608/c708 samples: + * 1) Raw CEA-608 pairs — the classic GPAC-extracted payload inside a + * cdat/cdt2 atom. Byte values sit in the 0x00-0x7F + parity range. + * 2) cc_data triplets — [cc_info][b1][b2] where cc_info has + * reserved=11111 in the top 5 bits (value 0xF8-0xFF). Modern + * libavformat delivers many c608 tracks in this shape. + * Distinguish by looking at the first byte and the length. */ + int looks_like_cc_data = (payload_len >= 3 && payload_len % 3 == 0 && + (payload[0] & 0xF8) == 0xF8); + + if (looks_like_cc_data && sub_type_c608) + { + /* c608 track with cc_data triplets: skip the cc_info byte and feed each + * valid field-1/field-2 pair into process608 directly, matching GPAC's + * process_clcp() -> process608 path. do_cb is avoided here because its + * CCX_H264 guard (set by interleaved H.264 packets earlier in the stream) + * suppresses the cb_field increments process608 relies on for + * caption-boundary timing. */ + for (unsigned int i = 0; i + 3 <= payload_len; i += 3) + { + unsigned char cc_info = payload[i]; + unsigned char cc_valid = (cc_info & 0x04) >> 2; + unsigned char cc_type = cc_info & 0x03; + if (!cc_valid || cc_type > 1) + continue; + /* process608 picks the field-1 or field-2 decoder context from + * dec_ctx->current_field; set it before each pair so both fields + * are routed correctly when they arrive interleaved. */ + dec_ctx->current_field = (cc_type == 0) ? 1 : 2; + unsigned char pair[2] = {payload[i + 1], payload[i + 2]}; + int ret = process608(pair, 2, dec_ctx, sub); + if (ret <= 0) + continue; + if (cc_type == 0) + cb_field1++; + else + cb_field2++; + if (sub->got_output) + { + encode_sub(enc_ctx, sub); + sub->got_output = 0; + } + } + } + else if (looks_like_cc_data) + { + ctx->dec_global_setting->settings_dtvcc->enabled = 1; + unsigned int cc_count = payload_len / 3; + process_cc_data(enc_ctx, dec_ctx, payload, (int)cc_count, sub); + if (sub->got_output) + { + encode_sub(enc_ctx, sub); + sub->got_output = 0; + } + } + else if (sub_type_c608) + { + /* Classic c608: payload is raw CEA-608 pairs */ + int ret = 0; + int len = (int)payload_len; + unsigned char *tdata = payload; + while (len > 0) + { + ret = process608(tdata, len > 2 ? 2 : len, dec_ctx, sub); + if (ret <= 0) + break; + len -= ret; + tdata += ret; + cb_field1++; + if (sub->got_output) + { + encode_sub(enc_ctx, sub); + sub->got_output = 0; + } + } + } + else + { + /* Classic c708: payload is a CDP packet */ + unsigned int cc_count; + unsigned char *cc_data = ccdp_find_data(payload, payload_len, &cc_count); + if (!cc_data) + return 0; + + ctx->dec_global_setting->settings_dtvcc->enabled = 1; + process_cc_data(enc_ctx, dec_ctx, cc_data, (int)cc_count, sub); + if (sub->got_output) + { + encode_sub(enc_ctx, sub); + sub->got_output = 0; + } + } + + return 0; +} + +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, + 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); + set_fts(dec_ctx->timing); + + /* tx3g format: 16-bit text length + UTF-8 text */ + if (data_length < 2) + return -1; + + unsigned int text_length = (data[0] << 8) | data[1]; + if (text_length == 0 || text_length > data_length - 2) + return -1; + + /* Set up subtitle for deferred encoding */ + sub->type = CC_TEXT; + sub->start_time = dec_ctx->timing->fts_now; + if (sub->data != NULL) + free(sub->data); + sub->data = malloc(text_length + 1); + if (!sub->data) + return -1; + sub->datatype = CC_DATATYPE_GENERIC; + memcpy(sub->data, data + 2, text_length); + *((char *)sub->data + text_length) = '\0'; + + return 0; +} + +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 (sub->data != NULL) + { + sub->end_time = dec_ctx->timing->fts_now; + encode_sub(enc_ctx, sub); + } +} + +void ccx_mp4_report_progress(struct lib_ccx_ctx *ctx, unsigned int cur, unsigned int total) +{ + if (total == 0) + return; + struct lib_cc_decode *dec_ctx = update_decoder_list(ctx); + int progress = (int)((cur * 100) / total); + if (ctx->last_reported_progress != progress) + { + int cur_sec = (int)(get_fts(dec_ctx->timing, dec_ctx->current_field) / 1000); + activity_progress(progress, cur_sec / 60, cur_sec % 60); + ctx->last_reported_progress = progress; + } +} + +#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..85619a024 --- /dev/null +++ b/src/lib_ccx/mp4_rust_bridge.h @@ -0,0 +1,99 @@ +/* + * C bridge functions for the Rust FFmpeg MP4 demuxer. + * These expose existing C processing functions with flat, FFI-safe signatures + * that Rust can call via bindgen. + */ +#ifndef MP4_RUST_BRIDGE_H +#define MP4_RUST_BRIDGE_H + +#ifdef ENABLE_FFMPEG_MP4 + +#include "lib_ccx.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* + * Process an AVC (H.264) or HEVC (H.265) sample through do_NAL(). + * Iterates the AVCC/HVCC length-prefixed NAL units in `data`. + * + * @param ctx CCExtractor library context + * @param timescale Media timescale for timestamp conversion + * @param nal_unit_size NAL unit length field size (1, 2, or 4 bytes) + * @param is_hevc Non-zero for HEVC, zero for AVC + * @param data Sample data buffer + * @param data_length Length of the sample data + * @param dts Decoding timestamp + * @param cts_offset Composition time offset + * @param sub Output subtitle structure + * @return 0 on success, -1 on unexpected nal_unit_size + */ + int ccx_mp4_process_nal_sample(struct lib_ccx_ctx *ctx, unsigned int timescale, + unsigned char nal_unit_size, int is_hevc, + unsigned char *data, unsigned int data_length, + long long dts, int cts_offset, + struct cc_subtitle *sub); + + /* + * Process a closed caption packet (CEA-608/708). + * Wraps the existing process_clcp() atom-level logic with flat arguments. + * + * @param ctx CCExtractor library context + * @param sub_type_c608 1 if CEA-608, 0 if CEA-708 + * @param data CC atom data (starting from the atom header) + * @param data_length Length of the data + * @param dts Decoding timestamp + * @param cts_offset Composition time offset + * @param timescale Media timescale + * @param sub Output subtitle structure + * @return 0 on success, non-zero on error + */ + int ccx_mp4_process_cc_packet(struct lib_ccx_ctx *ctx, int sub_type_c608, + unsigned char *data, unsigned int data_length, + long long dts, int cts_offset, + unsigned int timescale, + struct cc_subtitle *sub); + + /* + * Process a tx3g (3GPP timed text) subtitle sample. + * + * @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 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, + struct cc_subtitle *sub); + + /* + * Flush pending tx3g subtitle (encode the last one). + * + * @param ctx CCExtractor library context + * @param sub Output subtitle structure + */ + void ccx_mp4_flush_tx3g(struct lib_ccx_ctx *ctx, struct cc_subtitle *sub); + + /* + * Report progress for the MP4 demuxer. + * + * @param ctx CCExtractor library context + * @param cur Current sample index + * @param total Total sample count + */ + void ccx_mp4_report_progress(struct lib_ccx_ctx *ctx, unsigned int cur, unsigned int total); + +#ifdef __cplusplus +} +#endif + +#endif /* ENABLE_FFMPEG_MP4 */ +#endif /* MP4_RUST_BRIDGE_H */ diff --git a/src/rust/CMakeLists.txt b/src/rust/CMakeLists.txt index 85eddf28d..61e7482cc 100644 --- a/src/rust/CMakeLists.txt +++ b/src/rust/CMakeLists.txt @@ -19,6 +19,14 @@ else() set(FEATURES "") endif() +if(WITH_FFMPEG) + if(FEATURES STREQUAL "") + set(FEATURES "enable_mp4_ffmpeg") + else() + set(FEATURES "${FEATURES};enable_mp4_ffmpeg") + endif() +endif() + # Check rust version set(MSRV "1.87.0") if(Rust_VERSION VERSION_GREATER_EQUAL ${MSRV}) diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 34b704122..0c216dacf 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -49,6 +49,7 @@ tempfile = "3.20.0" [features] wtv_debug = [] enable_ffmpeg = [] +enable_mp4_ffmpeg = ["rsmpeg"] with_libcurl = [] # hardsubx_ocr enables OCR and the platform-appropriate rsmpeg diff --git a/src/rust/build.rs b/src/rust/build.rs index 6cce8fc38..4cecebeff 100644 --- a/src/rust/build.rs +++ b/src/rust/build.rs @@ -43,6 +43,22 @@ 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_process_tx3g_packet", + "ccx_mp4_flush_tx3g", + "ccx_mp4_report_progress", + "update_decoder_list", + "update_encoder_list", + "do_NAL", + "mprint", + "ccxr_dtvcc_set_encoder", + "ccxr_dtvcc_process_data", + ]); + let mut allowlist_types = Vec::new(); allowlist_types.extend_from_slice(&[ // Match both lowercase (dtvcc_*) and uppercase (DTVCC_*) patterns @@ -79,6 +95,31 @@ fn main() { // bindings for. .header("wrapper.h"); + // enable FFmpeg MP4 demuxer if feature is on + #[cfg(feature = "enable_mp4_ffmpeg")] + { + builder = builder.clang_arg("-DENABLE_FFMPEG_MP4"); + + // Add FFmpeg include paths (same logic as hardsubx_ocr) + if let Ok(ffmpeg_include) = env::var("FFMPEG_INCLUDE_DIR") { + builder = builder.clang_arg(format!("-I{}", ffmpeg_include)); + } + if cfg!(target_os = "macos") { + if std::path::Path::new("/opt/homebrew/include").exists() { + builder = builder.clang_arg("-I/opt/homebrew/include"); + } else if std::path::Path::new("/usr/local/include").exists() { + builder = builder.clang_arg("-I/usr/local/include"); + } + } + if cfg!(target_os = "linux") { + if let Ok(lib) = pkg_config::Config::new().probe("libavcodec") { + for path in lib.include_paths { + builder = builder.clang_arg(format!("-I{}", path.display())); + } + } + } + } + // enable hardsubx if and only if the feature is on #[cfg(feature = "hardsubx_ocr")] { diff --git a/src/rust/src/demuxer/demux.rs b/src/rust/src/demuxer/demux.rs index fec29b319..f14370731 100755 --- a/src/rust/src/demuxer/demux.rs +++ b/src/rust/src/demuxer/demux.rs @@ -279,14 +279,13 @@ impl CcxDemuxer<'_> { // Force stream mode to myth self.stream_mode = StreamMode::Myth; } - Some(false) => { + Some(false) if matches!( self.stream_mode, StreamMode::ElementaryOrNotFound | StreamMode::Program - ) && detect_myth(self) != 0 - { - self.stream_mode = StreamMode::Myth; - } + ) && detect_myth(self) != 0 => + { + self.stream_mode = StreamMode::Myth; } _ => {} } diff --git a/src/rust/src/demuxer/mod.rs b/src/rust/src/demuxer/mod.rs index 37698963f..6d64559ae 100644 --- a/src/rust/src/demuxer/mod.rs +++ b/src/rust/src/demuxer/mod.rs @@ -39,5 +39,7 @@ pub mod common_types; pub mod demux; pub mod demuxer_data; pub mod dvdraw; +#[cfg(feature = "enable_mp4_ffmpeg")] +pub mod mp4; pub mod scc; pub mod stream_functions; diff --git a/src/rust/src/demuxer/mp4.rs b/src/rust/src/demuxer/mp4.rs new file mode 100644 index 000000000..b5d81ddea --- /dev/null +++ b/src/rust/src/demuxer/mp4.rs @@ -0,0 +1,646 @@ +//! FFmpeg-based MP4 demuxer for CCExtractor +//! +//! Uses rsmpeg (Rust FFmpeg bindings) to open MP4 files and extract subtitle +//! tracks, delegating actual caption processing to existing C functions +//! through the mp4_rust_bridge. +//! +//! # Supported track types +//! - H.264 video (SEI-embedded CEA-608/708 in NAL units) +//! - HEVC video (SEI-embedded CEA-608/708 in NAL units) +//! - `c608` subtitle (CEA-608, either cdat/cdt2-wrapped or bare cc_data triplets) +//! - `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. + +#[cfg(feature = "enable_mp4_ffmpeg")] +use rsmpeg::avformat::AVFormatContextInput; +#[cfg(feature = "enable_mp4_ffmpeg")] +use rsmpeg::ffi; + +use std::ffi::CStr; +use std::os::raw::{c_char, c_int, c_uint}; + +use crate::bindings::{cc_subtitle, encoder_ctx, lib_cc_decode, lib_ccx_ctx}; + +// C bridge functions from mp4_rust_bridge.c +extern "C" { + fn ccx_mp4_process_nal_sample( + ctx: *mut lib_ccx_ctx, + timescale: c_uint, + nal_unit_size: u8, + is_hevc: c_int, + data: *mut u8, + data_length: c_uint, + dts: i64, + cts_offset: c_int, + sub: *mut cc_subtitle, + ) -> c_int; + + fn ccx_mp4_process_cc_packet( + ctx: *mut lib_ccx_ctx, + sub_type_c608: c_int, + data: *mut u8, + data_length: c_uint, + dts: i64, + cts_offset: c_int, + timescale: c_uint, + sub: *mut cc_subtitle, + ) -> c_int; + + fn ccx_mp4_process_tx3g_packet( + ctx: *mut lib_ccx_ctx, + data: *mut u8, + data_length: c_uint, + dts: i64, + cts_offset: c_int, + timescale: c_uint, + sub: *mut cc_subtitle, + ) -> c_int; + + fn ccx_mp4_flush_tx3g(ctx: *mut lib_ccx_ctx, sub: *mut cc_subtitle); + + fn ccx_mp4_report_progress(ctx: *mut lib_ccx_ctx, cur: c_uint, total: c_uint); + + 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; + + fn mprint(fmt: *const c_char, ...); + + fn encode_sub(enc_ctx: *mut encoder_ctx, sub: *mut cc_subtitle) -> c_int; +} + +/// Track types we can extract captions from +#[derive(Debug, Clone, Copy, PartialEq)] +enum TrackType { + AvcH264, + HevcH265, + Cea608, + Cea708, + Tx3g, +} + +/// Information about a track we want to process +#[derive(Debug)] +struct TrackInfo { + stream_index: usize, + track_type: TrackType, + timescale: u32, +} + +/// FourCC constants for codec identification +const FOURCC_C608: u32 = fourcc(b"c608"); +const FOURCC_C708: u32 = fourcc(b"c708"); +const FOURCC_TX3G: u32 = fourcc(b"tx3g"); + +const fn fourcc(s: &[u8; 4]) -> u32 { + ((s[0] as u32) << 24) | ((s[1] as u32) << 16) | ((s[2] as u32) << 8) | (s[3] as u32) +} + +/// AV_NOPTS_VALUE equivalent (INT64_MIN) +const AV_NOPTS_VALUE: i64 = i64::MIN; + +/// Get the NAL unit size from an AVC/HEVC extradata. +#[cfg(feature = "enable_mp4_ffmpeg")] +fn get_nal_unit_size_from_extradata(extradata: &[u8], is_hevc: bool) -> u8 { + if is_hevc { + // HEVCDecoderConfigurationRecord: byte 21 has lengthSizeMinusOne in bits [0:1] + if extradata.len() >= 23 { + (extradata[21] & 0x03) + 1 + } else { + 4 + } + } else { + // AVCDecoderConfigurationRecord: byte 4 has lengthSizeMinusOne in bits [0:1] + if extradata.len() >= 7 { + (extradata[4] & 0x03) + 1 + } else { + 4 + } + } +} + +/// Main MP4 processing function using FFmpeg via rsmpeg +/// +/// # Safety +/// ctx and sub must be valid pointers +#[cfg(feature = "enable_mp4_ffmpeg")] +pub unsafe fn processmp4_rust(ctx: *mut lib_ccx_ctx, path: &CStr, sub: *mut cc_subtitle) -> c_int { + let path_str = path.to_bytes(); + let path_display = String::from_utf8_lossy(path_str); + + let open_msg = format!("Opening '{}' with FFmpeg: \0", path_display); + mprint(open_msg.as_ptr() as *const c_char); + + // Open the file with FFmpeg + let fmt_ctx = match AVFormatContextInput::open(path, None, &mut None) { + Ok(ctx) => ctx, + Err(e) => { + let err_msg = format!("Failed to open input file with FFmpeg: {}\n\0", e); + mprint(err_msg.as_ptr() as *const c_char); + return -2; + } + }; + + let ok_msg = b"ok\n\0"; + mprint(ok_msg.as_ptr() as *const c_char); + + // Set up encoder/decoder + let dec_ctx = update_decoder_list(ctx); + let enc_ctx = update_encoder_list(ctx); + + if !enc_ctx.is_null() && !dec_ctx.is_null() { + (*enc_ctx).timing = (*dec_ctx).timing; + crate::bindings::ccxr_dtvcc_set_encoder((*dec_ctx).dtvcc_rust, enc_ctx); + } + + // Enumerate tracks + let mut tracks: Vec = Vec::new(); + let nb_streams = (*fmt_ctx.as_ptr()).nb_streams as usize; + + for i in 0..nb_streams { + let stream = *(*fmt_ctx.as_ptr()).streams.add(i); + let codecpar = (*stream).codecpar; + if codecpar.is_null() { + continue; + } + + let codec_type = (*codecpar).codec_type; + let codec_tag = (*codecpar).codec_tag; + let codec_id = (*codecpar).codec_id; + let time_base = (*stream).time_base; + let timescale = if time_base.den > 0 { + time_base.den as u32 + } else { + 90000 + }; + + let track_type = if codec_type == ffi::AVMEDIA_TYPE_VIDEO { + if codec_id == ffi::AV_CODEC_ID_H264 { + Some(TrackType::AvcH264) + } else if codec_id == ffi::AV_CODEC_ID_HEVC { + Some(TrackType::HevcH265) + } else { + None + } + } else if codec_type == ffi::AVMEDIA_TYPE_SUBTITLE { + if codec_tag == FOURCC_C608 || codec_id == ffi::AV_CODEC_ID_EIA_608 { + Some(TrackType::Cea608) + } else if codec_tag == FOURCC_C708 { + Some(TrackType::Cea708) + } else if codec_tag == FOURCC_TX3G || codec_id == ffi::AV_CODEC_ID_MOV_TEXT { + Some(TrackType::Tx3g) + } else { + None + } + } else { + None + }; + + if let Some(tt) = track_type { + let type_name = match tt { + TrackType::AvcH264 => "AVC/H.264", + TrackType::HevcH265 => "HEVC/H.265", + TrackType::Cea608 => "CEA-608", + TrackType::Cea708 => "CEA-708", + TrackType::Tx3g => "tx3g", + }; + let msg = format!( + "Track {}, type={} timescale={}\n\0", + i, type_name, timescale + ); + mprint(msg.as_ptr() as *const c_char); + + tracks.push(TrackInfo { + stream_index: i, + track_type: tt, + timescale, + }); + } + } + + // Count track types + let avc_count = tracks + .iter() + .filter(|t| t.track_type == TrackType::AvcH264) + .count(); + let hevc_count = tracks + .iter() + .filter(|t| t.track_type == TrackType::HevcH265) + .count(); + let cc_count = tracks + .iter() + .filter(|t| { + matches!( + t.track_type, + TrackType::Cea608 | TrackType::Cea708 | TrackType::Tx3g + ) + }) + .count(); + + let summary = format!( + "MP4 (FFmpeg): found {} tracks: {} avc, {} hevc, {} cc\n\0", + tracks.len(), + avc_count, + hevc_count, + cc_count + ); + mprint(summary.as_ptr() as *const c_char); + + if tracks.is_empty() { + let msg = b"No processable tracks found\n\0"; + mprint(msg.as_ptr() as *const c_char); + return 0; + } + + // Determine NAL unit sizes from extradata for video tracks + let mut nal_sizes: Vec<(usize, u8)> = Vec::new(); + for track in &tracks { + if track.track_type == TrackType::AvcH264 || track.track_type == TrackType::HevcH265 { + let stream = *(*fmt_ctx.as_ptr()).streams.add(track.stream_index); + let codecpar = (*stream).codecpar; + let extradata = (*codecpar).extradata; + let extradata_size = (*codecpar).extradata_size; + let is_hevc = track.track_type == TrackType::HevcH265; + + let nal_unit_size = if !extradata.is_null() && extradata_size > 0 { + let data = std::slice::from_raw_parts(extradata, extradata_size as usize); + process_extradata_params(ctx, data, is_hevc, sub); + get_nal_unit_size_from_extradata(data, is_hevc) + } else { + 4 + }; + nal_sizes.push((track.stream_index, nal_unit_size)); + } + } + + // 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; + + loop { + let ret = ffi::av_read_frame(fmt_ctx.as_ptr() as *mut _, &mut pkt); + if ret < 0 { + break; + } + + let stream_idx = pkt.stream_index as usize; + + if let Some(track) = tracks.iter().find(|t| t.stream_index == stream_idx) { + let dts = if pkt.dts != AV_NOPTS_VALUE { + pkt.dts + } else { + pkt.pts + }; + let pts = if pkt.pts != AV_NOPTS_VALUE { + pkt.pts + } else { + dts + }; + let cts_offset = (pts - dts) as c_int; + let timescale = track.timescale; + + match track.track_type { + TrackType::AvcH264 | TrackType::HevcH265 => { + let nal_unit_size = nal_sizes + .iter() + .find(|(idx, _)| *idx == stream_idx) + .map(|(_, s)| *s) + .unwrap_or(4); + let is_hevc = (track.track_type == TrackType::HevcH265) as c_int; + + if pkt.size > 0 && !pkt.data.is_null() { + ccx_mp4_process_nal_sample( + ctx, + timescale, + nal_unit_size, + is_hevc, + pkt.data, + pkt.size as c_uint, + dts, + cts_offset, + sub, + ); + } + } + TrackType::Cea608 => { + if pkt.size > 0 && !pkt.data.is_null() { + let r = ccx_mp4_process_cc_packet( + ctx, + 1, + pkt.data, + pkt.size as c_uint, + dts, + cts_offset, + timescale, + sub, + ); + if r == 0 { + mp4_ret = 1; + } + } + } + TrackType::Cea708 => { + if pkt.size > 0 && !pkt.data.is_null() { + let r = ccx_mp4_process_cc_packet( + ctx, + 0, + pkt.data, + pkt.size as c_uint, + dts, + cts_offset, + timescale, + sub, + ); + if r == 0 { + mp4_ret = 1; + } + } + } + TrackType::Tx3g => { + if pkt.size > 0 && !pkt.data.is_null() { + if has_tx3g { + ccx_mp4_flush_tx3g(ctx, sub); + } + let r = ccx_mp4_process_tx3g_packet( + ctx, + pkt.data, + pkt.size as c_uint, + dts, + cts_offset, + timescale, + sub, + ); + if r == 0 { + has_tx3g = true; + mp4_ret = 1; + } + } + } + } + } + + ffi::av_packet_unref(&mut pkt); + + packet_count += 1; + if packet_count.is_multiple_of(100) { + ccx_mp4_report_progress(ctx, packet_count, packet_count + 100); + } + } + + // Flush last tx3g subtitle + if has_tx3g { + ccx_mp4_flush_tx3g(ctx, sub); + } + + // 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. + // Intentionally NOT calling process_hdcc here: the last IDR already + // flushed has_ccdata_buffered through slice_header, and running + // process_hdcc again would re-process any cc_data that arrived after + // that IDR — which can include half-typed trailing characters the + // source encoder never intended to display. + let enc_ctx = update_encoder_list(ctx); + if (*sub).got_output != 0 { + encode_sub(enc_ctx, sub); + (*sub).got_output = 0; + } + + ccx_mp4_report_progress(ctx, 100, 100); + + let close_msg = b"\nClosing media: ok\n\0"; + mprint(close_msg.as_ptr() as *const c_char); + + if avc_count > 0 { + let msg = format!("Found {} AVC track(s). \0", avc_count); + mprint(msg.as_ptr() as *const c_char); + } else { + let msg = b"Found no AVC track(s). \0"; + mprint(msg.as_ptr() as *const c_char); + } + if hevc_count > 0 { + let msg = format!("Found {} HEVC track(s). \0", hevc_count); + mprint(msg.as_ptr() as *const c_char); + } + if cc_count > 0 { + let msg = format!("Found {} CC track(s).\n\0", cc_count); + mprint(msg.as_ptr() as *const c_char); + } else { + let msg = b"Found no dedicated CC track(s).\n\0"; + mprint(msg.as_ptr() as *const c_char); + } + + (*ctx).freport.mp4_cc_track_cnt = cc_count as u32; + + mp4_ret +} + +/// Process SPS/PPS/VPS NAL units from extradata +#[cfg(feature = "enable_mp4_ffmpeg")] +unsafe fn process_extradata_params( + ctx: *mut lib_ccx_ctx, + extradata: &[u8], + is_hevc: bool, + sub: *mut cc_subtitle, +) { + let dec_ctx = update_decoder_list(ctx); + let enc_ctx = update_encoder_list(ctx); + + extern "C" { + fn do_NAL( + enc_ctx: *mut encoder_ctx, + dec_ctx: *mut lib_cc_decode, + nal_start: *mut u8, + nal_length: i64, + sub: *mut cc_subtitle, + ); + } + + if is_hevc { + if let Some(avc) = (*dec_ctx).avc_ctx.as_mut() { + avc.is_hevc = 1; + } + // Parse HEVCDecoderConfigurationRecord + if extradata.len() < 23 { + return; + } + let num_arrays = extradata[22] as usize; + let mut offset = 23; + for _ in 0..num_arrays { + if offset + 3 > extradata.len() { + break; + } + let num_nalus = + ((extradata[offset + 1] as usize) << 8) | (extradata[offset + 2] as usize); + offset += 3; + for _ in 0..num_nalus { + if offset + 2 > extradata.len() { + break; + } + let nal_size = + ((extradata[offset] as usize) << 8) | (extradata[offset + 1] as usize); + offset += 2; + if offset + nal_size > extradata.len() { + break; + } + let mut nal_data = extradata[offset..offset + nal_size].to_vec(); + do_NAL( + enc_ctx, + dec_ctx, + nal_data.as_mut_ptr(), + nal_size as i64, + sub, + ); + offset += nal_size; + } + } + } else { + // Parse AVCDecoderConfigurationRecord + if extradata.len() < 7 { + return; + } + let num_sps = (extradata[5] & 0x1F) as usize; + let mut offset = 6; + for _ in 0..num_sps { + if offset + 2 > extradata.len() { + break; + } + let sps_size = ((extradata[offset] as usize) << 8) | (extradata[offset + 1] as usize); + offset += 2; + if offset + sps_size > extradata.len() { + break; + } + let mut sps_data = extradata[offset..offset + sps_size].to_vec(); + do_NAL( + enc_ctx, + dec_ctx, + sps_data.as_mut_ptr(), + sps_size as i64, + sub, + ); + offset += sps_size; + } + // PPS + if offset >= extradata.len() { + return; + } + let num_pps = extradata[offset] as usize; + offset += 1; + for _ in 0..num_pps { + if offset + 2 > extradata.len() { + break; + } + let pps_size = ((extradata[offset] as usize) << 8) | (extradata[offset + 1] as usize); + offset += 2; + if offset + pps_size > extradata.len() { + break; + } + let mut pps_data = extradata[offset..offset + pps_size].to_vec(); + do_NAL( + enc_ctx, + dec_ctx, + pps_data.as_mut_ptr(), + pps_size as i64, + sub, + ); + offset += pps_size; + } + } +} + +/// Dump chapters from MP4 file using FFmpeg +/// +/// # Safety +/// ctx must be a valid pointer, path must be a valid C string +#[cfg(feature = "enable_mp4_ffmpeg")] +pub unsafe fn dumpchapters_rust(_ctx: *mut lib_ccx_ctx, path: &CStr) -> c_int { + let path_str = path.to_bytes(); + let path_display = String::from_utf8_lossy(path_str); + + let open_msg = format!("Opening '{}': \0", path_display); + mprint(open_msg.as_ptr() as *const c_char); + + let fmt_ctx = match AVFormatContextInput::open(path, None, &mut None) { + Ok(ctx) => ctx, + Err(_) => { + let msg = b"failed to open\n\0"; + mprint(msg.as_ptr() as *const c_char); + return 5; + } + }; + + let ok_msg = b"ok\n\0"; + mprint(ok_msg.as_ptr() as *const c_char); + + let nb_chapters = (*fmt_ctx.as_ptr()).nb_chapters as usize; + if nb_chapters == 0 { + let msg = b"No chapters information found!\n\0"; + mprint(msg.as_ptr() as *const c_char); + return 0; + } + + let out_name = format!("{}.txt", path_display); + let mut file = match std::fs::File::create(&out_name) { + Ok(f) => f, + Err(_) => return 5, + }; + + let writing_msg = format!("Writing chapters into {}\n\0", out_name); + mprint(writing_msg.as_ptr() as *const c_char); + + use std::io::Write; + for i in 0..nb_chapters { + let chapter = *(*fmt_ctx.as_ptr()).chapters.add(i); + let start = (*chapter).start; + let time_base = (*chapter).time_base; + + let start_ms = (start as f64 * time_base.num as f64 / time_base.den as f64 * 1000.0) as u64; + let h = start_ms / 3600000; + let m = (start_ms / 60000) % 60; + let s = (start_ms / 1000) % 60; + let ms = start_ms % 1000; + + let title = if !(*chapter).metadata.is_null() { + let key = std::ffi::CString::new("title").unwrap(); + let entry = ffi::av_dict_get((*chapter).metadata, key.as_ptr(), std::ptr::null(), 0); + if !entry.is_null() && !(*entry).value.is_null() { + CStr::from_ptr((*entry).value) + .to_string_lossy() + .into_owned() + } else { + String::new() + } + } else { + String::new() + }; + + if writeln!( + file, + "CHAPTER{:02}={:02}:{:02}:{:02}.{:03}", + i + 1, + h, + m, + s, + ms + ) + .is_err() + { + return 5; + } + if writeln!(file, "CHAPTER{:02}NAME={}", i + 1, title).is_err() { + return 5; + } + } + + 1 +} diff --git a/src/rust/src/encoder/common.rs b/src/rust/src/encoder/common.rs index 91d2b163e..a072059c5 100644 --- a/src/rust/src/encoder/common.rs +++ b/src/rust/src/encoder/common.rs @@ -256,7 +256,7 @@ pub fn write_subtitle_file_header(ctx: &mut encoder_ctx, out: &mut ccx_s_write) unsafe { OutputFormat::from_ctype(ctx.write_format).unwrap_or(OutputFormat::Raw) }; match write_format { - OutputFormat::Ccd => { + OutputFormat::Ccd if write_raw( out.fh, CCD_HEADER.as_ptr() as *const c_void, @@ -266,23 +266,21 @@ pub fn write_subtitle_file_header(ctx: &mut encoder_ctx, out: &mut ccx_s_write) out.fh, ctx.encoded_crlf.as_ptr() as *const c_void, ctx.encoded_crlf_length as usize, - ) == -1 - { - info!("Unable to write CCD header to file\n"); - return -1; - } + ) == -1 => + { + info!("Unable to write CCD header to file\n"); + return -1; } - OutputFormat::Scc => { + OutputFormat::Scc if write_raw( out.fh, SCC_HEADER.as_ptr() as *const c_void, SCC_HEADER.len() - 1, - ) == -1 - { - info!("Unable to write SCC header to file\n"); - return -1; - } + ) == -1 => + { + info!("Unable to write SCC header to file\n"); + return -1; } OutputFormat::Srt @@ -395,16 +393,15 @@ pub fn write_subtitle_file_header(ctx: &mut encoder_ctx, out: &mut ccx_s_write) } } - OutputFormat::Raw => { + OutputFormat::Raw if write_raw( out.fh, BROADCAST_HEADER.as_ptr() as *const c_void, BROADCAST_HEADER.len(), - ) < BROADCAST_HEADER.len() as isize - { - info!("Unable to write Raw header\n"); - return -1; - } + ) < BROADCAST_HEADER.len() as isize => + { + info!("Unable to write Raw header\n"); + return -1; } OutputFormat::Mcc => { diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index 10a05290d..fe9276cc3 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/libccxr_exports/demuxer.rs b/src/rust/src/libccxr_exports/demuxer.rs index 2af60f6da..18d02752b 100755 --- a/src/rust/src/libccxr_exports/demuxer.rs +++ b/src/rust/src/libccxr_exports/demuxer.rs @@ -529,7 +529,7 @@ pub unsafe extern "C" fn ccxr_demuxer_get_file_size(ctx: *mut ccx_demuxer) -> i6 return -1; } let mut demux_ctx = copy_demuxer_from_c_to_rust(ctx); - demux_ctx.get_filesize() as i64 + demux_ctx.get_filesize() } // Extern function for ccx_demuxer_print_cfg diff --git a/src/rust/src/mp4_ffmpeg_exports.rs b/src/rust/src/mp4_ffmpeg_exports.rs new file mode 100644 index 000000000..760ed9533 --- /dev/null +++ b/src/rust/src/mp4_ffmpeg_exports.rs @@ -0,0 +1,49 @@ +//! C-callable FFI exports for the FFmpeg MP4 demuxer. +//! +//! These `#[no_mangle] extern "C"` functions are called from the C side +//! (ccextractor.c) when `ENABLE_FFMPEG_MP4` is defined. + +use std::ffi::CStr; +use std::os::raw::{c_char, c_int}; + +use crate::bindings::{cc_subtitle, lib_ccx_ctx}; +use crate::demuxer::mp4; + +/// Process an MP4 file using FFmpeg, extracting closed captions. +/// +/// This is the Rust replacement for `processmp4()` when building with +/// `ENABLE_FFMPEG_MP4`. Called from C code in ccextractor.c. +/// +/// # Safety +/// - `ctx` must be a valid pointer to an initialized `lib_ccx_ctx` +/// - `file` must be a valid null-terminated C string +#[no_mangle] +pub unsafe extern "C" fn ccxr_processmp4(ctx: *mut lib_ccx_ctx, file: *const c_char) -> c_int { + if ctx.is_null() || file.is_null() { + return -1; + } + + let path = CStr::from_ptr(file); + let mut sub: cc_subtitle = std::mem::zeroed(); + + mp4::processmp4_rust(ctx, path, &mut sub) +} + +/// Dump chapters from an MP4 file using FFmpeg. +/// +/// This is the Rust replacement for `dumpchapters()` when building with +/// `ENABLE_FFMPEG_MP4`. Called from C code in ccextractor.c. +/// +/// # Safety +/// - `ctx` must be a valid pointer to an initialized `lib_ccx_ctx` +/// - `file` must be a valid null-terminated C string +#[no_mangle] +pub unsafe extern "C" fn ccxr_dumpchapters(ctx: *mut lib_ccx_ctx, file: *const c_char) -> c_int { + if ctx.is_null() || file.is_null() { + return 5; + } + + let path = CStr::from_ptr(file); + + mp4::dumpchapters_rust(ctx, path) +} diff --git a/src/rust/src/parser.rs b/src/rust/src/parser.rs index 49fa73133..0f7898d15 100644 --- a/src/rust/src/parser.rs +++ b/src/rust/src/parser.rs @@ -1296,7 +1296,7 @@ impl OptionsExt for Options { if t == 0 { t = OffsetDateTime::now_utc().unix_timestamp() as u64; } - *UTC_REFVALUE.write().unwrap() = t as u64; + *UTC_REFVALUE.write().unwrap() = t; self.noautotimeref = true; } diff --git a/src/rust/wrapper.h b/src/rust/wrapper.h index 25b90c297..bec69b154 100644 --- a/src/rust/wrapper.h +++ b/src/rust/wrapper.h @@ -14,3 +14,6 @@ #include "../lib_ccx/ccx_gxf.h" #include "../lib_ccx/ccx_demuxer_mxf.h" #include "../lib_ccx/cc_bitstream.h" +#ifdef ENABLE_FFMPEG_MP4 +#include "../lib_ccx/mp4_rust_bridge.h" +#endif