]> git.decadent.org.uk Git - videolink.git/commitdiff
Moved generation of menu VOBs from videolink_window to dvd_generator.
authorBen Hutchings <ben@decadent.org.uk>
Sat, 14 Oct 2006 01:03:15 +0000 (01:03 +0000)
committerBen Hutchings <ben@decadent.org.uk>
Sun, 2 Nov 2008 23:58:05 +0000 (23:58 +0000)
Fixed operation of "top menu" or "title menu" button.
Added explicit aspect ratio to menu streams encoded with ffmpeg, since some players will default to 16:9 otherwise.
Fixed off-by-one error in link coordinates passed to spumux.
Changed default padding for the body element to 10% of frame dimensions, a common rule for the title-safe area.
Added explicit assignment of actions to direction buttons since dvdauthor's policy doesn't seem to work that well.
Documented limit of 119 menus discovered by Mark Burton.
Fixed an occasional problem of links appearing in their hover state initially due to the pointer being in the middle of the virtual framebuffer.

17 files changed:
ChangeLog
Makefile
README
TODO
debian/changelog
generate_dvd.cpp
generate_dvd.hpp
geometry.hpp [new file with mode: 0644]
ntsc.css [new file with mode: 0644]
pal.css [new file with mode: 0644]
pixbufs.cpp
pixbufs.hpp
video.hpp
videolink.cpp
videolink.css
warp_pointer.cpp [new file with mode: 0644]
warp_pointer.hpp [new file with mode: 0644]

index 8482890fa127e9b98ed396fe35ea1ee262438c58..2ad2df21bd8e5f1f228baaf1cfe3f11f1dd961d4 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,20 @@
+videolink (0.11) unstable; urgency=low
+
+  * Fixed operation of "top menu" or "title menu" button
+  * Added explicit aspect ratio to menu streams encoded with ffmpeg,
+    since some players will default to 16:9 otherwise
+  * Fixed off-by-one error in link coordinates passed to spumux
+  * Changed default padding for the body element to 10% of frame
+    dimensions, a common rule for the title-safe area
+  * Added explicit assignment of actions to direction buttons
+    since dvdauthor's policy doesn't seem to work that well
+  * Documented limit of 119 menus discovered by Mark Burton
+  * Fixed an occasional problem of links appearing in their hover state
+    initially due to the pointer being in the middle of the virtual
+    framebuffer
+
+ -- Ben Hutchings <ben@decadent.org.uk>  Mon, 25 Sep 2006 18:11:07 +0100
+
 videolink (0.10) unstable; urgency=low
 
   * Changed style-sheet application in Mozila/XULRunner 1.8 to override
index e001b0a9783505a7e36f1a983478e9208c67b021..02a8030579a95f16fc063525b4ffb587a983bf28 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -41,8 +41,8 @@ endif
 cxxsources := \
     auto_proc.cpp browser_widget.cpp child_iterator.cpp generate_dvd.cpp   \
     link_iterator.cpp null_prompt_service.cpp pixbufs.cpp style_sheets.cpp \
-    temp_file.cpp video.cpp vob_list.cpp videolink.cpp x_frame_buffer.cpp  \
-    xml_utils.cpp xpcom_support.cpp
+    temp_file.cpp video.cpp vob_list.cpp videolink.cpp warp_pointer.cpp    \
+    x_frame_buffer.cpp xml_utils.cpp xpcom_support.cpp
 csources := jquant2.c
 
 videolink : $(cxxsources:%.cpp=.objs/%.o) $(csources:%.c=.objs/%.o)
@@ -63,7 +63,7 @@ install :
        gzip -c9 videolink.1 >$(DESTDIR)$(mandir)/man1/videolink.1.gz
        chmod 644 $(DESTDIR)$(mandir)/man1/videolink.1.gz
        mkdir -p -m 755 $(DESTDIR)$(sharedir)/videolink
-       install -m 644 videolink.css $(DESTDIR)$(sharedir)/videolink
+       install -m 644 *.css $(DESTDIR)$(sharedir)/videolink
 
 .PHONY : clean install
 
@@ -72,8 +72,8 @@ install :
 .objs/videolink.% \
     : CPPFLAGS += -DVIDEOLINK_SHARE_DIR='"$(sharedir)/videolink"'
 
-.objs/browser_widget.% .objs/generate_dvd.% .objs/pixbufs.% \
-.objs/temp_file.% .objs/vob_list.% .objs/videolink.%        \
+.objs/browser_widget.% .objs/generate_dvd.% .objs/pixbufs.%               \
+.objs/temp_file.% .objs/vob_list.% .objs/videolink.% .objs/warp_pointer.% \
     : CPPFLAGS += $(shell pkg-config --cflags gtkmm-2.0)
 
 .objs/browser_widget.% .objs/child_iterator.% .objs/link_iterator.%   \
diff --git a/README b/README
index c273e4379124fa69b0882ee003b849229cc6f463..e51a3e6c240ce2ccd36ecc2e8f12f289042fb9ba 100644 (file)
--- a/README
+++ b/README
@@ -42,10 +42,10 @@ below.
 Linking to video
 
 You can link directly to local MPEG video files whose names end in
