-// Copyright 2005-6 Ben Hutchings <ben@decadentplace.org.uk>.
+// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
// See the file "COPYING" for licence details.
+#include <cerrno>
+#include <cstring>
#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <ostream>
+#include <sstream>
#include <stdexcept>
+#include <gdkmm/pixbuf.h>
+#include <glibmm/miscutils.h>
#include <glibmm/spawn.h>
#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 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());
+
+ // 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<Gdk::Pixbuf> background,
+ Glib::RefPtr<Gdk::Pixbuf> 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 <<
+ "<subpictures>\n"
+ " <stream>\n"
+ " <spu force='yes' start='00:00:00.00'\n"
+ " highlight='" << highlights_name << "'\n"
+ " select='" << highlights_name << "'>\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 << " <button"
+ " x0='" << this_entry.area.left << "'"
+ " y0='" << this_entry.area.top << "'"
+ " x1='" << this_entry.area.right << "'"
+ " y1='" << this_entry.area.bottom << "'"
+ " left='" << (i == 0 ? button_count : i) << "'"
+ " right='" << 1 + (i + 1) % button_count << "'"
+ " up='" << 1 + up_button << "'"
+ " down='" << 1 + down_button << "'"
+ "/>\n";
+ }
+ spumux_file <<
+ " </spu>\n"
+ " </stream>\n"
+ "</subpictures>\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"
+ " -r " << frame_params_.rate_numer <<
+ "/" << frame_params_.rate_denom <<
+ " -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<std::string>(
+ argv, sizeof(argv)/sizeof(argv[0]),
+ Glib::OWNERSHIP_NONE),
+ Glib::SPAWN_STDOUT_TO_DEV_NULL,
+ SigC::Slot0<void>(),
+ 0, 0,
+ &command_result);
+ if (command_result != 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 == 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
" <vmgm>\n"
" <menus>\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 <<
- " <pgc entry='title' pause='inf'>\n"
+ " <pgc entry='title'>\n"
" <pre>\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";
}
else
{
file <<
- " <pgc pause='inf'>\n"
+ " <pgc>\n"
" <pre>\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
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";
}
}
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"
" </pre>\n"
- " <vob file='" << menu.vob_temp->get_name() << "'/>\n";
+ " <vob file='"
+ << temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + menu_index)
+ << "'>\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.
+ " <cell start='0' end='"
+ << std::fixed << std::setprecision(4)
+ << menu_duration_seconds(frame_params_) << "'"
+ " chapter='yes' pause='inf'/>\n"
+ " </vob>\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 << " <button> ";
- if (target.type == dvd_contents::menu_pgc)
+ if (target.type == menu_pgc)
{
unsigned target_button_num;
// back to this one. If there is one, set that to
// be the highlighted button; otherwise, use the
// first button.
- const std::vector<dvd_contents::pgc_ref> &
- target_menu_entries =
- contents.menus[target.index].entries;
- dvd_contents::pgc_ref this_pgc(dvd_contents::menu_pgc,
- menu_num);
+ const std::vector<menu_entry> & target_menu_entries =
+ menus_[target.index].entries;
+ pgc_ref this_pgc(menu_pgc, menu_index);
target_button_num = target_menu_entries.size();
- while (target_button_num != 0
- && (target_menu_entries[--target_button_num]
+ while (target_button_num != 1
+ && (target_menu_entries[target_button_num - 1].target
!= this_pgc))
- ;
- target_button_num += 1;
+ --target_button_num;
}
- file << "g1 = "
- << (1 + target.index
- + target_button_num * button_mult)
- << "; jump menu " << 1 + target.index << ";";
+ file <<
+ // Set new menu location.
+ "g1 = " << (1 + target.index
+ + target_button_num * button_mult) << "; "
+ // Jump to the target menu.
+ "jump menu " << 1 + target.index << "; ";
}
else
{
- assert(target.type == dvd_contents::title_pgc);
-
- file << "g1 = "
- << 1 + menu_num + (1 + button_num) * button_mult
- << "; jump title "
- << 1 + target.index;
- // FIXME: Here we should check target.sub_index and
- // jump to a specific chapter if it is non-zero.
- // However, we can't jump directly to chapters from
- // the VMGM.
- file << ";";
+ assert(target.type == title_pgc);
+
+ file <<
+ // Record current menu location.
+ "g2 = " << (1 + menu_index
+ + (1 + button_index) * button_mult) << "; "
+ // Set target chapter number.
+ "g3 = " << target.sub_index << "; "
+ // Jump to the target title.
+ "jump title " << 1 + target.index << "; ";
}
- file << " </button>\n";
+ file << "</button>\n";
}
file <<
- // Some DVD players don't seem to obey pause='inf' so make
- // them loop.
- " <post>\n"
- " jump cell 1;\n"
- " </post>\n"
" </pgc>\n";
}
file <<
" </menus>\n"
- " </vmgm>\n";
-
+ " </vmgm>\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 <<
" <titleset>\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.
" <menus>\n"
" <pgc entry='root'>\n"
- " <pre> jump vmgm menu; </pre>\n"
+ " <pre> g1 = g2; jump vmgm menu; </pre>\n"
" </pgc>\n"
" </menus>\n"
" <titles>\n"
" <pgc>\n"
- // Record calling location.
- " <pre> g12 = g1; </pre>\n";
+ " <pre>\n";
- for (vob_list::const_iterator it = contents.titles[title_num].begin(),
- end = contents.titles[title_num].end();
+ // 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;
+ }
+ }
+
+ // 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 <<
+ " </pre>\n";
+
+ for (vob_list::const_iterator
+ it = titles_[title_index].begin(),
+ end = titles_[title_index].end();
it != end;
++it)
{
}
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.
- " <post> if (g1 eq g12) g1 = g1 + " << button_mult
- << "; call menu; </post>\n"
+ " <post>\n"
+ // Return to the source menu, but highlight the next button.
+ " g2 = g2 + " << button_mult << ";\n"
+ " call menu;\n"
+ " </post>\n"
" </pgc>\n"
" </titles>\n"
" </titleset>\n";
const char * argv[] = {
"dvdauthor",
"-o", output_dir.c_str(),
- "-x", temp.get_name().c_str(),
+ "-x", name.c_str(),
0
};
int command_result;