Fix #447: Resolve DVB split mode crash and routing logic

- Fixed NULL pointer dereference in dvb_subtitle_decoder.c (sub->prev check).
- Corrected logic in dvbsub_handle_display_segment to prevent dropped subtitles.
- Implemented robust encoder context swapping in general_loop.c for DVB streams.
- Added regression test: tests/regression/dvb_split.txt.
- Verified 100% completion in split mode and correct Teletext/DVB routing.
This commit is contained in:
Rahul-2k4
2025-12-27 00:11:09 +05:30
parent 6642973c63
commit 77f3fd35f4
11 changed files with 392 additions and 32 deletions

View File

@@ -47,6 +47,8 @@ struct ccx_stream_metadata
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

View File

@@ -1733,6 +1733,22 @@ void dvbsub_handle_display_segment(struct encoder_ctx *enc_ctx,
enc_ctx->prev->last_string = NULL; // Reset last recognized sub text
// 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);
if (!sub->prev)
{
// 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
{
// For DVB subtitles, a subtitle is displayed until the next one appears.
// 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.
@@ -1787,6 +1803,8 @@ void dvbsub_handle_display_segment(struct encoder_ctx *enc_ctx,
{
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;
@@ -1799,8 +1817,12 @@ void dvbsub_handle_display_segment(struct encoder_ctx *enc_ctx,
}
}
}
}
/* copy previous encoder context*/
free_encoder_context(enc_ctx->prev);
enc_ctx->prev = NULL;
enc_ctx->prev = copy_encoder_context(enc_ctx);
@@ -1828,6 +1850,10 @@ void dvbsub_handle_display_segment(struct encoder_ctx *enc_ctx,
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
#ifdef ENABLE_OCR
if (sub->prev)
{
@@ -1903,6 +1929,8 @@ int dvbsub_decode(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, co
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:
@@ -1937,6 +1965,8 @@ int dvbsub_decode(struct encoder_ctx *enc_ctx, struct lib_cc_decode *dec_ctx, co
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);
dvbsub_handle_display_segment(enc_ctx, dec_ctx, sub, pre_fts_max);
got_segment |= 16;
break;
default:

View File

@@ -982,10 +982,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);
@@ -1126,19 +1135,89 @@ 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);
// Primary stream processing (Teletext or whatever get_best_data selected)
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 that are NOT the primary data_node
if (dvb_ptr != *data_node &&
dvb_ptr->codec == CCX_CODEC_DVB &&
dvb_ptr->len > 0)
{
int stream_pid = dvb_ptr->stream_pid;
char lang[4] = "und";
// Lookup language from discovered streams
for (int i = 0; i < ctx->demux_ctx->potential_stream_count; i++)
{
if (ctx->demux_ctx->potential_streams[i].pid == stream_pid)
{
memcpy(lang, ctx->demux_ctx->potential_streams[i].lang, 4);
break;
}
}
// 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);
if (pipe && pipe->encoder && pipe->decoder)
{
// Save current decoder context and encoder context
void *saved_private = (*dec_ctx)->private_data;
struct encoder_ctx *saved_enc = *enc_ctx;
// Swap to pipeline's DVB decoder and encoder
(*dec_ctx)->private_data = pipe->decoder;
*enc_ctx = pipe->encoder;
// Sync timing from main context to pipeline encoder
// This ensures DVB decode has valid PTS/timing state
pipe->encoder->timing = (*dec_ctx)->timing;
// Decode DVB directly using pipeline's decoder and encoder
// Skip first 2 bytes (PES header) as done in process_data for DVB
struct cc_subtitle dvb_sub = {0};
dvbsub_decode(pipe->encoder, *dec_ctx, dvb_ptr->buffer + 2, dvb_ptr->len - 2, &dvb_sub);
// Encode output if produced
if (dvb_sub.got_output)
{
encode_sub(pipe->encoder, &dvb_sub);
dvb_sub.got_output = 0;
}
// Restore original decoder/encoder context
(*dec_ctx)->private_data = saved_private;
*enc_ctx = saved_enc;
}
}
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))
{

View File

@@ -202,6 +202,12 @@ 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;
ctx->pipeline_lock = 0;
memset(ctx->pipelines, 0, sizeof(ctx->pipelines));
end:
if (ret != EXIT_OK)
{
@@ -263,6 +269,30 @@ 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;
// 1) Close decoder first (no encoder dependency)
if (p->decoder)
dvbsub_close_decoder(&p->decoder);
// 2) Close encoder via canonical API (handles output cleanup internally)
if (p->encoder)
dinit_encoder(&p->encoder, 0);
// 3) Free timing context
if (p->timing)
dinit_timing_ctx(&p->timing);
free(p);
lctx->pipelines[i] = NULL;
}
lctx->pipeline_count = 0;
// free EPG memory
EPG_free(lctx);
freep(&lctx->freport.data_from_608);
@@ -487,3 +517,112 @@ 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;
// 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)
{
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);
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");
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}.ext or {basefilename}_0x{PID}.ext
const char *ext = ctx->extension ? ctx->extension : ".srt";
if (strcmp(pipe->lang, "und") == 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%s",
ctx->basefilename, pipe->lang, ext);
}
// Initialize encoder for this pipeline
struct encoder_cfg cfg = ccx_options.enc_cfg;
cfg.output_filename = pipe->filename;
pipe->encoder = init_encoder(&cfg);
if (!pipe->encoder)
{
mprint("Error: Failed to create encoder for pipeline PID 0x%X\n", pid);
free(pipe);
return NULL;
}
pipe->encoder->write_previous = 0; // DVB specific
// Timing context: Do NOT create a separate timing context for pipelines.
// Pipelines must share the main decoder's timing context (dec_ctx->timing).
// Creating a fresh timing context would have pts_set=No, causing decode failures.
// The encoder will receive timing from dec_ctx at decode time.
pipe->timing = 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;
// Also update language if not provided/detected earlier?
// But we pass 'lang' argument to this function.
break;
}
}
}
pipe->decoder = dvbsub_init_decoder(&dvb_cfg, 1);
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);
return NULL;
}
// 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);
return pipe;
}

