X-Git-Url: https://git.decadent.org.uk/gitweb/?p=videolink.git;a=blobdiff_plain;f=generate_dvd.cpp;h=398fdd15e1662d6a095ccc0eafb9a74e0e1247b1;hp=eeadc489bc1b75a92bae82a1dbc8cb3c2074f8ec;hb=ef1488deebfb1a8f84ea4c0c498e50b90b75ff3d;hpb=c13714f6498df33e02635421354f5fb88a60eb3d diff --git a/generate_dvd.cpp b/generate_dvd.cpp index eeadc48..398fdd1 100644 --- a/generate_dvd.cpp +++ b/generate_dvd.cpp @@ -1,33 +1,288 @@ -// Copyright 2005-6 Ben Hutchings . +// Copyright 2005-6 Ben Hutchings . // See the file "COPYING" for licence details. #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 "PAL" disc so let's say 0.5 seconds rounded down. + const char menu_duration_string[] = "0.5"; + unsigned menu_duration_frames(const video::frame_params & params) + { + return params.rate_numer / params.rate_denom / 2; + } +} + +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() +{ + pgc_ref next_menu(menu_pgc, menus_.size()); + + // Check against maximum number of menus. It appears that no more + // than 128 menus are reachable through LinkPGCN instructions, and + // dvdauthor uses some menu numbers for special purposes, resulting + // in a practical limit of 119 per domain. We can work around this + // later by spreading some menus across titlesets. + if (next_menu.index == 119) + throw std::runtime_error("No more than 119 menus can be used"); + + 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()); + 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 << "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 << "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 menu_entry & this_entry = this_menu.entries[i]; + + // 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) + { + 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; + } + } + } + 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::ostringstream command_stream; + unsigned frame_count(menu_duration_frames(frame_params_)); + if (encoder_ == mpeg_encoder_ffmpeg) + { + for (unsigned i = 0; i != frame_count; ++i) + { + std::string frame_name(background_name); + frame_name.push_back('-'); + frame_name.push_back('0' + i / 10); + frame_name.push_back('0' + i % 10); + if (symlink(background_name.c_str(), frame_name.c_str()) != 0) + throw std::runtime_error( + std::string("symlink: ").append(std::strerror(errno))); + } + command_stream << + "ffmpeg -f image2 -vcodec png -i " << background_name << "-%02d" + " -target " << frame_params_.common_name << "-dvd" + " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout"; + } + else + { + assert(encoder_ == mpeg_encoder_mjpegtools_old + || encoder_ == mpeg_encoder_mjpegtools_new); + command_stream + << "pngtopnm " << background_name + << " | ppmtoy4m -v0 -n" << frame_count << " -F" + << frame_params_.rate_numer << ":" << frame_params_.rate_denom + << " -A" << frame_params_.pixel_ratio_width + << ":" << frame_params_.pixel_ratio_height + << " -Ip "; + // The chroma subsampling keywords changed between + // versions 1.6.2 and 1.8 of mjpegtools. There is no + // keyword that works with both. + if (encoder_ == mpeg_encoder_mjpegtools_old) + command_stream << "-S420_mpeg2"; + else + command_stream << "-S420mpeg2"; + command_stream << + " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout" + " | mplex -v0 -f8 -o/dev/stdout /dev/stdin"; + } + command_stream + << " | spumux -v0 -mdvd " << spumux_name + << " > " << temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index); + std::string command(command_stream.str()); + const char * argv[] = { + "/bin/sh", "-c", command.c_str(), 0 + }; + std::cout << "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::Slot0(), + 0, 0, + &command_result); + if (command_result != 0) + throw std::runtime_error("spumux pipeline failed"); } -void generate_dvd(const dvd_contents & contents, - const std::string & output_dir) +dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content) { - temp_file temp("webdvd-dvdauthor-"); - temp.close(); - std::ofstream file(temp.get_name().c_str()); + pgc_ref next_title(title_pgc, titles_.size()); + + // Check against maximum number of titles. + if (next_title.index == 99) + throw std::runtime_error("No more than 99 titles can be used"); + + 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 +{ + std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor")); + std::ofstream file(name.c_str()); // We generate code that uses registers in the following way: // // g0: scratch - // g1: current location - // g12: location that last jumped to a video + // g1: target menu location + // g2: source/return menu location for title + // g3: target chapter number // // All locations are divided into two bitfields: the least // significant 10 bits are a page/menu number and the most @@ -44,21 +299,20 @@ void generate_dvd(const dvd_contents & contents, " \n" " \n"; - for (unsigned menu_num = 0; - menu_num != contents.menus.size(); - ++menu_num) + for (unsigned menu_index = 0; menu_index != menus_.size(); ++menu_index) { - const dvd_contents::menu & menu = contents.menus[menu_num]; + const menu & this_menu = menus_[menu_index]; - if (menu_num == 0) + if (menu_index == 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).
+		// Set a default target location if none is set.
+		// This covers first play and use of the "top menu"
+		// button.
 		"          if (g1 eq 0)\n"
 		"            g1 = " << 1 + button_mult << ";\n";
 	}
