]> git.decadent.org.uk Git - videolink.git/blobdiff - generate_dvd.cpp
Moved generation of menu VOBs from videolink_window to dvd_generator.
[videolink.git] / generate_dvd.cpp
index 32ce0f20a37a3cb580caf4ef4523e87a5a394ff2..ff1b63574eaf0f01bb7fae9bb78e3b8b061fcd9a 100644 (file)
 // See the file "COPYING" for licence details.
 
 #include <fstream>
+#include <iostream>
+#include <sstream>
 #include <stdexcept>
 
+#include <gdkmm/pixbuf.h>
 #include <glibmm/spawn.h>
 
 #include "dvd.hpp"
 #include "generate_dvd.hpp"
 #include "xml_utils.hpp"
 
-dvd_contents::menu::menu()
+namespace
+{
+    // 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;
+    }
+}
+
+dvd_generator::menu::menu()
        : vob_temp(new temp_file("videolink-vob-"))
 {
     vob_temp->close();
 }
 
-void generate_dvd(const dvd_contents & contents,
-                 const std::string & output_dir)
+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<Gdk::Pixbuf> background,
+                                     Glib::RefPtr<Gdk::Pixbuf> highlights)
+    const
+{
+    assert(index < menus_.size());
+    const menu & this_menu = menus_[index];
+
+    temp_file background_temp("videolink-back-");
+    background_temp.close();
+    std::cout << "saving " << background_temp.get_name() << std::endl;
+    background->save(background_temp.get_name(), "png");
+
+    temp_file highlights_temp("videolink-links-");
+    highlights_temp.close();
+    std::cout << "saving " << highlights_temp.get_name() << std::endl;
+    highlights->save(highlights_temp.get_name(), "png");
+
+    temp_file spumux_temp("videolink-spumux-");
+    spumux_temp.close();
+    std::ofstream spumux_file(spumux_temp.get_name().c_str());
+    spumux_file <<
+       "<subpictures>\n"
+       "  <stream>\n"
+       "    <spu force='yes' start='00:00:00.00'\n"
+       "        highlight='" << highlights_temp.get_name() << "'\n"
+       "        select='" << highlights_temp.get_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;
+    if (encoder_ == mpeg_encoder_ffmpeg)
+    {
+       command_stream
+           << "ffmpeg"
+           << " -f image2 -vcodec png -i "
+           << background_temp.get_name()
+           << " -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_temp.get_name()
+           << " | ppmtoy4m -v0 -n1 -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_temp.get_name()
+       << " > " << this_menu.vob_temp->get_name();
+    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
 {
     temp_file temp("videolink-dvdauthor-");
     temp.close();
@@ -26,8 +245,8 @@ void generate_dvd(const dvd_contents & contents,
     // We generate code that uses registers in the following way:
     //
     // g0:     scratch
-    // g1:     current location
-    // g2:     location that last jumped to a title
+    // g1:     target menu location
+    // g2:     source/return menu location for title
     // g3:     target chapter number
     //
     // All locations are divided into two bitfields: the least
@@ -45,11 +264,9 @@ void generate_dvd(const dvd_contents & contents,
        "  <vmgm>\n"
        "    <menus>\n";
            
-    for (unsigned menu_index = 0;
-        menu_index != contents.menus.size();
-        ++menu_index)
+    for (unsigned menu_index = 0; menu_index != menus_.size(); ++menu_index)
     {
-       const dvd_contents::menu & menu = contents.menus[menu_index];
+       const menu & this_menu = menus_[menu_index];
 
        if (menu_index == 0)
        {
@@ -58,8 +275,9 @@ void generate_dvd(const dvd_contents & contents,
            file <<
                "      <pgc entry='title' pause='inf'>\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";
        }
@@ -70,7 +288,7 @@ void generate_dvd(const dvd_contents & contents,
                "        <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
@@ -95,7 +313,7 @@ void generate_dvd(const dvd_contents & contents,
             menu_incr != 0;
             menu_incr /= 2)
        {
-           if (menu_index + menu_incr < contents.menus.size()
+           if (menu_index + menu_incr < menus_.size()
                && (menu_index & (menu_incr * 2 - 1)) == 0)
            {
                file <<
@@ -109,19 +327,22 @@ void generate_dvd(const dvd_contents & contents,
        file <<
            // Highlight the appropriate button.
            "          s8 = g1 &amp; " << 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='" << this_menu.vob_temp->get_name() << "'/>\n";
 
        for (unsigned button_index = 0;
-            button_index != menu.entries.size();
+            button_index != this_menu.entries.size();
             ++button_index)
        {
-           const dvd_contents::pgc_ref & target =
-               menu.entries[button_index];
+           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;
 
@@ -135,14 +356,12 @@ void generate_dvd(const dvd_contents & contents,
                    // 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_index);
+                   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 - 1]
+                   while (target_button_num != 1
+                          && (target_menu_entries[target_button_num - 1].target
                               != this_pgc))
                        --target_button_num;
                }
@@ -156,12 +375,11 @@ void generate_dvd(const dvd_contents & contents,
            }
            else
            {
-               assert(target.type == dvd_contents::title_pgc);
+               assert(target.type == title_pgc);
 
                file <<
-                   // Record current menu location (g1 specifies this
-                   // menu but not necessarily this button).
-                   "g1 = " << (1 + menu_index
+                   // Record current menu location.
+                   "g2 = " << (1 + menu_index
                                + (1 + button_index) * button_mult) << "; "
                    // Set target chapter number.
                    "g3 = " << target.sub_index << "; "
@@ -183,35 +401,34 @@ void generate_dvd(const dvd_contents & contents,
 
     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_index = 0;
-        title_index != contents.titles.size();
-        ++title_index)
+        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"
-           "        <pre>\n"
-           // Record calling location.
-           "          g2 = g1;\n";
+           "        <pre>\n";
 
        // Count chapters in the title.
        unsigned n_chapters = 0;
        for (vob_list::const_iterator
-                it = contents.titles[title_index].begin(),
-                end = contents.titles[title_index].end();
+                it = titles_[title_index].begin(),
+                end = titles_[title_index].end();
             it != end;
             ++it)
        {
@@ -241,8 +458,8 @@ void generate_dvd(const dvd_contents & contents,
            "        </pre>\n";
 
        for (vob_list::const_iterator
-                it = contents.titles[title_index].begin(),
-                end = contents.titles[title_index].end();
+                it = titles_[title_index].begin(),
+                end = titles_[title_index].end();
             it != end;
             ++it)
        {
@@ -256,12 +473,8 @@ void generate_dvd(const dvd_contents & contents,
 
        file <<
            "        <post>\n"
-           // If the menu location has not been changed during
-           // the title, set the location to be the following
-           // button in the menu.
-           "          if (g1 eq g2)\n"
-           "            g1 = g1 + " << button_mult << ";\n"
-           // In any case, return to some menu.
+           // Return to the source menu, but highlight the next button.
+           "          g2 = g2 + " << button_mult << ";\n"
            "          call menu;\n"
            "        </post>\n"
            "      </pgc>\n"