From 0260612eea3a24109c9b050dbf8e2c7683685777 Mon Sep 17 00:00:00 2001 From: ujjwalr27 Date: Mon, 30 Mar 2026 18:04:11 +0900 Subject: [PATCH 1/6] feat: auto-extract multi-language DVB subtitles into per-language files --- src/lib_ccx/ccx_demuxer.c | 4 +- src/lib_ccx/ccx_demuxer.h | 4 +- src/lib_ccx/ccx_encoders_common.c | 12 ++++- src/lib_ccx/ccx_encoders_common.h | 1 + src/lib_ccx/general_loop.c | 56 ++++++++++++++++++++ src/lib_ccx/lib_ccx.c | 78 ++++++++++++++++++++++++++-- src/lib_ccx/ts_functions.c | 4 +- src/lib_ccx/ts_info.c | 8 +-- src/lib_ccx/ts_tables.c | 22 +++++--- src/rust/src/common.rs | 1 + src/rust/src/ctorust.rs | 1 + src/rust/src/demuxer/common_types.rs | 2 + 12 files changed, 172 insertions(+), 21 deletions(-) diff --git a/src/lib_ccx/ccx_demuxer.c b/src/lib_ccx/ccx_demuxer.c index ec801f197..71881ff9f 100644 --- a/src/lib_ccx/ccx_demuxer.c +++ b/src/lib_ccx/ccx_demuxer.c @@ -402,9 +402,9 @@ struct ccx_demuxer *init_demuxer(void *parent, struct demuxer_cfg *cfg) for (i = 0; i < cfg->nb_ts_cappid; i++) { if (ctx->codec == CCX_CODEC_ANY) - update_capinfo(ctx, cfg->ts_cappids[i], cfg->ts_datastreamtype, CCX_CODEC_NONE, 0, NULL); + update_capinfo(ctx, cfg->ts_cappids[i], cfg->ts_datastreamtype, CCX_CODEC_NONE, 0, NULL, NULL); else - update_capinfo(ctx, cfg->ts_cappids[i], cfg->ts_datastreamtype, ctx->codec, 0, NULL); + update_capinfo(ctx, cfg->ts_cappids[i], cfg->ts_datastreamtype, ctx->codec, 0, NULL, NULL); } ctx->flag_ts_forced_cappid = cfg->nb_ts_cappid ? CCX_TRUE : CCX_FALSE; diff --git a/src/lib_ccx/ccx_demuxer.h b/src/lib_ccx/ccx_demuxer.h index 16cd6c10c..919eadc27 100644 --- a/src/lib_ccx/ccx_demuxer.h +++ b/src/lib_ccx/ccx_demuxer.h @@ -63,6 +63,7 @@ struct cap_info int prev_counter; void *codec_private_data; int ignore; + char lang[4]; /* ISO-639 language code (DVB subtitle) */ /** List joining all stream in TS @@ -185,7 +186,7 @@ struct ccx_demuxer *init_demuxer(void *parent, struct demuxer_cfg *cfg); void ccx_demuxer_delete(struct ccx_demuxer **ctx); struct demuxer_data *alloc_demuxer_data(void); void delete_demuxer_data(struct demuxer_data *data); -int update_capinfo(struct ccx_demuxer *ctx, int pid, enum ccx_stream_type stream, enum ccx_code_type codec, int pn, void *private_data); +int update_capinfo(struct ccx_demuxer *ctx, int pid, enum ccx_stream_type stream, enum ccx_code_type codec, int pn, void *private_data, const char *lang); struct cap_info *get_cinfo(struct ccx_demuxer *ctx, int pid); int need_cap_info(struct ccx_demuxer *ctx, int program_number); int need_cap_info_for_pid(struct ccx_demuxer *ctx, int pid); @@ -194,6 +195,7 @@ struct demuxer_data *get_data_stream(struct demuxer_data *data, int pid); int get_best_stream(struct ccx_demuxer *ctx); void ignore_other_stream(struct ccx_demuxer *ctx, int pid); void dinit_cap(struct ccx_demuxer *ctx); +int copy_capbuf_demux_data(struct ccx_demuxer *ctx, struct demuxer_data **data, struct cap_info *cinfo); int get_programme_number(struct ccx_demuxer *ctx, int pid); struct cap_info *get_best_sib_stream(struct cap_info *program); void ignore_other_sib_stream(struct cap_info *head, int pid); diff --git a/src/lib_ccx/ccx_encoders_common.c b/src/lib_ccx/ccx_encoders_common.c index 43c310891..749d2e75f 100644 --- a/src/lib_ccx/ccx_encoders_common.c +++ b/src/lib_ccx/ccx_encoders_common.c @@ -813,7 +813,17 @@ struct encoder_ctx *init_encoder(struct encoder_cfg *opt) ctx->is_mkv = 0; ctx->last_string = NULL; - ctx->transcript_settings = &opt->transcript_settings; + /* Deep-copy transcript_settings so the encoder owns it independently of + the caller's encoder_cfg (which may be a stack-local variable). */ + ctx->transcript_settings = malloc(sizeof(struct ccx_encoders_transcript_format)); + if (!ctx->transcript_settings) + { + freep(&ctx->out); + freep(&ctx->buffer); + free(ctx); + return NULL; + } + memcpy(ctx->transcript_settings, &opt->transcript_settings, sizeof(struct ccx_encoders_transcript_format)); 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 2165d23ee..d2b87cee6 100644 --- a/src/lib_ccx/ccx_encoders_common.h +++ b/src/lib_ccx/ccx_encoders_common.h @@ -166,6 +166,7 @@ struct encoder_ctx int new_sentence; // Capitalize next letter? int program_number; + char dvb_lang[4]; /* ISO-639 language code for DVB subtitle encoder */ struct list_head list; /* split-by-sentence stuff */ diff --git a/src/lib_ccx/general_loop.c b/src/lib_ccx/general_loop.c index bd98bde09..2a9400540 100644 --- a/src/lib_ccx/general_loop.c +++ b/src/lib_ccx/general_loop.c @@ -1317,6 +1317,62 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx, } } } + + /* ----------------------------------------------------------------------- + * Process additional DVB subtitle PIDs (extra languages). + * ----------------------------------------------------------------------- */ + { + struct cap_info *dvb_iter; + list_for_each_entry(dvb_iter, &ctx->demux_ctx->cinfo_tree.all_stream, all_stream, struct cap_info) + { + if (dvb_iter->codec != CCX_CODEC_DVB) + continue; + if (dvb_iter->pid == pid) + continue; + + struct demuxer_data *dvb_data = get_data_stream(*datalist, dvb_iter->pid); + if (!dvb_data || dvb_data->len == 0) + continue; + + struct encoder_ctx *dvb_enc = update_encoder_list_cinfo(ctx, dvb_iter); + struct lib_cc_decode *dvb_dec = update_decoder_list_cinfo(ctx, dvb_iter); + + if (!dvb_enc || !dvb_dec || !dvb_dec->timing) + continue; + + if (dvb_data->pts != CCX_NOPTS) + { + struct ccx_rational tb = {1, MPEG_CLOCK_FREQ}; + LLONG pts = (dvb_data->tb.num != 1 || dvb_data->tb.den != MPEG_CLOCK_FREQ) + ? change_timebase(dvb_data->pts, dvb_data->tb, tb) + : dvb_data->pts; + set_current_pts(dvb_dec->timing, pts); + if (dvb_dec->timing->min_pts == 0x01FFFFFFFFLL) + { + dvb_dec->timing->min_pts = pts; + dvb_dec->timing->pts_set = 2; + dvb_dec->timing->sync_pts = pts; + } + set_fts(dvb_dec->timing); + } + dvb_enc->timing = dvb_dec->timing; + + int dvb_ret = process_data(dvb_enc, dvb_dec, dvb_data); + if (dvb_ret || dvb_enc->srt_counter) + *caps = 1; + + if (!(!terminate_asap && !end_of_file && is_decoder_processed_enough(ctx) == CCX_FALSE)) + { + if (dvb_dec->dec_sub.prev && dvb_dec->dec_sub.prev->end_time == 0) + { + dvb_dec->dec_sub.prev->end_time = get_fts(dvb_dec->timing, dvb_dec->current_field); + if (dvb_enc != NULL && dvb_enc->prev != NULL) + encode_sub(dvb_enc->prev, dvb_dec->dec_sub.prev); + dvb_dec->dec_sub.prev->got_output = 0; + } + } + } + } return ret; } diff --git a/src/lib_ccx/lib_ccx.c b/src/lib_ccx/lib_ccx.c index 3f1e4e37d..cc23d3efb 100644 --- a/src/lib_ccx/lib_ccx.c +++ b/src/lib_ccx/lib_ccx.c @@ -222,10 +222,8 @@ void dinit_libraries(struct lib_ccx_ctx **ctx) struct encoder_ctx *enc_ctx; struct lib_cc_decode *dec_ctx; struct lib_cc_decode *dec_ctx1; - int i; list_for_each_entry_safe(dec_ctx, dec_ctx1, &lctx->dec_ctx_head, list, struct lib_cc_decode) { - LLONG cfts; void *saved_private_data = dec_ctx->private_data; // Save before close NULLs it if (dec_ctx->codec == CCX_CODEC_DVB) dvbsub_close_decoder(&dec_ctx->private_data); @@ -248,7 +246,6 @@ void dinit_libraries(struct lib_ccx_ctx **ctx) } flush_cc_decode(dec_ctx, &dec_ctx->dec_sub); - cfts = get_fts(dec_ctx->timing, dec_ctx->current_field); enc_ctx = get_encoder_by_pn(lctx, dec_ctx->program_number); if (enc_ctx && dec_ctx->dec_sub.got_output == CCX_TRUE) { @@ -257,8 +254,16 @@ void dinit_libraries(struct lib_ccx_ctx **ctx) } list_del(&dec_ctx->list); dinit_cc_decode(&dec_ctx); - if (enc_ctx) + } + /* Clean up all encoders separately. + With multi-DVB, multiple decoders share the same program_number + but have different language-specific encoders. Cleaning encoders + inside the decoder loop caused wrong encoder pairing and double-free. */ + { + struct encoder_ctx *enc_tmp; + list_for_each_entry_safe(enc_ctx, enc_tmp, &lctx->enc_ctx_head, list, struct encoder_ctx) { + LLONG cfts = 0; // Use 0 as fallback since decoders are already freed list_del(&enc_ctx->list); dinit_encoder(&enc_ctx, cfts); } @@ -337,6 +342,7 @@ struct lib_cc_decode *update_decoder_list(struct lib_ccx_ctx *ctx) dec_ctx->dec_sub.prev = malloc(sizeof(struct cc_subtitle)); if (!dec_ctx->prev || !dec_ctx->dec_sub.prev) ccx_common_logging.fatal_ftn(EXIT_NOT_ENOUGH_MEMORY, "update_decoder_list: Not enough memory for DVB context"); + memset(dec_ctx->prev, 0, sizeof(struct lib_cc_decode)); memset(dec_ctx->dec_sub.prev, 0, sizeof(struct cc_subtitle)); } } @@ -351,6 +357,14 @@ struct lib_cc_decode *update_decoder_list_cinfo(struct lib_ccx_ctx *ctx, struct { if (!cinfo || ctx->multiprogram == CCX_FALSE) { + /* For DVB subtitles, match by private_data (per-PID decoder) */ + if (cinfo && cinfo->codec == CCX_CODEC_DVB) + { + if (dec_ctx->program_number == cinfo->program_number && + dec_ctx->private_data == cinfo->codec_private_data) + return dec_ctx; + continue; + } /* Update private_data from cinfo if available. This is needed after PAT changes when dinit_cap() freed the old context and a new cap_info was created with a new codec_private_data. */ @@ -375,7 +389,16 @@ struct lib_cc_decode *update_decoder_list_cinfo(struct lib_ccx_ctx *ctx, struct } if (ctx->multiprogram == CCX_FALSE) { - if (list_empty(&ctx->dec_ctx_head)) + /* For DVB, always create a new per-PID decoder even in single-program mode, + because each DVB subtitle PID has its own codec_private_data context. */ + if (cinfo && cinfo->codec == CCX_CODEC_DVB) + { + dec_ctx = init_cc_decode(ctx->dec_global_setting); + if (!dec_ctx) + fatal(EXIT_NOT_ENOUGH_MEMORY, "In update_decoder_list_cinfo: Not enough memory allocating dec_ctx for DVB\n"); + list_add_tail(&(dec_ctx->list), &(ctx->dec_ctx_head)); + } + else if (list_empty(&ctx->dec_ctx_head)) { dec_ctx = init_cc_decode(ctx->dec_global_setting); if (!dec_ctx) @@ -394,6 +417,15 @@ struct lib_cc_decode *update_decoder_list_cinfo(struct lib_ccx_ctx *ctx, struct // DVB related dec_ctx->prev = NULL; dec_ctx->dec_sub.prev = NULL; + if (cinfo && cinfo->codec == CCX_CODEC_DVB) + { + dec_ctx->prev = malloc(sizeof(struct lib_cc_decode)); + dec_ctx->dec_sub.prev = malloc(sizeof(struct cc_subtitle)); + if (!dec_ctx->prev || !dec_ctx->dec_sub.prev) + ccx_common_logging.fatal_ftn(EXIT_NOT_ENOUGH_MEMORY, "update_decoder_list_cinfo: Not enough memory for DVB context"); + memset(dec_ctx->prev, 0, sizeof(struct lib_cc_decode)); + memset(dec_ctx->dec_sub.prev, 0, sizeof(struct cc_subtitle)); + } return dec_ctx; } @@ -420,7 +452,17 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca list_for_each_entry(enc_ctx, &ctx->enc_ctx_head, list, struct encoder_ctx) { if (ctx->multiprogram == CCX_FALSE) + { + /* For DVB subtitles with a language tag, match by program_number + language */ + if (cinfo && cinfo->codec == CCX_CODEC_DVB && cinfo->lang[0]) + { + if (enc_ctx->program_number == pn && + strcmp(enc_ctx->dvb_lang, cinfo->lang) == 0) + return enc_ctx; + continue; + } return enc_ctx; + } if (enc_ctx->program_number == pn) return enc_ctx; @@ -430,6 +472,32 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca if (!extension && ccx_options.enc_cfg.write_format != CCX_OF_CURL) return NULL; + /* Create per-language DVB encoder if needed */ + if (cinfo && cinfo->codec == CCX_CODEC_DVB && cinfo->lang[0]) + { + struct encoder_cfg local_cfg = ccx_options.enc_cfg; + local_cfg.program_number = pn; + local_cfg.in_format = in_format; + char *basefilename = get_basename(ctx->basefilename); + char suffix[8]; + snprintf(suffix, sizeof(suffix), "_%s", cinfo->lang); + local_cfg.output_filename = create_outfilename(basefilename, suffix, extension); + free(basefilename); + + enc_ctx = init_encoder(&local_cfg); + if (enc_ctx) + { + enc_ctx->program_number = pn; + strncpy(enc_ctx->dvb_lang, cinfo->lang, 3); + enc_ctx->dvb_lang[3] = '\0'; + list_add_tail(&enc_ctx->list, &ctx->enc_ctx_head); + enc_ctx->prev = NULL; + enc_ctx->write_previous = 0; + } + free(local_cfg.output_filename); + return enc_ctx; + } + if (ctx->multiprogram == CCX_FALSE) { if (ctx->out_interval != -1) diff --git a/src/lib_ccx/ts_functions.c b/src/lib_ccx/ts_functions.c index 8c8503f51..7a0ac0109 100644 --- a/src/lib_ccx/ts_functions.c +++ b/src/lib_ccx/ts_functions.c @@ -412,7 +412,7 @@ void look_for_caption_data(struct ccx_demuxer *ctx, struct ts_payload *payload) stream_type == CCX_STREAM_TYPE_VIDEO_H264 ? "H.264" : (stream_type == CCX_STREAM_TYPE_VIDEO_HEVC ? "HEVC" : "MPEG-2")); // Register this PID as a video stream that may contain captions - update_capinfo(ctx, payload->pid, stream_type, CCX_CODEC_ATSC_CC, 0, NULL); + update_capinfo(ctx, payload->pid, stream_type, CCX_CODEC_ATSC_CC, 0, NULL, NULL); ctx->PIDs_seen[payload->pid] = 3; return; } @@ -434,7 +434,7 @@ void look_for_caption_data(struct ccx_demuxer *ctx, struct ts_payload *payload) if (cinfo == NULL) { mprint("Registering PID %u as MPEG-2 video with captions (no PAT/PMT detected).\n", payload->pid); - update_capinfo(ctx, payload->pid, CCX_STREAM_TYPE_VIDEO_MPEG2, CCX_CODEC_ATSC_CC, 0, NULL); + update_capinfo(ctx, payload->pid, CCX_STREAM_TYPE_VIDEO_MPEG2, CCX_CODEC_ATSC_CC, 0, NULL, NULL); } ctx->PIDs_seen[payload->pid] = 3; diff --git a/src/lib_ccx/ts_info.c b/src/lib_ccx/ts_info.c index 9abbfcd18..f66524cee 100644 --- a/src/lib_ccx/ts_info.c +++ b/src/lib_ccx/ts_info.c @@ -42,7 +42,7 @@ void ignore_other_stream(struct ccx_demuxer *ctx, int pid) struct cap_info *iter; list_for_each_entry(iter, &ctx->cinfo_tree.all_stream, all_stream, struct cap_info) { - if (iter->pid != pid) + if (iter->pid != pid && iter->codec != CCX_CODEC_DVB) iter->ignore = 1; } } @@ -92,7 +92,7 @@ void ignore_other_sib_stream(struct cap_info *head, int pid) struct cap_info *iter; list_for_each_entry(iter, &head->sib_head, sib_stream, struct cap_info) { - if (iter->pid != pid) + if (iter->pid != pid && iter->codec != CCX_CODEC_DVB) iter->ignore = 1; } } @@ -178,7 +178,7 @@ static void *init_private_data(enum ccx_code_type codec) return NULL; } } -int update_capinfo(struct ccx_demuxer *ctx, int pid, enum ccx_stream_type stream, enum ccx_code_type codec, int pn, void *private_data) +int update_capinfo(struct ccx_demuxer *ctx, int pid, enum ccx_stream_type stream, enum ccx_code_type codec, int pn, void *private_data, const char *lang) { struct cap_info *ptr; struct cap_info *tmp; @@ -220,6 +220,7 @@ int update_capinfo(struct ccx_demuxer *ctx, int pid, enum ccx_stream_type stream tmp->capbufsize = 0; tmp->ignore = 0; } + if (lang) { strncpy(tmp->lang, lang, 3); tmp->lang[3] = '\0'; } else { tmp->lang[0] = '\0'; } return CCX_OK; } } @@ -247,6 +248,7 @@ int update_capinfo(struct ccx_demuxer *ctx, int pid, enum ccx_stream_type stream tmp->codec_private_data = init_private_data(codec); else tmp->codec_private_data = private_data; + if (lang) { strncpy(tmp->lang, lang, 3); tmp->lang[3] = '\0'; } else { tmp->lang[0] = '\0'; } list_add_tail(&(tmp->all_stream), &(ptr->all_stream)); diff --git a/src/lib_ccx/ts_tables.c b/src/lib_ccx/ts_tables.c index 7ff506cee..15e414350 100644 --- a/src/lib_ccx/ts_tables.c +++ b/src/lib_ccx/ts_tables.c @@ -380,7 +380,7 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr ptr = init_isdb_decoder(); if (ptr == NULL) break; - update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_ISDB_CC, program_number, ptr); + update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_ISDB_CC, program_number, ptr, NULL); } if (CCX_MPEG_DSC_DVB_SUBTITLE == descriptor_tag) { @@ -402,7 +402,15 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr ptr = dvbsub_init_decoder(&cnf); if (ptr == NULL) break; - update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_DVB, program_number, ptr); + /* Extract 3-byte ISO-639 language code from DVB subtitle descriptor */ + char dvb_lang[4] = {0}; + if (desc_len >= 3) { + dvb_lang[0] = (char)cctolower(es_info[0]); + dvb_lang[1] = (char)cctolower(es_info[1]); + dvb_lang[2] = (char)cctolower(es_info[2]); + } + update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_DVB, program_number, ptr, dvb_lang); + mprint("DVB subtitle PID %u language: %s\n", elementary_PID, dvb_lang[0] ? dvb_lang : "(unknown)"); max_dif = 30; } } @@ -440,7 +448,7 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr dbg_print(CCX_DMT_PMT, "%s", is_608 ? " CEA-608" : " CEA-708"); } } - update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_ATSC_CC, program_number, NULL); + update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_ATSC_CC, program_number, NULL, NULL); } } @@ -462,7 +470,7 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr desc_len = (*es_info++); if (!IS_VALID_TELETEXT_DESC(descriptor_tag)) continue; - update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_TELETEXT, program_number, NULL); + update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_TELETEXT, program_number, NULL, NULL); mprint("VBI/teletext stream ID %u (0x%x) for SID %u (0x%x)\n", elementary_PID, elementary_PID, program_number, program_number); } @@ -474,7 +482,7 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr unsigned descriptor_tag = buf[i + 5]; if (descriptor_tag == 0x45) { - update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_ATSC_CC, program_number, NULL); + update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_ATSC_CC, program_number, NULL, NULL); // mprint ("VBI stream ID %u (0x%x) for SID %u (0x%x) - teletext is disabled, will be processed as closed captions.\n", // elementary_PID, elementary_PID, program_number, program_number); } @@ -485,7 +493,7 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr { if (stream_type == CCX_STREAM_TYPE_VIDEO_HEVC) mprint("Detected HEVC video stream (0x24) - enabling ATSC CC parsing.\n"); - update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_ATSC_CC, program_number, NULL); + update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_ATSC_CC, program_number, NULL, NULL); } if (need_cap_info_for_pid(ctx, elementary_PID) == CCX_TRUE) @@ -498,7 +506,7 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr mprint("Please pass -streamtype to select manually.\n"); fatal(EXIT_FAILURE, "-streamtype has to be manually selected."); } - update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_NONE, program_number, NULL); + update_capinfo(ctx, elementary_PID, stream_type, CCX_CODEC_NONE, program_number, NULL, NULL); continue; } diff --git a/src/rust/src/common.rs b/src/rust/src/common.rs index 809427b68..415dc0263 100755 --- a/src/rust/src/common.rs +++ b/src/rust/src/common.rs @@ -1037,6 +1037,7 @@ impl CType for CapInfo { prev_counter: self.prev_counter, codec_private_data: self.codec_private_data, ignore: self.ignore, + lang: self.lang, all_stream: self.all_stream, sib_head: self.sib_head, sib_stream: self.sib_stream, diff --git a/src/rust/src/ctorust.rs b/src/rust/src/ctorust.rs index 2e075e0d5..c07deb9ff 100755 --- a/src/rust/src/ctorust.rs +++ b/src/rust/src/ctorust.rs @@ -568,6 +568,7 @@ impl FromCType for CapInfo { prev_counter: info.prev_counter, codec_private_data: info.codec_private_data, ignore: info.ignore, + lang: info.lang, all_stream: list_head { next: info.all_stream.next, prev: info.all_stream.prev, diff --git a/src/rust/src/demuxer/common_types.rs b/src/rust/src/demuxer/common_types.rs index d359db764..b83bf17e2 100644 --- a/src/rust/src/demuxer/common_types.rs +++ b/src/rust/src/demuxer/common_types.rs @@ -79,6 +79,7 @@ pub struct CapInfo { pub prev_counter: i32, pub codec_private_data: *mut std::ffi::c_void, pub ignore: i32, + pub lang: [i8; 4], /** * List joining all streams in TS @@ -325,6 +326,7 @@ impl Default for CapInfo { prev_counter: 0, codec_private_data: null_mut(), ignore: 0, + lang: [0; 4], all_stream: list_head::default(), sib_head: list_head::default(), From a26cce4df325952c69e6aaf35f50c40499d9515e Mon Sep 17 00:00:00 2001 From: ujjwalr27 Date: Mon, 30 Mar 2026 18:40:25 +0900 Subject: [PATCH 2/6] style: apply clang-format to multi-DVB subtitle extraction changes --- src/lib_ccx/ccx_demuxer.h | 5 +++-- src/lib_ccx/general_loop.c | 4 ++-- src/lib_ccx/ts_info.c | 20 ++++++++++++++++++-- src/lib_ccx/ts_tables.c | 3 ++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/lib_ccx/ccx_demuxer.h b/src/lib_ccx/ccx_demuxer.h index 919eadc27..a98691b35 100644 --- a/src/lib_ccx/ccx_demuxer.h +++ b/src/lib_ccx/ccx_demuxer.h @@ -47,7 +47,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 @@ -163,7 +163,8 @@ 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); }; struct demuxer_data diff --git a/src/lib_ccx/general_loop.c b/src/lib_ccx/general_loop.c index 2a9400540..112e9bbd4 100644 --- a/src/lib_ccx/general_loop.c +++ b/src/lib_ccx/general_loop.c @@ -1344,8 +1344,8 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx, { struct ccx_rational tb = {1, MPEG_CLOCK_FREQ}; LLONG pts = (dvb_data->tb.num != 1 || dvb_data->tb.den != MPEG_CLOCK_FREQ) - ? change_timebase(dvb_data->pts, dvb_data->tb, tb) - : dvb_data->pts; + ? change_timebase(dvb_data->pts, dvb_data->tb, tb) + : dvb_data->pts; set_current_pts(dvb_dec->timing, pts); if (dvb_dec->timing->min_pts == 0x01FFFFFFFFLL) { diff --git a/src/lib_ccx/ts_info.c b/src/lib_ccx/ts_info.c index f66524cee..19f705766 100644 --- a/src/lib_ccx/ts_info.c +++ b/src/lib_ccx/ts_info.c @@ -220,7 +220,15 @@ int update_capinfo(struct ccx_demuxer *ctx, int pid, enum ccx_stream_type stream tmp->capbufsize = 0; tmp->ignore = 0; } - if (lang) { strncpy(tmp->lang, lang, 3); tmp->lang[3] = '\0'; } else { tmp->lang[0] = '\0'; } + if (lang) + { + strncpy(tmp->lang, lang, 3); + tmp->lang[3] = '\0'; + } + else + { + tmp->lang[0] = '\0'; + } return CCX_OK; } } @@ -248,7 +256,15 @@ int update_capinfo(struct ccx_demuxer *ctx, int pid, enum ccx_stream_type stream tmp->codec_private_data = init_private_data(codec); else tmp->codec_private_data = private_data; - if (lang) { strncpy(tmp->lang, lang, 3); tmp->lang[3] = '\0'; } else { tmp->lang[0] = '\0'; } + if (lang) + { + strncpy(tmp->lang, lang, 3); + tmp->lang[3] = '\0'; + } + else + { + tmp->lang[0] = '\0'; + } list_add_tail(&(tmp->all_stream), &(ptr->all_stream)); diff --git a/src/lib_ccx/ts_tables.c b/src/lib_ccx/ts_tables.c index 15e414350..77c277ec1 100644 --- a/src/lib_ccx/ts_tables.c +++ b/src/lib_ccx/ts_tables.c @@ -404,7 +404,8 @@ int parse_PMT(struct ccx_demuxer *ctx, unsigned char *buf, int len, struct progr break; /* Extract 3-byte ISO-639 language code from DVB subtitle descriptor */ char dvb_lang[4] = {0}; - if (desc_len >= 3) { + if (desc_len >= 3) + { dvb_lang[0] = (char)cctolower(es_info[0]); dvb_lang[1] = (char)cctolower(es_info[1]); dvb_lang[2] = (char)cctolower(es_info[2]); From 510381bd8225500692d82165f3e06b1d83c41487 Mon Sep 17 00:00:00 2001 From: ujjwalr27 Date: Tue, 7 Apr 2026 14:38:01 +0900 Subject: [PATCH 3/6] fix: only add lang suffix when 2+ DVB PID --- src/lib_ccx/lib_ccx.c | 58 +++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/src/lib_ccx/lib_ccx.c b/src/lib_ccx/lib_ccx.c index cc23d3efb..751fcfc8f 100644 --- a/src/lib_ccx/lib_ccx.c +++ b/src/lib_ccx/lib_ccx.c @@ -453,8 +453,8 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca { if (ctx->multiprogram == CCX_FALSE) { - /* For DVB subtitles with a language tag, match by program_number + language */ - if (cinfo && cinfo->codec == CCX_CODEC_DVB && cinfo->lang[0]) + /* For DVB subtitles with multiple PIDs, match by language */ + if (cinfo && cinfo->codec == CCX_CODEC_DVB && cinfo->lang[0] && enc_ctx->dvb_lang[0]) { if (enc_ctx->program_number == pn && strcmp(enc_ctx->dvb_lang, cinfo->lang) == 0) @@ -472,30 +472,45 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca if (!extension && ccx_options.enc_cfg.write_format != CCX_OF_CURL) return NULL; - /* Create per-language DVB encoder if needed */ + /* Create per-language DVB encoder only when there are 2+ DVB subtitle PIDs. + Single-DVB-stream recordings fall through to the standard encoder path + to preserve backward compatible filenames. */ if (cinfo && cinfo->codec == CCX_CODEC_DVB && cinfo->lang[0]) { - struct encoder_cfg local_cfg = ccx_options.enc_cfg; - local_cfg.program_number = pn; - local_cfg.in_format = in_format; - char *basefilename = get_basename(ctx->basefilename); - char suffix[8]; - snprintf(suffix, sizeof(suffix), "_%s", cinfo->lang); - local_cfg.output_filename = create_outfilename(basefilename, suffix, extension); - free(basefilename); + /* Count DVB subtitle PIDs in this program */ + int dvb_pid_count = 0; + struct cap_info *ci; + list_for_each_entry(ci, &ctx->demux_ctx->cinfo_tree.all_stream, all_stream, struct cap_info) + { + if (ci->codec == CCX_CODEC_DVB && ci->program_number == cinfo->program_number) + dvb_pid_count++; + } - enc_ctx = init_encoder(&local_cfg); - if (enc_ctx) + if (dvb_pid_count >= 2) { - enc_ctx->program_number = pn; - strncpy(enc_ctx->dvb_lang, cinfo->lang, 3); - enc_ctx->dvb_lang[3] = '\0'; - list_add_tail(&enc_ctx->list, &ctx->enc_ctx_head); - enc_ctx->prev = NULL; - enc_ctx->write_previous = 0; + struct encoder_cfg local_cfg = ccx_options.enc_cfg; + local_cfg.program_number = pn; + local_cfg.in_format = in_format; + char *basefilename = get_basename(ctx->basefilename); + char suffix[8]; + snprintf(suffix, sizeof(suffix), "_%s", cinfo->lang); + local_cfg.output_filename = create_outfilename(basefilename, suffix, extension); + free(basefilename); + + enc_ctx = init_encoder(&local_cfg); + if (enc_ctx) + { + enc_ctx->program_number = pn; + strncpy(enc_ctx->dvb_lang, cinfo->lang, 3); + enc_ctx->dvb_lang[3] = '\0'; + list_add_tail(&enc_ctx->list, &ctx->enc_ctx_head); + enc_ctx->prev = NULL; + enc_ctx->write_previous = 0; + } + free(local_cfg.output_filename); + return enc_ctx; } - free(local_cfg.output_filename); - return enc_ctx; + /* Single DVB stream: fall through to standard encoder creation */ } if (ctx->multiprogram == CCX_FALSE) @@ -560,6 +575,7 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca } // DVB related enc_ctx->prev = NULL; + memset(enc_ctx->dvb_lang, 0, sizeof(enc_ctx->dvb_lang)); if (cinfo) if (cinfo->codec == CCX_CODEC_DVB) enc_ctx->write_previous = 0; From a4d10b96c8773c12ef6d57b569b6667e35f70814 Mon Sep 17 00:00:00 2001 From: ujjwalr27 Date: Tue, 7 Apr 2026 14:39:03 +0900 Subject: [PATCH 4/6] fix segfault from uninitialized dvb_lang --- src/lib_ccx/ccx_demuxer.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib_ccx/ccx_demuxer.h b/src/lib_ccx/ccx_demuxer.h index a98691b35..3af622b6e 100644 --- a/src/lib_ccx/ccx_demuxer.h +++ b/src/lib_ccx/ccx_demuxer.h @@ -163,7 +163,7 @@ 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) + LLONG(*get_filesize) (struct ccx_demuxer *ctx); }; From 4dd2c831fea0df36ae129213631df51d8ad02448 Mon Sep 17 00:00:00 2001 From: ujjwalr27 Date: Tue, 7 Apr 2026 14:54:27 +0900 Subject: [PATCH 5/6] docs: add CHANGES.TXT entry for multi-language DVB subtitle extraction --- docs/CHANGES.TXT | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGES.TXT b/docs/CHANGES.TXT index 4d114298f..e43582a54 100644 --- a/docs/CHANGES.TXT +++ b/docs/CHANGES.TXT @@ -2,6 +2,7 @@ ------------------- - New: Allow output \0 terminated frames via --null-terminated - New: Added ASS/SSA \pos-based positioning for CEA-608 captions when layout is simple (1–2 rows) (#1726) +- New: Auto-extract multi-language DVB subtitles into per-language files.(#2243) - Fix: Remove strdup() memory leaks in WebVTT styling encoder, fix invalid CSS rgba(0,256,0) green value, fix missing free(unescaped) on write-error path (#2154) - Fix: Prevent crash in Rust timing module when logging out-of-range PTS/FTS timestamps from malformed streams. - Fix: Resolve Windows MSVC debug build crash caused by cross-CRT invalid free on Rust-allocated output_filename (#2126) From 00a717a711f7304084d00ecf7360f0d20a2fabe6 Mon Sep 17 00:00:00 2001 From: ujjwalr27 Date: Tue, 7 Apr 2026 16:48:34 +0900 Subject: [PATCH 6/6] fix: skip non-DVB encoders in lookup --- src/lib_ccx/lib_ccx.c | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/lib_ccx/lib_ccx.c b/src/lib_ccx/lib_ccx.c index 751fcfc8f..1b174c3d1 100644 --- a/src/lib_ccx/lib_ccx.c +++ b/src/lib_ccx/lib_ccx.c @@ -453,10 +453,11 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca { if (ctx->multiprogram == CCX_FALSE) { - /* For DVB subtitles with multiple PIDs, match by language */ - if (cinfo && cinfo->codec == CCX_CODEC_DVB && cinfo->lang[0] && enc_ctx->dvb_lang[0]) + /* For DVB subtitles, match by language — skip non-DVB encoders */ + if (cinfo && cinfo->codec == CCX_CODEC_DVB && cinfo->lang[0]) { - if (enc_ctx->program_number == pn && + if (enc_ctx->dvb_lang[0] && + enc_ctx->program_number == pn && strcmp(enc_ctx->dvb_lang, cinfo->lang) == 0) return enc_ctx; continue; @@ -575,10 +576,16 @@ struct encoder_ctx *update_encoder_list_cinfo(struct lib_ccx_ctx *ctx, struct ca } // DVB related enc_ctx->prev = NULL; - memset(enc_ctx->dvb_lang, 0, sizeof(enc_ctx->dvb_lang)); - if (cinfo) - if (cinfo->codec == CCX_CODEC_DVB) - enc_ctx->write_previous = 0; + if (cinfo && cinfo->codec == CCX_CODEC_DVB && cinfo->lang[0]) + { + strncpy(enc_ctx->dvb_lang, cinfo->lang, 3); + enc_ctx->dvb_lang[3] = '\0'; + enc_ctx->write_previous = 0; + } + else + { + memset(enc_ctx->dvb_lang, 0, sizeof(enc_ctx->dvb_lang)); + } return enc_ctx; }