mirror of
https://gitlab.com/mbunkus/mkvtoolnix.git
synced 2024-12-24 11:54:01 +00:00
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:
parent
a684d519bb
commit
42f8538a70
4
NEWS.md
4
NEWS.md
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------
|
||||
|
@ -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
|
||||
|
@ -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 ¶m,
|
||||
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 ¶m,
|
||||
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") {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
28
tests/test-651sync_chapter_timestamps.rb
Executable file
28
tests/test-651sync_chapter_timestamps.rb
Executable 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
|
Loading…
Reference in New Issue
Block a user