-".vob".  If you wish to combine multiple files into a single video
-sequence ("title" in DVD terminology) or to add chapter marks to a
-video sequence, create and link to a VOB-list file (explained below)
-whose name ends in ".voblist".
+".mpeg", ".mpeg2" or ".vob".  If you wish to combine multiple files
+into a single video sequence ("title" in DVD terminology) or to add
+chapter marks to a video sequence, create and link to a VOB-list file
+(explained below) whose name ends in ".voblist".
 
 VOB-lists
 
@@ -135,9 +135,9 @@ multiple menus.  The frame size is dictated by the video standard; see
 above.  The exact visible area varies between TVs so the background
 should cover all or very nearly all the frame whereas the important
 content such as text must not be placed near the edge.  For this
-reason VideoLink applies a stylesheet to all pages that adds 60 pixels of
-padding on all sides of the body; this doesn't apply to the
-background.
+reason VideoLink applies a stylesheet to all pages that adds padding
+equal to 10% of the frame dimension on each side of the body; this
+doesn't apply to the background.
 
 Prior to version 1.8, Mozilla may signal that a page is completely
 loaded before any background images are loaded and displayed.  This
@@ -176,6 +176,12 @@ aspect ratio and sample rate.  However, each chapter will run into the
 next.  If this is a real problem, let me know, and I may be able to
 provide a better solution in a later version of VideoLink.
 
+There is a limit of 128 menus in each "domain" of a DVD, which is
+further reduced by dvdauthor to a practical limit of 119 menus.
+Currently VideoLink generates all menus in a single domain (the VMGM
+domain).  Later versions of VideoLink should support larger numbers of
+menus by using multiple domains.
+
 Bugs
 ----
 
diff --git a/TODO b/TODO
index 41c777089e39f14f6cc1ac70267958dd821cd99b..18c4fb94fc3afd8c3a56c343704f80739d2147a6 100644 (file)
--- a/TODO
+++ b/TODO
@@ -4,13 +4,14 @@ Priority 2
 Use ffmpeg to convert unsuitable video files (how do we check this?).
 Add keyboard command for "top menu" in preview mode.
 Add support for videos in preview mode.
