mkvmerge: implement chapter generation when appending/in fixed intervals

Implements mkvmerge's part of #1586.
This commit is contained in:
Moritz Bunkus 2016-03-01 21:23:36 +01:00
parent 89ca1e85ab
commit ce7c31f49b
12 changed files with 297 additions and 20 deletions

View File

@ -1,3 +1,14 @@
2016-03-01 Moritz Bunkus <moritz@bunkus.org>
* mkvmerge: new feature: added switches (»--generate-chapters« and
»--generate-chapter-name-template«) for generating chapters while
muxing. Two modes are currently supported: »when-appending« which
creates one chapter at the beginning and an additional one each
time a file is appended and »interval:…« which generates chapters
in fixed intervals.
Implements mkvmerge's part of #1586.
2016-02-28 Moritz Bunkus <moritz@bunkus.org>
* MKVToolNix GUI: job queue enhancement: completed jobs will now

View File

@ -19,6 +19,7 @@
#include "common/math.h"
#include "common/strings/formatting.h"
#include "common/tags/tags.h"
#include "common/translation.h"
#include "merge/cluster_helper.h"
#include "merge/cues.h"
#include "merge/libmatroska_extensions.h"
@ -264,6 +265,8 @@ cluster_helper_c::add_packet(packet_cptr packet) {
if (g_video_packetizer == packet->source)
++m->frame_field_number;
generate_chapters_if_necessary(packet);
}
int64_t
@ -694,4 +697,103 @@ cluster_helper_c::create_tags_for_track_statistics(KaxTags &tags,
m->track_statistics.clear();
}
void
cluster_helper_c::enable_chapter_generation(chapter_generation_mode_e mode,
std::string const &language) {
m->chapter_generation_mode = mode;
m->chapter_generation_language = !language.empty() ? language : "eng";
}
chapter_generation_mode_e
cluster_helper_c::get_chapter_generation_mode()
const {
return m->chapter_generation_mode;
}
void
cluster_helper_c::set_chapter_generation_interval(timestamp_c const &interval) {
m->chapter_generation_interval = interval;
}
void
cluster_helper_c::set_chapter_generation_name_template(std::string const &name_template) {
m->chapter_generation_name_template.override(name_template);
}
void
cluster_helper_c::verify_and_report_chapter_generation_parameters()
const {
if (chapter_generation_mode_e::none == m->chapter_generation_mode)
return;
if (!m->chapter_generation_reference_track)
mxerror(boost::format("Chapter generation is only possible if at least one video or audio track muxed.\n"));
mxinfo(boost::format("Using the track with the ID %1% from the file '%2%' as the reference for chapter generation.\n")
% m->chapter_generation_reference_track->m_ti.m_id % m->chapter_generation_reference_track->m_ti.m_fname);
}
void
cluster_helper_c::register_new_packetizer(generic_packetizer_c &ptzr) {
auto new_track_type = ptzr.get_track_type();
if (!g_video_packetizer && (track_video == new_track_type))
g_video_packetizer = &ptzr;
auto current_ptzr_prio = !m->chapter_generation_reference_track ? 0
: m->chapter_generation_reference_track->get_track_type() == track_video ? 100
: m->chapter_generation_reference_track->get_track_type() == track_audio ? 80
: 0;
auto new_ptzr_prio = new_track_type == track_video ? 100
: new_track_type == track_audio ? 80
: 0;
if (new_ptzr_prio > current_ptzr_prio)
m->chapter_generation_reference_track = &ptzr;
}
void
cluster_helper_c::generate_chapters_if_necessary(packet_cptr const &packet) {
if ((chapter_generation_mode_e::none == m->chapter_generation_mode) || !m->chapter_generation_reference_track)
return;
auto successor = m->chapter_generation_reference_track->get_connected_successor();
if (successor) {
if (chapter_generation_mode_e::when_appending == m->chapter_generation_mode)
m->chapter_generation_last_generated.reset();
while ((successor = m->chapter_generation_reference_track->get_connected_successor()))
m->chapter_generation_reference_track = successor;
}
auto ptzr = packet->source;
if (ptzr != m->chapter_generation_reference_track)
return;
if (chapter_generation_mode_e::when_appending == m->chapter_generation_mode) {
if (packet->is_key_frame() && !m->chapter_generation_last_generated.valid())
generate_one_chapter(timestamp_c::ns(packet->assigned_timecode));
return;
}
if (chapter_generation_mode_e::interval != m->chapter_generation_mode)
return;
auto now = timestamp_c::ns(packet->assigned_timecode);
while (!m->chapter_generation_last_generated.valid() || (m->chapter_generation_last_generated <= now))
generate_one_chapter(!m->chapter_generation_last_generated.valid() ? timestamp_c::ns(0) : m->chapter_generation_last_generated + m->chapter_generation_interval);
}
void
cluster_helper_c::generate_one_chapter(timestamp_c const &timestamp) {
m->chapter_generation_number += 1;
m->chapter_generation_last_generated = timestamp;
auto name = format_chapter_name_template(m->chapter_generation_name_template.get_translated(), m->chapter_generation_number, timestamp);
add_chapter_atom(timestamp, name, m->chapter_generation_language);
}
std::unique_ptr<cluster_helper_c> g_cluster_helper;

