mirror of
https://github.com/CCExtractor/ccextractor.git
synced 2026-07-09 02:16:41 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user