mkvmerge: splitting by parts based on frame/field numbers

Implements #819.
This commit is contained in:
Moritz Bunkus 2013-01-14 22:46:27 +01:00
parent 1e0b64418c
commit 5a152fccd5
8 changed files with 132 additions and 33 deletions

View File

@ -1,3 +1,9 @@
2013-01-14 Moritz Bunkus <moritz@bunkus.org>
* mkvmerge: new feature: Implemented splitting by parts based on
frame/field numbers ("--split parts-frames:" in
mkvmerge). Implements #819.
2013-01-13 Moritz Bunkus <moritz@bunkus.org>
* mkvmerge: bug fix: Re-writing the track headers after they'd

View File

@ -33,12 +33,14 @@ split_point_t::str()
const {
return (boost::format("<%1% %2% once:%3% discard:%4% create_file:%5%>")
% format_timecode(m_point)
% ( split_point_t::SPT_DURATION == m_type ? "duration"
: split_point_t::SPT_SIZE == m_type ? "size"
: split_point_t::SPT_TIMECODE == m_type ? "timecode"
: split_point_t::SPT_CHAPTER == m_type ? "chapter"
: split_point_t::SPT_PARTS == m_type ? "part"
: "unknown")
% ( split_point_t::SPT_DURATION == m_type ? "duration"
: split_point_t::SPT_SIZE == m_type ? "size"
: split_point_t::SPT_TIMECODE == m_type ? "timecode"
: split_point_t::SPT_CHAPTER == m_type ? "chapter"
: split_point_t::SPT_PARTS == m_type ? "part"
: split_point_t::SPT_PARTS_FRAME_FIELD == m_type ? "part(frame/field)"
: split_point_t::SPT_FRAME_FIELD == m_type ? "frame/field"
: "unknown")
% m_use_once % m_discard % m_create_new_file).str();
}
@ -170,7 +172,8 @@ cluster_helper_c::split_if_necessary(packet_cptr &packet) {
&& (packet->assigned_timecode >= m_current_split_point->m_point))
split_now = true;
else if ( (split_point_t::SPT_FRAME_FIELD == m_current_split_point->m_type)
else if ( ( (split_point_t::SPT_FRAME_FIELD == m_current_split_point->m_type)
|| (split_point_t::SPT_PARTS_FRAME_FIELD == m_current_split_point->m_type))
&& (m_frame_field_number >= m_current_split_point->m_point))
split_now = true;
@ -196,8 +199,9 @@ cluster_helper_c::split(packet_cptr &packet) {
if (m_current_split_point->m_use_once) {
if ( m_current_split_point->m_discard
&& (split_point_t::SPT_PARTS == m_current_split_point->m_type)
&& (m_split_points.end() == (m_current_split_point + 1))) {
&& ( (split_point_t::SPT_PARTS == m_current_split_point->m_type)
|| (split_point_t::SPT_PARTS_FRAME_FIELD == m_current_split_point->m_type))
&& (m_split_points.end() == (m_current_split_point + 1))) {
mxdebug_if(m_debug_splitting, boost::format("Splitting: Last part in 'parts:' splitting mode finished\n"));
m_splitting_and_processed_fully = true;
}
@ -685,7 +689,8 @@ cluster_helper_c::split_mode_produces_many_files()
if (!splitting())
return false;
if (m_split_points.front().m_type != split_point_t::SPT_PARTS)
if ( (split_point_t::SPT_PARTS != m_split_points.front().m_type)
&& (split_point_t::SPT_PARTS_FRAME_FIELD != m_split_points.front().m_type))
return true;
bool first = true;

View File

@ -48,6 +48,7 @@ struct split_point_t {
SPT_TIMECODE,
SPT_CHAPTER,
SPT_PARTS,
SPT_PARTS_FRAME_FIELD,
SPT_FRAME_FIELD,
};

View File

@ -121,6 +121,9 @@ set_usage() {
" Keep ranges of timecodes start-end, either in\n"
" separate files or append to previous range's file\n"
" if prefixed with '+'.\n");
usage_text += Y(" --split parts-frames:start1-end1[,[+]start2-end2,...]\n"
" Same as 'parts:', but 'startN'/'endN' are frame/\n"
" field numbers instead of timecodes.\n");
usage_text += Y(" --split frames:A[,B...]\n"
" Create a new file after each frame/field A, B\n"
" etc.\n");
@ -900,12 +903,16 @@ parse_arg_split_chapters(std::string const &arg) {
}
static void
parse_arg_split_parts(const std::string &arg) {
parse_arg_split_parts(const std::string &arg,
bool frames_fields) {
std::string s = arg;
if (balg::istarts_with(s, "parts:"))
s.erase(0, 6);
else if (balg::istarts_with(s, "parts-frames:"))
s.erase(0, 13);
if (s.empty())
mxerror(boost::format(Y("Missing start/end specifications for '--split' in '--split %1%'.\n")) % arg);
@ -925,35 +932,52 @@ parse_arg_split_parts(const std::string &arg) {
int64_t start;
if (pair[0].empty())
start = split_points.empty() ? 0 : std::get<1>(split_points.back());
else if (!parse_timecode(pair[0], start))
else if (!frames_fields && !parse_timecode(pair[0], start))
mxerror(boost::format(Y("Invalid start time for '--split' in '--split %1%' (current part: %2%). Additional error message: %3%.\n")) % arg % part_spec % timecode_parser_error);
else if (frames_fields && (!parse_number(pair[0], start) || (0 > start)))
mxerror(boost::format(Y("Invalid start frame/field number for '--split' in '--split %1%' (current part: %2%).\n")) % arg % part_spec);
int64_t end;
if (pair[1].empty())
end = std::numeric_limits<int64_t>::max();
else if (!parse_timecode(pair[1], end))
else if (!frames_fields && !parse_timecode(pair[1], end))
mxerror(boost::format(Y("Invalid end time for '--split' in '--split %1%' (current part: %2%). Additional error message: %3%.\n")) % arg % part_spec % timecode_parser_error);
if (end <= start)
mxerror(boost::format(Y("Invalid end time for '--split' in '--split %1%' (current part: %2%). The end time must be bigger than the start time.\n")) % arg % part_spec);
else if (frames_fields && (!parse_number(pair[1], end) || (0 > end)))
mxerror(boost::format(Y("Invalid end frame for '--split' in '--split %1%' (current part: %2%).\n")) % arg % part_spec);
if (!split_points.empty() && (start < std::get<1>(split_points.back())))
mxerror(boost::format(Y("Invalid start time for '--split' in '--split %1%' (current part: %2%). The start time must be bigger than or equal to the previous part's end time.\n")) % arg % part_spec);
if (end <= start) {
if (frames_fields)
mxerror(boost::format(Y("Invalid end frame/field number for '--split' in '--split %1%' (current part: %2%). The end number must be bigger than the start number.\n")) % arg % part_spec);
else
mxerror(boost::format(Y("Invalid end time for '--split' in '--split %1%' (current part: %2%). The end time must be bigger than the start time.\n")) % arg % part_spec);
}
if (!split_points.empty() && (start < std::get<1>(split_points.back()))) {
if (frames_fields)
mxerror(boost::format(Y("Invalid start frame/field number for '--split' in '--split %1%' (current part: %2%). The start number must be bigger than or equal to the previous part's end number.\n")) % arg % part_spec);
else
mxerror(boost::format(Y("Invalid start time for '--split' in '--split %1%' (current part: %2%). The start time must be bigger than or equal to the previous part's end time.\n")) % arg % part_spec);
}
split_points.push_back(std::make_tuple(start, end, create_new_file));
}
auto sp_type = frames_fields ? split_point_t::SPT_PARTS_FRAME_FIELD : split_point_t::SPT_PARTS;
int64_t previous_end = 0;
for (auto &split_point : split_points) {
if (previous_end < std::get<0>(split_point))
g_cluster_helper->add_split_point(split_point_t{ previous_end, split_point_t::SPT_PARTS, true, true, std::get<2>(split_point) });
g_cluster_helper->add_split_point(split_point_t{ std::get<0>(split_point), split_point_t::SPT_PARTS, true, false, std::get<2>(split_point) });
g_cluster_helper->add_split_point(split_point_t{ previous_end, sp_type, true, true, std::get<2>(split_point) });
g_cluster_helper->add_split_point(split_point_t{ std::get<0>(split_point), sp_type, true, false, std::get<2>(split_point) });
previous_end = std::get<1>(split_point);
}
if (std::get<1>(split_points.back()) < std::numeric_limits<int64_t>::max())
g_cluster_helper->add_split_point(split_point_t{ std::get<1>(split_points.back()), split_point_t::SPT_PARTS, true, true });
g_cluster_helper->add_split_point(split_point_t{ std::get<1>(split_points.back()), sp_type, true, true });
}
/** \brief Parse the size format to \c --split
@ -1028,7 +1052,10 @@ parse_arg_split(const std::string &arg) {
parse_arg_split_timecodes(arg);
else if (balg::istarts_with(s, "parts:"))
parse_arg_split_parts(arg);
parse_arg_split_parts(arg, false);
else if (balg::istarts_with(s, "parts-frames:"))
parse_arg_split_parts(arg, true);
else if (balg::istarts_with(s, "frames:"))
parse_arg_split_frames(arg);

View File

@ -1147,7 +1147,8 @@ mmg_dialog::update_command_line() {
: 2 == idx ? wxT("duration:")
: 3 == idx ? wxT("timecodes:")
: 4 == idx ? wxT("parts:")
: 5 == idx ? wxT("frames:")
: 5 == idx ? wxT("parts-frames:")
: 6 == idx ? wxT("frames:")
: wxT("chapters:");
clargs.Add(wxT("--split"));

View File

@ -248,6 +248,22 @@ tab_global::translate_split_args() {
new_tool_tip = format_tooltip(wxU(join(" ", help)));
} else if (5 == mode) {
st_split_args->SetLabel(Z("Parts:"));
std::vector<std::string> help = {
Y("A comma-separated list of frame/field number ranges of content to keep."),
Y("Each range consists of a start and end frame/field number with a '-' in the middle, e.g. '157-238'."),
Y("The numbering starts at 1."),
Y("This mode considers only the first video track that is output."),
Y("If no video track is output no splitting will occur."),
Y("The numbers given with this argument are interpreted based on the number of Matroska blocks that are output."),
Y("A single Matroska block contains either a full frame (for progressive content) or a single field (for interlaced content)."),
Y("mkvmerge does not distinguish between those two and simply counts the number of blocks."),
Y("If a start number is left out then the previous range's end number is used, or the start of the file if there was no previous range."),
Y("If a range's start number is prefixed with '+' then its content will be written to the same file as the previous range. Otherwise a new file will be created for this range."),
};
new_tool_tip = format_tooltip(wxU(join(" ", help)));
} else if (6 == mode) {
st_split_args->SetLabel(Z("Frames/fields:"));
std::vector<std::string> help = {
Y("A comma-separated list of frame/field numbers after which to split."),
@ -260,7 +276,7 @@ tab_global::translate_split_args() {
};
new_tool_tip = format_tooltip(wxU(join(" ", help)));
} else if (6 == mode) {
} else if (7 == mode) {
st_split_args->SetLabel(Z("Chapter numbers:"));
std::vector<std::string> help = {
Y("Either the word 'all' which selects all chapters or a comma-separated list of chapter numbers after which to split."),
@ -298,7 +314,8 @@ tab_global::translate_ui() {
cob_split_mode->Append(Z("split after size"));
cob_split_mode->Append(Z("split after duration"));
cob_split_mode->Append(Z("split after timecodes"));
cob_split_mode->Append(Z("split by parts"));
cob_split_mode->Append(Z("split by parts (timecode-based)"));
cob_split_mode->Append(Z("split by parts (frame/field-number-based)"));
cob_split_mode->Append(Z("split after frame/field numbers"));
cob_split_mode->Append(Z("split before chapters"));
cob_split_mode->SetSelection(split_mode);
@ -438,13 +455,14 @@ tab_global::load(wxConfigBase *cfg,
wxString split_mode;
cfg->Read(wxT("split_mode"), &split_mode, wxT("none"));
split_mode_idx = split_mode == wxT("size") ? 1
: split_mode == wxT("duration") ? 2
: split_mode == wxT("timecodes") ? 3
: split_mode == wxT("parts") ? 4
: split_mode == wxT("frames") ? 5
: split_mode == wxT("chapters") ? 6
: 0;
split_mode_idx = split_mode == wxT("size") ? 1
: split_mode == wxT("duration") ? 2
: split_mode == wxT("timecodes") ? 3
: split_mode == wxT("parts") ? 4
: split_mode == wxT("parts-frames") ? 5
: split_mode == wxT("frames") ? 6
: split_mode == wxT("chapters") ? 7
: 0;
}
}
@ -515,8 +533,9 @@ tab_global::save(wxConfigBase *cfg) {
: 2 == split_mode_idx ? wxT("duration")
: 3 == split_mode_idx ? wxT("timecodes")
: 4 == split_mode_idx ? wxT("parts")
: 5 == split_mode_idx ? wxT("frames")
: 6 == split_mode_idx ? wxT("chapters")
: 5 == split_mode_idx ? wxT("parts-frames")
: 6 == split_mode_idx ? wxT("frames")
: 7 == split_mode_idx ? wxT("chapters")
: wxT("none");
cfg->Write(wxT("split_mode"), value);
cfg->Write(wxT("split_args"), cob_split_args->GetValue());

View File

@ -230,3 +230,4 @@ T_381X_alac:bf27f11ef2cc46c8490fe6ecfbc9d47c-ed3dd47220947f8130974268f0af281d:pa
T_382split_chapters:352a6c27ffc26565b45c8d3c014a18aa+be25f966d8d05465491385a420cd1f9b+ok-e09092b9b0f3e8b370f1a4b741fe8539+03badbdab502b87e0f323e486e0cf40c+ok-0ec4a1de0fb74fc5924439245dd48267+4fb8cfcbefde6256464d2c2f5422e926+19d72aca3ea4267fdbb0c685f3a807a5+6a1598fcf8286bf6a18d9a5b0da44ea7+1b098adad89cf106aa7a7ff7ed85faba+8d7d5c1cb47a9f1bbdbbd34336251947+1018f6b30b3d610f27482408a1d2e209+e7db2fc928d431accfc426e2a716e4a9+5e26b821afb4f7091d71164dfb3106f3+82c85375d465486a288a144ed88343c2+eacf9cdf018cd1e334da2450929d9497+ec2af2541adbee9e24d8058f5d15ed70+a0a7c8ed361294e1326130c3fccc3e36+ce0bb503534f00e454f51ebc883afa4f+8d2ed3e7a632d9c6ac948f157c3753b3+d653a17ba7d0448549e3019e2da03d1e+8db5db08e53b7e72ecd64689fa2fdf50+ok-de4f62f696ad97032645248dc401d59f+3946ed32564f5a269935835734396341+74886de816bc98c7ac2ba7d48288e7ca+6ad323bcf3b392129c1e8ea9d3cbf3d7+7f220ae5a4fe9b1c837f9078a0cf3648+4db726777694397bd9a606f3120c8a4b+e1c0df9d1716b406cab866ddf5518ce3+2439a94e258047a36141a77d94876002+ac69a10174e80fabeb412231f11b3f53+70b9f25ff48423fb3adad372edc9abd8+23e771db8b2505c16a8c9eac42ea1119+774089c52b37fd31bc7858762051de75+2a3f668f947f0add929d887ae7caf032+96e857a44a6674d4a985cf21f9053c35+96e62109ad173b219422765def88759f+2df1b1666e689581dd412c8b092fe568+83e5e7ed87bc4010c12b4afe3f3da858+ok:passed:20121227-202501:1.612054564
T_383mp4_text_track_subtitles:76381141faab5737c5001b042d4b6235:passed:20121228-231519:0.072762138
T_384vobsub_in_mp4:62fd55de5e9ce07925d710395971a7ae:passed:20130112-201109:0.568759516
T_385split_parts_frames:14add8af7d016c048f0828708af04a86+c787a3d20fe2c26f7e0e0311f955a281+ok-e1001084ce3512e9dc8940d4c9ade87d+ok-d1c052f274338865a10c8ac6bbd1d1a3+ok-a4401edb1f7c56909267babaaa0e252b+ok-5eaaeb9e142363b1d3abff097b8a93c6+ok:passed:20130114-224502:1.316951981

View File

@ -0,0 +1,39 @@
#!/usr/bin/ruby -w
# T_385split_parts_frames
describe "mkvmerge / --split parts-frames:..."
test "data/avi/v-h264-aac.avi two output files" do
merge "--split parts-frames:480-720,-960 data/avi/v-h264-aac.avi", :output => "#{tmp}-%02d"
result = [ hash_file("#{tmp}-01"), hash_file("#{tmp}-02"), File.exist?("#{tmp}-03") ? "bad" : "ok" ].join('+')
unlink_tmp_files
result
end
test "data/avi/v-h264-aac.avi one output file" do
merge "--split parts-frames:480-720,+960-1198 data/avi/v-h264-aac.avi", :output => "#{tmp}-%02d"
result = [ hash_file("#{tmp}-%02d"), File.exist?("#{tmp}-02") ? fail("second file should not exist") : "ok" ].join('+')
unlink_tmp_files
result
end
test "data/avi/v-h264-aac.avi one output file, starting at 0" do
merge "--split parts-frames:-720,+960-1198 data/avi/v-h264-aac.avi", :output => "#{tmp}-%02d"
result = [ hash_file("#{tmp}-%02d"), File.exist?("#{tmp}-02") ? fail("second file should not exist") : "ok" ].join('+')
unlink_tmp_files
result
end
test "data/avi/v-h264-aac.avi three parts one output file, starting at 0" do
merge "--split parts-frames:-240,+480-720,+960-1198 data/avi/v-h264-aac.avi", :output => "#{tmp}-%02d"
result = [ hash_file("#{tmp}-%02d"), File.exist?("#{tmp}-02") ? fail("second file should not exist") : "ok" ].join('+')
unlink_tmp_files
result
end
test "data/avi/v-h264-aac.avi three parts one output file, starting at 240" do
merge "--split parts-frames:240-480,+720-960,+1198-1440 data/avi/v-h264-aac.avi", :output => "#{tmp}-%02d"
result = [ hash_file("#{tmp}-%02d"), File.exist?("#{tmp}-02") ? fail("second file should not exist") : "ok" ].join('+')
unlink_tmp_files
result
end