View File

@ -23,14 +23,22 @@
#include <matroska/KaxCluster.h>
#include "common/split_point.h"
#include "common/timestamp.h"
#include "merge/libmatroska_extensions.h"
#define RND_TIMECODE_SCALE(a) (std::llround(static_cast<double>(a) / static_cast<double>(g_timecode_scale)) * static_cast<int64_t>(g_timecode_scale))
class generic_packetizer_c;
class render_groups_c;
class packet_t;
using packet_cptr = std::shared_ptr<packet_t>;
enum class chapter_generation_mode_e {
none,
when_appending,
interval,
};
class cluster_helper_c {
private:
struct impl_t;
@ -69,6 +77,14 @@ public:
void create_tags_for_track_statistics(KaxTags &tags, std::string const &writing_app, boost::posix_time::ptime const &writing_date);
void register_new_packetizer(generic_packetizer_c &ptzr);
void enable_chapter_generation(chapter_generation_mode_e mode, std::string const &language = "");
chapter_generation_mode_e get_chapter_generation_mode() const;
void set_chapter_generation_interval(timestamp_c const &interval);
void set_chapter_generation_name_template(std::string const &name_template);
void verify_and_report_chapter_generation_parameters() const;
private:
void set_duration(render_groups_c *rg);
bool must_duration_be_set(render_groups_c *rg, packet_cptr &new_packet);
@ -76,6 +92,8 @@ private:
void render_before_adding_if_necessary(packet_cptr &packet);
void render_after_adding_if_necessary(packet_cptr &packet);
void split_if_necessary(packet_cptr &packet);
void generate_chapters_if_necessary(packet_cptr const &packet);
void generate_one_chapter(timestamp_c const &timestamp);
void split(packet_cptr &packet);
bool add_to_cues_maybe(packet_cptr &pack);

View File

@ -31,6 +31,7 @@
#include "common/strings/formatting.h"
#include "common/unique_numbers.h"
#include "common/xml/ebml_tags_converter.h"
#include "merge/cluster_helper.h"
#include "merge/filelist.h"
#include "merge/generic_packetizer.h"
#include "merge/generic_reader.h"
@ -305,14 +306,14 @@ generic_packetizer_c::set_track_type(int type,
if (track_audio == type)
m_reader->m_num_audio_tracks++;
else if (track_video == type) {
else if (track_video == type)
m_reader->m_num_video_tracks++;
if (!g_video_packetizer)
g_video_packetizer = this;
} else
else
m_reader->m_num_subtitle_tracks++;
g_cluster_helper->register_new_packetizer(*this);
if ( (TFA_AUTOMATIC == tfa_mode)
&& (TFA_AUTOMATIC == m_timestamp_factory_application_mode))
m_timestamp_factory_application_mode
@ -1089,7 +1090,7 @@ generic_packetizer_c::connect(generic_packetizer_c *src,
m_hcompression = src->m_hcompression;
m_compressor = compressor_c::create(m_hcompression);
m_last_cue_timecode = src->m_last_cue_timecode;
m_timestamp_factory = src->m_timestamp_factory;
m_timestamp_factory = src->m_timestamp_factory;
m_correction_timecode_offset = 0;
if (-1 == append_timecode_offset)
@ -1224,3 +1225,9 @@ generic_packetizer_c::before_file_finished() {
void
generic_packetizer_c::after_file_created() {
}
generic_packetizer_c *
generic_packetizer_c::get_connected_successor()
const {
return m_connected_successor;
}

View File

@ -253,6 +253,8 @@ public:
virtual void prevent_lacing();
virtual bool is_lacing_prevented() const;
virtual generic_packetizer_c *get_connected_successor() const;
// Callbacks
virtual void after_packet_rendered(packet_t const &packet);
virtual void before_file_finished();

View File

@ -95,6 +95,12 @@ set_usage() {
" entries to chapter names.\n");
usage_text += Y(" --default-language <lng> Use this language for all tracks unless\n"
" overridden with the --language option.\n");
usage_text += Y(" --generate-chapters <mode>\n"
" Automatically generate chapters according to\n"
" the mode ('when-appending' or 'interval:<duration>').\n");
usage_text += Y(" --generate-chapters-name-template <template>\n"
" Template for newly generated chapter names\n"
" (default: 'Chapter <NUM:2>').\n");
usage_text += "\n";
usage_text += Y(" Segment info handling:\n");
usage_text += Y(" --segmentinfo <file> Read segment information from the file.\n");
@ -439,11 +445,9 @@ identify(std::string &filename) {
It returns a number of nanoseconds.
*/
int64_t
static int64_t
parse_number_with_unit(const std::string &s,
const std::string &subject,
const std::string &argument,
std::string display_s = "") {
const std::string &argument) {
boost::regex re1("(-?\\d+\\.?\\d*)(s|ms|us|ns|fps|p|i)?", boost::regex::perl | boost::regex::icase);
boost::regex re2("(-?\\d+)/(-?\\d+)(s|ms|us|ns|fps|p|i)?", boost::regex::perl | boost::regex::icase);
@ -452,9 +456,6 @@ parse_number_with_unit(const std::string &s,
double d_value = 0.0;
bool is_fraction = false;
if (display_s.empty())
display_s = s;
boost::smatch matches;
if (boost::regex_match(s, matches, re1)) {
parse_number(matches[1], d_value);
@ -470,7 +471,7 @@ parse_number_with_unit(const std::string &s,
is_fraction = true;
} else
mxerror(boost::format(Y("'%1%' is not a valid %2% in '%3% %4%'.\n")) % s % subject % argument % display_s);
mxerror(boost::format(Y("'%1%' is not recognized as a valid number format in '%2%'.\n")) % s % argument);
int64_t multiplier = 1000000000;
balg::to_lower(unit);
@ -496,7 +497,7 @@ parse_number_with_unit(const std::string &s,
return (int64_t)(1000000000.0 / d_value);
} else if (unit != "s")
mxerror(boost::format(Y("'%1%' does not contain a valid unit ('s', 'ms', 'us', 'ns', 'fps', 'p' or 'i') in '%2% %3%'.\n")) % s % argument % display_s);
mxerror(boost::format(Y("'%1%' does not contain a valid unit ('s', 'ms', 'us', 'ns', 'fps', 'p' or 'i') in '%2%'.\n")) % s % argument);
if (is_fraction)
return multiplier * n / d;
@ -1351,7 +1352,7 @@ parse_arg_default_duration(const std::string &s,
if (!parse_number(parts[0], id))
mxerror(boost::format(Y("'%1%' is not a valid track ID in '--default-duration %2%'.\n")) % parts[0] % s);
ti.m_default_durations[id] = parse_number_with_unit(parts[1], "default duration", "--default-duration");
ti.m_default_durations[id] = parse_number_with_unit(parts[1], (boost::format("--default-duration %1%") % s).str());
}
/** \brief Parse the argument for \c --nalu-size-length
@ -1636,6 +1637,31 @@ parse_arg_chapters(const std::string &param,
g_kax_chapters = parse_chapters(g_chapter_file_name, 0, -1, 0, g_chapter_language.c_str(), g_chapter_charset.c_str(), false, nullptr, &g_tags_from_cue_chapters);
}
static void
parse_arg_generate_chapters(std::string const &arg) {
auto parts = split(arg, ":", 2);
if (parts[0] == "when-appending") {
g_cluster_helper->enable_chapter_generation(chapter_generation_mode_e::when_appending, g_chapter_language);
g_chapter_language.clear();
return;
}
if (parts[0] != "interval")
mxerror(boost::format("Invalid chapter generation mode in '--generate-chapters %1%'.\n") % arg);
if (parts.size() < 2)
parts.emplace_back("");
auto interval = int64_t{};
if (!parse_timecode(parts[1], interval) || (interval < 0))
mxerror(boost::format("The chapter generation interval must be a positive number in '--generate-chapters %1%'.\n") % arg);
g_cluster_helper->enable_chapter_generation(chapter_generation_mode_e::interval, g_chapter_language);
g_cluster_helper->set_chapter_generation_interval(timestamp_c::ns(interval));
g_chapter_language.clear();
}
static void
parse_arg_segmentinfo(const std::string &param,
const std::string &arg) {
@ -2154,6 +2180,20 @@ parse_args(std::vector<std::string> args) {
inputs_found = true;
} else if (this_arg == "--generate-chapters") {
if (no_next_arg)
mxerror(Y("'--generate-chapters' lacks the mode.\n"));
parse_arg_generate_chapters(next_arg);
sit++;
} else if (this_arg == "--generate-chapters-name-template") {
if (no_next_arg)
mxerror(Y("'--generate-chapters-name-template' lacks the name template.\n"));
g_cluster_helper->set_chapter_generation_name_template(next_arg);
sit++;
} else if (this_arg == "--segmentinfo") {
if (no_next_arg)
mxerror(Y("'--segmentinfo' lacks the file name.\n"));
@ -2632,6 +2672,7 @@ main(int argc,
check_track_id_validity();
create_append_mappings_for_playlists();
check_append_mapping();
g_cluster_helper->verify_and_report_chapter_generation_parameters();
calc_attachment_sizes();
calc_max_chapter_size();
}

View File

@ -48,6 +48,7 @@
#include "common/chapters/chapters.h"
#include "common/command_line.h"
#include "common/construct.h"
#include "common/container.h"
#include "common/date_time.h"
#include "common/debugging.h"
@ -69,6 +70,7 @@
#include "merge/webm.h"
using namespace libmatroska;
using namespace mtx::construct;
namespace libmatroska {
@ -168,6 +170,8 @@ static std::unique_ptr<EbmlVoid> s_kax_chapters_void;
static int64_t s_max_chapter_size = 0;
static std::unique_ptr<EbmlVoid> s_void_after_track_headers;
static std::vector<std::tuple<timestamp_c, std::string, std::string>> s_additional_chapter_atoms;
static mm_io_cptr s_out;
static bitvalue_c s_seguid_prev(128), s_seguid_current(128), s_seguid_next(128);
@ -1273,7 +1277,7 @@ add_tags_from_cue_chapters() {
*/
static void
render_chapter_void_placeholder() {
if (0 >= s_max_chapter_size)
if ((0 >= s_max_chapter_size) && (chapter_generation_mode_e::none == g_cluster_helper->get_chapter_generation_mode()))
return;
if (outputting_webm()) {
@ -1281,12 +1285,14 @@ render_chapter_void_placeholder() {
g_kax_chapters.reset();
s_max_chapter_size = 0;
g_cluster_helper->enable_chapter_generation(chapter_generation_mode_e::none);
return;
}
auto size = s_max_chapter_size + (chapter_generation_mode_e::none == g_cluster_helper->get_chapter_generation_mode() ? 100 : 1000);
s_kax_chapters_void = std::make_unique<EbmlVoid>();
s_kax_chapters_void->SetSize(s_max_chapter_size + 100);
s_kax_chapters_void->SetSize(size);
s_kax_chapters_void->Render(*s_out);
}
@ -1392,6 +1398,38 @@ add_chapters_for_current_part() {
sort_ebml_master(s_chapters_in_this_file.get());
}
void
add_chapter_atom(timestamp_c const &start_timestamp,
std::string const &name,
std::string const &language) {
s_additional_chapter_atoms.emplace_back(start_timestamp, name, language);
}
static void
prepare_additional_chapter_atoms_for_rendering() {
if (s_additional_chapter_atoms.empty())
return;
if (!s_chapters_in_this_file)
s_chapters_in_this_file = std::make_shared<KaxChapters>();
auto offset = timestamp_c::ns(g_no_linking ? g_cluster_helper->get_first_timecode_in_file() + g_cluster_helper->get_discarded_duration() : 0);
auto &edition = GetChild<KaxEditionEntry>(*s_chapters_in_this_file);
if (!FindChild<KaxEditionUID>(edition))
GetChild<KaxEditionUID>(edition).SetValue(create_unique_number(UNIQUE_EDITION_IDS));
for (auto const &additional_chapter : s_additional_chapter_atoms) {
auto atom = cons<KaxChapterAtom>(new KaxChapterUID, create_unique_number(UNIQUE_CHAPTER_IDS),
new KaxChapterTimeStart, (std::get<0>(additional_chapter) - offset).to_ns(),
cons<KaxChapterDisplay>(new KaxChapterString, std::get<1>(additional_chapter),
new KaxChapterLanguage, std::get<2>(additional_chapter)));
edition.PushElement(*atom);
}
s_additional_chapter_atoms.clear();
}
static void
render_chapters() {
auto s_debug = debugging_option_c{"splitting_chapters"};
@ -1401,12 +1439,23 @@ render_chapters() {
% !!s_kax_chapters_void % (s_kax_chapters_void ? s_kax_chapters_void ->ElementSize() : 0)
% !!s_chapters_in_this_file % (s_chapters_in_this_file ? s_chapters_in_this_file->ElementSize() : 0));
if (!s_kax_chapters_void)
return;
prepare_additional_chapter_atoms_for_rendering();
if (s_chapters_in_this_file)
if (!s_chapters_in_this_file) {
s_kax_chapters_void.reset();
return;
}
fix_mandatory_elements(s_chapters_in_this_file.get());
if (s_kax_chapters_void && (s_kax_chapters_void->ElementSize() >= s_chapters_in_this_file->ElementSize()))
s_kax_chapters_void->ReplaceWith(*s_chapters_in_this_file, *s_out, true, true);
else {
s_out->setFilePointer(0, seek_end);
s_chapters_in_this_file->Render(*s_out);
}
s_kax_chapters_void.reset();
}

View File

@ -201,6 +201,7 @@ void main_loop();
void add_packetizer_globally(generic_packetizer_c *packetizer);
void add_tags(KaxTag *tags);
void add_chapter_atom(timestamp_c const &start_timestamp, std::string const &name, std::string const &language);
void create_next_output_file();
void finish_file(bool last_file, bool create_new_file = false, bool previously_discarding = false);

View File

@ -51,6 +51,12 @@ public:
bool discarding{}, splitting_and_processed_fully{};
chapter_generation_mode_e chapter_generation_mode{chapter_generation_mode_e::none};
translatable_string_c chapter_generation_name_template{YT("Chapter <NUM:2>")};
timestamp_c chapter_generation_interval, chapter_generation_last_generated;
generic_packetizer_c *chapter_generation_reference_track{};
unsigned int chapter_generation_number{};
std::string chapter_generation_language;
std::unordered_map<uint64_t, track_statistics_c> track_statistics;

View File

@ -377,3 +377,5 @@ T_528handbrake_chapter_uids:f68bd8d577a3b4b2e8d83b7336bcc936:passed:20160119-173
T_529aac_program_config_element_with_empty_comment_at_end:e3582257eebed3fada2bd75d8b2dfc20:passed:20160124-120513:0.014071844
T_530avi_negative_video_height:6d5d6951adc7e2fc0743eade6e8bb6fd:passed:20160210-170826:0.014806634
T_531simple_chapter_extraction:6b6dccb8f5a4ab488da2f174e0d9450c-6b6dccb8f5a4ab488da2f174e0d9450c-e790f2e02daab6faabf712ccf9877ac2-b832af714e46ad7514e669297320b032-b832af714e46ad7514e669297320b032-59afc62025a19921cfa64280acbbb61c:passed:20160225-223308:0.063513455
T_532chapter_generation_when_appending:5680a0ca3f974d43ff7e4c7482812a4c-ea8f0f1e1246e42b6ba903b4eb33b9d4-88b2c0d2539b79b41f6cea5f8203bb89+ed0f627e76e018c68b2ed73d7c41133b+52fda80c406910138bcd95171052fa56+86a7d27b85289709141db725240a17dd+98c899d37d10d441cb1cfe9c33d6de06+cd7bd9aa38a1514838f86230e7ba9b5c+32c2cea8051c9f848e090aac1b761cb7+ok-926d7b0771fa08ea44710f260ef3b3bc:passed:20160301-194344:2.04755563
T_533chapter_generation_interval:63486951fe0717eec1e93cb8fadaed92-5a57214bb210f51311edc6e8fca19f3e-cbbd43701884c34ae5d1efd9baf39a04+0e5962f224c28b3a7fc2a318fddd7ea9+74621b5528a14b9bee0aa6b8ac28693b+dfb83af32a6472c8e2d41238b89c7521+b60c9078501798a674db97c83fd99b14+e662cf60c4365aaeff7e6b088904739d+32c2cea8051c9f848e090aac1b761cb7+ok-c3b953881e1f7d35d58ab0bfe590d7ba:passed:20160301-194459:2.043516559

View File

@ -0,0 +1,19 @@
#!/usr/bin/ruby -w
# coding: utf-8
file = "data/avi/v-h264-aac.avi"
# T_532chapter_generation_when_appending
describe "mkvmerge / generate chapter »when-appending«"
def hash_results max
( (1..max).collect { |i| hash_file(sprintf("%s-%02d", tmp, i)) } + [ File.exists?(sprintf("%s-%02d", tmp, max + 1)) ? 'bad' : 'ok' ]).join '+'
end
test_merge "#{file} + #{file} + #{file}", :args => "--generate-chapters when-appending"
test_merge "#{file} + #{file} + #{file}", :args => "--chapter-language ger --generate-chapters when-appending"
test "creation and splitting" do
merge "--chapter-language ger --generate-chapters when-appending --split 30s #{file} + #{file} + #{file}", :output => "#{tmp}-%02d"
hash_results 7
end
test_merge "#{file}", :args => "--generate-chapters when-appending"

View File

@ -0,0 +1,19 @@
#!/usr/bin/ruby -w
# coding: utf-8
file = "data/avi/v-h264-aac.avi"
# T_533chapter_generation_interval
describe "mkvmerge / generate chapter »interval«"
def hash_results max
( (1..max).collect { |i| hash_file(sprintf("%s-%02d", tmp, i)) } + [ File.exists?(sprintf("%s-%02d", tmp, max + 1)) ? 'bad' : 'ok' ]).join '+'
end
test_merge "#{file} + #{file} + #{file}", :args => "--generate-chapters interval:30s"
test_merge "#{file} + #{file} + #{file}", :args => "--chapter-language ger --generate-chapters interval:30s"
test "creation and splitting" do
merge "--chapter-language ger --generate-chapters interval:30s --split 30s #{file} + #{file} + #{file}", :output => "#{tmp}-%02d"
hash_results 7
end
test_merge "#{file}", :args => "--generate-chapters interval:30s"