Merge pull request #2020 - Add machine-readable JSON output for -out=report

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Carlos Fernandez
2026-02-15 14:02:44 -08:00
9 changed files with 374 additions and 7 deletions

View File

@@ -1,9 +1,11 @@
0.96.6 (unreleased)
-------------------
- New: Add optional machine-readable JSON output for -out=report via --report-format json
- Fix: Incorrect strlen argument when writing end timestamps in MKV subtitle extraction (WebVTT, SRT, ASS/SSA)
- Fix: File descriptor leak and missing open() error check in MKV subtitle track saving
- Fix: DVB EIT start time BCD decoding in XMLTV output causing invalid timestamps (#1835)
- New: Add Snap packaging support with Snapcraft configuration and GitHub Actions CI workflow.
- New: Implement dictionary-based capitalization and censorship for transcripts
- Fix: Clear status line output on Linux/WSL to prevent text artifacts (#2017)
- Fix: Prevent infinite loop on truncated MKV files
- Fix: Various memory safety and stability fixes in demuxers (MP4, PS, MKV, DVB)

View File

@@ -145,6 +145,7 @@ void init_options(struct ccx_s_options *options)
options->enc_cfg.scc_framerate = 0; // Default: 29.97fps for SCC output
options->enc_cfg.scc_accurate_timing = 0; // Default: off for backwards compatibility (issue #1120)
options->enc_cfg.extract_only_708 = 0;
options->report_format = NULL;
options->settings_dtvcc.enabled = 0;
options->settings_dtvcc.active_services_count = 0;

View File

@@ -189,6 +189,7 @@ struct ccx_s_options // Options from user parameters
enum ccx_datasource input_source; // Files, stdin or network
char *output_filename;
char *report_format; // NULL = default text, e.g. "json"
char **inputfile; // List of files to process
int num_input_files; // How many?

View File

@@ -120,11 +120,8 @@ int write_cc_subtitle_as_transcript(struct cc_subtitle *sub, struct encoder_ctx
start_time = sub->start_time;
end_time = sub->end_time;
}
if (context->sentence_cap)
{
// TODO capitalize (context, line_number,data);
// TODO correct_case_with_dictionary(line_number, data);
}
if (sub->data)
correct_spelling_and_censor_words(context, (unsigned char *)sub->data, strlen((char *)sub->data));
if (start_time == -1)
{

View File

@@ -1,9 +1,83 @@
#ifdef _WIN32
#define strcasecmp _stricmp
#endif
#include <stdlib.h>
#include "lib_ccx.h"
#include "ccx_common_option.h"
#include "teletext.h"
#include <string.h>
#include <stdbool.h>
#include "ccx_decoders_708.h"
void print_file_report_json(struct lib_ccx_ctx *ctx);
static void json_escape(const char *s)
{
if (!s)
{
printf("null");
return;
}
putchar('"');
for (; *s; s++)
{
switch (*s)
{
case '"':
printf("\\\"");
break;
case '\\':
printf("\\\\");
break;
case '\b':
printf("\\b");
break;
case '\f':
printf("\\f");
break;
case '\n':
printf("\\n");
break;
case '\r':
printf("\\r");
break;
case '\t':
printf("\\t");
break;
default:
if ((unsigned char)*s < 0x20)
printf("\\u%04x", (unsigned char)*s);
else
putchar(*s);
}
}
putchar('"');
}
static const char *stream_mode_to_string(enum ccx_stream_mode_enum mode)
{
switch (mode)
{
case CCX_SM_TRANSPORT:
return "Transport Stream";
case CCX_SM_PROGRAM:
return "Program Stream";
case CCX_SM_ASF:
return "ASF";
case CCX_SM_WTV:
return "WTV";
case CCX_SM_MP4:
return "MP4";
case CCX_SM_MCPOODLESRAW:
return "McPoodle Raw";
case CCX_SM_RCWT:
return "BIN";
default:
return "Unknown";
}
}
void params_dump(struct lib_ccx_ctx *ctx)
{
// Display parsed parameters
@@ -256,10 +330,278 @@ void print_cc_report(struct lib_ccx_ctx *ctx, struct cap_info *info)
}
}
void print_file_report_json(struct lib_ccx_ctx *ctx)
{
struct ccx_demuxer *demux = ctx->demux_ctx;
// Use PAT-based program count
int program_count = demux->nb_program;
// Build array of program numbers from PAT
unsigned *program_numbers = malloc(sizeof(unsigned) * program_count);
if (!program_numbers)
{
fprintf(stderr, "Out of memory while building report JSON\n");
return;
}
for (int i = 0; i < program_count; i++)
{
program_numbers[i] = demux->pinfo[i].program_number;
}
// Sort program numbers
for (int i = 0; i < program_count - 1; i++)
{
for (int j = i + 1; j < program_count; j++)
{
if (program_numbers[i] > program_numbers[j])
{
unsigned tmp = program_numbers[i];
program_numbers[i] = program_numbers[j];
program_numbers[j] = tmp;
}
}
}
printf("{\n");
/* schema */
printf(" \"schema\": {\n");
printf(" \"name\": \"ccextractor-report\",\n");
printf(" \"version\": \"1.0\"\n");
printf(" },\n");
/* input */
printf(" \"input\": {\n");
printf(" \"source\": ");
switch (ccx_options.input_source)
{
case CCX_DS_FILE:
printf("\"file\",\n");
printf(" \"path\": ");
json_escape(ctx->inputfile[ctx->current_file]);
printf("\n");
break;
case CCX_DS_STDIN:
printf("\"stdin\"\n");
break;
default:
printf("\"network\"\n");
break;
}
printf(" },\n");
/* stream */
printf(" \"stream\": {\n");
printf(" \"mode\": ");
json_escape(stream_mode_to_string(demux->stream_mode));
printf(",\n");
printf(" \"program_count\": %d,\n", program_count);
printf(" \"program_numbers\": [");
for (int i = 0; i < program_count; i++)
{
if (i)
printf(", ");
printf("%u", program_numbers[i]);
}
printf("],\n");
/* PIDs */
printf(" \"pids\": [\n");
int first = 1;
for (int pid = 0; pid < 65536; pid++)
{
if (!demux->PIDs_programs[pid])
continue;
if (!first)
printf(",\n");
first = 0;
printf(" { \"pid\": %d, \"program_number\": %u, \"codec\": ",
pid,
demux->PIDs_programs[pid]->program_number);
json_escape(desc[demux->PIDs_programs[pid]->printable_stream_type]);
printf(" }");
}
printf("\n ]\n");
printf(" },\n");
/* container-level metadata */
if (ctx->freport.mp4_cc_track_cnt > 0)
{
printf(" \"container\": {\n");
printf(" \"mp4\": {\n");
printf(" \"timed_text_tracks\": %d\n", ctx->freport.mp4_cc_track_cnt);
printf(" }\n");
printf(" },\n");
}
/* programs */
printf(" \"programs\": [\n");
first = 1;
for (int pi = 0; pi < program_count; pi++)
{
unsigned pn = program_numbers[pi];
// Find matching cap_info for this program number
struct cap_info *program_ci = NULL;
struct cap_info *iter;
list_for_each_entry(iter, &demux->cinfo_tree.pg_stream, pg_stream, struct cap_info)
{
if (iter->program_number == (int)pn)
{
program_ci = iter;
break;
}
}
// Compute caption metadata ONLY if cap_info exists
struct lib_cc_decode *dec_ctx = NULL;
bool has_608 = false;
bool has_708 = false;
bool has_dvb = false;
bool has_tt = false;
bool has_any_captions = false;
if (program_ci)
{
dec_ctx = update_decoder_list_cinfo(ctx, program_ci);
has_608 = (dec_ctx->cc_stats[0] || dec_ctx->cc_stats[1]);
has_708 = (dec_ctx->cc_stats[2] || dec_ctx->cc_stats[3]);
has_dvb = (get_sib_stream_by_type(program_ci, CCX_CODEC_DVB) != NULL);
has_tt = (get_sib_stream_by_type(program_ci, CCX_CODEC_TELETEXT) != NULL);
has_any_captions = has_608 || has_708 || has_dvb || has_tt;
}
if (!first)
printf(",\n");
first = 0;
printf(" {\n");
printf(" \"program_number\": %u,\n", pn);
printf(" \"summary\": {\n");
printf(" \"has_any_captions\": %s,\n", has_any_captions ? "true" : "false");
printf(" \"has_608\": %s,\n", has_608 ? "true" : "false");
printf(" \"has_708\": %s\n", has_708 ? "true" : "false");
printf(" },\n");
printf(" \"services\": {\n");
printf(" \"dvb_subtitles\": %s,\n",
(program_ci && get_sib_stream_by_type(program_ci, CCX_CODEC_DVB)) ? "true" : "false");
printf(" \"teletext\": %s,\n",
(program_ci && get_sib_stream_by_type(program_ci, CCX_CODEC_TELETEXT)) ? "true" : "false");
printf(" \"atsc_closed_caption\": %s\n",
(program_ci && get_sib_stream_by_type(program_ci, CCX_CODEC_ATSC_CC)) ? "true" : "false");
printf(" },\n");
printf(" \"captions\": {\n");
printf(" \"present\": %s,\n", has_any_captions ? "true" : "false");
printf(" \"eia_608\": {\n");
printf(" \"present\": %s,\n", has_608 ? "true" : "false");
// NOTE: EIA-608 / CEA-708 data is currently stream-global and therefore
// identical across programs in multi-program streams.
printf(" \"xds\": %s,\n",
(program_ci && ctx->freport.data_from_608->xds) ? "true" : "false");
printf(" \"channels\": {\n");
printf(" \"cc1\": %s,\n",
(program_ci && ctx->freport.data_from_608->cc_channels[0]) ? "true" : "false");
printf(" \"cc2\": %s,\n",
(program_ci && ctx->freport.data_from_608->cc_channels[1]) ? "true" : "false");
printf(" \"cc3\": %s,\n",
(program_ci && ctx->freport.data_from_608->cc_channels[2]) ? "true" : "false");
printf(" \"cc4\": %s\n",
(program_ci && ctx->freport.data_from_608->cc_channels[3]) ? "true" : "false");
printf(" }\n");
printf(" },\n");
printf(" \"cea_708\": {\n");
printf(" \"present\": %s,\n", has_708 ? "true" : "false");
printf(" \"services\": [");
int sf = 1;
if (program_ci)
{
for (int i = 0; i < CCX_DTVCC_MAX_SERVICES; i++)
{
if (!ctx->freport.data_from_708->services[i])
continue;
if (!sf)
printf(", ");
sf = 0;
printf("%d", i);
}
}
printf("]\n");
printf(" }\n"); // end cea_708
printf(" }"); // end captions
// Decide upfront if video will actually be printed
bool print_video = false;
struct cap_info *info = NULL;
if (program_ci)
{
info = get_best_sib_stream(program_ci);
if (info)
{
dec_ctx = update_decoder_list_cinfo(ctx, info);
if (dec_ctx->in_bufferdatatype == CCX_PES &&
(demux->stream_mode == CCX_SM_TRANSPORT ||
demux->stream_mode == CCX_SM_PROGRAM ||
demux->stream_mode == CCX_SM_ASF ||
demux->stream_mode == CCX_SM_WTV))
{
print_video = true;
}
}
}
if (print_video)
{
printf(",\n"); // comma ONLY because video follows
printf(" \"video\": {\n");
printf(" \"width\": %u,\n", dec_ctx->current_hor_size);
printf(" \"height\": %u,\n", dec_ctx->current_vert_size);
printf(" \"aspect_ratio\": ");
json_escape(aspect_ratio_types[dec_ctx->current_aspect_ratio]);
printf(",\n");
printf(" \"frame_rate\": ");
json_escape(framerates_types[dec_ctx->current_frame_rate]);
printf("\n");
printf(" }\n");
}
else
{
printf("\n"); // no video, just newline
}
printf(" }"); // end program object
}
printf("\n ]\n");
printf("}\n");
free(program_numbers);
}
void print_file_report(struct lib_ccx_ctx *ctx)
{
struct lib_cc_decode *dec_ctx = NULL;
struct ccx_demuxer *demux_ctx = ctx->demux_ctx;
const char *report_fmt = ccx_options.report_format;
if (report_fmt && strcasecmp(report_fmt, "json") == 0)
{
print_file_report_json(ctx);
goto cleanup;
}
printf("File: ");
switch (ccx_options.input_source)
@@ -422,7 +764,8 @@ void print_file_report(struct lib_ccx_ctx *ctx)
printf("MPEG-4 Timed Text tracks count: %d\n", ctx->freport.mp4_cc_track_cnt);
}
cleanup:
freep(&ctx->freport.data_from_608);
memset(&ctx->freport, 0, sizeof(struct file_report));
#undef Y_N
}
}

