]> git.decadent.org.uk Git - videolink.git/blobdiff - generate_dvd.cpp
Increased number of frames in menu, defined a cell covering each menu, and moved...
[videolink.git] / generate_dvd.cpp
index 5e129d4235b794fb0d68f54440f68c1976b59909..398fdd15e1662d6a095ccc0eafb9a74e0e1247b1 100644 (file)
-// 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 <fstream>
+#include <iostream>
+#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 "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<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 -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");
 }
 
-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,
        "  <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'>\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";
        }
@@ -69,7 +323,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
@@ -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 &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='"
+            << 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='" << menu_duration_string << "'"
+           " 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;
 
@@ -134,72 +399,105 @@ 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_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 << "      </pgc>\n";
+       file <<
+           "      </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";
+
+       // 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 <<
+           "        </pre>\n";
+
+       for (vob_list::const_iterator
+                it = titles_[title_index].begin(),
+                end = titles_[title_index].end();
             it != end;
             ++it)
        {
@@ -212,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.
-           "        <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";
@@ -232,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;