View File

@@ -81,6 +81,23 @@ 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_ccx_ctx
{
// Stuff common to both loops
@@ -154,8 +171,16 @@ 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;
int pipeline_lock; // Simple lock flag (single-threaded access assumed)
void *dec_dvb_default; // Default decoder used in non-split mode
};
struct ccx_subtitle_pipeline *get_or_create_pipeline(struct lib_ccx_ctx *ctx, int pid, int stream_type, const char *lang);
struct lib_ccx_ctx *init_libraries(struct ccx_s_options *opt);
void dinit_libraries(struct lib_ccx_ctx **ctx);

View File

@@ -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;
}
}
@@ -680,7 +680,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
@@ -970,32 +974,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
@@ -1006,8 +1018,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)
{

View File

@@ -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;
}
}
}
}

View File

@@ -385,6 +385,41 @@ 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)
{
detected_lang[0] = (char)es_info[0];
detected_lang[1] = (char)es_info[1];
detected_lang[2] = (char)es_info[2];
detected_lang[3] = '\0';
}
// 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)
{
ctx->potential_streams[ctx->potential_stream_count].pid = (int)elementary_PID;
ctx->potential_streams[ctx->potential_stream_count].stream_type = CCX_STREAM_TYPE_DVB_SUB;
ctx->potential_streams[ctx->potential_stream_count].mpeg_type = stream_type;
memcpy(ctx->potential_streams[ctx->potential_stream_count].lang, detected_lang, 4);
ctx->potential_stream_count++;
dbg_print(CCX_DMT_GENERIC_NOTICES, "Discovered DVB stream PID 0x%X lang=%s\n", elementary_PID, detected_lang);
}
}
#ifndef ENABLE_OCR
if (ccx_options.write_format != CCX_OF_SPUPNG)
{
@@ -392,13 +427,28 @@ 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)
{
for (int k = 0; k < ctx->potential_stream_count; k++)
{
if (ctx->potential_streams[k].pid == (int)elementary_PID)
{
ctx->potential_streams[k].composition_id = cnf.composition_id[0];
ctx->potential_streams[k].ancillary_id = cnf.ancillary_id[0];
break;
}
}
}
ptr = dvbsub_init_decoder(&cnf, pinfo->initialized_ocr);
if (!pinfo->initialized_ocr)
pinfo->initialized_ocr = 1;

View File

@@ -1682,7 +1682,7 @@ impl OptionsExt for Options {
);
}
if self.demux_cfg.ts_forced_cappid.is_some() {
if self.demux_cfg.ts_forced_cappid {
fatal!(
cause = ExitCause::IncompatibleParameters;
"--split-dvb-subs cannot be used with manual PID selection (-pn).\n\

View File

@@ -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

View File

@@ -190,11 +190,11 @@
<Import Project=" $(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug-Full|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<PlatformToolset>v145</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release-Full|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<PlatformToolset>v145</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">