X-Git-Url: https://git.decadent.org.uk/gitweb/?p=videolink.git;a=blobdiff_plain;f=generate_dvd.cpp;h=371894d41edc23abd7ac34abee8337fd52b64711;hp=9f15d2f96a903e3d8dd73d74cadeb2788bcef142;hb=HEAD;hpb=c5f98ff303d670f72a5d491a5f4e1989d2043219 diff --git a/generate_dvd.cpp b/generate_dvd.cpp index 9f15d2f..371894d 100644 --- a/generate_dvd.cpp +++ b/generate_dvd.cpp @@ -1,244 +1,700 @@ -// Copyright 2005-6 Ben Hutchings . +// Copyright 2005-8 Ben Hutchings . // See the file "COPYING" for licence details. +#include +#include +#include #include +#include +#include +#include +#include #include +#include +#include +#include + +#include +#include #include #include "dvd.hpp" #include "generate_dvd.hpp" #include "xml_utils.hpp" -dvd_contents::menu::menu() - : vob_temp(new temp_file("webdvd-vob-")) +namespace { - vob_temp->close(); + // Return a closeness metric of an "end" rectangle to a "start" + // rectangle in the upward (-1) or downward (+1) direction. Given + // several possible "end" rectangles, the one that seems visually + // closest in the given direction should have the highest value of + // this metric. This is necessarily a heuristic function! + double directed_closeness(const rectangle & start, const rectangle & end, + int y_dir) + { + // The obvious approach is to use the centres of the + // rectangles. However, for the "end" rectangle, using the + // horizontal position nearest the centre of the "start" + // rectangle seems to produce more reasonable results. For + // example, if there are two "end" rectangles equally near to + // the "start" rectangle in terms of vertical distance and one + // of them horizontally overlaps the centre of the "start" + // rectangle, we want to pick that one even if the centre of + // that rectangle is further away from the centre of the + // "start" rectangle. + int start_x = (start.left + start.right) / 2; + int start_y = (start.top + start.bottom) / 2; + int end_y = (end.top + end.bottom) / 2; + int end_x; + if (end.right < start_x) + end_x = end.right; + else if (end.left > start_x) + end_x = end.left; + else + end_x = start_x; + + // Return cosine of angle between the line between these points + // and the vertical, divided by the distance between the points + // if that is defined and positive; otherwise return 0. + int vertical_distance = (end_y - start_y) * y_dir; + if (vertical_distance <= 0) + return 0.0; + double distance_squared = + (end_x - start_x) * (end_x - start_x) + + (end_y - start_y) * (end_y - start_y); + return vertical_distance / distance_squared; + } + + std::string temp_file_name(const temp_dir & dir, + std::string base_name, + unsigned index=0) + { + if (index != 0) + { + std::size_t index_pos = base_name.find("%3d"); + assert(index_pos != std::string::npos); + base_name[index_pos] = '0' + index / 100; + base_name[index_pos + 1] = '0' + (index / 10) % 10; + base_name[index_pos + 2] = '0' + index % 10; + } + + return Glib::build_filename(dir.get_name(), base_name); + } + + // We would like to use just a single frame for the menu but this + // seems not to be legal or compatible. The minimum length of a + // cell is 0.4 seconds but I've seen a static menu using 12 frames + // on a commercial "PAL" disc so let's use 12 frames regardless. + unsigned menu_duration_frames(const video::frame_params & params) + { + return 12; + } + double menu_duration_seconds(const video::frame_params & params) + { + return double(menu_duration_frames(params)) + / double(params.rate_numer) + * double(params.rate_denom); + } + + void throw_length_error(const char * limit_type, std::size_t limit) + { + std::ostringstream oss; + oss << "exceeded DVD limit: " << limit_type << " > " << limit; + throw std::length_error(oss.str()); + } + + // dvdauthor uses some menu numbers to represent entry points - + // distinct from the actual numbers of the menus assigned as those + // entry points - resulting in a practical limit of 119 per + // domain. This seems to be an oddity of the parser that could be + // fixed, but for now we'll have to work with it. + const unsigned dvdauthor_anonymous_menus_max = dvd::domain_pgcs_max - 8; + + // The current navigation code packs menu and button number into a + // single register, so the number of menus is limited to + // dvd::reg_s8_button_mult - 1 == 1023. However temp_file_name() + // is limited to 999 numbered files and it seems pointless to + // change it to get another 24. + // If people really need more we could use separate menu and + // button number registers, possibly allowing up to 11900 menus + // (the size of the indirect jump tables might become a problem + // though). + const unsigned menus_max = 999; } -void generate_dvd(const dvd_contents & contents, - const std::string & output_dir) +dvd_generator::dvd_generator(const video::frame_params & frame_params, + mpeg_encoder encoder) + : temp_dir_("videolink-"), + frame_params_(frame_params), + encoder_(encoder) +{} + +dvd_generator::pgc_ref dvd_generator::add_menu() { - temp_file temp("webdvd-dvdauthor-"); - temp.close(); - std::ofstream file(temp.get_name().c_str()); + pgc_ref next_menu(menu_pgc, menus_.size()); - // We generate code that uses registers in the following way: - // - // g0: scratch - // g1: current location - // g12: location that last jumped to a video - // - // All locations are divided into two bitfields: the least - // significant 10 bits are a page/menu number and the most - // significant 6 bits are a link/button number, and numbering - // starts at 1, not 0. This is done for compatibility with - // the encoding of the s8 (button) register. - // - static const int button_mult = dvd::reg_s8_button_mult; - static const int menu_mask = button_mult - 1; - static const int button_mask = (1 << dvd::reg_bits) - button_mult; - - file << - "\n" - " \n" - " \n"; - - for (unsigned menu_num = 0; - menu_num != contents.menus.size(); - ++menu_num) + if (next_menu.index == menus_max) + throw_length_error("number of menus", menus_max); + + menus_.resize(next_menu.index + 1); + return next_menu; +} + +void dvd_generator::add_menu_entry(unsigned index, + const rectangle & area, + const pgc_ref & target) +{ + assert(index < menus_.size()); + assert(target.type == menu_pgc && target.index < menus_.size() + || target.type == title_pgc && target.index < titles_.size()); + + if (menus_[index].entries.size() == dvd::menu_buttons_max) + throw_length_error("number of buttons", dvd::menu_buttons_max); + + menu_entry new_entry = { area, target }; + menus_[index].entries.push_back(new_entry); +} + +void dvd_generator::generate_menu_vob(unsigned index, + Glib::RefPtr background, + Glib::RefPtr highlights) + const +{ + assert(index < menus_.size()); + const menu & this_menu = menus_[index]; + + std::string background_name( + temp_file_name(temp_dir_, "menu-%3d-back.png", 1 + index)); + std::cout << "INFO: Saving " << background_name << std::endl; + background->save(background_name, "png"); + + std::string highlights_name( + temp_file_name(temp_dir_, "menu-%3d-links.png", 1 + index)); + std::cout << "INFO: Saving " << highlights_name << std::endl; + highlights->save(highlights_name, "png"); + + std::string spumux_name( + temp_file_name(temp_dir_, "menu-%3d.subpictures", 1 + index)); + std::ofstream spumux_file(spumux_name.c_str()); + spumux_file << + "\n" + " \n" + " \n"; + int button_count = this_menu.entries.size(); + for (int i = 0; i != button_count; ++i) { - const dvd_contents::menu & menu = contents.menus[menu_num]; + const menu_entry & this_entry = this_menu.entries[i]; - if (menu_num == 0) - { - // This is the first (title) menu, displayed when the - // disc is first played. - file << - " \n" - "
\n"
-		// Initialise the current location if it is not set
-		// (all general registers are initially 0).
-		"          if (g1 eq 0)\n"
-		"            g1 = " << 1 + button_mult << ";\n";
-	}
-	else
+	// We program left and right to cycle through the buttons in
+	// the order the entries were added.  This should result in
+	// left and right behaving like the tab and shift-tab keys
+	// would in the browser.  Hopefully that's a sensible order.
+	// We program up and down to behave geometrically.
+	int up_button = i, down_button = i;
+	double up_closeness = 0.0, down_closeness = 0.0;
+	for (int j = 0; j != button_count; ++j)
 	{
-	    file <<
-		"      \n"
-		"        
\n";
+	    const menu_entry & other_entry = this_menu.entries[j];
+	    double closeness = directed_closeness(
+		this_entry.area, other_entry.area, -1);
+	    if (closeness > up_closeness)
+	    {
+		up_button = j;
+		up_closeness = closeness;
+	    }
+	    else
+	    {
+		closeness = directed_closeness(
+		    this_entry.area, other_entry.area, 1);
+		if (closeness > down_closeness)
+		{
+		    down_button = j;
+		    down_closeness = closeness;
+		}
+	    }
 	}
