diff --git a/docs/CHANGES.TXT b/docs/CHANGES.TXT index f250da30a..684bb7458 100644 --- a/docs/CHANGES.TXT +++ b/docs/CHANGES.TXT @@ -1,5 +1,6 @@ 1.0 (to be released) ----------------- +- Added Support to just show list of subtitle tracks in the file. - New: Create unit test for rust code (#1615) - Breaking: Major argument flags revamp for CCExtractor (#1564 & #1619) - New: Create a Docker image to simplify the CCExtractor usage without any environmental hustle (#1611) diff --git a/src/ccextractor.c b/src/ccextractor.c index a8676405c..c42ad37bb 100644 --- a/src/ccextractor.c +++ b/src/ccextractor.c @@ -88,6 +88,8 @@ int api_start(struct ccx_s_options api_options) int show_myth_banner = 0; + // Only show params dump if we're not just listing tracks + // if (!api_options.list_tracks_only) params_dump(ctx); // default teletext page @@ -161,6 +163,68 @@ int api_start(struct ccx_s_options api_options) #endif // ENABLE_SHARING stream_mode = ctx->demux_ctx->get_stream_mode(ctx->demux_ctx); + + // Check if we're listing tracks and we have an MKV file + if (api_options.list_tracks_only) + { + // Clear all output + ccx_common_logging.debug_mask = 0; + + // Format-specific track listing + switch (stream_mode) + { + case CCX_SM_MKV: + ret = matroska_loop(ctx); + return ret; + + case CCX_SM_MP4: + // MP4 will be handled in its own case + break; + + case CCX_SM_TRANSPORT: + case CCX_SM_PROGRAM: + if (api_options.list_tracks_only) + { + mprint("\nCCExtractor Track Listing\n"); + mprint("------------------------\n"); + + // Process some packets to populate program information + struct demuxer_data *data = NULL; + int i, ret = 0; + + // Read enough packets to detect program info (PAT and PMT) + // Usually this information appears early in the stream + for (i = 0; i < 2000 && !ctx->demux_ctx->nb_program; i++) + { + ret = ts_readstream(ctx->demux_ctx, &data); + if (ret == CCX_EOF) + break; + if (data) + delete_demuxer_data(data); + data = NULL; + } + + // Now list the tracks with the populated program information + list_ts_tracks(ctx); + return EXIT_OK; + } + + if (!api_options.use_gop_as_pts) // If !0 then the user selected something + api_options.use_gop_as_pts = 0; + if (api_options.ignore_pts_jumps) + ccx_common_timing_settings.disable_sync_check = 1; + mprint("\rAnalyzing data in general mode\n"); + tmp = general_loop(ctx); + if (!ret) + ret = tmp; + break; + + default: + mprint("\nTrack listing is only supported for MKV, MP4 and TS files.\n"); + return EXIT_OK; + } + } + // Disable sync check for raw formats - they have the right timeline. // Also true for bin formats, but -nosync might have created a // broken timeline for debug purposes. @@ -178,6 +242,7 @@ int api_start(struct ccx_s_options api_options) default: break; } + /* ----------------------------------------------------------------- MAIN LOOP ----------------------------------------------------------------- */ @@ -471,6 +536,11 @@ int main(int argc, char *argv[]) { exit(compile_ret); } + if (api_options->list_tracks_only) + { + // Disable all logger output except for our specific track listing + ccx_common_logging.debug_mask = 0; + } int start_ret = api_start(*api_options); return start_ret; diff --git a/src/lib_ccx/ccx_common_option.h b/src/lib_ccx/ccx_common_option.h index 7bc35ac1a..15adb9e64 100644 --- a/src/lib_ccx/ccx_common_option.h +++ b/src/lib_ccx/ccx_common_option.h @@ -99,6 +99,7 @@ struct ccx_s_options // Options from user parameters int notypesetting; struct ccx_boundary_time extraction_start, extraction_end; // Segment we actually process int print_file_reports; + int list_tracks_only; // Flag to only list tracks without processing them ccx_decoder_608_settings settings_608; // Contains the settings for the 608 decoder. ccx_decoder_dtvcc_settings settings_dtvcc; // Same for 708 decoder diff --git a/src/lib_ccx/ccx_encoders_common.c b/src/lib_ccx/ccx_encoders_common.c index c2540dd10..6ab4bc776 100644 --- a/src/lib_ccx/ccx_encoders_common.c +++ b/src/lib_ccx/ccx_encoders_common.c @@ -851,7 +851,7 @@ static int init_output_ctx(struct encoder_ctx *ctx, struct encoder_cfg *cfg) ctx->out[0].filename = NULL; ctx->out[0].with_semaphore = 0; ctx->out[0].semaphore_filename = NULL; - mprint("Sending captions to stdout.\n"); + // mprint("Sending captions to stdout.\n"); } if (cfg->send_to_srv == CCX_TRUE) diff --git a/src/lib_ccx/file_functions.c b/src/lib_ccx/file_functions.c index 064ba0d35..215bae13e 100644 --- a/src/lib_ccx/file_functions.c +++ b/src/lib_ccx/file_functions.c @@ -164,7 +164,11 @@ int switch_to_next_file(struct lib_ccx_ctx *ctx, LLONG bytesinbuffer) break; // The following \n keeps the progress percentage from being overwritten. - mprint("\n\r-----------------------------------------------------------------\n"); + if (!ccx_options.list_tracks_only) + { + // The following \n keeps the progress percentage from being overwritten. + mprint("\n\r-----------------------------------------------------------------\n"); + } mprint("\rOpening file: %s\n", ctx->inputfile[ctx->current_file]); ret = ctx->demux_ctx->open(ctx->demux_ctx, ctx->inputfile[ctx->current_file]); if (ret < 0) diff --git a/src/lib_ccx/lib_ccx.h b/src/lib_ccx/lib_ccx.h index a765ae8f9..b9e1144d4 100644 --- a/src/lib_ccx/lib_ccx.h +++ b/src/lib_ccx/lib_ccx.h @@ -246,6 +246,7 @@ void parse_EPG_packet (struct lib_ccx_ctx *ctx); void EPG_free(struct lib_ccx_ctx *ctx); char* EPG_DVB_decode_string(uint8_t *in, size_t size); void parse_SDT(struct ccx_demuxer *ctx); +void list_ts_tracks(struct lib_ccx_ctx *ctx); // ts_info.c int get_video_stream(struct ccx_demuxer *ctx); diff --git a/src/lib_ccx/matroska.c b/src/lib_ccx/matroska.c index 01b7e634f..b0517fa54 100644 --- a/src/lib_ccx/matroska.c +++ b/src/lib_ccx/matroska.c @@ -719,7 +719,6 @@ enum matroska_track_subtitle_codec_id get_track_subtitle_codec_id(char *codec_id void parse_segment_track_entry(struct matroska_ctx *mkv_ctx) { FILE *file = mkv_ctx->file; - mprint("\nTrack entry:\n"); ULLONG len = read_vint_length(file); ULLONG pos = get_current_byte(file); @@ -1358,9 +1357,41 @@ FILE *create_file(struct lib_ccx_ctx *ctx) return file; } +void print_track_list(struct matroska_ctx *mkv_ctx) +{ + mprint("\n"); + mprint("Available tracks in input file:\n"); + mprint("------------------------------\n"); + + for (int i = 0; i < mkv_ctx->sub_tracks_count; i++) + { + struct matroska_sub_track *track = mkv_ctx->sub_tracks[i]; + + mprint("Track %d: Type: Subtitle, Language: %s, Codec: %s\n", + (int)track->track_number, + track->lang ? track->lang : "unknown", + track->codec_id_string ? track->codec_id_string : "unknown"); + } + + // Print video track if found + if (mkv_ctx->avc_track_number > -1) + { + mprint("Track %d: Type: Video\n", (int)mkv_ctx->avc_track_number); + } + + mprint("\n"); +} + int matroska_loop(struct lib_ccx_ctx *ctx) { - if (ccx_options.write_format_rewritten) + // If we're just listing tracks, completely disable ALL output during parsing + int original_debug_mask = 0; + if (ccx_options.list_tracks_only) + { + original_debug_mask = ccx_common_logging.debug_mask; + ccx_common_logging.debug_mask = 0; + } + else if (ccx_options.write_format_rewritten) { mprint(MATROSKA_WARNING "You are using --out=, but Matroska parser extract subtitles in a recorded format\n"); mprint("--out= will be ignored\n"); @@ -1384,6 +1415,21 @@ int matroska_loop(struct lib_ccx_ctx *ctx) matroska_parse(mkv_ctx); + // Check if we're just listing tracks + if (ccx_options.list_tracks_only) + { + // Restore output capabilities for our specific output + ccx_common_logging.debug_mask = original_debug_mask; + + // Print tracks in a clean format + print_track_list(mkv_ctx); + + // Cleanup and exit early + matroska_free_all(mkv_ctx); + // mprint("\nTrack listing completed. Exiting as requested.\n"); + return 0; + } + // 100% done activity_progress(100, (int)(mkv_ctx->current_second / 60), (int)(mkv_ctx->current_second % 60)); diff --git a/src/lib_ccx/matroska.h b/src/lib_ccx/matroska.h index fe8506001..3a99129f7 100644 --- a/src/lib_ccx/matroska.h +++ b/src/lib_ccx/matroska.h @@ -276,6 +276,7 @@ void save_sub_track(struct matroska_ctx* mkv_ctx, struct matroska_sub_track* tra void free_sub_track(struct matroska_sub_track* track); void matroska_save_all(struct matroska_ctx* mkv_ctx,char* lang); void matroska_free_all(struct matroska_ctx* mkv_ctx); +void print_track_list(struct matroska_ctx *mkv_ctx); void matroska_parse(struct matroska_ctx* mkv_ctx); FILE* create_file(struct lib_ccx_ctx *ctx); diff --git a/src/lib_ccx/mp4.c b/src/lib_ccx/mp4.c index 05df43fe0..0ed70dc57 100644 --- a/src/lib_ccx/mp4.c +++ b/src/lib_ccx/mp4.c @@ -538,6 +538,7 @@ static int process_tx3g(struct lib_ccx_ctx *ctx, struct encoder_ctx *enc_ctx, } */ + int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file) { int mp4_ret = 0; @@ -553,7 +554,11 @@ int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file) enc_ctx->timing = dec_ctx->timing; memset(&dec_sub, 0, sizeof(dec_sub)); - mprint("Opening \'%s\': ", file); + + // Only show opening message if not in list_tracks mode + if (!ccx_options.list_tracks_only) + mprint("Opening \'%s\': ", file); + #ifdef MP4_DEBUG gf_log_set_tool_level(GF_LOG_CONTAINER, GF_LOG_DEBUG); #endif @@ -565,27 +570,105 @@ int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file) return -2; } - mprint("ok\n"); + if (!ccx_options.list_tracks_only) + mprint("ok\n"); track_count = gf_isom_get_track_count(f); - avc_track_count = 0; cc_track_count = 0; + // First pass: count tracks by type for (i = 0; i < track_count; i++) { const u32 type = gf_isom_get_media_type(f, i + 1); const u32 subtype = gf_isom_get_media_subtype(f, i + 1, 1); - mprint("Track %d, type=%c%c%c%c subtype=%c%c%c%c\n", i + 1, (unsigned char)(type >> 24 % 0x100), - (unsigned char)((type >> 16) % 0x100), (unsigned char)((type >> 8) % 0x100), (unsigned char)(type % 0x100), - (unsigned char)(subtype >> 24 % 0x100), - (unsigned char)((subtype >> 16) % 0x100), (unsigned char)((subtype >> 8) % 0x100), (unsigned char)(subtype % 0x100)); + if (type == GF_ISOM_MEDIA_CLOSED_CAPTION || type == GF_ISOM_MEDIA_SUBT || type == GF_ISOM_MEDIA_TEXT) cc_track_count++; if (type == GF_ISOM_MEDIA_VISUAL && subtype == GF_ISOM_SUBTYPE_AVC_H264) avc_track_count++; } + // If in track listing mode, show track information and exit + if (ccx_options.list_tracks_only) + { + mprint("\n"); + mprint("Available tracks in input file:\n"); + mprint("------------------------------\n"); + + for (i = 0; i < track_count; i++) + { + const u32 type = gf_isom_get_media_type(f, i + 1); + const u32 subtype = gf_isom_get_media_subtype(f, i + 1, 1); + + mprint("Track %d: ", i + 1); + + // Get descriptive track type + if (type == GF_ISOM_MEDIA_VISUAL) + { + if (subtype == GF_ISOM_SUBTYPE_AVC_H264) + mprint("Type: H.264 Video"); + else if (subtype == GF_ISOM_SUBTYPE_XDVB) + mprint("Type: XDVB Video"); + else + mprint("Type: Video"); + } + else if (type == GF_ISOM_MEDIA_AUDIO) + { + mprint("Type: Audio"); + } + else if (type == GF_ISOM_MEDIA_CLOSED_CAPTION) + { + if (subtype == GF_QT_SUBTYPE_C608) + mprint("Type: CEA-608 Closed Caption"); + else if (subtype == GF_ISOM_SUBTYPE_C708) + mprint("Type: CEA-708 Closed Caption"); + else + mprint("Type: Closed Caption"); + } + else if (type == GF_ISOM_MEDIA_SUBT || type == GF_ISOM_MEDIA_TEXT) + { + if (subtype == GF_ISOM_SUBTYPE_TX3G) + mprint("Type: TX3G Subtitle"); + else if (subtype == GF_ISOM_SUBTYPE_TEXT) + mprint("Type: Text Subtitle"); + else + mprint("Type: Subtitle"); + } + else + { + mprint("Type: Other (%c%c%c%c/%c%c%c%c)", + (unsigned char)(type >> 24 % 0x100), + (unsigned char)((type >> 16) % 0x100), + (unsigned char)((type >> 8) % 0x100), + (unsigned char)(type % 0x100), + (unsigned char)(subtype >> 24 % 0x100), + (unsigned char)((subtype >> 16) % 0x100), + (unsigned char)((subtype >> 8) % 0x100), + (unsigned char)(subtype % 0x100)); + } + + mprint("\n"); + } + + mprint("\nTrack listing completed. Exiting as requested.\n"); + gf_isom_close(f); + freep(&dec_ctx->xds_ctx); + return 0; + } + + // Regular processing for normal mode follows + + for (i = 0; i < track_count; i++) + { + const u32 type = gf_isom_get_media_type(f, i + 1); + const u32 subtype = gf_isom_get_media_subtype(f, i + 1, 1); + mprint("Track %d, type=%c%c%c%c subtype=%c%c%c%c\n", i + 1, (unsigned char)(type >> 24 % 0x100), + (unsigned char)((type >> 16) % 0x100), (unsigned char)((type >> 8) % 0x100), (unsigned char)(type % 0x100), + (unsigned char)(subtype >> 24 % 0x100), + (unsigned char)((subtype >> 16) % 0x100), (unsigned char)((subtype >> 8) % 0x100), (unsigned char)(subtype % 0x100)); + } + mprint("MP4: found %u tracks: %u avc and %u cc\n", track_count, avc_track_count, cc_track_count); for (i = 0; i < track_count; i++) diff --git a/src/lib_ccx/params.c b/src/lib_ccx/params.c index 570b18084..77d05c39f 100644 --- a/src/lib_ccx/params.c +++ b/src/lib_ccx/params.c @@ -535,6 +535,7 @@ void print_usage(void) mprint(" --datastreamtype: Instead of selecting the stream by its PID, select it\n"); mprint(" by its type (pick the stream that has this type in\n"); mprint(" the PMT)\n"); + mprint(" -lt, --list-tracks: List all tracks found in the input file and exit\n"); mprint(" --streamtype: Assume the data is of this type, don't autodetect. This\n"); mprint(" parameter may be needed if --datapid or -datastreamtype\n"); mprint(" is used and CCExtractor cannot determine how to process\n"); @@ -1618,6 +1619,13 @@ int parse_parameters(struct ccx_s_options *opt, int argc, char *argv[]) fatal(EXIT_MALFORMED_PARAMETER, "--codec has no argument.\n"); } } + // list tracks in the file + if (strcmp(argv[i], "--list-tracks") == 0 || strcmp(argv[i], "-lt") == 0) + { + opt->cc_to_stdout = 1; + opt->list_tracks_only = 1; + continue; + } /*user specified subtitle to be selected */ if (strcmp(argv[i], "--no-codec") == 0) diff --git a/src/lib_ccx/ts_functions.c b/src/lib_ccx/ts_functions.c index 624093f48..bd8e160d6 100644 --- a/src/lib_ccx/ts_functions.c +++ b/src/lib_ccx/ts_functions.c @@ -982,3 +982,60 @@ int ts_get_more_data(struct lib_ccx_ctx *ctx, struct demuxer_data **data) return ret; } + +void list_ts_tracks(struct lib_ccx_ctx *ctx) +{ + struct ccx_demuxer *ctx_demux = ctx->demux_ctx; + int i; + + mprint("\n"); + mprint("Available tracks in input file:\n"); + mprint("------------------------------\n"); + + // Display program information + for (i = 0; i < ctx_demux->nb_program; i++) + { + struct program_info *pinfo = &ctx_demux->pinfo[i]; + + if (pinfo->program_number == -1) + continue; + + mprint("Program: %d\n", pinfo->program_number); + + // PCR PID + if (pinfo->pcr_pid) + { + mprint(" PCR: PID: %u\n", pinfo->pcr_pid); + } + + // Find caption tracks by searching through PIDs + for (int j = 0; j <= MAX_PSI_PID; j++) + { + if (ctx_demux->PIDs_programs[j] && + ctx_demux->PIDs_programs[j]->program_number == pinfo->program_number) + { + struct cap_info *cinfo = get_cinfo(ctx_demux, j); + if (cinfo) + { + char *stream_type_name = get_buffer_type_str(cinfo); + + mprint(" Track %d: Type: %s, PID: %u\n", + j, + stream_type_name ? stream_type_name : "Subtitle/Caption", + j); + + if (stream_type_name) + free(stream_type_name); + } + } + } + } + + if (ctx_demux->nb_program == 0 || + (ctx_demux->nb_program == 1 && ctx_demux->pinfo[0].program_number == -1)) + { + mprint("No program information found in the stream.\n"); + } + + mprint("\nTrack listing completed. Exiting as requested.\n"); +} diff --git a/src/rust/lib_ccxr/src/common/options.rs b/src/rust/lib_ccxr/src/common/options.rs index 489605c44..d6cab03ed 100644 --- a/src/rust/lib_ccxr/src/common/options.rs +++ b/src/rust/lib_ccxr/src/common/options.rs @@ -384,6 +384,8 @@ pub struct Options { pub use_gop_as_pts: Option, /// Replace 0000 with 8080 in HDTV (needed for some cards) pub fix_padding: bool, + /// If true, only list tracks, don't process them + pub list_tracks_only: bool, /// If true, output in stderr progress updates so the GUI can grab them pub gui_mode_reports: bool, /// If true, suppress the output of the progress to stdout @@ -565,6 +567,7 @@ impl Default for Options { messages_target: Default::default(), timestamp_map: Default::default(), dolevdist: true, + list_tracks_only: false, levdistmincnt: 2, levdistmaxpct: 10, investigate_packets: Default::default(), diff --git a/src/rust/src/args.rs b/src/rust/src/args.rs index 92a656202..e9e777b0e 100644 --- a/src/rust/src/args.rs +++ b/src/rust/src/args.rs @@ -359,6 +359,9 @@ pub struct Args { /// the first one we find that contains a suitable stream. #[arg(long, verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)] pub autoprogram: bool, + /// List tracks in input file and exit + #[arg(long = "list-tracks", help_heading = OPTION_AFFECT_PROCESSED)] + pub list_tracks: bool, /// Uses multiple programs from the same input stream. #[arg(long, verbatim_doc_comment, help_heading=OPTIONS_AFFECTING_INPUT_FILES)] pub multiprogram: bool, diff --git a/src/rust/src/common.rs b/src/rust/src/common.rs index b182c5020..4f32b009d 100644 --- a/src/rust/src/common.rs +++ b/src/rust/src/common.rs @@ -62,6 +62,7 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options (*ccx_s_options).webvtt_create_css = options.webvtt_create_css as _; (*ccx_s_options).cc_channel = options.cc_channel as _; (*ccx_s_options).buffer_input = options.buffer_input as _; + (*ccx_s_options).list_tracks_only = options.list_tracks_only as _; (*ccx_s_options).nofontcolor = options.nofontcolor as _; (*ccx_s_options).write_format = options.write_format.to_ctype(); (*ccx_s_options).send_to_srv = options.send_to_srv as _; diff --git a/src/rust/src/parser.rs b/src/rust/src/parser.rs index d3833fbaa..ee6392b74 100644 --- a/src/rust/src/parser.rs +++ b/src/rust/src/parser.rs @@ -675,6 +675,10 @@ impl OptionsExt for Options { self.extract_chapters = true; } + if args.list_tracks { + self.list_tracks_only = true; + } + if args.bufferinput { self.buffer_input = true; }