-Support more than 99 videos somehow (grouping them into titlesets won't help)
-Allow explicit ordering of videos.
+Support more than 99 titles somehow (grouping them into titlesets won't help)
+Support more than 119 menus somehow.
+Track down and fix/suppress the NS_BINDING_ABORTED (0x804b002) error that occasionally appears in preview mode.
 
 Priority 3 (lowest)
 Provide a means to specify menu & audio language.
-May need to override default menu navigation actions because dvdauthor seems to cause rather undesirable behaviour when buttons are not arranged in a grid.
 For any page with no links, add a "back" link (this raises the issue of localisation, of course).
 If a page is too large, split it and add "next" and "previous" links (seems very difficult).
 Set Mozilla's idea of the pixel aspect ratio to match video pixels. (For extra credit, scale preview mode to correct for the differing aspect ratio.) This seems to be impossible.
 Avoid running Xvfb if we're only going to display an error message.
+Allow explicit ordering of titles.
index e8405199285e9618e8c766ac0d3fd0d6d0472a6b..f1e7959c86f75ab6532b411411a066fe3b7245a8 100644 (file)
@@ -1,3 +1,9 @@
+videolink (0.11-1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Ben Hutchings <ben@decadent.org.uk>  Tue, 26 Sep 2006 02:21:15 +0100
+
 videolink (0.10-1) unstable; urgency=low
 
   * New upstream version - closes: #386832
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"
index 91e8da8d5e808ec4e52eef5cf234483e1aaf6b36..7f3823fee19db43d98a41bf344dbec3550cfbcac 100644 (file)
@@ -9,13 +9,23 @@
 
 #include <boost/shared_ptr.hpp>
 
+#include <glibmm/refptr.h>
+
+#include "geometry.hpp"
 #include "temp_file.hpp"
+#include "video.hpp"
 #include "vob_list.hpp"
 
+namespace Gdk
+{
+    class Pixbuf;
+}
+
 // Description of menus and titles to go on a DVD.
 
-struct dvd_contents
+class dvd_generator
 {
+public:
     enum pgc_type { unknown_pgc,  menu_pgc, title_pgc };
 
     // Reference to some PGC (program chain).
@@ -41,6 +51,48 @@ struct dvd_contents
                            // unspecified; not compared!)
     };
 
+    // We can try using any of these encoders to convert PNG to MPEG.
+    enum mpeg_encoder
+    {
+       mpeg_encoder_ffmpeg,         // ffmpeg
+       mpeg_encoder_mjpegtools_old, // mjpegtools before version 1.8
+       mpeg_encoder_mjpegtools_new  // mjpegtools from version 1.8
+    };
+
+    dvd_generator(const video::frame_params & frame_params,
+                 mpeg_encoder encoder)
+           : frame_params_(frame_params),
+             encoder_(encoder)
+       {}
+
+    // Create a new empty menu; return a reference to it.
+    // The client must call generate_menu_vob() for each menu before
+    // calling generate().
+    pgc_ref add_menu();
+    // Add a menu entry (link) to an existing menu.
+    void add_menu_entry(unsigned index,
+                       const rectangle & area,
+                       const pgc_ref & target);
+    // Generate the menu VOB from a background image and button
+    // highlight image.
+    void generate_menu_vob(unsigned index,
+                          Glib::RefPtr<Gdk::Pixbuf> background,
+                          Glib::RefPtr<Gdk::Pixbuf> highlights) const;
+
+    // Create a new title using the given vob_list; return a reference
+    // to it.  The argument will be pilfered (i.e. emptied).
+    pgc_ref add_title(vob_list & list);
+
+    // Use dvdauthor to generate a DVD filesystem.
+    void generate(const std::string & output_dir) const;
+
+private:
+    struct menu_entry
+    {
+       rectangle area;
+       pgc_ref target;
+    };
+
     // Menu definition.
     struct menu
     {
@@ -52,15 +104,13 @@ struct dvd_contents
 
        // References to the menus and titles that the menu buttons
        // are meant to link to, in the same order as the buttons.
-       std::vector<pgc_ref> entries;
+       std::vector<menu_entry> entries;
     };
 
-    std::vector<menu> menus;
-    std::vector<vob_list> titles;
+    video::frame_params frame_params_;
+    mpeg_encoder encoder_;
+    std::vector<menu> menus_;
+    std::vector<vob_list> titles_;
 };
 
-// Use dvdauthor to generate a DVD filesystem with the given contents.
-void generate_dvd(const dvd_contents & contents,
-                 const std::string & output_dir);
-
 #endif // !INC_GENERATE_DVD_HPP
diff --git a/geometry.hpp b/geometry.hpp
new file mode 100644 (file)
index 0000000..a263327
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_GEOMETRY_HPP
+#define INC_GEOMETRY_HPP
+
+struct rectangle
+{
+    int left, top;     // inclusive
+    int right, bottom; // exclusive
+
+    rectangle operator|=(const rectangle & other)
+       {
+           if (other.empty())
+           {
+               // use current extents unchanged
+           }
+           else if (empty())
+           {
+               // use other extents
+               *this = other;
+           }
+           else
+           {
+               // find rectangle enclosing both extents
+               left = std::min(left, other.left);
+               top = std::min(top, other.top);
+               right = std::max(right, other.right);
+               bottom = std::max(bottom, other.bottom);
+           }
+
+           return *this;
+       }
+
+    rectangle operator&=(const rectangle & other)
+       {
+           // find rectangle enclosed in both extents
+           left = std::max(left, other.left);
+           top = std::max(top, other.top);
+           right = std::max(left, std::min(right, other.right));
+           bottom = std::max(top, std::min(bottom, other.bottom));
+           return *this;
+       }
+
+    bool empty() const
+       {
+           return left == right || bottom == top;
+       }
+};
+
+#endif // !INC_GEOMETRY_HPP
diff --git a/ntsc.css b/ntsc.css
new file mode 100644 (file)
index 0000000..73b6f92
--- /dev/null
+++ b/ntsc.css
@@ -0,0 +1,4 @@
+body {
+  /* Let the background overscan, but restrict the content to title-safe. */
+  padding: 48px 72px;
+}
diff --git a/pal.css b/pal.css
new file mode 100644 (file)
index 0000000..96cb753
--- /dev/null
+++ b/pal.css
@@ -0,0 +1,4 @@
+body {
+  /* Let the background overscan, but restrict the content to title-safe. */
+  padding: 58px 72px;
+}
index d6936de31abcfeb918af1358e9c8091d913cc6ad..2cef5d3eebd1d0f1f5129465065b3f4a7a1e14c5 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright 2005 Ben Hutchings <ben@decadent.org.uk>.
+// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
 // See the file "COPYING" for licence details.
 
 #include "pixbufs.hpp"
@@ -14,9 +14,9 @@
 // (or RGBA, but the alpha component will be ignored) and copy the
 // differing pixels from the new one to a third RGBA Pixbuf at the
 // specified offset with full opacity.
-// The width and height of the old and new Pixbufs must be equal
-// and match the specified dimensions.  The width and height of
-// the third Pixbuf must be large enough to store a rectangle of
+// The width and height of the new Pixbufs must be equal and match
+// the specified dimensions.  The width and height of the old and
+// third Pixbuf must be large enough to store a rectangle of
 // those dimensions at the specified offset.
 void diff_rgb_pixbufs(Glib::RefPtr<Gdk::Pixbuf> old_buf,
                      Glib::RefPtr<Gdk::Pixbuf> new_buf,
@@ -31,8 +31,8 @@ void diff_rgb_pixbufs(Glib::RefPtr<Gdk::Pixbuf> old_buf,
     int old_bpr = old_buf->get_rowstride();
     int old_bpp = old_buf->get_n_channels();
     assert(old_bpp >= 3);
-    assert(old_buf->get_width() == width);
-    assert(old_buf->get_height() == height);
+    assert(old_buf->get_width() >= offset_x + width);
+    assert(old_buf->get_height() >= offset_y + height);
     int new_bpr = new_buf->get_rowstride();
     int new_bpp = new_buf->get_n_channels();
     assert(new_bpp >= 3);
@@ -44,7 +44,9 @@ void diff_rgb_pixbufs(Glib::RefPtr<Gdk::Pixbuf> old_buf,
     assert(diff_buf->get_width() >= offset_x + width);
     assert(diff_buf->get_height() >= offset_y + height);
 
-    const guint8 * old_p = old_buf->get_pixels();
+    const guint8 * old_p = (old_buf->get_pixels()
+                           + old_bpr * offset_y
+                           + old_bpp * offset_x);
     const guint8 * new_p = new_buf->get_pixels();
     guint8 * diff_p = (diff_buf->get_pixels()
                       + diff_bpr * offset_y
index f0b8a26255f112b0bcac73785b992b1e93d3d87e..c61735d073ba5f39320fdf0020f602978cab7363 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright 2005 Ben Hutchings <ben@decadent.org.uk>.
+// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
 // See the file "COPYING" for licence details.
 
 #ifndef INC_PIXBUFS_HPP
@@ -15,9 +15,9 @@ namespace Gdk
 // (or RGBA, but the alpha component will be ignored) and copy the
 // differing pixels from the new one to a third RGBA Pixbuf at the
 // specified offset with full opacity.
-// The width and height of the old and new Pixbufs must be equal
-// and match the specified dimensions.  The width and height of
-// the third Pixbuf must be large enough to store a rectangle of
+// The width and height of the new Pixbufs must be equal and match
+// the specified dimensions.  The width and height of the old and
+// third Pixbuf must be large enough to store a rectangle of
 // those dimensions at the specified offset.
 void diff_rgb_pixbufs(Glib::RefPtr<Gdk::Pixbuf> old_buf,
                      Glib::RefPtr<Gdk::Pixbuf> new_buf,
index 1c546e5de164c9b34455a43119d2ccfdc50d6bac..94a21b775c352ee95d5e3f4be9e8935a5ccabc05 100644 (file)
--- a/video.hpp
+++ b/video.hpp
@@ -5,7 +5,7 @@ namespace video
 {
     struct frame_params
     {
-       const char * ffmpeg_name;
+       const char * common_name;
        unsigned int width, height;
        unsigned int rate_numer, rate_denom;
        unsigned int pixel_ratio_width, pixel_ratio_height;
index bda78ce113c51bca35894e3db3e14795eb7455b6..3d88f4edb7c57a1fe7e26c8a0ff53f348b00f361 100644 (file)
@@ -15,8 +15,6 @@
 
 #include <stdlib.h>
 
-#include <boost/shared_ptr.hpp>
-
 #include <gdkmm/pixbuf.h>
 #include <glibmm/convert.h>
 #include <glibmm/spawn.h>
 #include "child_iterator.hpp"
 #include "dvd.hpp"
 #include "generate_dvd.hpp"
+#include "geometry.hpp"
 #include "link_iterator.hpp"
 #include "null_prompt_service.hpp"
 #include "pixbufs.hpp"
 #include "style_sheets.hpp"
 #include "temp_file.hpp"
 #include "video.hpp"
+#include "warp_pointer.hpp"
 #include "x_frame_buffer.hpp"
 #include "xml_utils.hpp"
 #include "xpcom_support.hpp"
@@ -77,58 +77,6 @@ using xpcom_support::check;
 
 namespace
 {
-    // We can try using any of these encoders to convert PNG to MPEG.
-    enum mpeg_encoder
-    {
-       mpeg_encoder_ffmpeg,         // ffmpeg
-       mpeg_encoder_mjpegtools_old, // mjpegtools before version 1.8
-       mpeg_encoder_mjpegtools_new  // mjpegtools from version 1.8
-    };
-
-    struct rectangle
-    {
-       int left, top;     // inclusive
-       int right, bottom; // exclusive
-
-       rectangle operator|=(const rectangle & other)
-           {
-               if (other.empty())
-               {
-                   // use current extents unchanged
-               }
-               else if (empty())
-               {
-                   // use other extents
-                   *this = other;
-               }
-               else
-               {
-                   // find rectangle enclosing both extents
-                   left = std::min(left, other.left);
-                   top = std::min(top, other.top);
-                   right = std::max(right, other.right);
-                   bottom = std::max(bottom, other.bottom);
-               }
-
-               return *this;
-           }
-
-       rectangle operator&=(const rectangle & other)
-           {
-               // find rectangle enclosed in both extents
-               left = std::max(left, other.left);
-               top = std::max(top, other.top);
-               right = std::max(left, std::min(right, other.right));
-               bottom = std::max(top, std::min(bottom, other.bottom));
-               return *this;
-           }
-
-       bool empty() const
-           {
-               return left == right || bottom == top;
-           }
-    };
-
     rectangle get_elem_rect(nsIDOMNSDocument * ns_doc,
                            nsIDOMElement * elem)
     {
@@ -163,6 +111,40 @@ namespace
     }
 
 
+    enum video_format
+    {
+       video_format_none,
+       video_format_mpeg2_ps,
+       video_format_vob_list
+    };
+
+    video_format video_format_from_uri(const std::string & uri)
+    {
+       // FIXME: This is a bit of a hack.  Perhaps we could decide
+       // later based on the MIME type determined by Mozilla?
+       static struct {
+           const char * extension;
+           video_format format;
+       } const mapping[] = {
+           {".vob",     video_format_mpeg2_ps},
+           {".mpeg",    video_format_mpeg2_ps},
+           {".mpeg2",   video_format_mpeg2_ps},
+           {".voblist", video_format_vob_list}
+       };
+       for (std::size_t i = 0;
+            i != sizeof(mapping) / sizeof(mapping[0]);
+            ++i)
+       {
+           std::size_t ext_len = std::strlen(mapping[i].extension);
+           if (uri.size() > ext_len
+               && uri.compare(uri.size() - ext_len, ext_len,
+                              mapping[i].extension) == 0)
+               return mapping[i].format;
+       }
+       return video_format_none;
+    }
+
+    
     class videolink_window : public Gtk::Window
     {
     public:
@@ -170,13 +152,16 @@ namespace
            const video::frame_params & frame_params,
            const std::string & main_page_uri,
            const std::string & output_dir,
-           mpeg_encoder encoder);
+           dvd_generator::mpeg_encoder encoder);
 
        bool is_finished() const;
 
     private:
-       dvd_contents::pgc_ref add_menu(const std::string & uri);
-       dvd_contents::pgc_ref add_title(const std::string & uri);
+       struct page_state;
+
+       dvd_generator::pgc_ref add_menu(const std::string & uri);
+       dvd_generator::pgc_ref add_title(const std::string & uri,
+                                        video_format format);
        void load_next_page();
        bool on_idle();
        void on_net_state_change(const char * uri, gint flags, guint status);
@@ -184,28 +169,35 @@ namespace
            {
                return pending_window_update_ || pending_req_count_;
            }
-       bool process_page();
-       void save_screenshot();
-       void process_links(nsIPresShell * pres_shell,
-                          nsPresContext * pres_context,
-                          nsIDOMWindow * dom_window);
+       // Do as much processing as possible.  Return a flag indicating
+       // whether to call again once the browser is idle.
+       bool process();
+       // Return a Pixbuf containing a copy of the window contents.
+       Glib::RefPtr<Gdk::Pixbuf> get_screenshot();
+       // Do as much processing as possible on the page links.  Return
+       // a flag indicating whether to call again once the browser is
+       // idle.
+       bool process_links(
+           page_state * state,
+           nsIDOMDocument * basic_doc,
+           nsIPresShell * pres_shell,
+           nsPresContext * pres_context,
+           nsIDOMWindow * dom_window);
 
        video::frame_params frame_params_;
        std::string output_dir_;
-       mpeg_encoder encoder_;
        browser_widget browser_widget_;
-       agent_style_sheet_holder style_sheet_;
+       agent_style_sheet_holder main_style_sheet_, frame_style_sheet_;
 
-       dvd_contents contents_;
-       typedef std::map<std::string, dvd_contents::pgc_ref> resource_map_type;
+       dvd_generator generator_;
+       typedef std::map<std::string, dvd_generator::pgc_ref>
+           resource_map_type;
        resource_map_type resource_map_;
 
        std::queue<std::string> page_queue_;
        bool pending_window_update_;
        int pending_req_count_;
        bool have_tweaked_page_;
-       std::auto_ptr<temp_file> background_temp_;
-       struct page_state;
        std::auto_ptr<page_state> page_state_;
 
        bool finished_;
@@ -215,12 +207,19 @@ namespace
        const video::frame_params & frame_params,
        const std::string & main_page_uri,
        const std::string & output_dir,
-       mpeg_encoder encoder)
+       dvd_generator::mpeg_encoder encoder)
            : frame_params_(frame_params),
              output_dir_(output_dir),
-             encoder_(encoder),
-             style_sheet_(init_agent_style_sheet(
-                              "file://"VIDEOLINK_SHARE_DIR"/videolink.css")),
+             main_style_sheet_(
+                 init_agent_style_sheet(
+                     "file://" VIDEOLINK_SHARE_DIR "/videolink.css")),
+             frame_style_sheet_(
+                 init_agent_style_sheet(
+                     std::string("file://" VIDEOLINK_SHARE_DIR "/")
+                     .append(frame_params.common_name)
+                     .append(".css")
+                     .c_str())),
+             generator_(frame_params, encoder),
              pending_window_update_(false),
              pending_req_count_(0),
              have_tweaked_page_(false),
@@ -244,37 +243,23 @@ namespace
        return finished_;
     }
 
-    dvd_contents::pgc_ref videolink_window::add_menu(const std::string & uri)
+    dvd_generator::pgc_ref videolink_window::add_menu(const std::string & uri)
     {
-       dvd_contents::pgc_ref next_menu(dvd_contents::menu_pgc,
-                                       contents_.menus.size());
-       std::pair<resource_map_type::iterator, bool> insert_result(
-           resource_map_.insert(std::make_pair(uri, next_menu)));
-
-       if (!insert_result.second)
-       {
-           return insert_result.first->second;
-       }
-       else
+       dvd_generator::pgc_ref & pgc_ref = resource_map_[uri];
+       if (pgc_ref.type == dvd_generator::unknown_pgc)
        {
+           pgc_ref = generator_.add_menu();
            page_queue_.push(uri);
-           contents_.menus.resize(contents_.menus.size() + 1);
-           return next_menu;
        }
+       return pgc_ref;
     }
 
-    dvd_contents::pgc_ref videolink_window::add_title(const std::string & uri)
+    dvd_generator::pgc_ref videolink_window::add_title(const std::string & uri,
+                                                     video_format format)
     {
-       dvd_contents::pgc_ref next_title(dvd_contents::title_pgc,
-                                        contents_.titles.size());
-       std::pair<resource_map_type::iterator, bool> insert_result(
-           resource_map_.insert(std::make_pair(uri, next_title)));
+       dvd_generator::pgc_ref & pgc_ref = resource_map_[uri];
 
-       if (!insert_result.second)
-       {
-           return insert_result.first->second;
-       }
-       else
+       if (pgc_ref.type == dvd_generator::unknown_pgc)
        {
            Glib::ustring hostname;
            std::string path(Glib::filename_from_uri(uri, hostname));
@@ -284,7 +269,7 @@ namespace
 
            // Store a reference to a linked VOB file, or the contents
            // of a linked VOB list file.
-           if (path.compare(path.size() - 4, 4, ".vob") == 0)
+           if (format == video_format_mpeg2_ps)
            {
                if (!Glib::file_test(path, Glib::FILE_TEST_IS_REGULAR))
                    throw std::runtime_error(
@@ -293,16 +278,19 @@ namespace
                ref.file = path;
                list.push_back(ref);
            }
-           else
+           else if (format == video_format_vob_list)
            {
-               assert(path.compare(path.size() - 8, 8, ".voblist") == 0);
                read_vob_list(path).swap(list);
            }
+           else
+           {
+               assert(!"unrecognised format in add_title");
+           }
 
-           contents_.titles.resize(contents_.titles.size() + 1);
-           contents_.titles.back().swap(list);
-           return next_title;
+           pgc_ref = generator_.add_title(list);
        }
+
+       return pgc_ref;
     }
 
     void videolink_window::load_next_page()
@@ -316,6 +304,14 @@ namespace
 
     bool videolink_window::on_idle()
     {
+       if (!output_dir_.empty())
+       {
+           // Put pointer in the top-left so that no links appear in
+           // the hover state when we take a screenshot.
+           warp_pointer(get_window(),
+                        -frame_params_.width, -frame_params_.height);
+       }
+       
        load_next_page();
        return false; // don't call again thankyou
     }
@@ -396,7 +392,7 @@ namespace
        {
            try
            {
-               if (!process_page())
+               if (!process())
                {
                    finished_ = true;
                    Gtk::Main::quit();
@@ -423,7 +419,32 @@ namespace
        }
     }
 
-    bool videolink_window::process_page()
+    struct videolink_window::page_state
+    {
+       page_state(Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf,
+                  nsIDOMDocument * doc, int width, int height)
+               : norm_pixbuf(norm_pixbuf),
+                 diff_pixbuf(Gdk::Pixbuf::create(
+                                 Gdk::COLORSPACE_RGB,
+                                 true, 8, // has_alpha, bits_per_sample
+                                 width, height)),
+                 link_num(0),
+                 links_it(doc),
+                 link_changing(false)
+           {
+           }
+
+       Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
+       Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
+
+       unsigned link_num;
+       link_iterator links_it, links_end;
+
+       rectangle link_rect;
+       bool link_changing;
+    };
+
+    bool videolink_window::process()
     {
        assert(!page_queue_.empty());
 
@@ -441,7 +462,8 @@ namespace
        // disable scrollbars.
        if (!have_tweaked_page_)
        {
-           apply_agent_style_sheet(style_sheet_, pres_shell);
+           apply_agent_style_sheet(main_style_sheet_, pres_shell);
+           apply_agent_style_sheet(frame_style_sheet_, pres_shell);
 
            // This actually only needs to be done once.
            nsCOMPtr<nsIDOMBarProp> dom_bar_prop;
@@ -459,27 +481,44 @@ namespace
        // All further work should only be done if we're not in preview mode.
        if (!output_dir_.empty())
        {
-           // If we haven't already started work on this menu, save a
-           // screenshot of its normal appearance.
-           if (!page_state_.get())
-               save_screenshot();
+           nsCOMPtr<nsIDOMDocument> basic_doc;
+           check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
 
            // Start or continue processing links.
-           process_links(pres_shell, pres_context, dom_window);
-
-           // If we've finished work on the links, move on to the
-           // next page, if any, or else generate the DVD filesystem.
-           if (!page_state_.get())
+           std::auto_ptr<page_state> state(page_state_);
+           if (!state.get())
+               state.reset(
+                   new page_state(
+                       get_screenshot(),
+                       basic_doc, frame_params_.width, frame_params_.height));
+           if (process_links(
+                   state.get(),
+                   basic_doc, pres_shell, pres_context, dom_window))
            {
+               // Save iteration state for later.
+               page_state_ = state;
+           }
+           else
+           {
+               // We've finished work on the links so generate the
+               // menu VOB.
+               quantise_rgba_pixbuf(state->diff_pixbuf,
+                                    dvd::button_n_colours);
+               generator_.generate_menu_vob(
+                   resource_map_[page_queue_.front()].index,
+                   state->norm_pixbuf, state->diff_pixbuf);
+
+               // Move on to the next page, if any, or else generate
+               // the DVD filesystem.
                page_queue_.pop();
-               if (page_queue_.empty())
+               if (!page_queue_.empty())
                {
-                   generate_dvd(contents_, output_dir_);
-                   return false;
+                   load_next_page();
                }
                else
                {
-                   load_next_page();
+                   generator_.generate(output_dir_);
+                   return false;
                }
            }
        }
@@ -487,63 +526,28 @@ namespace
        return true;
     }
 
-    void videolink_window::save_screenshot()
+    Glib::RefPtr<Gdk::Pixbuf> videolink_window::get_screenshot()
     {
        Glib::RefPtr<Gdk::Window> window(get_window());
        assert(window);
        window->process_updates(true);
 
-       background_temp_.reset(new temp_file("videolink-back-"));
-       background_temp_->close();
-       std::cout << "saving " << background_temp_->get_name() << std::endl;
-       Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
-                           window->get_colormap(),
-                           0, 0, 0, 0,
-                           frame_params_.width, frame_params_.height)
-           ->save(background_temp_->get_name(), "png");
+       return Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
+                                  window->get_colormap(),
+                                  0, 0, 0, 0,
+                                  frame_params_.width, frame_params_.height);
     }
 
-    struct videolink_window::page_state
-    {
-       page_state(nsIDOMDocument * doc, int width, int height)
-               : diff_pixbuf(Gdk::Pixbuf::create(
-                                 Gdk::COLORSPACE_RGB,
-                                 true, 8, // has_alpha, bits_per_sample
-                                 width, height)),
-                 spumux_temp("videolink-spumux-"),
-                 links_temp("videolink-links-"),
-                 link_num(0),
-                 links_it(doc),
-                 link_changing(false)
-           {
-               spumux_temp.close();
-               links_temp.close();
-           }
-
-       Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
-
-       temp_file spumux_temp;
-       std::ofstream spumux_file;
-
-       temp_file links_temp;
-
-       unsigned link_num;
-       link_iterator links_it, links_end;
-
-       rectangle link_rect;
-       bool link_changing;
-       Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
-    };
-
-    void videolink_window::process_links(nsIPresShell * pres_shell,
-                                    nsPresContext * pres_context,
-                                    nsIDOMWindow * dom_window)
+    bool videolink_window::process_links(
+       page_state * state,
+       nsIDOMDocument * basic_doc,
+       nsIPresShell * pres_shell,
+       nsPresContext * pres_context,
+       nsIDOMWindow * dom_window)
     {
        Glib::RefPtr<Gdk::Window> window(get_window());
        assert(window);
 
-       nsCOMPtr<nsIDOMDocument> basic_doc;
-       check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
        nsCOMPtr<nsIDOMNSDocument> ns_doc(do_QueryInterface(basic_doc));
        assert(ns_doc);
        nsCOMPtr<nsIEventStateManager> event_state_man(
@@ -557,28 +561,11 @@ namespace
        nsCOMPtr<nsIDOMAbstractView> view;
        check(doc_view->GetDefaultView(getter_AddRefs(view)));
 
-       // Set up or recover our iteration state.
-       std::auto_ptr<page_state> state(page_state_);
-       if (!state.get())
-       {
-           state.reset(
-               new page_state(
-                   basic_doc, frame_params_.width, frame_params_.height));
-           
-           state->spumux_file.open(state->spumux_temp.get_name().c_str());
-           state->spumux_file <<
-               "<subpictures>\n"
-               "  <stream>\n"
-               "    <spu force='yes' start='00:00:00.00'\n"
-               "        highlight='" << state->links_temp.get_name() << "'\n"
-               "        select='" << state->links_temp.get_name() << "'>\n";
-       }
-
        rectangle window_rect = {
            0, 0, frame_params_.width, frame_params_.height
        };
 
-       unsigned menu_num = resource_map_[page_queue_.front()].index;
+       unsigned menu_index = resource_map_[page_queue_.front()].index;
 
        for (/* no initialisation */;
             state->links_it != state->links_end;
@@ -634,22 +621,12 @@ namespace
                    continue;
                }
 
-               state->spumux_file <<
-                   "      <button x0='" << state->link_rect.left << "'"
-                   " y0='" << state->link_rect.top << "'"
-                   " x1='" << state->link_rect.right - 1 << "'"
-                   " y1='" << state->link_rect.bottom - 1 << "'/>\n";
-
                // Check whether this is a link to a video or a page then
                // add it to the known resources if not already seen; then
                // add it to the menu entries.
-               dvd_contents::pgc_ref target;
-               // FIXME: This is a bit of a hack.  Perhaps we could decide
-               // later based on the MIME type determined by Mozilla?
-               if ((uri.size() > 4
-                    && uri.compare(uri.size() - 4, 4, ".vob") == 0)
-                   || (uri.size() > 8
-                       && uri.compare(uri.size() - 8, 8, ".voblist") == 0))
+               dvd_generator::pgc_ref target;
+               video_format format = video_format_from_uri(uri);
+               if (format != video_format_none)
                {
                    PRBool is_file;
                    check(uri_iface->SchemeIs("file", &is_file));
@@ -659,17 +636,19 @@ namespace
                                  << " scheme\n";
                        continue;
                    }
-                   target = add_title(uri);
+                   target = add_title(uri, format);
                    target.sub_index =
                        std::strtoul(fragment.c_str(), NULL, 10);
                }
-               else
+               else // video_format == video_format_none
                {
                    target = add_menu(uri);
                    // TODO: If there's a fragment, work out which button
                    // is closest and set target.sub_index.
                }
-               contents_.menus[menu_num].entries.push_back(target);
+
+               generator_.add_menu_entry(menu_index,
+                                         state->link_rect, target);
 
                nsCOMPtr<nsIContent> content(do_QueryInterface(node));
                assert(content);
@@ -677,16 +656,6 @@ namespace
                    do_QueryInterface(node));
                assert(event_target);
 
-               state->norm_pixbuf = Gdk::Pixbuf::create(
-                   Glib::RefPtr<Gdk::Drawable>(window),
-                   window->get_colormap(),
-                   state->link_rect.left,
-                   state->link_rect.top,
-                   0,
-                   0,
-                   state->link_rect.right - state->link_rect.left,
-                   state->link_rect.bottom - state->link_rect.top);
-
                nsCOMPtr<nsIDOMEvent> event;
                check(event_factory->CreateEvent(
                          NS_ConvertASCIItoUTF16("MouseEvents"),
@@ -726,8 +695,7 @@ namespace
                if (browser_is_busy())
                {
                    state->link_changing = true;
-                   page_state_ = state;
-                   return;
+                   return true;
                }
            }
 
@@ -753,79 +721,7 @@ namespace
                state->link_rect.bottom - state->link_rect.top);
        }
 
-       quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
-
-       std::cout << "saving " << state->links_temp.get_name()
-                 << std::endl;
-       state->diff_pixbuf->save(state->links_temp.get_name(), "png");
-
-       state->spumux_file <<
-           "    </spu>\n"
-           "  </stream>\n"
-           "</subpictures>\n";
-
-       state->spumux_file.close();
-
-       // TODO: if (!state->spumux_file) throw ...
-
-       {
-           std::ostringstream command_stream;
-           if (encoder_ == mpeg_encoder_ffmpeg)
-           {
-               command_stream
-                   << "ffmpeg"
-                   << " -f image2 -vcodec png -i "
-                   << background_temp_->get_name()
-                   << " -target " << frame_params_.ffmpeg_name <<  "-dvd"
-                   << " -vcodec mpeg2video -an -y /dev/stdout"
-                   << " | spumux -v0 -mdvd " << state->spumux_temp.get_name()
-                   << " > " << contents_.menus[menu_num].vob_temp->get_name();
-           }
-           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"
-                       " | spumux -v0 -mdvd ")
-                   << state->spumux_temp.get_name()
-                   << " > "
-                   << contents_.menus[menu_num].vob_temp->get_name();
-           }
-           std::string command(command_stream.str());
-           const char * argv[] = {
-               "/bin/sh", "-c", command.c_str(), 0
-           };
-           std::cout << "running " << argv[2] << 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");
-       }
+       return false;
     }
 
     const video::frame_params & lookup_frame_params(const char * str)
@@ -918,7 +814,8 @@ int main(int argc, char ** argv)
        bool preview_mode = false;
        std::string menu_url;
        std::string output_dir;
-       mpeg_encoder encoder = mpeg_encoder_ffmpeg;
+       dvd_generator::mpeg_encoder encoder =
+           dvd_generator::mpeg_encoder_ffmpeg;
 
        // Do initial option parsing.  We have to do this before
        // letting Gtk parse the arguments since we may need to spawn
@@ -1005,16 +902,16 @@ int main(int argc, char ** argv)
                }
                if (std::strcmp(argv[argi + 1], "ffmpeg") == 0)
                {
-                   encoder = mpeg_encoder_ffmpeg;
+                   encoder = dvd_generator::mpeg_encoder_ffmpeg;
                }
                else if (std::strcmp(argv[argi + 1], "mjpegtools-old") == 0)
                {
-                   encoder = mpeg_encoder_mjpegtools_old;
+                   encoder = dvd_generator::mpeg_encoder_mjpegtools_old;
                }
                else if (std::strcmp(argv[argi + 1], "mjpegtools") == 0
                         || std::strcmp(argv[argi + 1], "mjpegtools-new") == 0)
                {
-                   encoder = mpeg_encoder_mjpegtools_new;
+                   encoder = dvd_generator::mpeg_encoder_mjpegtools_new;
                }
                else
                {
index 86800b881e58cd7ed5bc9099374d50d4f2b4e1d2..e3ae32229bdb64ae4912c30ab8af6799a49cce17 100644 (file)
@@ -1,8 +1,6 @@
 body {
   /* Sans-serif fonts will be much more readable than serif on a TV. */
   font-family: sans-serif;
-  /* Let the background overscan, but not the content. */
-  padding: 60px;
   /* No scroll bars. */
   overflow: hidden;
 }
diff --git a/warp_pointer.cpp b/warp_pointer.cpp
new file mode 100644 (file)
index 0000000..aef397d
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright 2006 Ben Hutchings <ben@decadent.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include "warp_pointer.hpp"
+
+#include <gdk/gdkx.h>
+
+// Move the pointer by (dx, dy) relative to its current position on
+// the display which window appears on.  
+// This is separated from videolink.cpp solely because it uses Xlib
+// and Xlib.h defines many macros we don't want.
+void warp_pointer(const Glib::RefPtr<Gdk::Window> & window, int dx, int dy)
+{
+    XWarpPointer(gdk_x11_drawable_get_xdisplay(window->gobj()), None, None,
+                0, 0, 0, 0, dx, dy);
+}
diff --git a/warp_pointer.hpp b/warp_pointer.hpp
new file mode 100644 (file)
index 0000000..dd8c079
--- /dev/null
@@ -0,0 +1,10 @@
+// Copyright 2006 Ben Hutchings <ben@decadent.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include <gdkmm/window.h>
+
+// Move the pointer by (dx, dy) relative to its current position on
+// the display which window appears on.  
+// This is separated from videolink.cpp solely because it uses Xlib
+// and Xlib.h defines many macros we don't want.
+void warp_pointer(const Glib::RefPtr<Gdk::Window> & window, int dx, int dy);