View File

@@ -367,6 +367,7 @@ pub struct Options {
/// The end of the segment we actually process
pub extraction_end: Option<Timestamp>,
pub print_file_reports: bool,
pub report_format: Option<String>,
/// Contains the settings for the 608 decoder.
pub settings_608: Decoder608Settings,
/// Same for 708 decoder
@@ -631,6 +632,7 @@ impl Default for Options {
segment_on_key_frames_only: Default::default(),
scc_framerate: 0, // 0 = 29.97fps (default)
scc_accurate_timing: false, // Off by default for backwards compatibility (issue #1120)
report_format: None,
debug_mask: DebugMessageMask::new(
DebugMessageFlag::GENERIC_NOTICE,
DebugMessageFlag::VERBOSE,

View File

@@ -252,8 +252,14 @@ pub struct Args {
pub dvr_ms: bool,
#[arg(long, value_name="format", help_heading=OUTPUT_FORMATS)]
pub out: Option<OutFormat>,
/// Format for -out=report output (e.g. json)
#[arg(long = "report-format", value_name = "FORMAT", help_heading=OUTPUT_FORMATS)]
pub report_format: Option<String>,
#[arg(long, hide = true)]
pub srt: bool,
#[arg(long, hide = true)]
pub webvtt: bool,
#[arg(long, hide = true)]

View File

@@ -81,6 +81,11 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options
(*ccx_s_options).extraction_start = options.extraction_start.to_ctype();
(*ccx_s_options).extraction_end = options.extraction_end.to_ctype();
(*ccx_s_options).print_file_reports = options.print_file_reports as _;
// Report output format (e.g. "json")
if let Some(ref fmt) = options.report_format {
(*ccx_s_options).report_format =
replace_rust_c_string((*ccx_s_options).report_format, fmt.as_str());
}
// Preserve the original C-managed report pointer to avoid dangling pointer issues.
let saved_608_report = (*ccx_s_options).settings_608.report;
(*ccx_s_options).settings_608 = options.settings_608.to_ctype();
@@ -323,6 +328,7 @@ pub unsafe fn copy_to_rust(ccx_s_options: *const ccx_s_options) -> Options {
.expect("Invalid extraction end time"),
),
print_file_reports: (*ccx_s_options).print_file_reports != 0,
report_format: None,
// Handle settings_608 and settings_dtvcc - assuming FromCType trait is implemented for these
settings_608: Decoder608Settings::from_ctype((*ccx_s_options).settings_608)
.unwrap_or(Decoder608Settings::default()),
@@ -344,6 +350,10 @@ pub unsafe fn copy_to_rust(ccx_s_options: *const ccx_s_options) -> Options {
..Default::default()
};
if !(*ccx_s_options).report_format.is_null() {
options.report_format = Some(c_char_to_string((*ccx_s_options).report_format));
}
// Handle sentence_cap_file (C string to PathBuf)
if !(*ccx_s_options).sentence_cap_file.is_null() {
options.sentence_cap_file =

View File

@@ -773,6 +773,11 @@ impl OptionsExt for Options {
self.set_output_format(args);
}
// --- report-format (used by -out=report) ---
if let Some(ref fmt) = args.report_format {
self.report_format = Some(fmt.to_lowercase());
}
if let Some(ref startcreditstext) = args.startcreditstext {
self.enc_cfg.start_credits_text.clone_from(startcreditstext);
}