diff --git a/docs/CHANGES.TXT b/docs/CHANGES.TXT index 33217c660..8ecc80c1e 100644 --- a/docs/CHANGES.TXT +++ b/docs/CHANGES.TXT @@ -1,5 +1,6 @@ 0.96.6 (unreleased) ------------------- +- Add optional machine-readable JSON output for -out=report via --report-format json - 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. - Fix: Clear status line output on Linux/WSL to prevent text artifacts (#2017) diff --git a/src/lib_ccx/ccx_common_option.c b/src/lib_ccx/ccx_common_option.c index ecfe17f7f..0eda6ab06 100644 --- a/src/lib_ccx/ccx_common_option.c +++ b/src/lib_ccx/ccx_common_option.c @@ -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; diff --git a/src/lib_ccx/ccx_common_option.h b/src/lib_ccx/ccx_common_option.h index aa7e14207..85e0f1562 100644 --- a/src/lib_ccx/ccx_common_option.h +++ b/src/lib_ccx/ccx_common_option.h @@ -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? diff --git a/src/lib_ccx/params_dump.c b/src/lib_ccx/params_dump.c index f679eea73..8063721a8 100644 --- a/src/lib_ccx/params_dump.c +++ b/src/lib_ccx/params_dump.c @@ -1,9 +1,83 @@ +#ifdef _WIN32 +#define strcasecmp _stricmp +#endif +#include #include "lib_ccx.h" #include "ccx_common_option.h" #include "teletext.h" - +#include +#include #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 -} +} \ No newline at end of file diff --git a/src/rust/lib_ccxr/src/common/options.rs b/src/rust/lib_ccxr/src/common/options.rs index 90bd3d215..4a15e37c8 100644 --- a/src/rust/lib_ccxr/src/common/options.rs +++ b/src/rust/lib_ccxr/src/common/options.rs @@ -367,6 +367,7 @@ pub struct Options { /// The end of the segment we actually process pub extraction_end: Option, pub print_file_reports: bool, + pub report_format: Option, /// 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, diff --git a/src/rust/src/args.rs b/src/rust/src/args.rs index b7e9e4f39..ec394919e 100644 --- a/src/rust/src/args.rs +++ b/src/rust/src/args.rs @@ -252,8 +252,14 @@ pub struct Args { pub dvr_ms: bool, #[arg(long, value_name="format", help_heading=OUTPUT_FORMATS)] pub out: Option, + + /// Format for -out=report output (e.g. json) + #[arg(long = "report-format", value_name = "FORMAT", help_heading=OUTPUT_FORMATS)] + pub report_format: Option, + #[arg(long, hide = true)] pub srt: bool, + #[arg(long, hide = true)] pub webvtt: bool, #[arg(long, hide = true)] diff --git a/src/rust/src/common.rs b/src/rust/src/common.rs index 0f22e50c2..aa5df41ff 100755 --- a/src/rust/src/common.rs +++ b/src/rust/src/common.rs @@ -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 = diff --git a/src/rust/src/parser.rs b/src/rust/src/parser.rs index 47573a996..ca8a2f6a3 100644 --- a/src/rust/src/parser.rs +++ b/src/rust/src/parser.rs @@ -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); }