diff --git a/.gitignore b/.gitignore index 925e24025..4fbd10521 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,8 @@ plans/ tess.log **/tess.log ut=srt* + +# User custom ignores +.env +test_split_dvb/output/ +test_split_dvb/logs/ diff --git a/docs/CHANGES.TXT b/docs/CHANGES.TXT index 5e37a7913..4a24f2499 100644 --- a/docs/CHANGES.TXT +++ b/docs/CHANGES.TXT @@ -21,6 +21,10 @@ 0.96.3 (2025-12-29) ------------------- +- New: DVB multi-stream subtitle extraction with --split-dvb-subs option (#447) + - Extract multiple DVB subtitle streams to separate output files simultaneously + - Output files named with language and PID (e.g., output_dan_0x091F.srt) + - Handles PAT changes in transport streams without crashing - New: VOBSUB subtitle extraction with OCR support for MP4 files - New: VOBSUB subtitle extraction support for MKV/Matroska files - New: Native SCC (Scenarist Closed Caption) input file support - CCExtractor can now read SCC files diff --git a/src/lib_ccx/cc_bitstream.h b/src/lib_ccx/cc_bitstream.h index 162806d76..66219ba38 100644 --- a/src/lib_ccx/cc_bitstream.h +++ b/src/lib_ccx/cc_bitstream.h @@ -26,25 +26,25 @@ struct bitstream int _i_bpos; }; -#define read_u8(bstream) (uint8_t)bitstream_get_num(bstream, 1, 1) -#define read_u16(bstream) (uint16_t)bitstream_get_num(bstream, 2, 1) -#define read_u32(bstream) (uint32_t)bitstream_get_num(bstream, 4, 1) -#define read_u64(bstream) (uint64_t)bitstream_get_num(bstream, 8, 1) -#define read_i8(bstream) (int8_t)bitstream_get_num(bstream, 1, 1) -#define read_i16(bstream) (int16_t)bitstream_get_num(bstream, 2, 1) -#define read_i32(bstream) (int32_t)bitstream_get_num(bstream, 4, 1) -#define read_i64(bstream) (int64_t)bitstream_get_num(bstream, 8, 1) +#define read_u8(bstream) (uint8_t) bitstream_get_num(bstream, 1, 1) +#define read_u16(bstream) (uint16_t) bitstream_get_num(bstream, 2, 1) +#define read_u32(bstream) (uint32_t) bitstream_get_num(bstream, 4, 1) +#define read_u64(bstream) (uint64_t) bitstream_get_num(bstream, 8, 1) +#define read_i8(bstream) (int8_t) bitstream_get_num(bstream, 1, 1) +#define read_i16(bstream) (int16_t) bitstream_get_num(bstream, 2, 1) +#define read_i32(bstream) (int32_t) bitstream_get_num(bstream, 4, 1) +#define read_i64(bstream) (int64_t) bitstream_get_num(bstream, 8, 1) #define skip_u32(bstream) (void)bitstream_get_num(bstream, 4, 1) -#define next_u8(bstream) (uint8_t)bitstream_get_num(bstream, 1, 0) -#define next_u16(bstream) (uint16_t)bitstream_get_num(bstream, 2, 0) -#define next_u32(bstream) (uint32_t)bitstream_get_num(bstream, 4, 0) -#define next_u64(bstream) (uint64_t)bitstream_get_num(bstream, 8, 0) -#define next_i8(bstream) (int8_t)bitstream_get_num(bstream, 1, 0) -#define next_i16(bstream) (int16_t)bitstream_get_num(bstream, 2, 0) -#define next_i32(bstream) (int32_t)bitstream_get_num(bstream, 4, 0) -#define next_i64(bstream) (int64_t)bitstream_get_num(bstream, 8, 0) +#define next_u8(bstream) (uint8_t) bitstream_get_num(bstream, 1, 0) +#define next_u16(bstream) (uint16_t) bitstream_get_num(bstream, 2, 0) +#define next_u32(bstream) (uint32_t) bitstream_get_num(bstream, 4, 0) +#define next_u64(bstream) (uint64_t) bitstream_get_num(bstream, 8, 0) +#define next_i8(bstream) (int8_t) bitstream_get_num(bstream, 1, 0) +#define next_i16(bstream) (int16_t) bitstream_get_num(bstream, 2, 0) +#define next_i32(bstream) (int32_t) bitstream_get_num(bstream, 4, 0) +#define next_i64(bstream) (int64_t) bitstream_get_num(bstream, 8, 0) int init_bitstream(struct bitstream *bstr, unsigned char *start, unsigned char *end); uint64_t next_bits(struct bitstream *bstr, unsigned bnum); diff --git a/src/lib_ccx/ccx_common_option.c b/src/lib_ccx/ccx_common_option.c index ecfe17f7f..4aac54661 100644 --- a/src/lib_ccx/ccx_common_option.c +++ b/src/lib_ccx/ccx_common_option.c @@ -181,4 +181,6 @@ void init_options(struct ccx_s_options *options) stringztoms(DEF_VAL_STARTCREDITSFORATMOST, &options->enc_cfg.startcreditsforatmost); stringztoms(DEF_VAL_ENDCREDITSFORATLEAST, &options->enc_cfg.endcreditsforatleast); stringztoms(DEF_VAL_ENDCREDITSFORATMOST, &options->enc_cfg.endcreditsforatmost); + + options->split_dvb_subs = 0; // Default: legacy single-stream behavior } diff --git a/src/lib_ccx/ccx_common_option.h b/src/lib_ccx/ccx_common_option.h index aa7e14207..48577fcc7 100644 --- a/src/lib_ccx/ccx_common_option.h +++ b/src/lib_ccx/ccx_common_option.h @@ -201,7 +201,8 @@ struct ccx_s_options // Options from user parameters int multiprogram; int out_interval; int segment_on_key_frames_only; - int scc_framerate; // SCC input framerate: 0=29.97 (default), 1=24, 2=25, 3=30 + int split_dvb_subs; // Enable per-stream DVB subtitle extraction (0=disabled, 1=enabled) + int scc_framerate; // SCC input framerate: 0=29.97 (default), 1=24, 2=25, 3=30 #ifdef WITH_LIBCURL char *curlposturl; #endif diff --git a/src/lib_ccx/ccx_common_platform.h b/src/lib_ccx/ccx_common_platform.h index 6c69af787..fbc5425ff 100644 --- a/src/lib_ccx/ccx_common_platform.h +++ b/src/lib_ccx/ccx_common_platform.h @@ -1,122 +1,122 @@ -#ifndef CCX_PLATFORM_H -#define CCX_PLATFORM_H - -// Default includes (cross-platform) -#include -#include -#include -#include -#include -#include -#include -#include - -#define __STDC_FORMAT_MACROS - -#ifdef _WIN32 -#define inline _inline -#define typeof decltype -#include -#include -#include -#define STDIN_FILENO 0 -#define STDOUT_FILENO 1 -#define STDERR_FILENO 2 -#include "inttypes.h" -#undef UINT64_MAX -#define UINT64_MAX _UI64_MAX -typedef int socklen_t; -#if !defined(__MINGW64__) && !defined(__MINGW32__) -typedef int ssize_t; -#endif -typedef uint32_t in_addr_t; -#ifndef IN_CLASSD -#define IN_CLASSD(i) (((INT32)(i) & 0xf0000000) == 0xe0000000) -#define IN_MULTICAST(i) IN_CLASSD(i) -#endif -#include -#define mkdir(path, mode) _mkdir(path) -#ifndef snprintf -// Added ifndef because VS2013 warns for macro redefinition. -#define snprintf(buf, len, fmt, ...) _snprintf(buf, len, fmt, __VA_ARGS__) -#endif -#define sleep(sec) Sleep((sec) * 1000) - -#include -#else // _WIN32 -#include -#define __STDC_LIMIT_MACROS -#include -#include -#include -#include -#include -#include -#include -#include -#endif // _WIN32 - -// #include "disable_warnings.h" - -#if defined(_MSC_VER) && !defined(__clang__) -#include "stdintmsc.h" -// Don't bug me with strcpy() deprecation warnings -#pragma warning(disable : 4996) -#else -#include -#endif - -#ifdef __OpenBSD__ -#define FOPEN64 fopen -#define OPEN open -#define FSEEK fseek -#define FTELL ftell -#define LSEEK lseek -#define FSTAT fstat -#else -#ifdef _WIN32 -#define OPEN _open -// 64 bit file functions -#if defined(_MSC_VER) -#define FSEEK _fseeki64 -#define FTELL _ftelli64 -#else -// For MinGW -#define FSEEK fseeko64 -#define FTELL ftello64 -#endif -#define TELL _telli64 -#define LSEEK _lseeki64 -typedef struct _stati64 FSTATSTRUCT; -#else -// Linux internally maps these functions to 64bit usage, -// if _FILE_OFFSET_BITS macro is set to 64 -#define FOPEN64 fopen -#define OPEN open -#define LSEEK lseek -#define FSEEK fseek -#define FTELL ftell -#define FSTAT fstat -#define TELL tell -#include -#endif -#endif - -#ifndef int64_t_C -#define int64_t_C(c) (c##LL) -#define uint64_t_C(c) (c##ULL) -#endif - -#ifndef O_BINARY -#define O_BINARY 0 // Not present in Linux because it's always binary -#endif - -#ifndef max -#define max(a, b) (((a) > (b)) ? (a) : (b)) -#endif - -typedef int64_t LLONG; -typedef uint64_t ULLONG; -typedef uint8_t UBYTE; - -#endif // CCX_PLATFORM_H +#ifndef CCX_PLATFORM_H +#define CCX_PLATFORM_H + +// Default includes (cross-platform) +#include +#include +#include +#include +#include +#include +#include +#include + +#define __STDC_FORMAT_MACROS + +#ifdef _WIN32 +#define inline _inline +#define typeof decltype +#include +#include +#include +#define STDIN_FILENO 0 +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 +#include "inttypes.h" +#undef UINT64_MAX +#define UINT64_MAX _UI64_MAX +typedef int socklen_t; +#if !defined(__MINGW64__) && !defined(__MINGW32__) +typedef int ssize_t; +#endif +typedef uint32_t in_addr_t; +#ifndef IN_CLASSD +#define IN_CLASSD(i) (((INT32)(i)&0xf0000000) == 0xe0000000) +#define IN_MULTICAST(i) IN_CLASSD(i) +#endif +#include +#define mkdir(path, mode) _mkdir(path) +#ifndef snprintf +// Added ifndef because VS2013 warns for macro redefinition. +#define snprintf(buf, len, fmt, ...) _snprintf(buf, len, fmt, __VA_ARGS__) +#endif +#define sleep(sec) Sleep((sec)*1000) + +#include +#else // _WIN32 +#include +#define __STDC_LIMIT_MACROS +#include +#include +#include +#include +#include +#include +#include +#include +#endif // _WIN32 + +// #include "disable_warnings.h" + +#if defined(_MSC_VER) && !defined(__clang__) +#include "stdintmsc.h" +// Don't bug me with strcpy() deprecation warnings +#pragma warning(disable : 4996) +#else +#include +#endif + +#ifdef __OpenBSD__ +#define FOPEN64 fopen +#define OPEN open +#define FSEEK fseek +#define FTELL ftell +#define LSEEK lseek +#define FSTAT fstat +#else +#ifdef _WIN32 +#define OPEN _open +// 64 bit file functions +#if defined(_MSC_VER) +#define FSEEK _fseeki64 +#define FTELL _ftelli64 +#else +// For MinGW +#define FSEEK fseeko64 +#define FTELL ftello64 +#endif +#define TELL _telli64 +#define LSEEK _lseeki64 +typedef struct _stati64 FSTATSTRUCT; +#else +// Linux internally maps these functions to 64bit usage, +// if _FILE_OFFSET_BITS macro is set to 64 +#define FOPEN64 fopen +#define OPEN open +#define LSEEK lseek +#define FSEEK fseek +#define FTELL ftell +#define FSTAT fstat +#define TELL tell +#include +#endif +#endif + +#ifndef int64_t_C +#define int64_t_C(c) (c##LL) +#define uint64_t_C(c) (c##ULL) +#endif + +#ifndef O_BINARY +#define O_BINARY 0 // Not present in Linux because it's always binary +#endif + +#ifndef max +#define max(a, b) (((a) > (b)) ? (a) : (b)) +#endif + +typedef int64_t LLONG; +typedef uint64_t ULLONG; +typedef uint8_t UBYTE; + +#endif // CCX_PLATFORM_H diff --git a/src/lib_ccx/ccx_decoders_common.c b/src/lib_ccx/ccx_decoders_common.c index d2b440e54..d53041f6d 100644 --- a/src/lib_ccx/ccx_decoders_common.c +++ b/src/lib_ccx/ccx_decoders_common.c @@ -513,11 +513,13 @@ void flush_cc_decode(struct lib_cc_decode *ctx, struct cc_subtitle *sub) } struct encoder_ctx *copy_encoder_context(struct encoder_ctx *ctx) { + // mprint("DEBUG-TRACE: copy_encoder_context entered\n"); fflush(stdout); struct encoder_ctx *ctx_copy = NULL; ctx_copy = malloc(sizeof(struct encoder_ctx)); if (!ctx_copy) fatal(EXIT_NOT_ENOUGH_MEMORY, "In copy_encoder_context: Out of memory allocating ctx_copy."); memcpy(ctx_copy, ctx, sizeof(struct encoder_ctx)); + // mprint("DEBUG-TRACE: copy_encoder_context struct copied\n"); fflush(stdout); // Initialize copied pointers to NULL before re-allocating ctx_copy->buffer = NULL; @@ -529,7 +531,7 @@ struct encoder_ctx *copy_encoder_context(struct encoder_ctx *ctx) ctx_copy->start_credits_text = NULL; ctx_copy->end_credits_text = NULL; ctx_copy->prev = NULL; - ctx_copy->last_string = NULL; + ctx_copy->last_str = NULL; if (ctx->buffer) { @@ -555,6 +557,7 @@ struct encoder_ctx *copy_encoder_context(struct encoder_ctx *ctx) } if (ctx->timing) { + // mprint("DEBUG-TRACE: copy_encoder_context copying timing\n"); fflush(stdout); ctx_copy->timing = malloc(sizeof(struct ccx_common_timing_ctx)); if (!ctx_copy->timing) fatal(EXIT_NOT_ENOUGH_MEMORY, "In copy_encoder_context: Out of memory allocating timing."); @@ -670,13 +673,53 @@ struct cc_subtitle *copy_subtitle(struct cc_subtitle *sub) memcpy(sub_copy, sub, sizeof(struct cc_subtitle)); sub_copy->datatype = sub->datatype; sub_copy->data = NULL; + sub_copy->prev = NULL; // Don't copy prev chain to avoid double-free if (sub->data) { - sub_copy->data = malloc(sub->nb_data * sizeof(struct eia608_screen)); - if (!sub_copy->data) - fatal(EXIT_NOT_ENOUGH_MEMORY, "In copy_subtitle: Out of memory allocating data."); - memcpy(sub_copy->data, sub->data, sub->nb_data * sizeof(struct eia608_screen)); + if (sub->datatype == CC_DATATYPE_DVB) + { + // Deep copy for DVB bitmap - must copy pixel data to avoid aliasing + struct cc_bitmap *src_bmp = (struct cc_bitmap *)sub->data; + struct cc_bitmap *dst_bmp = malloc(sizeof(struct cc_bitmap)); + if (!dst_bmp) + fatal(EXIT_NOT_ENOUGH_MEMORY, "In copy_subtitle: Out of memory allocating bitmap."); + memcpy(dst_bmp, src_bmp, sizeof(struct cc_bitmap)); + + // Deep copy pixel data buffers (these are the actual bitmap pixels) + dst_bmp->data0 = NULL; + dst_bmp->data1 = NULL; + if (src_bmp->data0 && src_bmp->h > 0 && src_bmp->linesize0 > 0) + { + size_t size0 = (size_t)src_bmp->h * (size_t)src_bmp->linesize0; + dst_bmp->data0 = malloc(size0); + if (dst_bmp->data0) + memcpy(dst_bmp->data0, src_bmp->data0, size0); + } + if (src_bmp->data1 && src_bmp->h > 0 && src_bmp->linesize1 > 0) + { + size_t size1 = (size_t)src_bmp->h * (size_t)src_bmp->linesize1; + dst_bmp->data1 = malloc(size1); + if (dst_bmp->data1) + memcpy(dst_bmp->data1, src_bmp->data1, size1); + } +#ifdef ENABLE_OCR + // Deep copy OCR text if present + if (src_bmp->ocr_text) + dst_bmp->ocr_text = strdup(src_bmp->ocr_text); + else + dst_bmp->ocr_text = NULL; +#endif + sub_copy->data = dst_bmp; + } + else + { + // Original behavior for eia608_screen (608 captions) + sub_copy->data = malloc(sub->nb_data * sizeof(struct eia608_screen)); + if (!sub_copy->data) + fatal(EXIT_NOT_ENOUGH_MEMORY, "In copy_subtitle: Out of memory allocating data."); + memcpy(sub_copy->data, sub->data, sub->nb_data * sizeof(struct eia608_screen)); + } } return sub_copy; } @@ -694,7 +737,7 @@ void free_encoder_context(struct encoder_ctx *ctx) freep(&ctx->start_credits_text); freep(&ctx->end_credits_text); freep(&ctx->prev); - freep(&ctx->last_string); + freep(&ctx->last_str); freep(&ctx); } void free_decoder_context(struct lib_cc_decode *ctx) 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_demuxer.c b/src/lib_ccx/ccx_demuxer.c index 8fbcdd50e..f87bb484b 100644 --- a/src/lib_ccx/ccx_demuxer.c +++ b/src/lib_ccx/ccx_demuxer.c @@ -323,6 +323,11 @@ void ccx_demuxer_delete(struct ccx_demuxer **ctx) } freep(&lctx->filebuffer); + + // Reset potential stream discovery data + lctx->potential_stream_count = 0; + memset(lctx->potential_streams, 0, sizeof(lctx->potential_streams)); + freep(ctx); } @@ -406,6 +411,9 @@ struct ccx_demuxer *init_demuxer(void *parent, struct demuxer_cfg *cfg) init_ts(ctx); ctx->filebuffer = NULL; + // Initialize stream discovery for multi-stream DVB subtitle extraction + ctx->potential_stream_count = 0; + return ctx; } diff --git a/src/lib_ccx/ccx_demuxer.h b/src/lib_ccx/ccx_demuxer.h index 16cd6c10c..13411f2de 100644 --- a/src/lib_ccx/ccx_demuxer.h +++ b/src/lib_ccx/ccx_demuxer.h @@ -31,6 +31,26 @@ struct ccx_demux_report unsigned mp4_cc_track_cnt; }; +#define MAX_POTENTIAL_STREAMS 64 + +/** Stream type identifiers for internal classification */ +#define CCX_STREAM_TYPE_UNKNOWN 0 +#define CCX_STREAM_TYPE_DVB_SUB 1 +#define CCX_STREAM_TYPE_TELETEXT 2 + +/** + * ccx_stream_metadata - Metadata for a discovered subtitle stream + */ +struct ccx_stream_metadata +{ + int pid; // Transport Stream Packet ID (0-8191) + int stream_type; // Logical type (CCX_STREAM_TYPE_*) + int mpeg_type; // Raw MPEG stream type from PMT (e.g., 0x06) + char lang[4]; // ISO 639-2/B three-letter language code + int composition_id; + int ancillary_id; +}; + struct program_info { int pid; @@ -47,7 +67,7 @@ struct program_info int16_t pcr_pid; uint64_t got_important_streams_min_pts[COUNT]; int has_all_min_pts; - char virtual_channel[16]; // Stores ATSC virtual channel like "2.1" + char virtual_channel[16]; // Stores ATSC virtual channel like "2.1" }; struct cap_info @@ -63,6 +83,7 @@ struct cap_info int prev_counter; void *codec_private_data; int ignore; + char lang[4]; // ISO 639-2 language code for DVB split mode /** List joining all stream in TS @@ -162,7 +183,12 @@ struct ccx_demuxer int (*open)(struct ccx_demuxer *ctx, const char *file_name); int (*is_open)(struct ccx_demuxer *ctx); int (*get_stream_mode)(struct ccx_demuxer *ctx); - LLONG (*get_filesize)(struct ccx_demuxer *ctx); + LLONG(*get_filesize) + (struct ccx_demuxer *ctx); + + // Stream discovery for multi-stream DVB subtitle extraction + struct ccx_stream_metadata potential_streams[MAX_POTENTIAL_STREAMS]; + int potential_stream_count; }; struct demuxer_data diff --git a/src/lib_ccx/ccx_encoders_common.c b/src/lib_ccx/ccx_encoders_common.c index 4c3b6db9d..68e3e4d54 100644 --- a/src/lib_ccx/ccx_encoders_common.c +++ b/src/lib_ccx/ccx_encoders_common.c @@ -740,7 +740,8 @@ struct encoder_ctx *init_encoder(struct encoder_cfg *opt) { int ret; int i; - struct encoder_ctx *ctx = malloc(sizeof(struct encoder_ctx)); + // Use calloc to initialize all fields to 0/NULL (Safety fix for copy_encoder_context) + struct encoder_ctx *ctx = calloc(1, sizeof(struct encoder_ctx)); if (!ctx) return NULL; @@ -787,10 +788,20 @@ struct encoder_ctx *init_encoder(struct encoder_cfg *opt) ctx->encoding = opt->encoding; ctx->write_format = opt->write_format; - ctx->is_mkv = 0; - ctx->last_string = NULL; + ctx->last_str = NULL; - ctx->transcript_settings = &opt->transcript_settings; + // Deep copy transcript settings because opt is often stack-allocated and temporary + // Storing &opt->transcript_settings leads to Use-After-Free in copy_encoder_context + ctx->transcript_settings = malloc(sizeof(struct ccx_encoders_transcript_format)); + if (ctx->transcript_settings) + memcpy(ctx->transcript_settings, &opt->transcript_settings, sizeof(struct ccx_encoders_transcript_format)); + else + { + freep(&ctx->buffer); + dinit_output_ctx(ctx); + free(ctx); + return NULL; + } ctx->no_bom = opt->no_bom; ctx->sentence_cap = opt->sentence_cap; ctx->filter_profanity = opt->filter_profanity; diff --git a/src/lib_ccx/ccx_encoders_common.h b/src/lib_ccx/ccx_encoders_common.h index 4a1866e51..e446fba17 100644 --- a/src/lib_ccx/ccx_encoders_common.h +++ b/src/lib_ccx/ccx_encoders_common.h @@ -173,8 +173,8 @@ struct encoder_ctx struct encoder_ctx *prev; int write_previous; // for dvb in .mkv - int is_mkv; // are we working with .mkv file - char *last_string; // last recognized DVB sub + int is_mkv; // are we working with .mkv file + char *last_str; // last recognized DVB sub // Segmenting int segment_pending; @@ -245,11 +245,11 @@ int write_cc_subtitle_as_smptett(struct cc_subtitle *sub, struct encoder_ctx *co int write_cc_subtitle_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *context); int write_cc_subtitle_as_transcript(struct cc_subtitle *sub, struct encoder_ctx *context); -int write_stringz_as_srt(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); -int write_stringz_as_ssa(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); -int write_stringz_as_webvtt(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); -int write_stringz_as_sami(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); -void write_stringz_as_smptett(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); +int write_stringz_as_srt(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); +int write_stringz_as_ssa(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); +int write_stringz_as_webvtt(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); +int write_stringz_as_sami(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); +void write_stringz_as_smptett(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end); int write_cc_bitmap_as_srt(struct cc_subtitle *sub, struct encoder_ctx *context); int write_cc_bitmap_as_ssa(struct cc_subtitle *sub, struct encoder_ctx *context); diff --git a/src/lib_ccx/ccx_encoders_sami.c b/src/lib_ccx/ccx_encoders_sami.c index 212f66504..6fb26fe8f 100644 --- a/src/lib_ccx/ccx_encoders_sami.c +++ b/src/lib_ccx/ccx_encoders_sami.c @@ -6,7 +6,7 @@ #include "utility.h" #include "ccx_encoders_helpers.h" -int write_stringz_as_sami(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) +int write_stringz_as_sami(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) { int used; int len = 0; @@ -26,7 +26,7 @@ int write_stringz_as_sami(char *string, struct encoder_ctx *context, LLONG ms_st if (ret != used) return ret; - len = strlen(string); + len = strlen(str_arg); unescaped = (unsigned char *)malloc(len + 1); if (unescaped == NULL) { @@ -47,14 +47,14 @@ int write_stringz_as_sami(char *string, struct encoder_ctx *context, LLONG ms_st // Scan for \n in the string and replace it with a 0 while (pos_r < len) { - if (string[pos_r] == '\\' && string[pos_r + 1] == 'n') + if (str_arg[pos_r] == '\\' && str_arg[pos_r + 1] == 'n') { unescaped[pos_w] = 0; pos_r += 2; } else { - unescaped[pos_w] = string[pos_r]; + unescaped[pos_w] = str_arg[pos_r]; pos_r++; } pos_w++; diff --git a/src/lib_ccx/ccx_encoders_smptett.c b/src/lib_ccx/ccx_encoders_smptett.c index 794f770ae..da1eb6720 100644 --- a/src/lib_ccx/ccx_encoders_smptett.c +++ b/src/lib_ccx/ccx_encoders_smptett.c @@ -29,12 +29,12 @@ #include "utility.h" #include "ccx_encoders_helpers.h" -void write_stringz_as_smptett(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) +void write_stringz_as_smptett(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) { int used; unsigned h1, m1, s1, ms1; unsigned h2, m2, s2, ms2; - int len = strlen(string); + int len = strlen(str_arg); unsigned char *unescaped = (unsigned char *)malloc(len + 1); if (!unescaped) fatal(EXIT_NOT_ENOUGH_MEMORY, "In write_stringz_as_smptett() - not enough memory for unescaped buffer.\n"); @@ -61,14 +61,14 @@ void write_stringz_as_smptett(char *string, struct encoder_ctx *context, LLONG m // Scan for \n in the string and replace it with a 0 while (pos_r < len) { - if (string[pos_r] == '\\' && string[pos_r + 1] == 'n') + if (str_arg[pos_r] == '\\' && str_arg[pos_r + 1] == 'n') { unescaped[pos_w] = 0; pos_r += 2; } else { - unescaped[pos_w] = string[pos_r]; + unescaped[pos_w] = str_arg[pos_r]; pos_r++; } pos_w++; diff --git a/src/lib_ccx/ccx_encoders_srt.c b/src/lib_ccx/ccx_encoders_srt.c index 865c6e229..c2f3c7583 100644 --- a/src/lib_ccx/ccx_encoders_srt.c +++ b/src/lib_ccx/ccx_encoders_srt.c @@ -8,7 +8,7 @@ /* Helper function to write SRT to a specific output file (issue #665 - teletext multi-page) Takes output file descriptor and counter pointer as parameters */ -static int write_stringz_as_srt_to_output(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end, +static int write_stringz_as_srt_to_output(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end, int out_fh, unsigned int *srt_counter) { int used; @@ -16,7 +16,7 @@ static int write_stringz_as_srt_to_output(char *string, struct encoder_ctx *cont unsigned h2, m2, s2, ms2; char timeline[128]; - if (!string || !string[0]) + if (!str_arg || !str_arg[0]) return 0; millis_to_time(ms_start, &h1, &m1, &s1, &ms1); @@ -32,7 +32,7 @@ static int write_stringz_as_srt_to_output(char *string, struct encoder_ctx *cont dbg_print(CCX_DMT_DECODER_608, "%s", timeline); write_wrapped(out_fh, context->buffer, used); - int len = strlen(string); + int len = strlen(str_arg); unsigned char *unescaped = (unsigned char *)malloc(len + 1); if (!unescaped) fatal(EXIT_NOT_ENOUGH_MEMORY, "In write_stringz_as_srt() - not enough memory for unescaped buffer.\n"); @@ -47,14 +47,14 @@ static int write_stringz_as_srt_to_output(char *string, struct encoder_ctx *cont // Scan for \n in the string and replace it with a 0 while (pos_r < len) { - if (string[pos_r] == '\\' && string[pos_r + 1] == 'n') + if (str_arg[pos_r] == '\\' && str_arg[pos_r + 1] == 'n') { unescaped[pos_w] = 0; pos_r += 2; } else { - unescaped[pos_w] = string[pos_r]; + unescaped[pos_w] = str_arg[pos_r]; pos_r++; } pos_w++; @@ -86,9 +86,9 @@ static int write_stringz_as_srt_to_output(char *string, struct encoder_ctx *cont /* The timing here is not PTS based, but output based, i.e. user delay must be accounted for if there is any */ -int write_stringz_as_srt(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) +int write_stringz_as_srt(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) { - return write_stringz_as_srt_to_output(string, context, ms_start, ms_end, + return write_stringz_as_srt_to_output(str_arg, context, ms_start, ms_end, context->out->fh, &context->srt_counter); } @@ -117,7 +117,7 @@ int write_cc_bitmap_as_srt(struct cc_subtitle *sub, struct encoder_ctx *context) if (context->is_mkv == 1) { // Save recognized string for later use in matroska.c - context->last_string = str; + context->last_str = str; } else { diff --git a/src/lib_ccx/ccx_encoders_ssa.c b/src/lib_ccx/ccx_encoders_ssa.c index 1f2ecf119..28757d345 100644 --- a/src/lib_ccx/ccx_encoders_ssa.c +++ b/src/lib_ccx/ccx_encoders_ssa.c @@ -5,14 +5,14 @@ #include "ccx_encoders_helpers.h" #include "ocr.h" -int write_stringz_as_ssa(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) +int write_stringz_as_ssa(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) { int used; unsigned h1, m1, s1, ms1; unsigned h2, m2, s2, ms2; char timeline[128]; - if (!string || !string[0]) + if (!str_arg || !str_arg[0]) return 0; millis_to_time(ms_start, &h1, &m1, &s1, &ms1); @@ -25,7 +25,7 @@ int write_stringz_as_ssa(char *string, struct encoder_ctx *context, LLONG ms_sta dbg_print(CCX_DMT_DECODER_608, "%s", timeline); write_wrapped(context->out->fh, context->buffer, used); - int len = strlen(string); + int len = strlen(str_arg); unsigned char *unescaped = (unsigned char *)malloc(len + 1); if (!unescaped) fatal(EXIT_NOT_ENOUGH_MEMORY, "In write_stringz_as_ssa() - not enough memory for unescaped buffer.\n"); @@ -40,14 +40,14 @@ int write_stringz_as_ssa(char *string, struct encoder_ctx *context, LLONG ms_sta // Scan for \n in the string and replace it with a 0 while (pos_r < len) { - if (string[pos_r] == '\\' && string[pos_r + 1] == 'n') + if (str_arg[pos_r] == '\\' && str_arg[pos_r + 1] == 'n') { unescaped[pos_w] = 0; pos_r += 2; } else { - unescaped[pos_w] = string[pos_r]; + unescaped[pos_w] = str_arg[pos_r]; pos_r++; } pos_w++; diff --git a/src/lib_ccx/ccx_encoders_webvtt.c b/src/lib_ccx/ccx_encoders_webvtt.c index 68f161579..5eb86ecea 100644 --- a/src/lib_ccx/ccx_encoders_webvtt.c +++ b/src/lib_ccx/ccx_encoders_webvtt.c @@ -120,7 +120,7 @@ static const char *webvtt_pac_row_percent[] = {"10", "15.33", "20.66", "26", "31 /* The timing here is not PTS based, but output based, i.e. user delay must be accounted for if there is any */ -int write_stringz_as_webvtt(char *string, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) +int write_stringz_as_webvtt(char *str_arg, struct encoder_ctx *context, LLONG ms_start, LLONG ms_end) { int used; unsigned h1, m1, s1, ms1; @@ -140,7 +140,7 @@ int write_stringz_as_webvtt(char *string, struct encoder_ctx *context, LLONG ms_ written = write(context->out->fh, context->buffer, used); if (written != used) return -1; - int len = strlen(string); + int len = strlen(str_arg); unsigned char *unescaped = (unsigned char *)malloc(len + 1); if (!unescaped) fatal(EXIT_NOT_ENOUGH_MEMORY, "In write_stringz_as_webvtt() - not enough memory for unescaped buffer.\n"); @@ -155,14 +155,14 @@ int write_stringz_as_webvtt(char *string, struct encoder_ctx *context, LLONG ms_ // Scan for \n in the string and replace it with a 0 while (pos_r < len) { - if (string[pos_r] == '\\' && string[pos_r + 1] == 'n') + if (str_arg[pos_r] == '\\' && str_arg[pos_r + 1] == 'n') { unescaped[pos_w] = 0; pos_r += 2; } else { - unescaped[pos_w] = string[pos_r]; + unescaped[pos_w] = str_arg[pos_r]; pos_r++; } pos_w++; diff --git a/src/lib_ccx/dvb_subtitle_decoder.c b/src/lib_ccx/dvb_subtitle_decoder.c index 4ef9f3cbb..92092bc20 100644 --- a/src/lib_ccx/dvb_subtitle_decoder.c +++ b/src/lib_ccx/dvb_subtitle_decoder.c @@ -25,6 +25,24 @@ #include "utility.h" #include "ccx_decoders_common.h" #include "ocr.h" +#include +#include +#include +#include + +// Debug stub for dump_rect_and_log - used in OCR debugging but not critical +static void dump_rect_and_log(const char *label, const uint8_t *data, int w, int h, int linesize, int mode, int pid, int idx) +{ + // Intentionally empty - enable for debugging bitmap data if needed + (void)label; + (void)data; + (void)w; + (void)h; + (void)linesize; + (void)mode; + (void)pid; + (void)idx; +} #define DVBSUB_PAGE_SEGMENT 0x10 #define DVBSUB_REGION_SEGMENT 0x11 @@ -192,6 +210,41 @@ typedef struct DVBSubContext DVBSubDisplayDefinition *display_definition; } DVBSubContext; +size_t dvbsub_get_context_size(void) +{ + return sizeof(DVBSubContext); +} + +void dvbsub_copy_context(void *dst, void *src) +{ + if (dst && src) + { + DVBSubContext *d = (DVBSubContext *)dst; + DVBSubContext *s = (DVBSubContext *)src; + + // Copy scalar values only - DO NOT copy pointers to avoid aliasing + // The linked lists (region_list, clut_list, object_list, display_list) + // are owned by the source context and must not be shared + d->composition_id = s->composition_id; + d->ancillary_id = s->ancillary_id; + d->lang_index = s->lang_index; + d->version = s->version; + d->time_out = s->time_out; + + // Initialize pointers to NULL to avoid use-after-free + d->region_list = NULL; + d->clut_list = NULL; + d->object_list = NULL; + d->display_list = NULL; + d->display_definition = NULL; + +#ifdef ENABLE_OCR + // OCR context is shared, just copy the pointer (it's managed externally) + d->ocr_ctx = s->ocr_ctx; +#endif + } +} + static __inline unsigned int bytestream_get_byte(const uint8_t **b) { (*b) += 1; @@ -1072,7 +1125,7 @@ static int dvbsub_parse_object_segment(void *dvb_ctx, const uint8_t *buf, object = get_object(ctx, object_id); if (!object) - return 0; // Unsure if we should return error + return 0; coding_method = ((*buf) >> 2) & 3; non_modifying_color = ((*buf++) >> 1) & 1; @@ -1083,7 +1136,6 @@ static int dvbsub_parse_object_segment(void *dvb_ctx, const uint8_t *buf, buf += 2; bottom_field_len = RB16(buf); buf += 2; - if (buf + top_field_len + bottom_field_len > buf_end) { mprint("dvbsub_parse_object_segment(): Field data size too large\n"); @@ -1113,7 +1165,6 @@ static int dvbsub_parse_object_segment(void *dvb_ctx, const uint8_t *buf, if (dvbsub_parse_pixel_data_block(dvb_ctx, display, block, bfl, 1, non_modifying_color)) { - // Problems. Hope for the best. mprint("dvbsub_parse_object_segment(): Something went wrong. Giving up on block (2).\n"); return -1; } @@ -1396,6 +1447,7 @@ static void dvbsub_parse_page_segment(void *dvb_ctx, const uint8_t *buf, int page_state; int timeout; int version; + int has_region_definitions; // Declaration moved here for C89 compliance if (buf_size < 2) return; @@ -1404,74 +1456,92 @@ static void dvbsub_parse_page_segment(void *dvb_ctx, const uint8_t *buf, version = ((*buf) >> 4) & 15; page_state = ((*buf++) >> 2) & 3; - // if version same mean we are already updated - if (ctx->version == version) - { - return; - } + // Version check removed to always allow page state check (Fix for Arte stream) + /* Convert time from second to ms */ ctx->time_out = timeout * 1000; ctx->version = version; + // + // Issue 5: Spec-compliant Page Segment handling + // KEY FIX: Only rebuild display_list if new regions are defined + has_region_definitions = (buf + 6 <= buf_end); // Need at least 6 bytes for one region + if (page_state == 1 || page_state == 2) { - dbg_print(CCX_DMT_DVB, ", PAGE STATE %d", page_state); + // Mode change (1) or Acquisition point (2): Always clear display list delete_regions(ctx); delete_objects(ctx); delete_cluts(ctx); - } - tmp_display_list = ctx->display_list; - ctx->display_list = NULL; + tmp_display_list = ctx->display_list; + ctx->display_list = NULL; - while (buf + 5 < buf_end) + // If regions are provided, parsing loop below will rebuild ctx->display_list + // If no regions provided, ctx->display_list remains NULL (empty) + } + else if (has_region_definitions) { - region_id = *buf++; - buf += 1; - - dbg_print(CCX_DMT_DVB, ", REGION %d ADDED", region_id); - - display = tmp_display_list; - tmp_ptr = &tmp_display_list; + // Normal case (0) WITH new region definitions: clear and rebuild + tmp_display_list = ctx->display_list; + ctx->display_list = NULL; + } + else + { + // Normal case (0) WITHOUT region definitions: keep existing display_list (Arte fix) + // Do not clear ctx->display_list + tmp_display_list = NULL; // Nothing to free from old list + } - while (display && display->region_id != region_id) + // Rebuild display list if we have regions to parse + if (has_region_definitions) + { + while (buf + 6 <= buf_end) { - tmp_ptr = &display->next; - display = display->next; - } + region_id = *buf++; + buf += 1; + display = tmp_display_list; + tmp_ptr = &tmp_display_list; + + while (display && display->region_id != region_id) + { + tmp_ptr = &display->next; + display = display->next; + } - if (!display) - { - display = (struct DVBSubRegionDisplay *)malloc( - sizeof(struct DVBSubRegionDisplay)); if (!display) { - fatal(EXIT_NOT_ENOUGH_MEMORY, "In dvbsub_parse_page_segment: Out of memory allocating display."); + display = (struct DVBSubRegionDisplay *)malloc( + sizeof(struct DVBSubRegionDisplay)); + if (!display) + { + fatal(EXIT_NOT_ENOUGH_MEMORY, "In dvbsub_parse_page_segment: Out of memory allocating display."); + } + memset(display, 0, sizeof(struct DVBSubRegionDisplay)); } - memset(display, 0, sizeof(struct DVBSubRegionDisplay)); - } - display->region_id = region_id; + display->region_id = region_id; - display->x_pos = RB16(buf); - buf += 2; - display->y_pos = RB16(buf); - buf += 2; + display->x_pos = RB16(buf); + buf += 2; + display->y_pos = RB16(buf); + buf += 2; - *tmp_ptr = display->next; + *tmp_ptr = display->next; - display->next = ctx->display_list; - ctx->display_list = display; + display->next = ctx->display_list; + ctx->display_list = display; + } } + // Free any leftover regions that weren't reused while (tmp_display_list) { display = tmp_display_list; - tmp_display_list = display->next; - free(display); } + assert(buf <= buf_end); } @@ -1537,6 +1607,61 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub) ctx = (DVBSubContext *)dec_ctx->private_data; + // Safety check: Context may be NULL after PAT change + if (!ctx) + return -1; + + // Validate we have something to display + if (!ctx->display_list) + { + if (ctx->region_list) + { + // Heuristic Fix for Arte stream: Valid regions exist but Page Segment was empty. + // We auto-populate the display list assuming (0,0) coordinates. + DVBSubRegion *r = ctx->region_list; + while (r) + { + // Only add regions with actual pixel data (non-empty pbuf) + int has_content = 0; + if (r->pbuf && r->buf_size > 0) + { + for (int i = 0; i < r->buf_size; i++) + { + if (r->pbuf[i] != 0) + { + has_content = 1; + break; + } + } + } + + if (has_content) + { + DVBSubRegionDisplay *d = (DVBSubRegionDisplay *)malloc(sizeof(struct DVBSubRegionDisplay)); + if (d) + { + memset(d, 0, sizeof(*d)); + d->region_id = r->id; + d->x_pos = 0; + d->y_pos = 0; + d->next = ctx->display_list; + ctx->display_list = d; + // Force dirty so this region gets rendered + r->dirty = 1; + } + } + else + { + } + r = r->next; + } + } + else + { + return 0; + } + } + display_def = ctx->display_definition; sub->type = CC_BITMAP; sub->lang_index = ctx->lang_index; @@ -1547,11 +1672,17 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub) offset_y = display_def->y; } + // Initialize nb_data before counting dirty regions to avoid stale values + // from copy_subtitle() carrying over between calls + sub->nb_data = 0; + for (display = ctx->display_list; display; display = display->next) { region = get_region(ctx, display->region_id); if (region && region->dirty) + { sub->nb_data++; + } } if (sub->nb_data <= 0) { @@ -1650,6 +1781,9 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub) } memset(rect->data1, 0, 1024); memcpy(rect->data1, clut_table, (1 << region->depth) * sizeof(uint32_t)); + rect->nb_colors = (1 << region->depth); // CRITICAL FIX: OCR needs this to know palette size + + // User Quick Test assert(((1 << region->depth) * sizeof(uint32_t)) <= 1024); } @@ -1679,6 +1813,8 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub) int x_off = display->x_pos - x_pos; int y_off = display->y_pos - y_pos; + static int oob_warning_printed = 0; + int oob_count = 0; for (int y = 0; y < region->height; y++) { for (int x = 0; x < region->width; x++) @@ -1686,21 +1822,36 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub) int offset = ((y + y_off) * width) + x_off + x; if (offset >= (width * height) || offset < 0) { - mprint("write_dvb_sub(): Offset %d (out of bounds!) ignored.\n", - offset); - mprint(" Formula: offset=((y + y_off) * width) + x_off + x\n"); - mprint(" y=%d, y_off=%d, width=%d, x_off=%d, x=%d\n", - y, y_off, width, x_off, x); + oob_count++; + if (!oob_warning_printed) + { + mprint("write_dvb_sub(): Out of bounds pixels detected (showing first occurrence only)\n"); + mprint(" Formula: offset=((y + y_off) * width) + x_off + x\n"); + mprint(" y=%d, y_off=%d, width=%d, x_off=%d, x=%d, offset=%d\n", + y, y_off, width, x_off, x, offset); + oob_warning_printed = 1; + } } else { uint8_t c = (uint8_t)region->pbuf[y * region->width + x]; + if (c != 0) + { + // DEBUG: Found a non-zero pixel! + // mprint("DEBUG-PBUF: Found valid pixel %d at %d,%d\n", c, x, y); + // Only print once per frame to avoid spam, or rely on the final dump + } rect->data0[offset] = c; } } } } + // DEBUG: Verify nonzero count manually + int nz_count = 0; + for (int k = 0; k < width * height; k++) + if (rect->data0[k]) + nz_count++; sub->nb_data = 1; // Set nb_data to 1 since we have merged the images into one image. // Perform OCR @@ -1714,12 +1865,21 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub) } if (ctx->ocr_ctx) { + // DEBUG: Dump before OCR + // dump_rect_and_log("before_ocr", rect->data0, rect->w, rect->h, rect->linesize0, 1, 0, 0); + int ret = ocr_rect(ctx->ocr_ctx, rect, &ocr_str, region->bgcolor, dec_ctx->ocr_quantmode); - if (ret >= 0) + if (ret >= 0 && ocr_str) + { rect->ocr_text = ocr_str; + } else + { rect->ocr_text = NULL; - dbg_print(CCX_DMT_DVB, "\nOCR Result: %s\n", rect->ocr_text ? rect->ocr_text : "NULL"); + } + + // DEBUG: Dump after OCR (if modified) + // dump_rect_and_log("after_ocr", rect->data0, rect->w, rect->h, rect->linesize0, ctx->display_definition ? 3 : 1, 0, 0); } else { @@ -1740,77 +1900,92 @@ void dvbsub_handle_display_segment(struct encoder_ctx *enc_ctx, return; if (enc_ctx->write_previous) // this condition is used for the first subtitle - write_previous will be 0 first so we don't encode a non-existing previous sub { - enc_ctx->prev->last_string = NULL; // Reset last recognized sub text + enc_ctx->prev->last_str = NULL; // Reset last recognized sub text + // Validate current_field before calling get_fts (valid: 1=field1, 2=field2, 3=CEA-708) + int caption_field = dec_ctx->current_field; + if (caption_field < 1 || caption_field > 3) + { + dbg_print(CCX_DMT_DVB, "DVB: invalid current_field %d, using default 1\n", caption_field); + caption_field = 1; + } // Get the current FTS, which will be the start_time of the new subtitle - LLONG next_start_time = get_fts(dec_ctx->timing, dec_ctx->current_field); - // For DVB subtitles, a subtitle is displayed until the next one appears. - // Use next_start_time as the end_time to ensure subtitle N ends when N+1 starts. - // This prevents any overlap between consecutive subtitles. - if (next_start_time > sub->prev->start_time) + LLONG next_start_time = get_fts(dec_ctx->timing, caption_field); + + if (!sub->prev) { - sub->prev->end_time = next_start_time; + // Previous subtitle is missing or invalid, skipping write_previous + enc_ctx->write_previous = 0; + if (enc_ctx->prev) + { + free_encoder_context(enc_ctx->prev); + enc_ctx->prev = NULL; + enc_ctx->prev = copy_encoder_context(enc_ctx); + } } else { - // PTS jump or timeline reset - next_start is at or before our start. - // Calculate duration from raw PTS, but cap to reasonable maximum (5 seconds) - // to avoid creating subtitles that overlap excessively with subsequent ones. - LLONG duration_ms = 0; - if (sub->prev->start_pts > 0 && current_pts > sub->prev->start_pts) + + // For DVB subtitles, a subtitle is displayed until the next one appears. + // Use next_start_time as the end_time to ensure subtitle N ends when N+1 starts. + // This prevents any overlap between consecutive subtitles. + // Issue 7: Use FTS-based duration calculation always + LLONG fts_end_time = next_start_time; + + // If we have a timeout, respect it + if (sub->prev->time_out > 0) { - duration_ms = (current_pts - sub->prev->start_pts) / (MPEG_CLOCK_FREQ / 1000); + LLONG max_end = sub->prev->start_time + sub->prev->time_out; + if (fts_end_time > max_end || fts_end_time <= sub->prev->start_time) + { + fts_end_time = max_end; + } } - // Cap duration to 4 seconds or timeout if smaller - LLONG max_duration = 4000; // 4 seconds - if (sub->prev->time_out > 0 && sub->prev->time_out < max_duration) + else { - max_duration = sub->prev->time_out; + // No timeout specified, clamp to reasonable max (e.g. 5s) if next sub is too far + if (fts_end_time - sub->prev->start_time > 5000) + fts_end_time = sub->prev->start_time + 5000; } - if (duration_ms > max_duration) + + sub->prev->end_time = fts_end_time; + + // Sanity check: if end_time still <= start_time (e.g. due to resets), force 1ms + if (sub->prev->end_time <= sub->prev->start_time) { - duration_ms = max_duration; + dbg_print(CCX_DMT_DVB, "DVB timing: end <= start, using start+1\n"); + sub->prev->end_time = sub->prev->start_time + 1; } - sub->prev->end_time = sub->prev->start_time + duration_ms; - } - // Sanity check: if end_time still <= start_time, use minimal duration - if (sub->prev->end_time <= sub->prev->start_time) - { - dbg_print(CCX_DMT_DVB, "DVB timing: end <= start, using start+1\n"); - sub->prev->end_time = sub->prev->start_time + 1; - } - // Apply timeout limit if specified - if (sub->prev->time_out > 0 && sub->prev->time_out < sub->prev->end_time - sub->prev->start_time) - { - sub->prev->end_time = sub->prev->start_time + sub->prev->time_out; - } - int timeok = 1; - if (dec_ctx->extraction_start.set && - sub->prev->start_time < dec_ctx->extraction_start.time_in_ms) - timeok = 0; - if (dec_ctx->extraction_end.set && - sub->prev->end_time > dec_ctx->extraction_end.time_in_ms) - { - timeok = 0; - dec_ctx->processed_enough = 1; - } - if (timeok) - { - encode_sub(enc_ctx->prev, sub->prev); // we encode it + int timeok = 1; + if (dec_ctx->extraction_start.set && + sub->prev->start_time < dec_ctx->extraction_start.time_in_ms) + timeok = 0; + if (dec_ctx->extraction_end.set && + sub->prev->end_time > dec_ctx->extraction_end.time_in_ms) + { + timeok = 0; + dec_ctx->processed_enough = 1; + } + if (timeok) + { + encode_sub(enc_ctx->prev, sub->prev); // we encode it - enc_ctx->last_string = enc_ctx->prev->last_string; // Update last recognized string (used in Matroska) - enc_ctx->prev->last_string = NULL; + enc_ctx->last_str = enc_ctx->prev->last_str; // Update last recognized string (used in Matroska) + enc_ctx->prev->last_str = NULL; - enc_ctx->srt_counter = enc_ctx->prev->srt_counter; // for dvb subs we need to update the current srt counter because we always encode the previous subtitle (and the counter is increased for the previous context) - enc_ctx->prev_start = enc_ctx->prev->prev_start; - sub->prev->got_output = 0; - if (enc_ctx->write_format == CCX_OF_WEBVTT) - { // we already wrote header, but since we encoded last sub, we must prevent multiple headers in future - enc_ctx->wrote_webvtt_header = 1; + enc_ctx->srt_counter = enc_ctx->prev->srt_counter; // for dvb subs we need to update the current srt counter because we always encode the previous subtitle (and the counter is increased for the previous context) + enc_ctx->prev_start = enc_ctx->prev->prev_start; + sub->prev->got_output = 0; + if (enc_ctx->write_format == CCX_OF_WEBVTT) + { // we already wrote header, but since we encoded last sub, we must prevent multiple headers in future + enc_ctx->wrote_webvtt_header = 1; + } } } } /* copy previous encoder context*/ + free_encoder_context(enc_ctx->prev); + enc_ctx->prev = NULL; enc_ctx->prev = copy_encoder_context(enc_ctx); @@ -1825,19 +2000,31 @@ void dvbsub_handle_display_segment(struct encoder_ctx *enc_ctx, { fatal(EXIT_NOT_ENOUGH_MEMORY, "In dvbsub_handle_display_segment: Out of memory allocating private_data."); } - memcpy(dec_ctx->prev->private_data, dec_ctx->private_data, sizeof(struct DVBSubContext)); + // Use safe copy that doesn't alias linked list pointers + dvbsub_copy_context(dec_ctx->prev->private_data, dec_ctx->private_data); + + // Issue 6: Removed workaround. Version management should be handled by logic, not forcing -1. + // Reference: ((DVBSubContext *)dec_ctx->prev->private_data)->version = -1; + /* copy previous subtitle */ free_subtitle(sub->prev); sub->time_out = ctx->time_out; sub->prev = NULL; sub->prev = copy_subtitle(sub); // Use get_fts() which properly handles PTS jumps and maintains monotonic timing - sub->prev->start_time = get_fts(dec_ctx->timing, dec_ctx->current_field); + // Validate current_field (valid: 1=field1, 2=field2, 3=CEA-708) + int sub_caption_field = dec_ctx->current_field; + if (sub_caption_field < 1 || sub_caption_field > 3) + sub_caption_field = 1; + sub->prev->start_time = get_fts(dec_ctx->timing, sub_caption_field); // Store the raw PTS for accurate duration calculation (not affected by PTS jump handling) sub->prev->start_pts = current_pts; - write_dvb_sub(dec_ctx->prev, sub->prev); // we write the current dvb sub to update decoder context - enc_ctx->write_previous = 1; // we update our boolean value so next time the program reaches this block of code, it encodes the previous sub + // Use current dec_ctx (not prev) because we need valid region/object data for rendering + // dec_ctx->prev has NULL pointers to avoid memory corruption from aliased linked lists + write_dvb_sub(dec_ctx, sub->prev); // we write the current dvb sub to update decoder context + enc_ctx->write_previous = 1; // we update our boolean value so next time the program reaches this block of code, it encodes the previous sub + #ifdef ENABLE_OCR if (sub->prev) { @@ -1873,6 +2060,10 @@ int dvbsub_decode(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, co int ret = 0; int got_segment = 0; + // Safety check: Context may be NULL after PAT change + if (!ctx) + return -1; + if (buf_size <= 6 || *buf != 0x0f) { mprint("dvbsub_decode: incomplete, broken or empty packet (size = %d, first byte=%02X)\n", @@ -1912,7 +2103,6 @@ int dvbsub_decode(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, co dbg_print(CCX_DMT_DVB, "DVBSUB - PTS: %" PRId64 ", ", dec_ctx->timing->current_pts); dbg_print(CCX_DMT_DVB, "FTS: %d, ", dec_ctx->timing->fts_now); dbg_print(CCX_DMT_DVB, "SEGMENT TYPE: %2X, ", segment_type); - switch (segment_type) { case DVBSUB_PAGE_SEGMENT: @@ -1945,11 +2135,18 @@ int dvbsub_decode(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, co segment_length); break; case DVBSUB_DISPLAY_SEGMENT: // when we get a display segment, we save the current page - dbg_print(CCX_DMT_DVB, "(DVBSUB_DISPLAY_SEGMENT), SEGMENT LENGTH: %d", segment_length); + // dbg_print(CCX_DMT_DVB, "(DVBSUB_DISPLAY_SEGMENT), SEGMENT LENGTH: %d", segment_length); dvbsub_handle_display_segment(enc_ctx, dec_ctx, sub, pre_fts_max); + got_segment |= 16; break; default: + if (segment_type == 0) // Padding + { + p += segment_length; + continue; + } + dbg_print(CCX_DMT_DVB, "Subtitling segment type 0x%x, page id %d, length %d\n", segment_type, page_id, segment_length); break; @@ -2001,7 +2198,7 @@ int parse_dvb_description(struct dvb_config *cfg, unsigned char *data, if (cfg->n_language > 1) { - mprint("DVB subtitles with multiple languages"); + mprint("DVB subtitles with multiple languages\n"); } if (cfg->n_language > MAX_LANGUAGE_PER_DESC) diff --git a/src/lib_ccx/dvb_subtitle_decoder.h b/src/lib_ccx/dvb_subtitle_decoder.h index abb01ef5d..1ba095a8e 100644 --- a/src/lib_ccx/dvb_subtitle_decoder.h +++ b/src/lib_ccx/dvb_subtitle_decoder.h @@ -70,6 +70,23 @@ extern "C" int parse_dvb_description(struct dvb_config *cfg, unsigned char *data, unsigned int len); + /* + * @func dvbsub_get_context_size + * Get the size of DVBSubContext for external allocation + * + * @return size in bytes of DVBSubContext structure + */ + size_t dvbsub_get_context_size(void); + + /* + * @func dvbsub_copy_context + * Copy DVBSubContext from src to dst + * + * @param dst destination pointer (must be allocated with at least dvbsub_get_context_size() bytes) + * @param src source DVBSubContext pointer + */ + void dvbsub_copy_context(void *dst, void *src); + /* * @func dvbsub_set_write the output structure in dvb * set ccx_s_write structure in dvb_ctx diff --git a/src/lib_ccx/general_loop.c b/src/lib_ccx/general_loop.c index 070866bb4..d48313006 100644 --- a/src/lib_ccx/general_loop.c +++ b/src/lib_ccx/general_loop.c @@ -202,6 +202,8 @@ int ps_get_more_data(struct lib_ccx_ctx *ctx, struct demuxer_data **ppdata) } // FIXME: Temporary bypass data->bufferdatatype = CCX_DVD_SUBTITLE; + // Use substream ID as stream_pid for PS files to differentiate DVB subtitle streams + data->stream_pid = nextheader[7]; data->len = result; enough = 1; @@ -871,10 +873,14 @@ int process_data(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, str } else if (data_node->bufferdatatype == CCX_DVB_SUBTITLE) { - ret = dvbsub_decode(enc_ctx, dec_ctx, data_node->buffer + 2, data_node->len - 2, dec_sub); - if (ret < 0) - mprint("Return from dvbsub_decode: %d\n", ret); - set_fts(dec_ctx->timing); + // Safety check: Skip if decoder was freed due to PAT change + if (dec_ctx->private_data) + { + ret = dvbsub_decode(enc_ctx, dec_ctx, data_node->buffer + 2, data_node->len - 2, dec_sub); + if (ret < 0) + mprint("Return from dvbsub_decode: %d\n", ret); + set_fts(dec_ctx->timing); + } got = data_node->len; } else if (data_node->bufferdatatype == CCX_PES) @@ -1133,10 +1139,19 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx, // struct encoder_ctx *enc_ctx = NULL; // Find most promising stream: teletex, DVB, ISDB int pid = get_best_stream(ctx->demux_ctx); + + // NOTE: For DVB split mode, we do NOT mutate pid here. + // Mutating pid to -1 causes demuxer PES buffer errors because it changes + // the stream selection semantics unexpectedly. Instead, we keep primary + // stream processing unchanged and handle DVB streams in a separate + // read-only secondary pass after the primary stream is processed. + if (pid < 0) { + // Let get_best_data pick the primary stream (usually Teletext) *data_node = get_best_data(*datalist); } + else { ignore_other_stream(ctx->demux_ctx, pid); @@ -1281,23 +1296,132 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx, } if ((*data_node)->bufferdatatype == CCX_TELETEXT && (*dec_ctx)->private_data) // if we have teletext subs, we set the min_pts here set_tlt_delta(*dec_ctx, (*dec_ctx)->timing->current_pts); - ret = process_data(*enc_ctx, *dec_ctx, *data_node); + + // Primary stream processing + // In split DVB mode, DVB streams are handled in the secondary pass below. + // We skip primary processing for DVB here to avoid double-processing (or processing as 'default' output). + // Teletext/other streams still go through here. + if (*data_node && !(ccx_options.split_dvb_subs && (*dec_ctx)->codec == CCX_CODEC_DVB)) + { + ret = process_data(*enc_ctx, *dec_ctx, *data_node); + } + if (*enc_ctx != NULL) { if ((*enc_ctx)->srt_counter || (*enc_ctx)->cea_708_counter || (*dec_ctx)->saw_caption_block || ret == 1) { *caps = 1; - /* Also update ret to indicate captions were found. - This is needed for CEA-708 which writes directly via Rust - and doesn't set got_output like CEA-608/DVB do. */ ret = 1; } } + // SECONDARY PASS: Process DVB streams in split mode + // DVB streams are parallel consumers, processed AFTER the primary stream + // This ensures Teletext controls timing/loop lifetime while DVB is extracted separately + if (ccx_options.split_dvb_subs) + { + // Guard: Only process DVB if timing has been initialized by Teletext + // if ((*dec_ctx)->timing == NULL || (*dec_ctx)->timing->pts_set == 0) + // { + // goto skip_dvb_secondary_pass; + struct demuxer_data *dvb_ptr = *datalist; + while (dvb_ptr) + { + // Process DVB nodes (in split mode, even if they were the "best" stream, + // we route them here to ensure they get a proper named pipeline) + if (dvb_ptr->codec == CCX_CODEC_DVB && + dvb_ptr->len > 0) + { + int stream_pid = dvb_ptr->stream_pid; + char *lang = "unk"; + + // Find language for this PID - check potential_streams first (PMT discovery) + for (int k = 0; k < ctx->demux_ctx->potential_stream_count; k++) + { + if (ctx->demux_ctx->potential_streams[k].pid == stream_pid && + ctx->demux_ctx->potential_streams[k].lang[0]) + { + lang = ctx->demux_ctx->potential_streams[k].lang; + break; + } + } + + // Fallback to cinfo if potential_streams didn't have it + if (strcmp(lang, "unk") == 0) + { + struct cap_info *cinfo = get_cinfo(ctx->demux_ctx, stream_pid); + if (cinfo && cinfo->lang[0]) + lang = cinfo->lang; + } + + // Get or create pipeline for this DVB stream + struct ccx_subtitle_pipeline *pipe = get_or_create_pipeline(ctx, stream_pid, CCX_STREAM_TYPE_DVB_SUB, lang); + + // Reinitialize decoder if it was NULLed out by PAT change + if (pipe && pipe->dec_ctx && !pipe->decoder) + { + struct dvb_config dvb_cfg = {0}; + dvb_cfg.n_language = 1; + // Lookup composition/ancillary IDs from potential_streams + if (ctx->demux_ctx) + { + for (int j = 0; j < ctx->demux_ctx->potential_stream_count; j++) + { + if (ctx->demux_ctx->potential_streams[j].pid == stream_pid) + { + dvb_cfg.composition_id[0] = ctx->demux_ctx->potential_streams[j].composition_id; + dvb_cfg.ancillary_id[0] = ctx->demux_ctx->potential_streams[j].ancillary_id; + break; + } + } + } + pipe->decoder = dvbsub_init_decoder(&dvb_cfg); + if (pipe->decoder) + pipe->dec_ctx->private_data = pipe->decoder; + } + + if (pipe && pipe->encoder && pipe->decoder && pipe->dec_ctx) + { + // Use pipeline's own independent timing context + // This avoids synchronization issues if primary stream (Teletext) has different timing characteristics + pipe->dec_ctx->timing = pipe->timing; + pipe->encoder->timing = pipe->timing; + + // Set the PTS for this DVB packet before decoding + // Without this, the DVB decoder will use stale timing + set_pipeline_pts(pipe, dvb_ptr->pts); + + // Create subtitle structure if needed + if (!pipe->dec_ctx->dec_sub.prev) + { + // This should be handled by get_or_create_pipeline but safety check + struct cc_subtitle *sub = malloc(sizeof(struct cc_subtitle)); + memset(sub, 0, sizeof(struct cc_subtitle)); + pipe->dec_ctx->dec_sub.prev = sub; + } + + // Decode DVB using the per-pipeline decoder context + // This ensures each stream has its own prev pointers + // Skip first 2 bytes (PES header) as done in process_data for DVB + + // Safety check: Skip if decoder was freed due to PAT change + // When PAT changes, dinit_cap NULLs out the decoder references + if (pipe->decoder && pipe->dec_ctx->private_data) + { + dvbsub_decode(pipe->encoder, pipe->dec_ctx, dvb_ptr->buffer + 2, dvb_ptr->len - 2, &pipe->dec_ctx->dec_sub); + } + } + } + dvb_ptr = dvb_ptr->next_stream; + } + } + skip_dvb_secondary_pass: + // Process the last subtitle for DVB if (!(!terminate_asap && !end_of_file && is_decoder_processed_enough(ctx) == CCX_FALSE)) { - if ((*data_node)->bufferdatatype == CCX_DVB_SUBTITLE && (*dec_ctx)->dec_sub.prev->end_time == 0) + if ((*data_node)->bufferdatatype == CCX_DVB_SUBTITLE && + (*dec_ctx)->dec_sub.prev && (*dec_ctx)->dec_sub.prev->end_time == 0) { // Use get_fts() which properly handles PTS jumps and maintains monotonic timing (*dec_ctx)->dec_sub.prev->end_time = get_fts((*dec_ctx)->timing, (*dec_ctx)->current_field); diff --git a/src/lib_ccx/lib_ccx.c b/src/lib_ccx/lib_ccx.c index cfeb7db26..597507248 100644 --- a/src/lib_ccx/lib_ccx.c +++ b/src/lib_ccx/lib_ccx.c @@ -5,6 +5,10 @@ #include "dvb_subtitle_decoder.h" #include "ccx_decoders_708.h" #include "ccx_decoders_isdb.h" +#include "ccx_decoders_common.h" +#ifdef ENABLE_OCR +#include "ocr.h" +#endif struct ccx_common_logging_t ccx_common_logging; static struct ccx_decoders_common_settings_t *init_decoder_setting( @@ -202,6 +206,18 @@ struct lib_ccx_ctx *init_libraries(struct ccx_s_options *opt) ctx->segment_counter = 0; ctx->system_start_time = -1; + // Initialize pipeline infrastructure + ctx->pipeline_count = 0; + ctx->dec_dvb_default = NULL; +#ifdef _WIN32 + InitializeCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_init(&ctx->pipeline_mutex, NULL); +#endif + ctx->pipeline_mutex_initialized = 1; + ctx->shared_ocr_ctx = NULL; + memset(ctx->pipelines, 0, sizeof(ctx->pipelines)); + end: if (ret != EXIT_OK) { @@ -263,6 +279,82 @@ void dinit_libraries(struct lib_ccx_ctx **ctx) } } + // Cleanup subtitle pipelines (split DVB mode) + for (i = 0; i < lctx->pipeline_count; i++) + { + struct ccx_subtitle_pipeline *p = lctx->pipelines[i]; + if (!p) + continue; + + // Correct closing order to ensure last subtitle is written + if (p->dec_ctx && p->encoder) + { + // Check if there's a pending subtitle in the prev buffer + if (p->dec_ctx->dec_sub.prev && p->dec_ctx->dec_sub.prev->data && p->encoder->prev) + { + // Calculate end time for the last subtitle + LLONG current_fts = 0; + if (p->dec_ctx->timing) + { + current_fts = get_fts(p->dec_ctx->timing, p->dec_ctx->current_field); + } + + // Force end time if missing + if (p->dec_ctx->dec_sub.prev->end_time == 0) + { + if (current_fts > p->dec_ctx->dec_sub.prev->start_time) + p->dec_ctx->dec_sub.prev->end_time = current_fts; + else + p->dec_ctx->dec_sub.prev->end_time = p->dec_ctx->dec_sub.prev->start_time + 2000; // 2s fallback + } + + encode_sub(p->encoder->prev, p->dec_ctx->dec_sub.prev); + } + } + + if (p->decoder) + dvbsub_close_decoder(&p->decoder); + +#ifdef ENABLE_OCR + if (p->ocr_ctx) + delete_ocr(&p->ocr_ctx); +#endif + + if (p->encoder) + dinit_encoder(&p->encoder, 0); + + if (p->timing) + dinit_timing_ctx(&p->timing); + + if (p->dec_ctx) + { + // private_data points to p->decoder which is freed above + if (p->dec_ctx->prev) + { + // prev->private_data is allocated by copy_decoder_context + freep(&p->dec_ctx->prev->private_data); + free(p->dec_ctx->prev); + } + p->dec_ctx->private_data = NULL; + free(p->dec_ctx); + } + free_subtitle(p->sub.prev); + free(p); + lctx->pipelines[i] = NULL; + } + lctx->pipeline_count = 0; + + // Cleanup mutex + if (lctx->pipeline_mutex_initialized) + { +#ifdef _WIN32 + DeleteCriticalSection(&lctx->pipeline_mutex); +#else + pthread_mutex_destroy(&lctx->pipeline_mutex); +#endif + lctx->pipeline_mutex_initialized = 0; + } + // free EPG memory EPG_free(lctx); freep(&lctx->freport.data_from_608); @@ -275,6 +367,11 @@ void dinit_libraries(struct lib_ccx_ctx **ctx) for (i = 0; i < lctx->num_input_files; i++) freep(&lctx->inputfile[i]); freep(&lctx->inputfile); + freep(&lctx->inputfile); +#ifdef ENABLE_OCR + if (lctx->shared_ocr_ctx) + delete_ocr(&lctx->shared_ocr_ctx); +#endif freep(ctx); } @@ -487,3 +584,247 @@ struct encoder_ctx *update_encoder_list(struct lib_ccx_ctx *ctx) { return update_encoder_list_cinfo(ctx, NULL); } + +/** + * Get or create a subtitle pipeline for a specific PID/language. + * Used when --split-dvb-subs is enabled to route each DVB stream to its own output file. + */ +struct ccx_subtitle_pipeline *get_or_create_pipeline(struct lib_ccx_ctx *ctx, int pid, int stream_type, const char *lang) +{ + int i; + + // Lock mutex for thread safety +#ifdef _WIN32 + EnterCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_lock(&ctx->pipeline_mutex); +#endif + + // Search for existing pipeline + for (i = 0; i < ctx->pipeline_count; i++) + { + struct ccx_subtitle_pipeline *p = ctx->pipelines[i]; + if (p && p->pid == pid && p->stream_type == stream_type && + strcmp(p->lang, lang) == 0) + { +#ifdef _WIN32 + LeaveCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_unlock(&ctx->pipeline_mutex); +#endif + return p; + } + } + + // Check capacity + if (ctx->pipeline_count >= MAX_SUBTITLE_PIPELINES) + { + mprint("Warning: Maximum subtitle pipelines (%d) reached, cannot create new pipeline for PID 0x%X\n", + MAX_SUBTITLE_PIPELINES, pid); +#ifdef _WIN32 + LeaveCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_unlock(&ctx->pipeline_mutex); +#endif + return NULL; + } + + // Allocate new pipeline + struct ccx_subtitle_pipeline *pipe = calloc(1, sizeof(struct ccx_subtitle_pipeline)); + if (!pipe) + { + mprint("Error: Failed to allocate memory for subtitle pipeline\n"); +#ifdef _WIN32 + LeaveCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_unlock(&ctx->pipeline_mutex); +#endif + return NULL; + } + + pipe->pid = pid; + pipe->stream_type = stream_type; + snprintf(pipe->lang, sizeof(pipe->lang), "%.3s", lang ? lang : "und"); + + // Generate output filename: {basefilename}_{lang}_{PID}.ext + // Always include PID to handle multiple streams with same language + const char *ext = ctx->extension ? ctx->extension : ".srt"; + if (strcmp(pipe->lang, "und") == 0 || strcmp(pipe->lang, "unk") == 0 || pipe->lang[0] == '\0') + { + snprintf(pipe->filename, sizeof(pipe->filename), "%s_0x%04X%s", + ctx->basefilename, pid, ext); + } + else + { + snprintf(pipe->filename, sizeof(pipe->filename), "%s_%s_0x%04X%s", + ctx->basefilename, pipe->lang, pid, ext); + } + + // Initialize encoder for this pipeline + struct encoder_cfg cfg = ccx_options.enc_cfg; + cfg.output_filename = pipe->filename; + pipe->encoder = init_encoder(&cfg); + // DVB subtitles require a 'prev' encoder context for buffering (write_previous logic). + // Without this, the first call to dvbsub_handle_display_segment may fail or result in no output. + if (pipe->encoder) + { + pipe->encoder->prev = copy_encoder_context(pipe->encoder); + if (!pipe->encoder->prev) + { + mprint("Error: Failed to allocate prev context for PID 0x%X\n", pid); + dinit_encoder(&pipe->encoder, 0); + free(pipe); + return NULL; + } + // FIX: Set write_previous=1 so the FIRST subtitle gets written + // DVB pattern: "write N-1 when N arrives" + pipe->encoder->write_previous = 0; + + // Issue 4: Ensure prev context exists and is initialized + // This forces the "previous" subtitle (which is effectively the first one we see) + // to be eligible for writing when the NEXT segment arrives. + // pipe->sub.prev will be allocated when we set up dec_ctx below. + } + + if (!pipe->encoder) + { + mprint("Error: Failed to create encoder for pipeline PID 0x%X\n", pid); + free(pipe); +#ifdef _WIN32 + LeaveCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_unlock(&ctx->pipeline_mutex); +#endif + return NULL; + } + + // Timing context: Create independent timing context for pipeline + // This ensures DVB streams track their own PTS/FTS without race conditions + // with the primary Teletext stream. + pipe->timing = init_timing_ctx(&ccx_common_timing_settings); + if (!pipe->timing) + { + mprint("Error: Failed to initialize timing for pipeline PID 0x%X\n", pid); + dinit_encoder(&pipe->encoder, 0); + free(pipe); +#ifdef _WIN32 + LeaveCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_unlock(&ctx->pipeline_mutex); +#endif + return NULL; + } + + // Initialize DVB decoder + struct dvb_config dvb_cfg = {0}; + dvb_cfg.n_language = 1; + + // Lookup metadata to use correct Composition and Ancillary Page IDs + // This ensures we respect the configuration advertised in the PMT + if (ctx->demux_ctx) + { + for (i = 0; i < ctx->demux_ctx->potential_stream_count; i++) + { + if (ctx->demux_ctx->potential_streams[i].pid == pid) + { + dvb_cfg.composition_id[0] = ctx->demux_ctx->potential_streams[i].composition_id; + dvb_cfg.ancillary_id[0] = ctx->demux_ctx->potential_streams[i].ancillary_id; + break; + } + } + } + + pipe->decoder = dvbsub_init_decoder(&dvb_cfg); + if (!pipe->decoder) + { + mprint("Error: Failed to create DVB decoder for pipeline PID 0x%X\n", pid); + dinit_encoder(&pipe->encoder, 0); + dinit_timing_ctx(&pipe->timing); + free(pipe); +#ifdef _WIN32 + LeaveCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_unlock(&ctx->pipeline_mutex); +#endif + return NULL; + } + + // Initialize per-pipeline decoder context for DVB state management + pipe->dec_ctx = calloc(1, sizeof(struct lib_cc_decode)); + if (!pipe->dec_ctx) + { + mprint("Error: Failed to create decoder context for pipeline PID 0x%X\n", pid); + dvbsub_close_decoder(&pipe->decoder); + dinit_encoder(&pipe->encoder, 0); + free(pipe); +#ifdef _WIN32 + LeaveCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_unlock(&ctx->pipeline_mutex); +#endif + return NULL; + } + pipe->dec_ctx->private_data = pipe->decoder; + pipe->dec_ctx->codec = CCX_CODEC_DVB; + pipe->dec_ctx->prev = calloc(1, sizeof(struct lib_cc_decode)); + if (pipe->dec_ctx->prev) + { + pipe->dec_ctx->prev->private_data = malloc(dvbsub_get_context_size()); + if (pipe->dec_ctx->prev->private_data) + { + dvbsub_copy_context(pipe->dec_ctx->prev->private_data, pipe->decoder); + } + pipe->dec_ctx->prev->codec = CCX_CODEC_DVB; + } + + // Initialize persistent cc_subtitle for DVB prev tracking + memset(&pipe->sub, 0, sizeof(struct cc_subtitle)); + pipe->sub.prev = calloc(1, sizeof(struct cc_subtitle)); + if (pipe->sub.prev) + { + pipe->sub.prev->start_time = -1; + pipe->sub.prev->end_time = 0; + } + + // Register pipeline + ctx->pipelines[ctx->pipeline_count++] = pipe; + + mprint("Created subtitle pipeline for PID 0x%X lang=%s -> %s\n", pid, pipe->lang, pipe->filename); + +#ifdef _WIN32 + LeaveCriticalSection(&ctx->pipeline_mutex); +#else + pthread_mutex_unlock(&ctx->pipeline_mutex); +#endif + return pipe; +} + +/** + * Set PTS for a subtitle pipeline's timing context. + * This ensures the DVB decoder uses the correct timestamp for each packet. + */ +void set_pipeline_pts(struct ccx_subtitle_pipeline *pipe, LLONG pts) +{ + if (!pipe || !pipe->timing || pts == CCX_NOPTS) + return; + + set_current_pts(pipe->timing, pts); + + // Initialize min_pts if not set + if (pipe->timing->min_pts == 0x01FFFFFFFFLL) + { + pipe->timing->min_pts = pts; + pipe->timing->pts_set = 2; // MinPtsSet + pipe->timing->sync_pts = pts; + } + + // For DVB subtitle pipelines, directly calculate fts_now + // The standard set_fts() relies on video frame type detection which doesn't + // work for DVB-only streams. Simple calculation: (current_pts - min_pts) in ms + // MPEG_CLOCK_FREQ = 90000, so divide by 90 to get milliseconds + pipe->timing->fts_now = (pts - pipe->timing->min_pts) / 90; + + // Also update fts_max if this is the highest timestamp seen + if (pipe->timing->fts_now > pipe->timing->fts_max) + pipe->timing->fts_max = pipe->timing->fts_now; +} diff --git a/src/lib_ccx/lib_ccx.h b/src/lib_ccx/lib_ccx.h index eca4b4922..050d124f0 100644 --- a/src/lib_ccx/lib_ccx.h +++ b/src/lib_ccx/lib_ccx.h @@ -25,6 +25,12 @@ #include #endif +#ifdef _WIN32 +#include +#else +#include +#endif + // #include "ccx_decoders_708.h" /* Report information */ @@ -81,6 +87,28 @@ struct ccx_s_mp4Cfg unsigned int mp4vidtrack : 1; }; +#define MAX_SUBTITLE_PIPELINES 64 + +/** + * ccx_subtitle_pipeline - Encapsulates all components for a single subtitle output stream + */ +struct ccx_subtitle_pipeline +{ + int pid; + int stream_type; + char lang[4]; + char filename[1024]; // Using fixed size instead of PATH_MAX to avoid header issues + struct ccx_s_write *writer; + struct encoder_ctx *encoder; + struct ccx_common_timing_ctx *timing; + void *decoder; // Pointer to decoder context (e.g., ccx_decoders_dvb_context) + struct lib_cc_decode *dec_ctx; // Full decoder context for DVB state management + struct cc_subtitle sub; // Persistent cc_subtitle for DVB prev tracking +#ifdef ENABLE_OCR + void *ocr_ctx; // Per-pipeline OCR context for thread safety +#endif +}; + struct lib_ccx_ctx { // Stuff common to both loops @@ -154,8 +182,23 @@ struct lib_ccx_ctx int segment_on_key_frames_only; int segment_counter; LLONG system_start_time; + + // Registration for multi-stream subtitle extraction + struct ccx_subtitle_pipeline *pipelines[MAX_SUBTITLE_PIPELINES]; + int pipeline_count; +#ifdef _WIN32 + CRITICAL_SECTION pipeline_mutex; +#else + pthread_mutex_t pipeline_mutex; +#endif + int pipeline_mutex_initialized; + void *dec_dvb_default; // Default decoder used in non-split mode + void *shared_ocr_ctx; // Shared OCR context to reduce memory usage }; +struct ccx_subtitle_pipeline *get_or_create_pipeline(struct lib_ccx_ctx *ctx, int pid, int stream_type, const char *lang); +void set_pipeline_pts(struct ccx_subtitle_pipeline *pipe, LLONG pts); + struct lib_ccx_ctx *init_libraries(struct ccx_s_options *opt); void dinit_libraries(struct lib_ccx_ctx **ctx); diff --git a/src/lib_ccx/list.h b/src/lib_ccx/list.h index f89228061..5ba976564 100644 --- a/src/lib_ccx/list.h +++ b/src/lib_ccx/list.h @@ -18,7 +18,7 @@ */ #ifndef ccx_offsetof -#define ccx_offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) +#define ccx_offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *)0)->MEMBER) #endif /** @@ -53,7 +53,10 @@ struct list_head struct list_head *next, *prev; }; -#define LIST_HEAD_INIT(name) {&(name), &(name)} +#define LIST_HEAD_INIT(name) \ + { \ + &(name), &(name) \ + } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) @@ -371,7 +374,10 @@ struct hlist_node struct hlist_node *next, **pprev; }; -#define HLIST_HEAD_INIT {.first = NULL} +#define HLIST_HEAD_INIT \ + { \ + .first = NULL \ + } #define HLIST_HEAD(name) struct hlist_head name = {.first = NULL} #define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) #define INIT_HLIST_NODE(ptr) ((ptr)->next = NULL, (ptr)->pprev = NULL) diff --git a/src/lib_ccx/matroska.c b/src/lib_ccx/matroska.c index 9030ec522..ddcc38bfd 100644 --- a/src/lib_ccx/matroska.c +++ b/src/lib_ccx/matroska.c @@ -1,4 +1,4 @@ -#include "lib_ccx.h" +#include "lib_ccx.h" #include "utility.h" #include "matroska.h" #include "ccx_encoders_helpers.h" @@ -348,7 +348,7 @@ struct matroska_sub_sentence *parse_segment_cluster_block_group_block(struct mat There can be 2 types of DVB in .mkv. One is when each display block is followed by empty block in order to allow gaps in time between display blocks. Another one is when display block is followed by another display block. This code handles both cases but we don't save and use empty blocks as sentences, only time_starts of them. */ - char *dvb_message = enc_ctx->last_string; + char *dvb_message = enc_ctx->last_str; if (ret < 0 || dvb_message == NULL) { // No text - no sentence is returned. Free the memory diff --git a/src/lib_ccx/params.c b/src/lib_ccx/params.c index f344cf162..870f1220e 100644 --- a/src/lib_ccx/params.c +++ b/src/lib_ccx/params.c @@ -363,6 +363,12 @@ void print_usage(void) mprint(" stream will be processed. e.g. 'eng' for English.\n"); mprint(" If there are multiple languages, only this specified\n"); mprint(" language stream will be processed (default).\n"); + mprint(" --split-dvb-subs: Extract each DVB subtitle stream to a separate file.\n"); + mprint(" Each file will be named with the base filename plus a\n"); + mprint(" language suffix (e.g., output_deu.srt, output_fra.srt).\n"); + mprint(" For streams without language tags, uses PID as suffix.\n"); + mprint(" Incompatible with: stdout output, manual PID selection,\n"); + mprint(" multiprogram mode. Only works with SRT, SAMI, WebVTT.\n"); mprint(" --ocrlang: Manually select the name of the Tesseract .traineddata\n"); mprint(" file. Helpful if you want to OCR a caption stream of\n"); mprint(" one language with the data of another language.\n"); diff --git a/src/lib_ccx/teletext.h b/src/lib_ccx/teletext.h index 79dcbed5b..f3df90b51 100644 --- a/src/lib_ccx/teletext.h +++ b/src/lib_ccx/teletext.h @@ -8,7 +8,7 @@ // #include #define MAX_TLT_PAGES 1000 -#define MAX_TLT_PAGES_EXTRACT 8 // Maximum pages to extract simultaneously (must match lib_ccx.h) +#define MAX_TLT_PAGES_EXTRACT 8 // Maximum pages to extract simultaneously (must match lib_ccx.h) typedef struct { @@ -22,23 +22,23 @@ typedef struct // Per-page state for multi-page extraction (issue #665) typedef struct { - uint16_t page_number; // BCD-encoded page number (0 = unused slot) - teletext_page_t page_buffer; // Current page content being received - char *page_buffer_prev; // Previous formatted output - char *page_buffer_cur; // Current formatted output + uint16_t page_number; // BCD-encoded page number (0 = unused slot) + teletext_page_t page_buffer; // Current page content being received + char *page_buffer_prev; // Previous formatted output + char *page_buffer_cur; // Current formatted output unsigned page_buffer_cur_size; unsigned page_buffer_cur_used; unsigned page_buffer_prev_size; unsigned page_buffer_prev_used; - uint64_t *ucs2_buffer_prev; // Previous comparison string - uint64_t *ucs2_buffer_cur; // Current comparison string + uint64_t *ucs2_buffer_prev; // Previous comparison string + uint64_t *ucs2_buffer_cur; // Current comparison string unsigned ucs2_buffer_cur_size; unsigned ucs2_buffer_cur_used; unsigned ucs2_buffer_prev_size; unsigned ucs2_buffer_prev_used; uint64_t prev_hide_timestamp; uint64_t prev_show_timestamp; - uint8_t receiving_data; // Currently receiving data for this page + uint8_t receiving_data; // Currently receiving data for this page } teletext_page_state_t; // application states -- flags for notices that should be printed only once @@ -86,10 +86,10 @@ struct TeletextCtx uint32_t global_timestamp; // Multi-page extraction state (issue #665) - teletext_page_state_t page_states[MAX_TLT_PAGES_EXTRACT]; // Per-page state - int num_active_pages; // Number of pages being extracted - int current_page_idx; // Index of page currently receiving data (-1 = none) - int multi_page_mode; // 1 = multi-page mode active + teletext_page_state_t page_states[MAX_TLT_PAGES_EXTRACT]; // Per-page state + int num_active_pages; // Number of pages being extracted + int current_page_idx; // Index of page currently receiving data (-1 = none) + int multi_page_mode; // 1 = multi-page mode active // Current and previous page buffers (legacy single-page mode) // These are still used when multi_page_mode == 0 for backward compatibility diff --git a/src/lib_ccx/telxcc.c b/src/lib_ccx/telxcc.c index bcb0a051f..b68122dfb 100644 --- a/src/lib_ccx/telxcc.c +++ b/src/lib_ccx/telxcc.c @@ -445,9 +445,9 @@ void remap_g0_charset(uint8_t c) fprintf(stderr, "- G0 Latin National Subset ID 0x%1x.%1x is not implemented\n", (c >> 3), (c & 0x7)); return; } - else if (m >= 14) + else if (m >= array_length(G0_LATIN_NATIONAL_SUBSETS)) { - fprintf(stderr, "- G0 Latin National Subset index %d is out of bounds\n", m); + fprintf(stderr, "- G0 Latin National Subset index %d is out of bounds (max %zu)\n", m, array_length(G0_LATIN_NATIONAL_SUBSETS) - 1); return; } else diff --git a/src/lib_ccx/ts_functions.c b/src/lib_ccx/ts_functions.c index 2f31b0f78..6b85f9b8e 100644 --- a/src/lib_ccx/ts_functions.c +++ b/src/lib_ccx/ts_functions.c @@ -525,7 +525,7 @@ struct demuxer_data *get_best_data(struct demuxer_data *data) { if (ptr->codec == CCX_CODEC_DVB) { - ret = data; + ret = ptr; goto end; } } @@ -596,6 +596,7 @@ int copy_capbuf_demux_data(struct ccx_demuxer *ctx, struct demuxer_data **data, if (ccx_options.hauppauge_mode) { + // if (cinfo->pid == 0x104) mprint("DEBUG-HAUP: 0x104 detected\n"); if (haup_capbuflen % 12 != 0) mprint("Warning: Inconsistent Hauppage's buffer length\n"); if (!haup_capbuflen) @@ -645,11 +646,9 @@ int copy_capbuf_demux_data(struct ccx_demuxer *ctx, struct demuxer_data **data, { if (ptr->len + databuflen >= BUFSIZE) { - fatal(CCX_COMMON_EXIT_BUG_BUG, - "PES data packet (%ld) larger than remaining buffer (%lld).\n" - "Please send bug report!", - databuflen, BUFSIZE - ptr->len); - return CCX_EAGAIN; + mprint("Warning: PES data packet (%ld) larger than remaining buffer (%lld), skipping packet.\n", + databuflen, BUFSIZE - ptr->len); + return CCX_OK; } memcpy(ptr->buffer + ptr->len, databuf, databuflen); ptr->len += databuflen; @@ -677,7 +676,11 @@ int copy_payload_to_capbuf(struct cap_info *cinfo, struct ts_payload *payload) cinfo->stream != CCX_STREAM_TYPE_VIDEO_HEVC) || !ccx_options.analyze_video_stream)) { - return CCX_OK; + // In split DVB mode, allow DVB subtitle packets even if ignored + if (!(ccx_options.split_dvb_subs && cinfo->codec == CCX_CODEC_DVB)) + { + return CCX_OK; + } } // Verify PES before copy to capbuf @@ -780,6 +783,11 @@ int64_t ts_readstream(struct ccx_demuxer *ctx, struct demuxer_data **data) if (ret != CCX_OK) break; + if (payload.pid == 0x104) + { + // mprint("DEBUG-RAW: pid=0x104 err=%d len=%d\n", payload.transport_error, payload.length); + } + // Skip damaged packets, they could do more harm than good if (payload.transport_error) { @@ -957,6 +965,15 @@ int64_t ts_readstream(struct ccx_demuxer *ctx, struct demuxer_data **data) } cinfo = get_cinfo(ctx, payload.pid); + cinfo = get_cinfo(ctx, payload.pid); + if (payload.pid == 0x104 || payload.pid == 0x106) + { + // mprint("DEBUG-PID: pid=0x%X cinfo=%p len=%d\n", payload.pid, cinfo, payload.length); + if (cinfo) + { + // mprint("DEBUG-INFO: ignore=%d codec=%d pesstart=%d\n", cinfo->ignore, cinfo->codec, payload.pesstart); + } + } if (cinfo == NULL) { if (!packet_analysis_mode) @@ -972,32 +989,40 @@ int64_t ts_readstream(struct ccx_demuxer *ctx, struct demuxer_data **data) cinfo->stream != CCX_STREAM_TYPE_VIDEO_HEVC) || !ccx_options.analyze_video_stream)) { - if (cinfo->codec_private_data) + // In split DVB mode, do NOT skip/cleanup DVB streams + if (ccx_options.split_dvb_subs && cinfo->codec == CCX_CODEC_DVB) { - switch (cinfo->codec) + // Fall through - process this DVB packet + } + else + { + if (cinfo->codec_private_data) { - case CCX_CODEC_TELETEXT: - telxcc_close(&cinfo->codec_private_data, NULL); - break; - case CCX_CODEC_DVB: - dvbsub_close_decoder(&cinfo->codec_private_data); - break; - case CCX_CODEC_ISDB_CC: - delete_isdb_decoder(&cinfo->codec_private_data); - default: - break; + switch (cinfo->codec) + { + case CCX_CODEC_TELETEXT: + telxcc_close(&cinfo->codec_private_data, NULL); + break; + case CCX_CODEC_DVB: + dvbsub_close_decoder(&cinfo->codec_private_data); + break; + case CCX_CODEC_ISDB_CC: + delete_isdb_decoder(&cinfo->codec_private_data); + default: + break; + } + cinfo->codec_private_data = NULL; } - cinfo->codec_private_data = NULL; - } - if (cinfo->capbuflen > 0) - { - freep(&cinfo->capbuf); - cinfo->capbufsize = 0; - cinfo->capbuflen = 0; - delete_demuxer_data_node_by_pid(data, cinfo->pid); + if (cinfo->capbuflen > 0) + { + freep(&cinfo->capbuf); + cinfo->capbufsize = 0; + cinfo->capbuflen = 0; + delete_demuxer_data_node_by_pid(data, cinfo->pid); + } + continue; } - continue; } // Video PES start @@ -1008,8 +1033,14 @@ int64_t ts_readstream(struct ccx_demuxer *ctx, struct demuxer_data **data) } // Discard packets when no pesstart was found. + // Exception: DVB in split mode - allow packets to accumulate if (!cinfo->saw_pesstart) - continue; + { + if (!(ccx_options.split_dvb_subs && cinfo->codec == CCX_CODEC_DVB)) + { + continue; + } + } if ((cinfo->prev_counter == 15 ? 0 : cinfo->prev_counter + 1) != payload.counter) { diff --git a/src/lib_ccx/ts_info.c b/src/lib_ccx/ts_info.c index 9abbfcd18..d90f3d0ec 100644 --- a/src/lib_ccx/ts_info.c +++ b/src/lib_ccx/ts_info.c @@ -43,7 +43,18 @@ void ignore_other_stream(struct ccx_demuxer *ctx, int pid) list_for_each_entry(iter, &ctx->cinfo_tree.all_stream, all_stream, struct cap_info) { if (iter->pid != pid) - iter->ignore = 1; + { + // In split DVB mode, do NOT ignore DVB subtitle streams + // They need to remain in the datalist for secondary pass processing + if (ccx_options.split_dvb_subs && iter->codec == CCX_CODEC_DVB) + { + iter->ignore = 0; // Keep DVB streams active + } + else + { + iter->ignore = 1; + } + } } } @@ -297,6 +308,20 @@ void dinit_cap(struct ccx_demuxer *ctx) if (dec_ctx->private_data == saved_private_data) dec_ctx->private_data = NULL; } + + // Also check subtitle pipelines for shared decoder references + // Pipelines have their own decoder and dec_ctx->private_data + for (int i = 0; i < lctx->pipeline_count; i++) + { + struct ccx_subtitle_pipeline *p = lctx->pipelines[i]; + if (p) + { + if (p->decoder == saved_private_data) + p->decoder = NULL; + if (p->dec_ctx && p->dec_ctx->private_data == saved_private_data) + p->dec_ctx->private_data = NULL; + } + } } } free(iter); diff --git a/src/lib_ccx/ts_tables.c b/src/lib_ccx/ts_tables.c index 89611907c..f31c96a5a 100644 --- a/src/lib_ccx/ts_tables.c +++ b/src/lib_ccx/ts_tables.c @@ -281,7 +281,7 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr if (i + 5 + ES_info_length > len) { dbg_print(CCX_DMT_GENERIC_NOTICES, "Warning: ES_info_length exceeds buffer, skipping.\n"); - break; + continue; } unsigned char *es_info = buf + i + 5; @@ -346,7 +346,7 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr if (i + 5 + ES_info_length > len) { dbg_print(CCX_DMT_GENERIC_NOTICES, "Warning: ES_info_length exceeds buffer, skipping.\n"); - break; + continue; } unsigned char *es_info = buf + i + 5; @@ -385,6 +385,60 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr if (CCX_MPEG_DSC_DVB_SUBTITLE == descriptor_tag) { struct dvb_config cnf; + char detected_lang[4] = "und"; + + if (desc_len >= 3) + { + // ISO 639-2 compliant: accept only [a-zA-Z]{3}, convert to lowercase + int is_valid_lang = 1; + for (int li = 0; li < 3; li++) + { + char c = (char)es_info[li]; + if (c >= 'A' && c <= 'Z') + detected_lang[li] = c + 32; // to lowercase + else if (c >= 'a' && c <= 'z') + detected_lang[li] = c; + else + { + is_valid_lang = 0; + break; + } + } + detected_lang[3] = '\0'; + + if (!is_valid_lang) + { + strcpy(detected_lang, "und"); + dbg_print(CCX_DMT_PMT, "Warning: Invalid language code, using 'und'\n"); + } + } + + // If split mode enabled, track for pipeline creation + if (ccx_options.split_dvb_subs && ctx->potential_stream_count < MAX_POTENTIAL_STREAMS) + { + int found = 0; + for (int k = 0; k < ctx->potential_stream_count; k++) + { + if (ctx->potential_streams[k].pid == (int)elementary_PID) + { + found = 1; + break; + } + } + + if (!found) + { + int p_idx = ctx->potential_stream_count; + ctx->potential_streams[p_idx].pid = (int)elementary_PID; + ctx->potential_streams[p_idx].stream_type = CCX_STREAM_TYPE_DVB_SUB; + ctx->potential_streams[p_idx].mpeg_type = stream_type; + memcpy(ctx->potential_streams[p_idx].lang, detected_lang, 4); + // Issue 2: Race Condition fix - populate metadata BEFORE incrementing count + // We can't fully populate yet (composition_id is parsed below), so we defer increment + // OR we just use the index p_idx and increment later. + } + } + #ifndef ENABLE_OCR if (ccx_options.write_format != CCX_OF_SPUPNG) { @@ -392,17 +446,67 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr continue; } #endif - if (!IS_FEASIBLE(ctx->codec, ctx->nocodec, CCX_CODEC_DVB)) + if (!IS_FEASIBLE(ctx->codec, ctx->nocodec, CCX_CODEC_DVB) && + !(ccx_options.split_dvb_subs && ctx->codec != CCX_CODEC_DVB)) continue; memset((void *)&cnf, 0, sizeof(struct dvb_config)); ret = parse_dvb_description(&cnf, es_info, desc_len); if (ret < 0) break; + + // Update metadata with specific IDs + if (ccx_options.split_dvb_subs) + { + int k_idx = -1; + int found = 0; + // Find if we already added it (or find the spot we are about to add) + for (int k = 0; k < ctx->potential_stream_count; k++) + { + if (ctx->potential_streams[k].pid == (int)elementary_PID) + { + k_idx = k; + found = 1; + break; + } + } + + if (!found && ctx->potential_stream_count < MAX_POTENTIAL_STREAMS) + { + // It's the new one we are building + k_idx = ctx->potential_stream_count; + ctx->potential_streams[k_idx].pid = (int)elementary_PID; + ctx->potential_streams[k_idx].stream_type = CCX_STREAM_TYPE_DVB_SUB; + ctx->potential_streams[k_idx].mpeg_type = stream_type; + memcpy(ctx->potential_streams[k_idx].lang, detected_lang, 4); + } + + if (k_idx != -1) + { + ctx->potential_streams[k_idx].composition_id = cnf.composition_id[0]; + ctx->potential_streams[k_idx].ancillary_id = cnf.ancillary_id[0]; + dbg_print(CCX_DMT_GENERIC_NOTICES, "Discovered DVB stream PID 0x%X lang=%s composition_id=%d ancillary_id=%d\n", + elementary_PID, detected_lang, cnf.composition_id[0], cnf.ancillary_id[0]); + + // Only increment if it was a new one + if (!found && ctx->potential_stream_count < MAX_POTENTIAL_STREAMS) + { + ctx->potential_stream_count++; + } + } + } ptr = dvbsub_init_decoder(&cnf); if (ptr == NULL) break; update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_DVB, program_number, ptr); + + // Populate cap_info.lang with discovered language for fallback lookup + struct cap_info *cinfo = get_cinfo(ctx, elementary_PID); + if (cinfo && detected_lang[0] && detected_lang[0] != 'u') // Skip "und" + { + memset(cinfo->lang, 0, sizeof(cinfo->lang)); + strncpy(cinfo->lang, detected_lang, sizeof(cinfo->lang) - 1); + } max_dif = 30; } } diff --git a/src/lib_ccx/zvbi/misc.h b/src/lib_ccx/zvbi/misc.h index 1ccb55a5b..d34351088 100644 --- a/src/lib_ccx/zvbi/misc.h +++ b/src/lib_ccx/zvbi/misc.h @@ -1,7 +1,7 @@ /* * libzvbi -- Miscellaneous cows and chickens * - * Copyright (C) 2000-2003 Iñaki García Etxebarria + * Copyright (C) 2000-2003 Iñaki García Etxebarria * Copyright (C) 2002-2007 Michael H. Schimek * * This library is free software; you can redistribute it and/or diff --git a/src/rust/lib_ccxr/src/common/options.rs b/src/rust/lib_ccxr/src/common/options.rs index 77f909b3e..4a45b72de 100644 --- a/src/rust/lib_ccxr/src/common/options.rs +++ b/src/rust/lib_ccxr/src/common/options.rs @@ -521,6 +521,8 @@ pub struct Options { pub multiprogram: bool, pub out_interval: i32, pub segment_on_key_frames_only: bool, + /// Enable per-stream DVB subtitle extraction + pub split_dvb_subs: bool, /// SCC input framerate: 0=29.97 (default), 1=24, 2=25, 3=30 pub scc_framerate: i32, /// SCC accurate timing (issue #1120): if true, use bandwidth-aware timing for broadcast compliance @@ -628,6 +630,7 @@ impl Default for Options { multiprogram: Default::default(), out_interval: -1, segment_on_key_frames_only: Default::default(), + split_dvb_subs: Default::default(), scc_framerate: 0, // 0 = 29.97fps (default) scc_accurate_timing: false, // Off by default for backwards compatibility (issue #1120) debug_mask: DebugMessageMask::new( diff --git a/src/rust/src/args.rs b/src/rust/src/args.rs index 10844d93a..d73bb4cd8 100644 --- a/src/rust/src/args.rs +++ b/src/rust/src/args.rs @@ -591,6 +591,14 @@ pub struct Args { /// language stream will be processed (default). #[arg(long, verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_OUTPUT_FILES)] pub dvblang: Option, + /// Extract each DVB subtitle stream to a separate file. + /// Each file will be named with the base filename plus a + /// language suffix (e.g., output_deu.srt, output_fra.srt). + /// For streams without language tags, uses PID as suffix. + /// Incompatible with: stdout output, manual PID selection, + /// multiprogram mode. Only works with SRT, SAMI, WebVTT. + #[arg(long, verbatim_doc_comment, help_heading=OUTPUT_AFFECTING_OUTPUT_FILES)] + pub split_dvb_subs: bool, /// Manually select the name of the Tesseract .traineddata /// file. Helpful if you want to OCR a caption stream of /// one language with the data of another language. diff --git a/src/rust/src/common.rs b/src/rust/src/common.rs index 342fa23ce..00b684f29 100755 --- a/src/rust/src/common.rs +++ b/src/rust/src/common.rs @@ -277,6 +277,7 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options (*ccx_s_options).multiprogram = options.multiprogram as _; (*ccx_s_options).out_interval = options.out_interval; (*ccx_s_options).segment_on_key_frames_only = options.segment_on_key_frames_only as _; + (*ccx_s_options).split_dvb_subs = options.split_dvb_subs as _; (*ccx_s_options).scc_framerate = options.scc_framerate; // Also copy to enc_cfg so the encoder uses the same frame rate for SCC output (*ccx_s_options).enc_cfg.scc_framerate = options.scc_framerate; @@ -539,6 +540,7 @@ pub unsafe fn copy_to_rust(ccx_s_options: *const ccx_s_options) -> Options { options.multiprogram = (*ccx_s_options).multiprogram != 0; options.out_interval = (*ccx_s_options).out_interval; options.segment_on_key_frames_only = (*ccx_s_options).segment_on_key_frames_only != 0; + options.split_dvb_subs = (*ccx_s_options).split_dvb_subs != 0; options.scc_framerate = (*ccx_s_options).scc_framerate; options.scc_accurate_timing = (*ccx_s_options).enc_cfg.scc_accurate_timing != 0; @@ -1025,6 +1027,7 @@ impl CType for CapInfo { prev_counter: self.prev_counter, codec_private_data: self.codec_private_data, ignore: self.ignore, + lang: [0; 4], all_stream: self.all_stream, sib_head: self.sib_head, sib_stream: self.sib_stream, diff --git a/src/rust/src/libccxr_exports/time.rs b/src/rust/src/libccxr_exports/time.rs index 82df6838c..4345c75e6 100644 --- a/src/rust/src/libccxr_exports/time.rs +++ b/src/rust/src/libccxr_exports/time.rs @@ -477,7 +477,11 @@ pub unsafe extern "C" fn ccxr_get_fts( 1 => CaptionField::Field1, 2 => CaptionField::Field2, 3 => CaptionField::Cea708, - _ => panic!("incorrect value for caption field"), + _ => { + // DVB subtitles may pass 0 or other values when decoder context + // current_field is uninitialized. Default to Field1 to avoid crash. + CaptionField::Field1 + } }; let ans = c::get_fts(&mut context, caption_field); diff --git a/src/rust/src/parser.rs b/src/rust/src/parser.rs index 8fa6b33ed..0c7fc15d6 100644 --- a/src/rust/src/parser.rs +++ b/src/rust/src/parser.rs @@ -757,6 +757,11 @@ impl OptionsExt for Options { self.ocrlang = Some(ocrlang.clone()); } + // Handle --split-dvb-subs flag + if args.split_dvb_subs { + self.split_dvb_subs = true; + } + if let Some(ref quant) = args.quant { if !(0..=2).contains(quant) { fatal!( @@ -1698,6 +1703,51 @@ impl OptionsExt for Options { { self.enc_cfg.curlposturl = self.curlposturl.clone(); } + + // Validate --split-dvb-subs conflicts + if self.split_dvb_subs { + if self.cc_to_stdout { + fatal!( + cause = ExitCause::IncompatibleParameters; + "--split-dvb-subs cannot be used with stdout output.\n\ + Multiple output files cannot be written to stdout." + ); + } + + if self.demux_cfg.ts_forced_cappid { + fatal!( + cause = ExitCause::IncompatibleParameters; + "--split-dvb-subs cannot be used with manual PID selection (-pn).\n\ + Automatic stream detection is required for multi-stream extraction." + ); + } + + if self.multiprogram { + fatal!( + cause = ExitCause::IncompatibleParameters; + "--split-dvb-subs cannot be used with -multiprogram.\n\ + These modes have conflicting output file naming schemes." + ); + } + + // Validate supported output formats + match self.write_format { + OutputFormat::Srt + | OutputFormat::Sami + | OutputFormat::WebVtt + | OutputFormat::Null + | OutputFormat::Transcript + | OutputFormat::Ssa + | OutputFormat::SimpleXml + | OutputFormat::SmpteTt => {} + _ => { + fatal!( + cause = ExitCause::IncompatibleParameters; + "Unsupported OutputFormat: {:?}. --split-dvb-subs requires SRT, SAMI, WebVTT, Transcript, SSA, SimpleXML, SMPTE-TT or NULL output format.", self.write_format + ); + } + } + } } } #[cfg(test)] diff --git a/tests/regression/dvb_split.txt b/tests/regression/dvb_split.txt new file mode 100644 index 000000000..e1caee180 --- /dev/null +++ b/tests/regression/dvb_split.txt @@ -0,0 +1,6 @@ +TEST: split_dvb_multilang +INPUT: arte_multiaudio.ts +ARGS: --split-dvb-subs -o output_split +EXPECT: output_split_fra.srt exists + +EXPECT: output_split_deu.srt exists