Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGES.TXT
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
0.96.6 (unreleased)
-------------------
- Add optional machine-readable JSON output for -out=report via --report-format json
- 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)
- Fix: Prevent infinite loop on truncated MKV files
Expand Down
1 change: 1 addition & 0 deletions src/lib_ccx/ccx_common_option.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/lib_ccx/ccx_common_option.h
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
347 changes: 345 additions & 2 deletions src/lib_ccx/params_dump.c
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
}
Loading
Loading