diff --git a/ChangeLog b/ChangeLog index 177255cea..4546d99f6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2016-04-02 Moritz Bunkus + + * mkvmerge: enhancement: added the minimum timestamp for each + track in verbose/JSON identification outputs (key + "minimum_timestamp") when identifying Matroska files. At most the + first ten seconds are probed; if no block is found for a track + within that range then the key is not output for the track. + 2016-03-31 Moritz Bunkus * MKVToolNix GUI: merge tool change: attachments from source files diff --git a/doc/json-schema/mkvmerge-identification-output-schema-v4.json b/doc/json-schema/mkvmerge-identification-output-schema-v4.json new file mode 100644 index 000000000..ca40f81ed --- /dev/null +++ b/doc/json-schema/mkvmerge-identification-output-schema-v4.json @@ -0,0 +1,370 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://mkvtoolnix.download/doc/mkvmerge-identification-output-schema-v4.json", + "title": "mkvmerge identification output", + "description": "The JSON output produced by mkvmerge's file identification mode", + "type": "object", + "properties": { + "attachments": { + "description": "an array describing the attachments found if any", + "type": "array", + "items": { + "type": "object", + "properties": { + "content_type": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "file_name": { + "type": "string" + }, + "id": { + "type": "integer", + "minimum": 0 + }, + "size": { + "type": "integer", + "minimum": 0 + }, + "properties": { + "type": "object", + "properties": { + "uid": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "type": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "file_name", + "id", + "properties", + "size" + ] + } + }, + "chapters": { + "type": "array", + "items": { + "type": "object", + "properties": { + "num_entries": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "num_entries" + ] + } + }, + "container": { + "description": "information about the identified container", + "type": "object", + "properties": { + "properties": { + "description": "additional properties for the container varying by container format", + "type": "object", + "properties": { + "container_type": { + "description": "A unique number identifying the container type that's supposed to stay constant over all future releases of MKVToolNix", + "type": "integer", + "minLength": 1 + }, + "duration": { + "description": "The file's/segment's duration in nanoseconds", + "type": "integer", + "minimum": 0 + }, + "is_providing_timecodes": { + "description": "States whether or not the container has timestamps for the packets (e.g. Matroska, MP4) or not (e.g. SRT, MP3)", + "type": "boolean" + }, + "segment_uid": { + "description": "A hexadecimal string of the segment's UID (only for Matroska files)", + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "next_segment_uid": { + "description": "A hexadecimal string of the next segment's UID (only for Matroska files)", + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "previous_segment_uid": { + "description": "A hexadecimal string of the previous segment's UID (only for Matroska files)", + "type": "string", + "minLength": 32, + "maxLength": 32 + }, + "other_file": { + "description": "An array of names of additional files processed as well", + "type": "array", + "items": { + "type": "string" + } + }, + "playlist": { + "description": "States whether or not the identified file is a playlist (e.g. MPLS) referring to several other files", + "type": "boolean" + }, + "playlist_chapters": { + "description": "The number of chapters in a playlist if it is a one", + "type": "integer", + "minimum": 0 + }, + "playlist_duration": { + "description": "The total duration in nanoseconds of all files referenced by the playlist if it is a one", + "type": "integer", + "minimum": 0 + }, + "playlist_file": { + "description": "An array of file names the playlist contains", + "type": "array", + "items": { + "type": "string" + } + }, + "playlist_size": { + "description": "The total size in bytes of all files referenced by the playlist if it is a one", + "type": "integer", + "minimum": 0 + }, + "title": { + "type": "string" + } + }, + "additionalProperties": false + }, + "recognized": { + "description": "States whether or not mkvmerge knows about the format", + "type": "boolean" + }, + "supported": { + "description": "States whether or not mkvmerge can read the format", + "type": "boolean" + }, + "type": { + "description": "A human-readable description/name for the container format", + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false, + "required": [ + "recognized", + "supported" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "file_name": { + "description": "the identified file's name", + "type": "string", + "minLength": 1 + }, + "global_tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "num_entries": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "num_entries" + ] + } + }, + "identification_format_version": { + "description": "The output format's version", + "type": "integer", + "minimum": 4, + "maximum": 4 + }, + "track_tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "num_entries": { + "type": "integer" + }, + "track_id": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "num_entries", + "track_id" + ] + } + }, + "tracks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "codec": { + "type": "string", + "minLength": 1 + }, + "id": { + "type": "integer", + "minLength": 0 + }, + "type": { + "type": "string" + }, + "properties": { + "type": "object", + "properties": { + "aac_is_sbr": { + "type": "string", + "enum": [ + "true", + "false", + "unknown" + ] + }, + "audio_bits_per_sample": { + "type": "integer", + "minimum": 0 + }, + "audio_channels": { + "type": "integer", + "minimum": 0 + }, + "audio_sampling_frequency": { + "type": "integer", + "minimum": 0 + }, + "codec_id": { + "type": "string" + }, + "codec_private_data": { + "type": "string" + }, + "codec_private_length": { + "type": "integer", + "minimum": 0 + }, + "content_encoding_algorithms": { + "type": "string", + "minLength": 1 + }, + "default_duration": { + "type": "integer", + "minimum": 0 + }, + "default_track": { + "type": "boolean" + }, + "display_dimensions": { + "type": "string", + "pattern": "^[0-9]+x[0-9]+$" + }, + "enabled_track": { + "type": "boolean" + }, + "forced_track": { + "type": "boolean" + }, + "language": { + "type": "string" + }, + "minimum_timestamp": { + "description": "The minimum timestamp in nanoseconds of all the frames of this track found within the first couple of seconds of the file", + "type": "integer", + "minimum": 0 + }, + "number": { + "type": "integer", + "minimum": 0 + }, + "packetizer": { + "type": "string", + "minLength": 1 + }, + "pixel_dimensions": { + "type": "string", + "pattern": "^[0-9]+x[0-9]+$" + }, + "stereo_mode": { + "type": "integer", + "minimum": 0 + }, + "stream_id": { + "type": "string", + "pattern": "^[0-9a-f]{2}$" + }, + "sub_stream_id": { + "type": "string", + "pattern": "^[0-9a-f]{2}$" + }, + "tag_artist": { + "type": "string" + }, + "tag_bitsps": { + "type": "string" + }, + "tag_bps": { + "type": "string" + }, + "tag_fps": { + "type": "string" + }, + "tag_title": { + "type": "string" + }, + "text_subtitles": { + "type": "boolean" + }, + "track_name": { + "type": "string" + }, + "ts_pid": { + "type": "integer", + "minimum": 0 + }, + "uid": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": [ + "codec", + "id", + "type" + ] + } + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/src/common/id_info.h b/src/common/id_info.h index 549a2bcba..8ad59d5f3 100644 --- a/src/common/id_info.h +++ b/src/common/id_info.h @@ -36,6 +36,7 @@ char const * const duration = "duration"; char const * const enabled_track = "enabled_track"; // track boolean char const * const forced_track = "forced_track"; // track boolean char const * const language = "language"; // track ascii-string format:^\w{3}$ +char const * const minimum_timestamp = "minimum_timestamp"; // track unsigned-integer char const * const mpeg4_p10_es_video = "mpeg4_p10_es_video"; // track boolean char const * const mpeg4_p10_video = "mpeg4_p10_video"; // track boolean char const * const mpegh_p2_es_video = "mpegh_p2_es_video"; // track boolean diff --git a/src/input/r_matroska.cpp b/src/input/r_matroska.cpp index 952b15709..932c2255d 100644 --- a/src/input/r_matroska.cpp +++ b/src/input/r_matroska.cpp @@ -2303,8 +2303,88 @@ kax_reader_c::set_headers() { PTZR(track->ptzr)->get_track_entry()->EnableLacing(track->lacing_flag); } +std::unordered_map +kax_reader_c::determine_minimum_timestamps() { + if (m_tracks.empty()) + return {}; + + std::unordered_map timestamps_by_number; + std::unordered_map tracks_by_number; + + timestamp_c first_timestamp, last_timestamp; + auto probe_time_limit = timestamp_c::s(10); + auto video_time_limit = timestamp_c::s(1); + + for (auto &track : m_tracks) + tracks_by_number[track->track_number] = track; + + while (true) { + try { + auto cluster = std::shared_ptr(m_in_file->read_next_cluster()); + if (!cluster) + return timestamps_by_number; + + auto cluster_tc = FindChildValue(*cluster); + cluster->InitTimecode(cluster_tc, m_tc_scale); + + for (auto const &element : *cluster) { + uint64_t track_number{}; + + if (Is(element)) { + auto block = static_cast(element); + block->SetParent(*cluster); + + last_timestamp = timestamp_c::ns(block->GlobalTimecode()); + track_number = block->TrackNum(); + + } else if (Is(element)) { + auto block = FindChild(static_cast(element)); + if (!block) + continue; + + block->SetParent(*cluster); + + last_timestamp = timestamp_c::ns(block->GlobalTimecode()); + track_number = block->TrackNum(); + + } else + continue; + + if (!first_timestamp.valid()) + first_timestamp = last_timestamp; + + if ((last_timestamp - first_timestamp) >= probe_time_limit) + return timestamps_by_number; + + auto &track = tracks_by_number[track_number]; + if (!track) + continue; + + auto &recorded_timestamp = timestamps_by_number[track_number]; + if (!recorded_timestamp.valid() || (last_timestamp < recorded_timestamp)) + recorded_timestamp = last_timestamp; + + if ( (track->type == 'v') + && ((last_timestamp - recorded_timestamp) < video_time_limit)) + continue; + + tracks_by_number.erase(track_number); + if (tracks_by_number.empty()) + return timestamps_by_number; + } + + } catch (...) { + break; + } + } + + return timestamps_by_number; +} + void kax_reader_c::identify() { + auto minimum_timestamps_by_number = determine_minimum_timestamps(); + auto info = mtx::id::info_c{}; if (!m_title.empty()) @@ -2389,6 +2469,10 @@ kax_reader_c::identify() { track->add_track_tags_to_identification(info); + auto &minimum_timestamp = minimum_timestamps_by_number[track->track_number]; + if (minimum_timestamp.valid()) + info.set(mtx::id::minimum_timestamp, minimum_timestamp.to_ns()); + id_result_track(track->tnum, track->type == 'v' ? ID_RESULT_TRACK_VIDEO : track->type == 'a' ? ID_RESULT_TRACK_AUDIO diff --git a/src/input/r_matroska.h b/src/input/r_matroska.h index 8abd950cd..10081e3b3 100644 --- a/src/input/r_matroska.h +++ b/src/input/r_matroska.h @@ -302,6 +302,8 @@ protected: void init_l1_position_storage(deferred_positions_t &storage); virtual bool has_deferred_element_been_processed(deferred_l1_type_e type, int64_t position); + + virtual std::unordered_map determine_minimum_timestamps(); }; #endif // MTX_INPUT_R_MATROSKA_H diff --git a/src/merge/id_result.h b/src/merge/id_result.h index 3e02d3707..c99d98765 100644 --- a/src/merge/id_result.h +++ b/src/merge/id_result.h @@ -27,7 +27,7 @@ #define ID_RESULT_TAGS "tags" #define ID_RESULT_GLOBAL_TAGS_ID -1 -#define ID_JSON_FORMAT_VERSION 3 +#define ID_JSON_FORMAT_VERSION 4 struct id_result_t { int64_t id; diff --git a/tests/results.txt b/tests/results.txt index c9e4c3e4d..1e6d90943 100644 --- a/tests/results.txt +++ b/tests/results.txt @@ -63,7 +63,7 @@ T_213mp4_broken_pixel_dimensions:b0ffac7af09e87a2e4c7ace7b09b1b46:passed:2005091 T_214one_frame_avi:a4b223e7f22b5e3c2bcf70e455188f79:passed:20051004-192755:0.039489971 T_215X_codec_extradata_avi:6d668338e66c695c72098902b0ce513c-74ac799ad899f703cbb6c6654e5f9f51:passed:20051004-194707:0.052219855 T_216mp4_editlists:5c548c99e1fa4f27afaa5c4fe198924c:passed:20051118-191453:0.106975045 -T_217file_identification:7a2a506954a56f21739e7912897e07ad-903514f1cd84055d9b06ecff5e8d1ea1-d02809ab6dc92a41ef60b571bd117e4d-9b7ea962c56888037231707f9311e080-279167d14c30edc4788cc066f22c941f-ccd6c289299801382b09a9bc13326092-44256b48783ae449ceca65e8419d72e8-cd6039be3553f8bf857c77c8bb99ad1b-55543e6744a979d4a3ffbf0e0bcc855c-d2389a900373adc4864fb3534e145fb0-7febbb46072c2b256735786471c02df4-e2853a83b5964834faa6aa34318aad9c-734e635a1b254319593e24ba89e2434c-1c31748e6eaabc9b84eb737124f457d3-f4bf52b0bf7c773ec337a9d29043b84c-ab8604871b63846cd7df9d5282db3f35-2b05e8b45ff5b8568be5f4aed1bd18bc-2000bb80eaecc682af955c3d64847fc8-3b9e8de7136f2fffa4e2bbf1b4aa38e0-db05f705a1e059b29f4db1ea3ee9d59e-e38a8502cd0c407d8ce517913a2db8c0-588d6dd39935990b73cbb2158cf960fe:passed:20051209-180815:1.882517588 +T_217file_identification:7a2a506954a56f21739e7912897e07ad-903514f1cd84055d9b06ecff5e8d1ea1-5079cbde3f69747c622c08b7fdcee66f-80fa1910c6a50d00753313ebd89e67ab-279167d14c30edc4788cc066f22c941f-ccd6c289299801382b09a9bc13326092-44256b48783ae449ceca65e8419d72e8-cd6039be3553f8bf857c77c8bb99ad1b-55543e6744a979d4a3ffbf0e0bcc855c-d2389a900373adc4864fb3534e145fb0-7febbb46072c2b256735786471c02df4-e2853a83b5964834faa6aa34318aad9c-734e635a1b254319593e24ba89e2434c-1c31748e6eaabc9b84eb737124f457d3-f4bf52b0bf7c773ec337a9d29043b84c-ab8604871b63846cd7df9d5282db3f35-2b05e8b45ff5b8568be5f4aed1bd18bc-2000bb80eaecc682af955c3d64847fc8-3b9e8de7136f2fffa4e2bbf1b4aa38e0-db05f705a1e059b29f4db1ea3ee9d59e-e38a8502cd0c407d8ce517913a2db8c0-588d6dd39935990b73cbb2158cf960fe:passed:20051209-180815:1.882517588 T_218theora:eb650da44ce14cd749ebfb94d270e33c-902b1d711e150c3e923aa43a88970ff4:passed:20060428-105054:0.392912102 T_219srt_short_timecodes:4d58c1d5ddab6368080d54a7585b0f83:passed:20060926-112658:0.117747192 T_220ass_with_comments_at_start:30926355189808086b52edf95c8f49d0:passed:20060926-120101:0.382410266 @@ -190,7 +190,7 @@ T_341vob_interlaced_mpeg2:c51b19eee5fad05ab8cde09ae561677b:passed:20120304-16331 T_342m2ts_interlaced_h264_from_arte:b02e855c2c8bcdde94081df8f43f895f:passed:20120304-165917:4.5443942 T_343m2ts_interlaced_h264_match_of_the_day:5b902a498c5e473ec584a2d1bb39517b:passed:20120304-171453:3.858753744 T_344microdvd_recognition:ok:passed:20120304-175209:0.114439546 -T_345flag_enabled:105245e80fc4a79be3ea27a50ae4564e-24dd401b3d6e814e8a412be30e8a0669:passed:20120304-181150:0.410047671 +T_345flag_enabled:b7a4b2b1ae9e9b946f828a8272e94158-24dd401b3d6e814e8a412be30e8a0669:passed:20120304-181150:0.410047671 T_347h264_misdetected_as_ac3:f2e3101ea1fad1752d24c9143fbea954:passed:20120305-160017:1.722170533 T_348srt_negative_timecodes2:beaee30a910a72560a844628f3a072ac:passed:20120307-115726:0.120842971 T_349h264_interlaced_default_duration:abc9dd7b4579a2e14783271d1d64d486-051cfe8f2749c2c8bdda0f2e2110af68-051cfe8f2749c2c8bdda0f2e2110af68-ff863149bd403bf445a8542f0d830e99:passed:20120307-184849:7.012794592 @@ -273,7 +273,7 @@ T_424avc_recover_point_sei_before_second_field:6398b8fc42da442ccc2d599a9df63d89: T_425mpeg_ts_timestamp_outlier:a5204ef83771296b32ea8d7052cba956:passed:20140305-203603:2.509694471 T_426extract_write_bom_only_once:a9255d40de93e2731aaead0a746e582f-a9255d40de93e2731aaead0a746e582f:passed:20140310-195606:0.0 T_427ui_locale_pt_BR:8719aedc77a0435129c79e3a061642bf-344b51e9ae6fe2d8ce60fef18ee0e7d1:passed:20140418-103113:0.143370167 -T_428mkv_misdetected_as_ass:1f0df0dbdbbe6f310e6edc12fc22b29e:passed:20140518-155446:0.033341203 +T_428mkv_misdetected_as_ass:b5818edd4d65f28fae73bfb2e7d3be55:passed:20140518-155446:0.033341203 T_429track_statistics_tags:43d4c0ec5cbbc31018715e62a99377df-b726a7c00558fd45468ceb163016eeb4-68d639b8e42bfd0e2b7b4105dea7c091-e179bf70e6b4f2316292d70f4eda2c2e:passed:20140524-194544:0.635343822 T_430cues_multiple_blocks_same_timecode:f1ab5c927064537eb59ab0f5195d6a1d:passed:20140525-173642:0.033316759 T_431ssa_comments_exclamation_mark:3caa9ad1716134cc1f3e229b88ff94ea:passed:20140618-232324:0.072735677 @@ -349,7 +349,7 @@ T_500mp4_eac3_fourcc_ec_3:c98039cef2a6919c6fa3d9b1912873cf:passed:20150621-22424 T_501mpeg_ts_pat_and_pmt_crc_errors:bc9d2e9e0fa7d962b04e719e9bce3856:passed:20150704-110859:0.857051787 T_502ui_locale_sr_RS:650e05c9c091234882980f4c1da05dc4-25fa402006e7971d8e5a2b1139a4c4d8:passed:20150829-213708:0.062939538 T_503pcm_in_mkv_varying_samples_per_packet:091442963a5879e8e1d8fca62458a06a:passed:20151004-215848:0.430291421 -T_504dts_96_24_identification:1837ab5b411944e143f9dae6fe6436d8-bb7c41b5aa1b57f18741b792897b0b59-8ad5d7ed98bee700e91689784c82717f:passed:20151006-223804:2.278995107 +T_504dts_96_24_identification:1837ab5b411944e143f9dae6fe6436d8-5d139c9c7c7233563b2ab98d5f63deb2-8ad5d7ed98bee700e91689784c82717f:passed:20151006-223804:2.278995107 T_505cisco_talos_can_0036:bf0fedc494cf99a0920d7a6e69edf952-6ef415b0f84d3e5dd435244362a37584:passed:20151020-161153:0.071686357 T_506cisco_talos_can_0037:5461288548eac976164cd13f01bc9426-ed695caee29b1456da8629d38321ec9c-92b7169fc05ddf54c46816869c108f31-54a55a6d87bd4c08269891efb03980b3-fef3d018523c7d1fbed763f6666c1ae2-ac584cc44854f9396739df6e93d78acc-b415b2ef2a6dddf5d89733446fae2970-dd53fee23372c569d35e0b2918d86239:passed:20151020-161234:0.319298931 T_507rerender_track_headers:0572728ed778526af9a0e36c7872fab6-c49848577778e5c5146e5b875e3a07d6:passed:20151022-104930:2.573204876 @@ -357,7 +357,7 @@ T_508splitting_by_parts_with_segment_linking:existence0-true-true-true-existence T_509rerender_track_headers_chapters_attachments:21e5c9991875b114014050d68bd2a31c:passed:20151115-230226:0.287840782 T_510propedit_add_attachments_without_meta_seek_present:770103c238a0f502c9ec55f0599d8544:passed:20151121-101043:0.070892905 T_511propedit_ensure_seek_head_exists_at_front:20f53afd94e39f5bbf3f1091eefbe31d:passed:20151129-194025:0.152563199 -T_512json_identification:e2c1bd814ea805d9711a3875e646a51d+ok-d815a6390e25b861d21093ef66f17191+ok-19f449d85ad9c8c2a3d3840bc3734e58+ok-a86b43982a842c6b9c8572534e40dcca+ok-bf4664fb8aac8ad51e16d018d2c6697e+ok-e6e342718925ef84c9c65288d477ac76+ok-59c47f40f7c35d90a4a8ba0f15aebf5b+ok-b524a708c8e65517026daf4e18c6efcf+ok-eabca6419fcd27291e5db66c1e391c0d+ok-01f6792cb05fb5dd4823a5919a5565b4+ok-3f9fd90d34f591fa205eb235b1efe36c+ok-f7c9d7c612f38a0a2c0548a38008882f+ok-10bd29b4b37ba8e8fd746a45e67af4a1+ok-b137e5af83e68f2c52af9bfe3976a977+ok-c8b2bd3f66486fc47ad98d7b6a06f713+ok:passed:20151207-223859:1.876447112 +T_512json_identification:e2c1bd814ea805d9711a3875e646a51d+ok-d815a6390e25b861d21093ef66f17191+ok-19f449d85ad9c8c2a3d3840bc3734e58+ok-a86b43982a842c6b9c8572534e40dcca+ok-bf4664fb8aac8ad51e16d018d2c6697e+ok-e6e342718925ef84c9c65288d477ac76+ok-59c47f40f7c35d90a4a8ba0f15aebf5b+ok-40e49f1a1c18e4a2975e7472e6b20c60+ok-198c8949e157e08526189cc0aec964c8+ok-01f6792cb05fb5dd4823a5919a5565b4+ok-3f9fd90d34f591fa205eb235b1efe36c+ok-f7c9d7c612f38a0a2c0548a38008882f+ok-4acc3a60e97069072c3e586888a32776+ok-b137e5af83e68f2c52af9bfe3976a977+ok-c8b2bd3f66486fc47ad98d7b6a06f713+ok:passed:20151207-223859:1.876447112 T_513vp9_10bit_key_frame_detection:9eab6e85ec792dcf670873d70a87f6ea:passed:20151208-224613:0.267556245 T_514remove_track_statistics_tags_during_remux:43d4c0ec5cbbc31018715e62a99377df-afe190e36be530592fe3b83fb28d3e69-a7f246fe02132a1fb9cd3d7d0f85f180:passed:20151215-134129:1.426290351 T_515aac_sampling_frequency_8000_is_not_sbr:b8f857faf759eae5362fefbb7cbeed23:passed:20151219-130357:0.066237884 diff --git a/tests/test.d/simple_test.rb b/tests/test.d/simple_test.rb index df5dcbe65..5f5fadf94 100644 --- a/tests/test.d/simple_test.rb +++ b/tests/test.d/simple_test.rb @@ -319,7 +319,7 @@ class SimpleTest json_store = JsonSchema::DocumentStore.new parser = JsonSchema::Parser.new expander = JsonSchema::ReferenceExpander.new - schema = parser.parse JSON.load(File.read("../doc/json-schema/mkvmerge-identification-output-schema-v3.json")) + schema = parser.parse JSON.load(File.read("../doc/json-schema/mkvmerge-identification-output-schema-v4.json")) expander.expand(schema, store: json_store) json_store.add_schema schema