mkvmerge: implement adjusting chapter timestamps with --sync

There's a new option called `--chapter-sync …` that behaves just like
`--sync -2:…`. Both can be used to adjust the timestamps of chapters
read from containers (such as Matroska or MP4 files) and chapter
files (both the XML format and the Ogg comment style format).

Part of the implementation of #2358.
This commit is contained in:
Moritz Bunkus 2018-09-18 22:07:41 +02:00
parent a684d519bb
commit 42f8538a70
No known key found for this signature in database
GPG Key ID: 74AF00ADF2E32C85
11 changed files with 127 additions and 22 deletions

View File

@ -6,6 +6,10 @@
user is about to create a file that won't contain audio tracks. It does this
by default if at least one source file contains an audio track. Implements
#2380.
* mkvmerge: chapters: the timestamps of chapters read from containers or from
chapter files can be adjusted (multiplication and addition) with the new
`--chapter-sync` option or using the special track ID `-2` for the existing
`--sync` option. Part of the implementation of #2358.
## Bug fixes

View File

@ -962,7 +962,9 @@ move_by_edition(KaxChapters &dst,
*/
void
adjust_timestamps(EbmlMaster &master,
int64_t offset) {
int64_t offset,
int64_t numerator,
int64_t denominator) {
size_t master_idx;
for (master_idx = 0; master.ListSize() > master_idx; master_idx++) {
if (!Is<KaxChapterAtom>(master[master_idx]))
@ -973,16 +975,16 @@ adjust_timestamps(EbmlMaster &master,
auto end = FindChild<KaxChapterTimeEnd>(atom);
if (start)
start->SetValue(std::max<int64_t>(static_cast<int64_t>(start->GetValue()) + offset, 0));
start->SetValue(std::max<int64_t>(static_cast<int64_t>((start->GetValue()) * numerator / denominator) + offset, 0));
if (end)
end->SetValue(std::max<int64_t>(static_cast<int64_t>(end->GetValue()) + offset, 0));
end->SetValue(std::max<int64_t>(static_cast<int64_t>((end->GetValue()) * numerator / denominator) + offset, 0));
}
for (master_idx = 0; master.ListSize() > master_idx; master_idx++) {
auto work_master = dynamic_cast<EbmlMaster *>(master[master_idx]);
if (work_master)
adjust_timestamps(*work_master, offset);
adjust_timestamps(*work_master, offset, numerator, denominator);
}
}

View File

@ -84,7 +84,7 @@ libmatroska::KaxEditionEntry *find_edition_with_uid(libmatroska::KaxChapters &ch
libmatroska::KaxChapterAtom *find_chapter_with_uid(libmatroska::KaxChapters &chapters, uint64_t uid);
void move_by_edition(libmatroska::KaxChapters &dst, libmatroska::KaxChapters &src);
void adjust_timestamps(libebml::EbmlMaster &master, int64_t offset);
void adjust_timestamps(libebml::EbmlMaster &master, int64_t offset, int64_t numerator = 1, int64_t denominator = 1);
void merge_entries(libebml::EbmlMaster &master);
int count_atoms(libebml::EbmlMaster &master);
void regenerate_uids(libebml::EbmlMaster &master);

View File

@ -1447,8 +1447,15 @@ kax_reader_c::read_headers() {
void
kax_reader_c::adjust_chapter_timestamps() {
if (m_chapters && (0 != m_global_timestamp_offset))
mtx::chapters::adjust_timestamps(*m_chapters, -m_global_timestamp_offset);
if (!m_chapters)
return;
auto const &sync = mtx::includes(m_ti.m_timestamp_syncs, track_info_c::chapter_track_id) ? m_ti.m_timestamp_syncs[track_info_c::chapter_track_id]
: mtx::includes(m_ti.m_timestamp_syncs, track_info_c::all_tracks_id) ? m_ti.m_timestamp_syncs[track_info_c::all_tracks_id]
: timestamp_sync_t{};
mtx::chapters::adjust_timestamps(*m_chapters, -m_global_timestamp_offset);
mtx::chapters::adjust_timestamps(*m_chapters, sync.displacement, sync.numerator, sync.denominator);
}
void
@ -2787,6 +2794,9 @@ void
kax_reader_c::add_available_track_ids() {
for (auto &track : m_tracks)
add_available_track_id(track->tnum);
if (m_chapters)
add_available_track_id(track_info_c::chapter_track_id);
}
void

View File

@ -1308,6 +1308,11 @@ reader_c::process_chapter_entries() {
}
mtx::chapters::align_uids(m_chapters.get());
auto const &sync = mtx::includes(m_ti.m_timestamp_syncs, track_info_c::chapter_track_id) ? m_ti.m_timestamp_syncs[track_info_c::chapter_track_id]
: mtx::includes(m_ti.m_timestamp_syncs, track_info_c::all_tracks_id) ? m_ti.m_timestamp_syncs[track_info_c::all_tracks_id]
: timestamp_sync_t{};
mtx::chapters::adjust_timestamps(*m_chapters, sync.displacement, sync.numerator, sync.denominator);
}
reader_c::~reader_c() {
@ -2375,6 +2380,9 @@ reader_c::create_packetizers() {
void
reader_c::add_available_track_ids() {
add_available_track_id_range(0, m_tracks.size() - 1);
if (m_chapters)
add_available_track_id(track_info_c::chapter_track_id);
}
bool

View File

@ -867,6 +867,11 @@ ogm_reader_c::handle_stream_comments() {
chapters_set = true;
mtx::chapters::align_uids(m_chapters.get());
auto const &sync = mtx::includes(m_ti.m_timestamp_syncs, track_info_c::chapter_track_id) ? m_ti.m_timestamp_syncs[track_info_c::chapter_track_id]
: mtx::includes(m_ti.m_timestamp_syncs, track_info_c::all_tracks_id) ? m_ti.m_timestamp_syncs[track_info_c::all_tracks_id]
: timestamp_sync_t{};
mtx::chapters::adjust_timestamps(*m_chapters, sync.displacement, sync.numerator, sync.denominator);
} catch (...) {
exception_parsing_chapters = true;
}
@ -893,6 +898,9 @@ ogm_reader_c::handle_stream_comments() {
void
ogm_reader_c::add_available_track_ids() {
add_available_track_id_range(sdemuxers.size());
if (m_chapters)
add_available_track_id(track_info_c::chapter_track_id);
}
// -----------------------------------------------------------

View File

@ -1211,6 +1211,11 @@ qtmp4_reader_c::process_chapter_entries(int level,
m_chapters = mtx::chapters::parse(&text_out, 0, -1, 0, m_ti.m_chapter_language, "", true);
mtx::chapters::align_uids(m_chapters.get());
auto const &sync = mtx::includes(m_ti.m_timestamp_syncs, track_info_c::chapter_track_id) ? m_ti.m_timestamp_syncs[track_info_c::chapter_track_id]
: mtx::includes(m_ti.m_timestamp_syncs, track_info_c::all_tracks_id) ? m_ti.m_timestamp_syncs[track_info_c::all_tracks_id]
: timestamp_sync_t{};
mtx::chapters::adjust_timestamps(*m_chapters, sync.displacement, sync.numerator, sync.denominator);
} catch (mtx::chapters::parser_x &ex) {
mxerror(boost::format(Y("The MP4 file '%1%' contains chapters whose format was not recognized. This is often the case if the chapters are not encoded in UTF-8. Use the '--chapter-charset' option in order to specify the charset to use.\n")) % m_ti.m_fname);
}
@ -2039,6 +2044,9 @@ qtmp4_reader_c::add_available_track_ids() {
for (i = 0; i < m_demuxers.size(); ++i)
add_available_track_id(m_demuxers[i]->id);
if (m_chapters)
add_available_track_id(track_info_c::chapter_track_id);
}
std::string

View File

@ -93,6 +93,13 @@ set_usage() {
usage_text += Y(" --chapters <file> Read chapter information from the file.\n");
usage_text += Y(" --chapter-language <lng> Set the 'language' element in chapter entries.\n");
usage_text += Y(" --chapter-charset <cset> Charset for a simple chapter file.\n");
usage_text += Y(" --chapter-sync <d[,o[/p]]>\n"
" Synchronize, adjust the chapters's timestamps\n"
" by 'd' ms.\n"
" 'o/p': Adjust the timestamps by multiplying with\n"
" 'o/p' to fix linear drifts. 'p' defaults to\n"
" 1 if omitted. Both 'o' and 'p' can be\n"
" floating point numbers.\n");
usage_text += Y(" --cue-chapter-name-format <format>\n"
" Pattern for the conversion from cue sheet\n"
" entries to chapter names.\n");
@ -563,24 +570,31 @@ parse_arg_tracks(std::string s,
static void
parse_arg_sync(std::string s,
std::string const &opt,
track_info_c &ti) {
track_info_c &ti,
boost::optional<int64_t> force_track_id) {
timestamp_sync_t tcsync;
// Extract the track number.
std::string orig = s;
std::vector<std::string> parts = split(s, ":", 2);
if (parts.size() != 2)
mxerror(boost::format(Y("Invalid sync option. No track ID specified in '%1% %2%'.\n")) % opt % s);
auto orig = s;
int64_t id = 0;
if (!parse_number(parts[0], id))
mxerror(boost::format(Y("Invalid track ID specified in '%1% %2%'.\n")) % opt % s);
s = parts[1];
if (s.size() == 0)
mxerror(boost::format(Y("Invalid sync option specified in '%1% %2%'.\n")) % opt % orig);
if (!force_track_id) {
auto parts = split(s, ":", 2);
if (parts.size() != 2)
mxerror(boost::format(Y("Invalid sync option. No track ID specified in '%1% %2%'.\n")) % opt % s);
if (parts[1] == "reset") {
if (!parse_number(parts[0], id))
mxerror(boost::format(Y("Invalid track ID specified in '%1% %2%'.\n")) % opt % s);
s = parts[1];
if (s.size() == 0)
mxerror(boost::format(Y("Invalid sync option specified in '%1% %2%'.\n")) % opt % orig);
} else
id = *force_track_id;
if (s == "reset") {
ti.m_reset_timestamps_specs[id] = true;
return;
}
@ -1860,7 +1874,8 @@ parse_arg_chapter_charset(const std::string &arg,
static void
parse_arg_chapters(const std::string &param,
const std::string &arg) {
const std::string &arg,
track_info_c &ti) {
if (g_chapter_file_name != "")
mxerror(boost::format(Y("Only one chapter file allowed in '%1% %2%'.\n")) % param % arg);
@ -1868,6 +1883,15 @@ parse_arg_chapters(const std::string &param,
g_chapter_file_name = arg;
g_kax_chapters = mtx::chapters::parse(g_chapter_file_name, 0, -1, 0, g_chapter_language.c_str(), g_chapter_charset.c_str(), false, &format, &g_tags_from_cue_chapters);
auto sync = ti.m_timestamp_syncs.find(track_info_c::chapter_track_id);
if (sync == ti.m_timestamp_syncs.end())
sync = ti.m_timestamp_syncs.find(track_info_c::all_tracks_id);
if (sync != ti.m_timestamp_syncs.end()) {
mtx::chapters::adjust_timestamps(*g_kax_chapters, sync->second.displacement, sync->second.numerator, sync->second.denominator);
ti.m_timestamp_syncs.erase(sync);
}
if (g_segment_title_set || !g_tags_from_cue_chapters || (mtx::chapters::format_e::cue != format))
return;
@ -2450,6 +2474,13 @@ parse_args(std::vector<std::string> args) {
parse_arg_chapter_charset(next_arg, *ti);
sit++;
} else if (this_arg == "--chapter-sync") {
if (no_next_arg)
mxerror(boost::format(Y("'%1%' lacks the delay.\n")) % this_arg);
parse_arg_sync(next_arg, this_arg, *ti, track_info_c::chapter_track_id);
sit++;
} else if (this_arg == "--cue-chapter-name-format") {
if (no_next_arg)
mxerror(Y("'--cue-chapter-name-format' lacks the format.\n"));
@ -2464,7 +2495,7 @@ parse_args(std::vector<std::string> args) {
if (no_next_arg)
mxerror(Y("'--chapters' lacks the file name.\n"));
parse_arg_chapters(this_arg, next_arg);
parse_arg_chapters(this_arg, next_arg, *ti);
sit++;
inputs_found = true;
@ -2757,7 +2788,7 @@ parse_args(std::vector<std::string> args) {
if (no_next_arg)
mxerror(boost::format(Y("'%1%' lacks the delay.\n")) % this_arg);
parse_arg_sync(next_arg, this_arg, *ti);
parse_arg_sync(next_arg, this_arg, *ti, boost::none);
sit++;
} else if (this_arg == "--cues") {

View File

@ -228,6 +228,11 @@ protected:
bool m_initialized;
public:
enum special_track_id_e {
all_tracks_id = -1,
chapter_track_id = -2,
};
// The track ID.
int64_t m_id;

View File

@ -496,3 +496,4 @@ T_647recode_textsubs_from_matroska:2e63dc90381d8f5191b852aac6cc3b05-b297cba0182c
T_648append_matroska_first_timestamp_not_zero:80d6193277012fde546499d832b4bab3:passed:20180724-210331:0.023501574
T_649unsupported_file_types:ok-ok-ok-ok-ok:passed:20180806-201225:0.045725267
T_650chapter_generation_no_names:65b6db53e376326e7e2f3d1c04caf63d-ok:passed:20180821-140249:0.030599141
T_651sync_chapter_timestamps:840ec2d37a993e2da78c51b05d7c80f0-96a2f405d04267a1d1e62da4d9521a10-809ab88c5f5d48cfaf5f9bcf94b310f5-809ab88c5f5d48cfaf5f9bcf94b310f5-e0869563706cd2a0b3ec8cb65d1d0cd0-6890c5bac2022d3624958b950d36c77e-6890c5bac2022d3624958b950d36c77e-3e619d7ec77e8395eda20d91382df5cc-822141ff77af0c4c4c7a889ec7374190-3cc5d932c804e0bb1808e63501ea3d1e-3cc5d932c804e0bb1808e63501ea3d1e-7b65c3ff93bf15ca36889d4d9cfda9c1-9a6595a1dbab62f1174c4056e0fdba5b-9a6595a1dbab62f1174c4056e0fdba5b-0621718db4ce9cb7ed4c8c5dc843a962-b276bee9b55fc8aa874608bc2863ba46-00f74b92df721cd1dc363deddc4cf6d8-00f74b92df721cd1dc363deddc4cf6d8-f5d087300a71d86b1ca57ab7929758ed-df9f1da488db25294f457e3576ed8a2d-df9f1da488db25294f457e3576ed8a2d-a5e0963d2fa4ed1dc6e4a57e81c6ef75-7f60d2a169cd162d1de5e5df2a191a8a-7f60d2a169cd162d1de5e5df2a191a8a-7f60d2a169cd162d1de5e5df2a191a8a-91cd9376b3245c3e23413889f66333a1-91cd9376b3245c3e23413889f66333a1-91cd9376b3245c3e23413889f66333a1:passed:20180918-223701:2.330201791

View File

@ -0,0 +1,28 @@
#!/usr/bin/ruby -w
# T_651sync_chapter_timestamps
describe "mkvmerge / syncing chapter timestamps"
sources = [
[ "data/ogg/with_chapters.ogm", "--chapter-charset ISO-8859-15" ],
"data/mp4/o12-short.m4v",
"data/mkv/chapters-with-ebmlvoid.mkv",
]
sources.each do |source|
source = [ source, "" ] unless source.is_a? Array
test_merge source[0], :args => "#{source[1]}"
[ "", "-" ].each do |sign|
[ -1, -2 ].each { |id| test_merge source[0], :args => "#{source[1]} --sync #{id}:#{sign}1000,3/2" }
test_merge source[0], :args => "#{source[1]} --chapter-sync #{sign}1000,3/2"
end
end
[ "",
"--chapter-sync 1000,3/2", "--sync -1:1000,3/2", "--sync -2:1000,3/2",
"--chapter-sync -1000,3/2", "--sync -1:-1000,3/2", "--sync -2:-1000,3/2",
].each do |args|
test_merge "data/subtitles/srt/ven.srt", :args => "#{args} --chapters data/chapters/uk-and-gb.xml"
end