@@ -69,7 +323,7 @@ void generate_dvd(const dvd_contents & contents,
 		"        
\n";
 	}
 
-	// When a title finishes or the user presses the menu
+	// 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
@@ -94,13 +348,13 @@ void generate_dvd(const dvd_contents & contents,
 	     menu_incr != 0;
 	     menu_incr /= 2)
 	{
-	    if (menu_num + menu_incr < contents.menus.size()
-		&& (menu_num & (menu_incr * 2 - 1)) == 0)
+	    if (menu_index + menu_incr < menus_.size()
+		&& (menu_index & (menu_incr * 2 - 1)) == 0)
 	    {
 		file <<
-		    "          if (g0 ge " << 1 + menu_num + menu_incr
+		    "          if (g0 ge " << 1 + menu_index + menu_incr
 					   << ")\n"
-		    "            jump menu " << 1 + menu_num + menu_incr
+		    "            jump menu " << 1 + menu_index + menu_incr
 					   << ";\n";
 	    }
 	}
@@ -108,19 +362,30 @@ void generate_dvd(const dvd_contents & contents,
 	file <<
 	    // Highlight the appropriate button.
 	    "          s8 = g1 & " << button_mask << ";\n"
+	    // Forget the link target.  If we don't do this, pressing
+	    // the "top menu" button will result in jumping back to
+	    // this same menu!
+	    "          g1 = 0;\n"
 	    "        
\n" - " \n"; + " \n" + // Define a cell covering the whole menu and set a still + // time at the end of that, since it seems all players + // support that but some ignore a still time set on a PGC. + " \n" + " \n"; - for (unsigned button_num = 0; - button_num != menu.entries.size(); - ++button_num) + for (unsigned button_index = 0; + button_index != this_menu.entries.size(); + ++button_index) { - const dvd_contents::pgc_ref & target = - menu.entries[button_num]; + const pgc_ref & target = this_menu.entries[button_index].target; file << " \n"; + file << "\n"; } - file << " \n"; + file << + " \n"; } file << " \n" - " \n"; - + " \n"; + // Generate a titleset for each title. This appears to make // jumping to titles a whole lot simpler (but limits us to 99 // titles). - for (unsigned title_num = 0; - title_num != contents.titles.size(); - ++title_num) + for (unsigned title_index = 0; + title_index != titles_.size(); + ++title_index) { - file << + file << " \n" - // Generate a dummy menu so that the menu button on the - // remote control will work. + // Generate a dummy menu so that the "menu" button will + // work. This returns to the source menu via the title + // menu. " \n" " \n" - "
 jump vmgm menu; 
\n" + "
 g1 = g2; jump vmgm menu; 
\n" "
\n" "
\n" " \n" " \n" - // Record calling location. - "
 g12 = g1; 
\n"; + "
\n";
+
+	// Count chapters in the title.
+	unsigned n_chapters = 0;
+	for (vob_list::const_iterator
+		 it = titles_[title_index].begin(),
+		 end = titles_[title_index].end();
+	     it != end;
+	     ++it)
+	{
+	    // Chapter start times may be specified in the "chapters"
+	    // attribute as a comma-separated list.  If this is not
+	    // specified then the beginning of each file starts a new
+	    // chapter.  Thus the number of chapters in each file is
+	    // the number of commas in the chapter attribute, plus 1.
+	    ++n_chapters;
+	    std::size_t pos = 0;
+	    while ((pos = it->chapters.find(',', pos)) != std::string::npos)
+	    {
+		++n_chapters;
+		++pos;
+	    }
+	}
 
-	for (vob_list::const_iterator it = contents.titles[title_num].begin(),
-		 end = contents.titles[title_num].end();
+	// Generate jump "table" for chapters.
+	for (unsigned chapter_num = 1;
+	     chapter_num <= n_chapters;
+	     ++chapter_num)
+	    file <<
+		"          if (g3 == " << chapter_num << ")\n"
+		"            jump chapter " << chapter_num << ";\n";
+
+	file <<
+	    "        
\n"; + + for (vob_list::const_iterator + it = titles_[title_index].begin(), + end = titles_[title_index].end(); it != end; ++it) { @@ -210,12 +510,11 @@ void generate_dvd(const dvd_contents & contents, } file << - // If the menu location has not been changed during - // the title, set the location to be the following - // button in the menu. In any case, return to some - // menu. - " if (g1 eq g12) g1 = g1 + " << button_mult - << "; call menu; \n" + " \n" + // Return to the source menu, but highlight the next button. + " g2 = g2 + " << button_mult << ";\n" + " call menu;\n" + " \n" "
\n" "
\n" "
\n"; @@ -230,7 +529,7 @@ void generate_dvd(const dvd_contents & contents, const char * argv[] = { "dvdauthor", "-o", output_dir.c_str(), - "-x", temp.get_name().c_str(), + "-x", name.c_str(), 0 }; int command_result;