diff --git a/ChangeLog b/ChangeLog index 999378c9a..84a8be066 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2015-01-01 Moritz Bunkus + + * mkvmerge: new feature: implemented support for MP4 DASH + files. Implements #1038. + 2014-12-31 Moritz Bunkus * mkvmerge: new feature: implemented reading MPEG-H p2/HEVC video diff --git a/src/input/r_qtmp4.cpp b/src/input/r_qtmp4.cpp index 94c13de74..4a8fdee2a 100644 --- a/src/input/r_qtmp4.cpp +++ b/src/input/r_qtmp4.cpp @@ -121,12 +121,14 @@ qtmp4_reader_c::probe_file(mm_io_c *in, qtmp4_reader_c::qtmp4_reader_c(const track_info_c &ti, const mm_io_cptr &in) : generic_reader_c(ti, in) - , m_mdat_pos(-1) - , m_mdat_size(0) , m_time_scale(1) , m_compression_algorithm{} , m_main_dmx(-1) , m_audio_encoder_delay_samples(0) + , m_moof_offset{} + , m_fragment_implicit_offset{} + , m_fragment{} + , m_track_for_fragment{} , m_debug_chapters{ "qtmp4|qtmp4_full|qtmp4_chapters"} , m_debug_headers{ "qtmp4|qtmp4_full|qtmp4_headers"} , m_debug_tables{ "qtmp4_full|qtmp4_tables"} @@ -213,45 +215,75 @@ qtmp4_reader_c::parse_headers() { m_in->setFilePointer(0); bool headers_parsed = false; - do { - qt_atom_t atom = read_atom(); - mxdebug_if(m_debug_headers, boost::format("atom %1% human readable? %2%\n") % atom % atom.fourcc.human_readable()); + bool moof_found = false; + bool mdat_found = false; - if (atom.fourcc == "ftyp") { - auto tmp = fourcc_c{m_in}; - mxdebug_if(m_debug_headers, boost::format(" File type major brand: %1%\n") % tmp); - tmp = fourcc_c{m_in}; - mxdebug_if(m_debug_headers, boost::format(" File type minor brand: %1%\n") % tmp); + try { + while (true) { + qt_atom_t atom = read_atom(); + mxdebug_if(m_debug_headers, boost::format("'%1%' atom, size %2%, at %3%–%4%, human readable? %5%\n") % atom.fourcc % atom.size % atom.pos % (atom.pos + atom.size) % atom.fourcc.human_readable()); - for (idx = 0; idx < ((atom.size - 16) / 4); ++idx) { + if (atom.fourcc == "ftyp") { + auto tmp = fourcc_c{m_in}; + mxdebug_if(m_debug_headers, boost::format(" File type major brand: %1%\n") % tmp); tmp = fourcc_c{m_in}; - mxdebug_if(m_debug_headers, boost::format(" File type compatible brands #%1%: %2%\n") % idx % tmp); - } + mxdebug_if(m_debug_headers, boost::format(" File type minor brand: %1%\n") % tmp); - } else if (atom.fourcc == "moov") { - handle_moov_atom(atom.to_parent(), 0); - headers_parsed = true; + for (idx = 0; idx < ((atom.size - 16) / 4); ++idx) { + tmp = fourcc_c{m_in}; + mxdebug_if(m_debug_headers, boost::format(" File type compatible brands #%1%: %2%\n") % idx % tmp); + } - } else if (atom.fourcc == "mdat") { - m_mdat_pos = m_in->getFilePointer(); - m_mdat_size = atom.size; - skip_atom(); + } else if (atom.fourcc == "moov") { + handle_moov_atom(atom.to_parent(), 0); + headers_parsed = true; - } else if (atom.fourcc.human_readable()) - skip_atom(); + } else if (atom.fourcc == "mdat") { + skip_atom(); + mdat_found = true; - else if (!resync_to_top_level_atom(atom.pos)) - break; + } else if (atom.fourcc == "moof") { + handle_moof_atom(atom.to_parent(), 0, atom); + moof_found = true; - } while (!m_in->eof() && (!headers_parsed || (-1 == m_mdat_pos))); + } else if (atom.fourcc.human_readable()) + skip_atom(); + + else if (!resync_to_top_level_atom(atom.pos)) + break; + + if (m_in->eof()) + break; + + if (headers_parsed && mdat_found && !moof_found) + break; + + } + } catch (mtx::mm_io::exception &) { + } if (!headers_parsed) mxerror(Y("Quicktime/MP4 reader: Have not found any header atoms.\n")); - if (-1 == m_mdat_pos) + + if (!mdat_found) mxerror(Y("Quicktime/MP4 reader: Have not found the 'mdat' atom. No movie data found.\n")); - m_in->setFilePointer(m_mdat_pos); + verify_track_parameters_and_update_indexes(); + read_chapter_track(); + + brng::remove_erase_if(m_demuxers, [this](qtmp4_demuxer_cptr const &dmx) { return !dmx->ok || dmx->is_chapters(); }); + + detect_interleaving(); + + if (!g_identifying) + calculate_timecodes(); + + mxdebug_if(m_debug_headers, boost::format("Number of valid tracks found: %1%\n") % m_demuxers.size()); +} + +void +qtmp4_reader_c::verify_track_parameters_and_update_indexes() { for (auto &dmx : m_demuxers) { if (m_chapter_track_ids[dmx->container_id]) dmx->type = 'C'; @@ -273,17 +305,6 @@ qtmp4_reader_c::parse_headers() { dmx->ok = dmx->update_tables(m_time_scale); } - - read_chapter_track(); - - brng::remove_erase_if(m_demuxers, [this](qtmp4_demuxer_cptr const &dmx) { return !dmx->ok || dmx->is_chapters(); }); - - detect_interleaving(); - - if (!g_identifying) - calculate_timecodes(); - - mxdebug_if(m_debug_headers, boost::format("Number of valid tracks found: %1%\n") % m_demuxers.size()); } void @@ -314,7 +335,7 @@ qtmp4_reader_c::handle_audio_encoder_delay(qtmp4_demuxer_cptr &dmx) { } #define print_basic_atom_info() \ - mxdebug_if(m_debug_headers, boost::format("%1%'%2%' atom, size %3%, at %4%\n") % space(2 * level + 1) % atom.fourcc % atom.size % atom.pos); + mxdebug_if(m_debug_headers, boost::format("%1%'%2%' atom, size %3%, at %4%–%5%\n") % space(2 * level + 1) % atom.fourcc % atom.size % atom.pos % (atom.pos + atom.size)); #define print_atom_too_small_error(name, type) \ mxerror(boost::format(Y("Quicktime/MP4 reader: '%1%' atom is too small. Expected size: >= %2%. Actual size: %3%.\n")) \ @@ -555,6 +576,9 @@ qtmp4_reader_c::handle_moov_atom(qt_atom_t parent, else if (atom.fourcc == "udta") handle_udta_atom(atom.to_parent(), level + 1); + else if (atom.fourcc == "mvex") + handle_mvex_atom(atom.to_parent(), level + 1); + else if (atom.fourcc == "trak") { qtmp4_demuxer_cptr new_dmx(new qtmp4_demuxer_c); new_dmx->id = m_demuxers.size(); @@ -569,6 +593,182 @@ qtmp4_reader_c::handle_moov_atom(qt_atom_t parent, } } +void +qtmp4_reader_c::handle_mvex_atom(qt_atom_t parent, + int level) { + while (8 <= parent.size) { + qt_atom_t atom = read_atom(); + print_basic_atom_info(); + + if (atom.fourcc == "trex") + handle_trex_atom(atom.to_parent(), level + 1); + + skip_atom(); + parent.size -= atom.size; + } +} + +void +qtmp4_reader_c::handle_trex_atom(qt_atom_t, + int level) { + m_in->skip(1 + 3); // Version, flags + + auto track_id = m_in->read_uint32_be(); + auto &defaults = m_track_defaults[track_id]; + defaults.sample_description_id = m_in->read_uint32_be(); + defaults.sample_duration = m_in->read_uint32_be(); + defaults.sample_size = m_in->read_uint32_be(); + defaults.sample_flags = m_in->read_uint32_be(); + + mxdebug_if(m_debug_headers, boost::format("%1%Sample defaults for track ID %2%: description idx %3% duration %4% size %5% flags %6%\n") + % space(level * 2 + 1) % track_id % defaults.sample_description_id % defaults.sample_duration % defaults.sample_size % defaults.sample_flags); +} + +void +qtmp4_reader_c::handle_moof_atom(qt_atom_t parent, + int level, + qt_atom_t const &moof_atom) { + m_moof_offset = moof_atom.pos; + m_fragment_implicit_offset = moof_atom.pos; + + while (8 <= parent.size) { + qt_atom_t atom = read_atom(); + print_basic_atom_info(); + + if (atom.fourcc == "traf") + handle_traf_atom(atom.to_parent(), level + 1); + + skip_atom(); + parent.size -= atom.size; + } +} + +void +qtmp4_reader_c::handle_traf_atom(qt_atom_t parent, + int level) { + while (8 <= parent.size) { + qt_atom_t atom = read_atom(); + print_basic_atom_info(); + + if (atom.fourcc == "tfhd") + handle_tfhd_atom(atom.to_parent(), level + 1); + + else if (atom.fourcc == "trun") + handle_trun_atom(atom.to_parent(), level + 1); + + skip_atom(); + parent.size -= atom.size; + } + + m_fragment = nullptr; + m_track_for_fragment = nullptr; +} + +void +qtmp4_reader_c::handle_tfhd_atom(qt_atom_t, + int level) { + m_in->skip(1); // Version + + auto flags = m_in->read_uint24_be(); + auto track_id = m_in->read_uint32_be(); + auto track_itr = brng::find_if(m_demuxers, [this, track_id](qtmp4_demuxer_cptr const &dmx) { return dmx->container_id == track_id; }); + + if (!track_id || !map_has_key(m_track_defaults, track_id) || (m_demuxers.end() == track_itr)) { + mxdebug_if(m_debug_headers, + boost::format("%1%tfhd atom with track_id(%2%) == 0, no entry in trex for it or no track(%3%) found\n") + % space(level * 2 + 1) % track_id % (m_demuxers.end() == track_itr ? nullptr : track_itr->get())); + return; + } + + auto &defaults = m_track_defaults[track_id]; + auto &track = **track_itr; + + track.m_fragments.emplace_back(); + auto &fragment = track.m_fragments.back(); + + fragment.track_id = track_id; + fragment.moof_offset = m_moof_offset; + fragment.implicit_offset = m_fragment_implicit_offset; + fragment.base_data_offset = flags & QTMP4_TFHD_BASE_DATA_OFFSET ? m_in->read_uint64_be() + : flags & QTMP4_TFHD_DEFAULT_BASE_IS_MOOF ? fragment.moof_offset + : fragment.implicit_offset; + fragment.sample_description_id = flags & QTMP4_TFHD_SAMPLE_DESCRIPTION_ID ? m_in->read_uint32_be() : defaults.sample_description_id; + fragment.sample_duration = flags & QTMP4_TFHD_DEFAULT_DURATION ? m_in->read_uint32_be() : defaults.sample_duration; + fragment.sample_size = flags & QTMP4_TFHD_DEFAULT_SIZE ? m_in->read_uint32_be() : defaults.sample_size; + fragment.sample_flags = flags & QTMP4_TFHD_DEFAULT_FLAGS ? m_in->read_uint32_be() : defaults.sample_flags; + + m_fragment = &fragment; + m_track_for_fragment = &track; + + mxdebug_if(m_debug_headers, + boost::format("%1%Atom flags 0x%|2$05x| track_id %3% fragment content: moof_off %4% implicit_off %5% base_data_off %6% stsd_id %7% sample_duration %8% sample_size %9% sample_flags %10%\n") + % space(level * 2 + 1) % flags % track_id + % fragment.moof_offset % fragment.implicit_offset % fragment.base_data_offset % fragment.sample_description_id % fragment.sample_duration % fragment.sample_size % fragment.sample_flags); +} + +void +qtmp4_reader_c::handle_trun_atom(qt_atom_t, + int level) { + if (!m_fragment || !m_track_for_fragment) { + mxdebug_if(m_debug_headers, boost::format("%1%No current fragment (%2%) or track for that fragment (%3%)\n") % space(2 * level + 1) % m_fragment % m_track_for_fragment); + return; + } + + m_in->skip(1); // Version + auto flags = m_in->read_uint24_be(); + auto entries = m_in->read_uint32_be(); + auto &track = *m_track_for_fragment; + + if (track.raw_frame_offset_table.empty() && !track.sample_table.empty()) + track.raw_frame_offset_table.emplace_back(track.sample_table.size(), 0); + + auto data_offset = flags & QTMP4_TRUN_DATA_OFFSET ? m_in->read_uint32_be() : 0; + auto first_sample_flags = flags & QTMP4_TRUN_FIRST_SAMPLE_FLAGS ? m_in->read_uint32_be() : m_fragment->sample_flags; + auto offset = m_fragment->base_data_offset + data_offset; + + for (auto idx = 0u; idx < entries; ++idx) { + auto sample_duration = flags & QTMP4_TRUN_SAMPLE_DURATION ? m_in->read_uint32_be() : m_fragment->sample_duration; + auto sample_size = flags & QTMP4_TRUN_SAMPLE_SIZE ? m_in->read_uint32_be() : m_fragment->sample_size; + auto sample_flags = flags & QTMP4_TRUN_SAMPLE_FLAGS ? m_in->read_uint32_be() : idx > 0 ? m_fragment->sample_flags : first_sample_flags; + auto ctts_duration = flags & QTMP4_TRUN_SAMPLE_CTS_OFFSET ? m_in->read_uint32_be() : 0; + auto keyframe = track.type == 'v' ? true : !(sample_flags & (QTMP4_FRAG_SAMPLE_FLAG_IS_NON_SYNC | QTMP4_FRAG_SAMPLE_FLAG_DEPENDS_YES)); + + track.durmap_table.emplace_back(1, sample_duration); + track.sample_table.emplace_back(sample_size); + track.chunk_table.emplace_back(1, offset); + track.raw_frame_offset_table.emplace_back(1, ctts_duration); + + if (keyframe) + track.keyframe_table.emplace_back(track.num_frames_from_trun); + + offset += sample_size; + + track.num_frames_from_trun++; + } + + m_fragment->implicit_offset = offset; + m_fragment_implicit_offset = offset; + + mxdebug_if(m_debug_headers, boost::format("%1%Number of entries: %2%\n") % space((level + 1) * 2 + 1) % entries); + + if (m_debug_tables) { + auto spc = space((level + 2) * 2 + 1); + auto durmap_start = track.durmap_table.size() - entries; + auto sample_start = track.sample_table.size() - entries; + auto chunk_start = track.chunk_table.size() - entries; + auto frame_offset_start = track.raw_frame_offset_table.size() - entries; + + for (auto idx = 0u; idx < entries; ++idx) + mxdebug(boost::format("%1%%2%: duration %3% size %4% data start %5% end %6% pts offset %7%\n") + % spc % idx + % track.durmap_table[durmap_start + idx].duration + % track.sample_table[sample_start + idx].size + % track.chunk_table[chunk_start + idx].pos + % (track.sample_table[sample_start + idx].size + track.chunk_table[chunk_start + idx].pos) + % track.raw_frame_offset_table[frame_offset_start + idx].offset); + } +} + void qtmp4_reader_c::handle_mvhd_atom(qt_atom_t atom, int level) { @@ -845,13 +1045,8 @@ qtmp4_reader_c::handle_stco_atom(qtmp4_demuxer_cptr &new_dmx, mxdebug_if(m_debug_headers, boost::format("%1%Chunk offset table: %2% entries\n") % space(level * 2 + 1) % count); - size_t i; - for (i = 0; i < count; ++i) { - qt_chunk_t chunk; - - chunk.pos = m_in->read_uint32_be(); - new_dmx->chunk_table.push_back(chunk); - } + for (auto i = 0u; i < count; ++i) + new_dmx->chunk_table.emplace_back(0, m_in->read_uint32_be()); if (m_debug_tables) for (auto const &chunk : new_dmx->chunk_table) @@ -867,13 +1062,8 @@ qtmp4_reader_c::handle_co64_atom(qtmp4_demuxer_cptr &new_dmx, mxdebug_if(m_debug_headers, boost::format("%1%64bit chunk offset table: %2% entries\n") % space(level * 2 + 1) % count); - size_t i; - for (i = 0; i < count; ++i) { - qt_chunk_t chunk; - - chunk.pos = m_in->read_uint64_be(); - new_dmx->chunk_table.push_back(chunk); - } + for (auto i = 0u; i < count; ++i) + new_dmx->chunk_table.emplace_back(0, m_in->read_uint64_be()); if (m_debug_tables) for (auto const &chunk : new_dmx->chunk_table) diff --git a/src/input/r_qtmp4.h b/src/input/r_qtmp4.h index f9030ad87..2d7323518 100644 --- a/src/input/r_qtmp4.h +++ b/src/input/r_qtmp4.h @@ -25,6 +25,24 @@ #include "output/p_video.h" #include "input/qtmp4_atoms.h" +#define QTMP4_TFHD_BASE_DATA_OFFSET 0x000001 +#define QTMP4_TFHD_SAMPLE_DESCRIPTION_ID 0x000002 +#define QTMP4_TFHD_DEFAULT_DURATION 0x000008 +#define QTMP4_TFHD_DEFAULT_SIZE 0x000010 +#define QTMP4_TFHD_DEFAULT_FLAGS 0x000020 +#define QTMP4_TFHD_DURATION_IS_EMPTY 0x010000 +#define QTMP4_TFHD_DEFAULT_BASE_IS_MOOF 0x020000 + +#define QTMP4_TRUN_DATA_OFFSET 0x000001 +#define QTMP4_TRUN_FIRST_SAMPLE_FLAGS 0x000004 +#define QTMP4_TRUN_SAMPLE_DURATION 0x000100 +#define QTMP4_TRUN_SAMPLE_SIZE 0x000200 +#define QTMP4_TRUN_SAMPLE_FLAGS 0x000400 +#define QTMP4_TRUN_SAMPLE_CTS_OFFSET 0x000800 + +#define QTMP4_FRAG_SAMPLE_FLAG_IS_NON_SYNC 0x00010000 +#define QTMP4_FRAG_SAMPLE_FLAG_DEPENDS_YES 0x01000000 + struct qt_durmap_t { uint32_t number; uint32_t duration; @@ -34,6 +52,12 @@ struct qt_durmap_t { , duration{} { } + + qt_durmap_t(uint32_t p_number, uint32_t p_duration) + : number{p_number} + , duration{p_duration} + { + } }; struct qt_chunk_t { @@ -49,6 +73,14 @@ struct qt_chunk_t { , pos{} { } + + qt_chunk_t(uint32_t p_size, uint64_t p_pos) + : samples{} + , size{p_size} + , desc{} + , pos{p_pos} + { + } }; struct qt_chunkmap_t { @@ -96,6 +128,13 @@ struct qt_sample_t { , pos{} { } + + qt_sample_t(uint32_t p_size) + : pts{} + , size{p_size} + , pos{} + { + } }; struct qt_frame_offset_t { @@ -107,6 +146,12 @@ struct qt_frame_offset_t { , offset{} { } + + qt_frame_offset_t(uint32_t p_count, uint32_t p_offset) + : count{p_count} + , offset{p_offset} + { + } }; struct qt_index_t { @@ -133,6 +178,34 @@ struct qt_index_t { } }; +struct qt_track_defaults_t { + unsigned int sample_description_id, sample_duration, sample_size, sample_flags; + + qt_track_defaults_t() + : sample_description_id{} + , sample_duration{} + , sample_size{} + , sample_flags{} + { + } +}; + +struct qt_fragment_t { + unsigned int track_id, sample_description_id, sample_duration, sample_size, sample_flags; + uint64_t base_data_offset, moof_offset, implicit_offset; + + qt_fragment_t() + : track_id{} + , sample_description_id{} + , sample_duration{} + , sample_size{} + , sample_flags{} + , base_data_offset{} + , moof_offset{} + , implicit_offset{} + {} +}; + struct qtmp4_demuxer_c { bool ok; @@ -144,7 +217,7 @@ struct qtmp4_demuxer_c { codec_c codec; pcm_packetizer_c::pcm_format_e m_pcm_format; - int64_t time_scale, duration, global_duration, constant_editlist_offset_ns; + int64_t time_scale, duration, global_duration, constant_editlist_offset_ns, num_frames_from_trun; uint32_t sample_size; std::vector sample_table; @@ -159,6 +232,7 @@ struct qtmp4_demuxer_c { std::vector timecodes, durations, frame_indices; std::vector m_index; + std::vector m_fragments; double fps; @@ -197,6 +271,7 @@ struct qtmp4_demuxer_c { , duration{0} , global_duration{0} , constant_editlist_offset_ns{0} + , num_frames_from_trun{} , sample_size{0} , fps{0.0} , esds_parsed{false} @@ -335,13 +410,18 @@ class qtmp4_reader_c: public generic_reader_c { private: std::vector m_demuxers; std::unordered_map m_chapter_track_ids; - int64_t m_mdat_pos, m_mdat_size; + std::unordered_map m_track_defaults; + uint32_t m_time_scale; fourcc_c m_compression_algorithm; int m_main_dmx; unsigned int m_audio_encoder_delay_samples; + uint64_t m_moof_offset, m_fragment_implicit_offset; + qt_fragment_t *m_fragment; + qtmp4_demuxer_c *m_track_for_fragment; + debugging_option_c m_debug_chapters, m_debug_headers, m_debug_tables, m_debug_interleaving, m_debug_resync; public: @@ -364,6 +444,7 @@ public: protected: virtual void parse_headers(); + virtual void verify_track_parameters_and_update_indexes(); virtual void calculate_timecodes(); virtual qt_atom_t read_atom(mm_io_c *read_from = nullptr, bool exit_on_error = true); virtual bool resync_to_top_level_atom(uint64_t start_pos); @@ -384,6 +465,12 @@ protected: virtual void handle_meta_atom(qt_atom_t parent, int level); virtual void handle_ilst_atom(qt_atom_t parent, int level); virtual void handle_4dashes_atom(qt_atom_t parent, int level); + virtual void handle_mvex_atom(qt_atom_t parent, int level); + virtual void handle_trex_atom(qt_atom_t parent, int level); + virtual void handle_moof_atom(qt_atom_t parent, int level, qt_atom_t const &moof_atom); + virtual void handle_traf_atom(qt_atom_t parent, int level); + virtual void handle_tfhd_atom(qt_atom_t parent, int level); + virtual void handle_trun_atom(qt_atom_t parent, int level); virtual void handle_stbl_atom(qtmp4_demuxer_cptr &new_dmx, qt_atom_t parent, int level); virtual void handle_stco_atom(qtmp4_demuxer_cptr &new_dmx, qt_atom_t parent, int level); virtual void handle_co64_atom(qtmp4_demuxer_cptr &new_dmx, qt_atom_t parent, int level); diff --git a/tests/results.txt b/tests/results.txt index d775017f3..13f574456 100644 --- a/tests/results.txt +++ b/tests/results.txt @@ -299,3 +299,4 @@ T_450aac_loas_latm_in_mpeg_ts:e386768eaef3c9efb6d34529651f815f:passed:20141229-2 T_451aac_loas_latm_raw:5a2db12f6eea0bcd37a5d524d8c4c91c:passed:20141230-155351:0.589778873 T_452mkvinfo_track_statistics_frame_order:a8664e39f2f29619bb5f81a27f1e7524-0192882c02e10ee431069209340af477:passed:20141230-182428:1.579605124 T_453mp4_with_hevc:c0c41f1942550b8ae5fac8b93e914185:passed:20141231-125834:0.046223562 +T_454mp4_dash:e2e5f8441f1dcf48256d49faa588b47c-01518029ec5ea87609b3fb388b2bd751:passed:20141231-214733:0.429881173 diff --git a/tests/test-454mp4_dash.rb b/tests/test-454mp4_dash.rb new file mode 100755 index 000000000..8232e2073 --- /dev/null +++ b/tests/test-454mp4_dash.rb @@ -0,0 +1,8 @@ +#!/usr/bin/ruby -w + +# T_454mp4_dash +describe "mkvmerge / MP4 DASH files" + +dir = "data/mp4/dash" +test_merge "#{dir}/car-20120827-85.mp4 #{dir}/car-20120827-8c.mp4" +test_merge "#{dir}/dragon-age-inquisition-H1LkM6IVlm4-video.mp4 #{dir}/dragon-age-inquisition-H1LkM6IVlm4-audio.mp4"