+	// Pad vertically to even y coordinates since dvdauthor claims
+	// odd values may result in incorrect display.
+	// XXX This may cause overlappping where it wasn't previously
+	// a problem.
+	spumux_file << "      \n";
+    }
+    spumux_file <<
+	"    \n"
+	"  \n"
+	"\n";
+    spumux_file.close();
+    if (!spumux_file)
+	throw std::runtime_error("Failed to write control file for spumux");
+
+    std::string output_name(
+	temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index));
+
+    std::ostringstream command_stream;
+    if (encoder_ == mpeg_encoder_ffmpeg)
+    {
+	command_stream <<
+	    "ffmpeg -f image2 -vcodec png"
+	    " -r " << frame_params_.rate_numer <<
+	    "/" << frame_params_.rate_denom <<
+	    " -loop_input -i " << background_name <<
+	    " -t " << menu_duration_seconds(frame_params_) <<
+	    " -target " << frame_params_.common_name <<  "-dvd"
+	    " -aspect 4:3 -an -y /dev/stdout";
+    }
+    else
+    {
+	assert(encoder_ == mpeg_encoder_mjpegtools);
+	command_stream
+	    << "pngtopnm " << background_name
+	    << " | ppmtoy4m -v0 -n" << menu_duration_frames(frame_params_)
+	    << " -F" << frame_params_.rate_numer << ":" << frame_params_.rate_denom
+	    << " -A" << frame_params_.pixel_ratio_width
+	    << ":" << frame_params_.pixel_ratio_height
+	    << " -Ip -S420mpeg2"
+	    " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
+	    " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
+    }
+    command_stream
+	<< " | spumux -v0 -mdvd " << spumux_name << " > " << output_name;
+    std::string command(command_stream.str());
+    const char * argv[] = {
+	"/bin/sh", "-c", command.c_str(), 0
+    };
+    std::cout << "INFO: Running " << command << std::endl;
+    int command_result;
+    Glib::spawn_sync(".",
+		     Glib::ArrayHandle(
+			 argv, sizeof(argv)/sizeof(argv[0]),
+			 Glib::OWNERSHIP_NONE),
+		     Glib::SPAWN_STDOUT_TO_DEV_NULL,
+		     sigc::slot(),
+		     0, 0,
+		     &command_result);
+    struct stat stat_buf;
+    if (command_result != 0 || stat(output_name.c_str(), &stat_buf) != 0
+	|| stat_buf.st_size == 0)
+	throw std::runtime_error("spumux pipeline failed");
+}
+
+dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
+{
+    pgc_ref next_title(title_pgc, titles_.size());
+
+    // Check against maximum number of titles.
+    if (next_title.index == dvd::titles_max)
+	throw_length_error("number of titles", dvd::titles_max);
+
+    titles_.resize(next_title.index + 1);
+    titles_[next_title.index].swap(content);
+    return next_title;
+}
+
+void dvd_generator::generate(const std::string & output_dir) const
+{
+    // This function uses a mixture of 0-based and 1-based numbering,
+    // due to the differing conventions of the language and the DVD
+    // format.  Variable names ending in "_index" indicate 0-based
+    // indices and variable names ending in "_num" indicate 1-based
+    // numbers.
+
+    std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
+    std::ofstream file(name.c_str());
+    file << "\n";
 
-	// When a title finishes or the user presses the menu
-	// button, this always jumps to the titleset's root menu.
-	// We want to return the user to the last menu they used.
-	// So we arrange for each titleset's root menu to return
-	// to the vmgm title menu and then dispatch from there to
-	// whatever the correct menu is.  We determine the correct
-	// menu by looking at the menu part of g1.
-
-	file << "          g0 = g1 & " << menu_mask << ";\n";
-
-	// There is a limit of 128 VM instructions in each PGC.
-	// Therefore in each menu's 
 section we generate
-	// jumps to menus with numbers greater by 512, 256, 128,
-	// ..., 1 where (a) such a menu exists, (b) this menu
-	// number is divisible by twice that increment and (c) the
-	// correct menu is that or a later menu.  Thus each menu
-	// has at most 10 such conditional jumps and is reachable
-	// by at most 10 jumps from the title menu.  This chain of
-	// jumps might take too long on some players; this has yet
-	// to be investigated.
-	    
-	for (std::size_t menu_incr = (menu_mask + 1) / 2;
-	     menu_incr != 0;
-	     menu_incr /= 2)
+    // We generate code that uses registers in the following way:
+    //
+    // g0: Scratch.
+    // g1: Target location when jumping between menus.  Top 6 bits are
+    //     the button number (like s8) and bottom 10 bits are the menu
+    //     number.  This is used for selecting the appropriate button
+    //     when entering a menu, for completing indirect jumps between
+    //     domains, and for jumping to the correct menu after exiting a
+    //     title.  This is set to 0 in the pre-routine of the target
+    //     menu.
+    // g2: Current location in menus.  This is used for jumping to the
+    //     correct menu when the player exits a title.
+    // g3: Target chapter number plus 1 when jumping to a title.
+    //     This is used to jump to the correct chapter and to
+    //     distinguish between indirect jumps to menus and titles.
+    //     This is set to 0 in the pre-routine of the target title.
+    // g4: Source menu location used to jump to a title.  This is
+    //     compared with g2 to determine whether to increment the
+    //     button number if the title is played to the end.
+
+    static const unsigned button_mult = dvd::reg_s8_button_mult;
+    static const unsigned menu_mask = button_mult - 1;
+    static const unsigned button_mask = (1U << dvd::reg_bits) - button_mult;
+
+    // Iterate over VMGM and titlesets.  For these purposes, we
+    // consider the VMGM to be titleset 0.
+
+    // We need a titleset for each title, and we may also need titlesets to
+    // hold extra menus if we have too many for the VMGM.
+    // Also, we need at least one titleset.
+    const unsigned titleset_end = std::max(
+	    1U + std::max(1U, titles_.size()),
+	    (menus_.size() + dvdauthor_anonymous_menus_max - 1)
+		 / dvdauthor_anonymous_menus_max);
+
+    for (unsigned titleset_num = 0;
+	 titleset_num != titleset_end;
+	 ++titleset_num)
+    {
+	const char * const outer_element_name =
+	    titleset_num == 0 ? "vmgm" : "titleset";
+	const bool have_real_title =
+	    titleset_num != 0 && titleset_num <= titles_.size();
+	const bool have_real_menus =
+	    titleset_num * dvdauthor_anonymous_menus_max < menus_.size();
+
+	file << "  <" << outer_element_name << ">\n" <<
+	     "    \n"
+	     "      \n";
+
+	if (have_real_title)
+	{
+	    file <<
+		"    \n"
+		"      \n"
-	    "  \n";
+	file << "  \n";
     }
 
-    file <<
-	"\n";
-
+    file << "\n";
     file.close();
 
     {
 	const char * argv[] = {
 	    "dvdauthor",
 	    "-o", output_dir.c_str(),
-	    "-x", temp.get_name().c_str(),
+	    "-x", name.c_str(),
 	    0
 	};
 	int command_result;
@@ -248,7 +704,7 @@ void generate_dvd(const dvd_contents & contents,
 			     Glib::OWNERSHIP_NONE),
 			 Glib::SPAWN_SEARCH_PATH
 			 | Glib::SPAWN_STDOUT_TO_DEV_NULL,
-			 SigC::Slot0(),
+			 sigc::slot(),
 			 0, 0,
 			 &command_result);
 	if (command_result != 0)