Renamed package due to name clash.
authorBen Hutchings <ben@decadent.org.uk>
Wed, 9 Aug 2006 01:42:07 +0000 (01:42 +0000)
committerBen Hutchings <ben@decadent.org.uk>
Sun, 2 Nov 2008 23:47:20 +0000 (23:47 +0000)
16 files changed:
COPYING
INSTALL
Makefile
README
debian/control
debian/rules
debian/videolink.docs [new file with mode: 0644]
debian/webdvd.docs [deleted file]
generate_dvd.cpp
null_prompt_service.cpp
videolink.cpp [new file with mode: 0644]
videolink.css [new file with mode: 0644]
videolink.hpp [new file with mode: 0644]
webdvd.cpp [deleted file]
webdvd.css [deleted file]
webdvd.hpp [deleted file]

diff --git a/COPYING b/COPYING
index a115095..d7bc9df 100644 (file)
--- a/COPYING
+++ b/COPYING
@@ -1,11 +1,11 @@
-WebDVD is licenced under the GNU GPL, version 2, with the following
+VideoLink is licenced under the GNU GPL, version 2, with the following
 additions:
 
-A. You may combine and distribute WebDVD and derivative works with
+A. You may combine and distribute VideoLink and derivative works with
    libraries licenced under the GNU LGPL without exercising the option to
    treat them as licenced under the GPL.
 
-B. When distributing WebDVD or derivative works without source code
+B. When distributing VideoLink or derivative works without source code
    included you must ensure that the documentation retains the notice that
    "this software is based in part on the work of the Independent JPEG
    Group", or else remove the code contained in jquant2.c to which that
diff --git a/INSTALL b/INSTALL
index e272e3d..61da855 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -1,8 +1,8 @@
-Building WebDVD
-===============
+Building VideoLink
+==================
 
-WebDVD is written in C++ and requires a recent C++ compiler e.g. g++
-3.3.
+VideoLink is written in C++ and requires a recent C++ compiler
+e.g. g++ 3.3.
 
 It requires headers and libraries for Boost, gtkmm, Mozilla and expat.
 I have developed and tested it with Boost 1.32, gtkmm 2.2.12, Mozilla
index bc26293..671ba39 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 prefix := /usr/local
 
-webdvd_lib_dir := $(prefix)/lib/webdvd
+videolink_lib_dir := $(prefix)/lib/videolink
 
 moz_include_dir := \
     $(shell pkg-config --variable=prefix mozilla-gtkmozembed)/include/mozilla
@@ -33,46 +33,46 @@ 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 webdvd.cpp x_frame_buffer.cpp     \
+    temp_file.cpp video.cpp vob_list.cpp videolink.cpp x_frame_buffer.cpp  \
     xml_utils.cpp xpcom_support.cpp
 csources := jquant2.c
 
-webdvd : $(cxxsources:%.cpp=.objs/%.o) $(csources:%.c=.objs/%.o)
+videolink : $(cxxsources:%.cpp=.objs/%.o) $(csources:%.c=.objs/%.o)
        $(CXX) $(LDFLAGS) -o $@ $^
 
 clean :
        rm -rf .objs
-       rm -f webdvd *~ .\#* *.orig *.rej svn-commit*.tmp
+       rm -f videolink *~ .\#* *.orig *.rej svn-commit*.tmp
 
 distclean : clean
        rm -rf .svn
 
 install :
-       mkdir -p -m 755 $(prefix)/bin $(prefix)/lib/webdvd
-       install -m 755 -s webdvd $(prefix)/bin
-       install -m 644 webdvd.css $(prefix)/lib/webdvd
+       mkdir -p -m 755 $(prefix)/bin $(videolink_lib_dir)
+       install -m 755 -s videolink $(prefix)/bin
+       install -m 644 videolink.css $(videolink_lib_dir)
 
 .PHONY : clean distclean install
 
 .objs/browser_widget.% : CPPFLAGS += -DMOZ_LIB_DIR='"$(moz_lib_dir)"'
 
-.objs/webdvd.% \
-    : CPPFLAGS += -DWEBDVD_LIB_DIR='"$(webdvd_lib_dir)"'             \
+.objs/videolink.% \
+    : CPPFLAGS += -DVIDEOLINK_LIB_DIR='"$(videolink_lib_dir)"'       \
                   -DMOZ_VERSION_MAJOR=$(moz_version_major)           \
                   -DMOZ_VERSION_MINOR=$(moz_version_minor)           \
                   -DMOZ_VERSION_PATCHLEVEL=$(moz_version_patchlevel)
 
 .objs/browser_widget.% .objs/generate_dvd.% .objs/pixbufs.% \
-.objs/temp_file.% .objs/vob_list.% .objs/webdvd.%           \
+.objs/temp_file.% .objs/vob_list.% .objs/videolink.%        \
     : CPPFLAGS += $(shell pkg-config --cflags gtkmm-2.0)
 
 .objs/browser_widget.% .objs/child_iterator.% .objs/link_iterator.% \
-.objs/null_prompt_service.% .objs/style_sheets.% .objs/webdvd.%            \
+.objs/null_prompt_service.% .objs/style_sheets.% .objs/videolink.%  \
 .objs/xpcom_support.%                                               \
     : CPPFLAGS += $(shell pkg-config --cflags mozilla-gtkmozembed)
 
 # These dig a bit deeper into Mozilla
-.objs/link_iterator.% .objs/style_sheets.% .objs/webdvd.%                  \
+.objs/link_iterator.% .objs/style_sheets.% .objs/videolink.%               \
     : CPPFLAGS += $(addprefix -I$(moz_include_dir)/,                       \
                     content docshell dom gfx layout necko webshell widget)
 
diff --git a/README b/README
index e209fff..dfa4ff5 100644 (file)
--- a/README
+++ b/README
@@ -1,16 +1,19 @@
-WebDVD
-======
+VideoLink
+=========
 
-WebDVD is intended to provide a simple way of producing DVDs with
+VideoLink is intended to provide a simple way of producing DVDs with
 attractive and usable menus.  It converts HTML pages into DVD menus by
 rendering them in Mozilla and reproducing their link structure.  This
 allows you to design DVDs using familiar HTML editing tools or your
 favourite text editor.
 
+Prior to version 0.8, VideoLink was called WebDVD, but that name is
+also used for an extended DVD format.
+
 Requirements
 ------------
 
-WebDVD depends on the following software:
+VideoLink depends on the following software:
 
 - dvdauthor
 - expat 1.x
@@ -91,19 +94,19 @@ Currently
 
 Preview
 
-To get a rough preview of the menus, run "webdvd --preview menu-url"
+To get a rough preview of the menus, run "videolink --preview menu-url"
 where menu-url is the URL or filename of the first page to show.
 Currently videos cannot be displayed in this preview mode.
 
 Processing
 
-To create a DVD filesystem, run "webdvd menu-url output-dir" where
+To create a DVD filesystem, run "videolink menu-url output-dir" where
 menu-url is the URL or filename of the top menu page and output-dir is
 the directory in which to create the filesystem (which should be
-either nonexistent or empty).  WebDVD will automatically follow links
+either nonexistent or empty).  VideoLink will automatically follow links
 to the other pages and to the video files.
 
-By default, WebDVD now calls ffmpeg to generate MPEG-2 streams for
+By default, VideoLink now calls ffmpeg to generate MPEG-2 streams for
 menus.  If you want it to use mjpegtools as it previously did, you
 must add the option "--encoder mjpegtools".  If you use mjpegtools
 1.6.2 or earlier you must instead use "--encoder mjpegtools-old".
@@ -127,12 +130,12 @@ Limitations
 -----------
 
 Each page must fit within the frame - DVD players do not support
-scrolling menus and WebDVD currently is not able to split them into
+scrolling menus and VideoLink currently is not able to split them into
 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 WebDVD applies a stylesheet to all pages that adds 60 pixels of
+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.
 
@@ -140,28 +143,28 @@ Prior to Mozilla version 1.8, which I have not yet tested, Mozilla may
 signal that a page is completely loaded before any background images
 are loaded and displayed.  This results in snapshots that do not
 include background images.  You can work around this by using
-absolutely-positioned "inline" images, or attempt to build WebDVD
+absolutely-positioned "inline" images, or attempt to build VideoLink
 against Mozilla 1.8.
 
 DVD players do not have "back" buttons, so you should generally
 provide links to "higher" menu pages.  However, they do have a button
 for returning to the top menu.
 
-WebDVD sends a "mouseover" event for each link and sets it into its
+VideoLink sends a "mouseover" event for each link and sets it into its
 "hover" state, then records how this changes its appearance.  This
 change is then shown when the corresponding button on the DVD menu is
-highlighted.  WebDVD applies a stylesheet which changes the colour of
+highlighted.  VideoLink applies a stylesheet which changes the colour of
 text links in the "hover" state, but this has no effect on image
 links.  You must ensure that image links are highlighted in an obvious
 way when the mouse pointer is over them.
 
 The DVD specifications limit each menu to having no more than 36
 buttons.  In any case, it is poor design to have very large numbers of
-buttons on a single menu.  WebDVD will warn you if you use more than
+buttons on a single menu.  VideoLink will warn you if you use more than
 this number of a links on a page, and will ignore any additional ones.
 
 The DVD specification also limits the overlays that are used for
-highlighting of buttons to using no more than 4 colours.  WebDVD will
+highlighting of buttons to using no more than 4 colours.  VideoLink will
 reduce link highlighting to 1 transparent and 3 opaque colours using
 Floyd-Steinberg dithering, which is certainly good enough for
 anti-aliased text but may not be so good for complex highlighting.
@@ -171,12 +174,12 @@ than this number of video sequences, you could arrange them as
 chapters of a title, so long as they use the same codecs, resolution,
 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 WebDVD.
+provide a better solution in a later version of VideoLink.
 
 Author and copyright
 --------------------
 
-WebDVD was written by Ben Hutchings <ben@decadent.org.uk>.
+VideoLink was written by Ben Hutchings <ben@decadent.org.uk>.
 Copyright 2005-2006 Ben Hutchings.
 
 This software is based in part on the work of the Independent JPEG Group.
index de94297..67b3c3c 100644 (file)
@@ -1,19 +1,19 @@
-Source: webdvd
+Source: videolink
 Maintainer: Ben Hutchings <ben@decadent.org.uk>
 Section: graphics
 Priority: extra
 Build-Depends: debhelper (>=4), libboost-dev, libgtkmm2.0-dev, mozilla-dev, libexpat1-dev
 Standards-Version: 3.6.2
 
-Package: webdvd
+Package: videolink
 Architecture: any
 Depends: xvfb, xfonts-base, dvdauthor, ffmpeg | mjpegtools, netpbm, ${shlibs:Depends}, ${mozilla:Depends}
 Recommends: mkisofs
 Description: Converts HTML pages into DVD menus.
WebDVD is intended to provide a simple way of producing DVDs with
VideoLink is intended to provide a simple way of producing DVDs with
  attractive and usable menus.  It converts HTML pages into DVD menus by
  rendering them in Mozilla and reproducing their link structure.  This
  allows you to design DVDs using familiar HTML editing tools or your
  favourite text editor.
  .
-  Homepage: http://womble.decadent.org.uk/software/webdvd/
+  Homepage: http://womble.decadent.org.uk/software/videolink/
index 2806168..b18a9a3 100755 (executable)
@@ -7,13 +7,13 @@ binary : binary-arch binary-indep
 
 binary-arch : build
        dh_testroot
-       make prefix=debian/webdvd/usr install
+       make prefix=debian/videolink/usr install
        dh_strip
        dh_shlibdeps
        @echo "Despite the warnings from dh_shlibdeps, this should complete dependencies:"
-       echo "mozilla:Depends=mozilla-browser (= $$(dpkg-query -W --showformat='$${version}' mozilla-browser))" >> debian/webdvd.substvars
-       mkdir -p -m755 debian/webdvd/usr/share/doc/webdvd
-       cp COPYING debian/webdvd/usr/share/doc/webdvd/copyright
+       echo "mozilla:Depends=mozilla-browser (= $$(dpkg-query -W --showformat='$${version}' mozilla-browser))" >> debian/videolink.substvars
+       mkdir -p -m755 debian/videolink/usr/share/doc/videolink
+       cp COPYING debian/videolink/usr/share/doc/videolink/copyright
        dh_installchangelogs
        dh_installdocs
        dh_compress
diff --git a/debian/videolink.docs b/debian/videolink.docs
new file mode 100644 (file)
index 0000000..e845566
--- /dev/null
@@ -0,0 +1 @@
+README
diff --git a/debian/webdvd.docs b/debian/webdvd.docs
deleted file mode 100644 (file)
index e845566..0000000
+++ /dev/null
@@ -1 +0,0 @@
-README
index 6c507b9..32ce0f2 100644 (file)
@@ -11,7 +11,7 @@
 #include "xml_utils.hpp"
 
 dvd_contents::menu::menu()
-       : vob_temp(new temp_file("webdvd-vob-"))
+       : vob_temp(new temp_file("videolink-vob-"))
 {
     vob_temp->close();
 }
@@ -19,7 +19,7 @@ dvd_contents::menu::menu()
 void generate_dvd(const dvd_contents & contents,
                  const std::string & output_dir)
 {
-    temp_file temp("webdvd-dvdauthor-");
+    temp_file temp("videolink-dvdauthor-");
     temp.close();
     std::ofstream file(temp.get_name().c_str());
 
index 37303c0..03355ac 100644 (file)
@@ -14,7 +14,7 @@
 #include <nsIUnicodeEncoder.h>
 
 #include "null_prompt_service.hpp"
-#include "webdvd.hpp"
+#include "videolink.hpp"
 #include "xpcom_support.hpp"
 
 using xpcom_support::check;
diff --git a/videolink.cpp b/videolink.cpp
new file mode 100644 (file)
index 0000000..3d2daeb
--- /dev/null
@@ -0,0 +1,1057 @@
+// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include <cassert>
+#include <cstring>
+#include <exception>
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <memory>
+#include <queue>
+#include <set>
+#include <sstream>
+#include <string>
+
+#include <stdlib.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <gdkmm/pixbuf.h>
+#include <glibmm/convert.h>
+#include <glibmm/spawn.h>
+#include <gtkmm/main.h>
+#include <gtkmm/window.h>
+
+#include <imglib2/ImageErrors.h>
+#include <nsGUIEvent.h>
+#include <nsIBoxObject.h>
+#include <nsIContent.h>
+#include <nsIDocShell.h>
+#include <nsIDOMAbstractView.h>
+#include <nsIDOMBarProp.h>
+#include <nsIDOMDocumentEvent.h>
+#include <nsIDOMDocumentView.h>
+#include <nsIDOMElement.h>
+#include <nsIDOMEventTarget.h>
+#include <nsIDOMHTMLDocument.h>
+#include <nsIDOMMouseEvent.h>
+#include <nsIDOMNSDocument.h>
+#include <nsIDOMWindow.h>
+#include <nsIEventStateManager.h>
+#include <nsIInterfaceRequestorUtils.h>
+#include <nsIURI.h> // required before nsILink.h
+#include <nsILink.h>
+#include <nsIPrefBranch.h>
+#include <nsIPrefService.h>
+#include <nsIPresContext.h>
+#include <nsIPresShell.h>
+#include <nsIServiceManagerUtils.h>
+#include <nsIWebBrowser.h>
+#include <nsString.h>
+
+#include "browser_widget.hpp"
+#include "child_iterator.hpp"
+#include "dvd.hpp"
+#include "generate_dvd.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 "x_frame_buffer.hpp"
+#include "xml_utils.hpp"
+#include "xpcom_support.hpp"
+
+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)
+    {
+       rectangle result;
+
+       // Start with this element's bounding box
+       nsCOMPtr<nsIBoxObject> box;
+       check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
+       int width, height;
+       check(box->GetScreenX(&result.left));
+       check(box->GetScreenY(&result.top));
+       check(box->GetWidth(&width));
+       check(box->GetHeight(&height));
+       result.right = result.left + width;
+       result.bottom = result.top + height;
+
+       // Merge bounding boxes of all child elements
+       for (child_iterator it = child_iterator(elem), end; it != end; ++it)
+       {
+           nsCOMPtr<nsIDOMNode> child_node(*it);
+           PRUint16 child_type;
+           if (check(child_node->GetNodeType(&child_type)),
+               child_type == nsIDOMNode::ELEMENT_NODE)
+           {
+               nsCOMPtr<nsIDOMElement> child_elem(
+                   do_QueryInterface(child_node));
+               result |= get_elem_rect(ns_doc, child_elem);
+           }
+       }
+
+       return result;
+    }
+
+
+    class videolink_window : public Gtk::Window
+    {
+    public:
+       videolink_window(
+           const video::frame_params & frame_params,
+           const std::string & main_page_uri,
+           const std::string & output_dir,
+           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);
+       void load_next_page();
+       bool on_idle();
+       void on_net_state_change(const char * uri, gint flags, guint status);
+       bool browser_is_busy() const
+           {
+               return pending_window_update_ || pending_req_count_;
+           }
+       bool process_page();
+       void save_screenshot();
+       void process_links(nsIPresShell * pres_shell,
+                          nsIPresContext * pres_context,
+                          nsIDOMWindow * dom_window);
+
+       video::frame_params frame_params_;
+       std::string output_dir_;
+       mpeg_encoder encoder_;
+       browser_widget browser_widget_;
+       nsCOMPtr<nsIStyleSheet> stylesheet_;
+
+       dvd_contents contents_;
+       typedef std::map<std::string, dvd_contents::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_;
+    };
+
+    videolink_window::videolink_window(
+       const video::frame_params & frame_params,
+       const std::string & main_page_uri,
+       const std::string & output_dir,
+       mpeg_encoder encoder)
+           : frame_params_(frame_params),
+             output_dir_(output_dir),
+             encoder_(encoder),
+             stylesheet_(load_css("file://" VIDEOLINK_LIB_DIR "/videolink.css")),
+             pending_window_update_(false),
+             pending_req_count_(0),
+             have_tweaked_page_(false),
+             finished_(false)
+    {
+       set_size_request(frame_params_.width, frame_params_.height);
+       set_resizable(false);
+
+       add(browser_widget_);
+       browser_widget_.show();
+       Glib::signal_idle().connect(
+           SigC::slot(*this, &videolink_window::on_idle));
+       browser_widget_.signal_net_state().connect(
+           SigC::slot(*this, &videolink_window::on_net_state_change));
+
+       add_menu(main_page_uri);
+    }
+
+    bool videolink_window::is_finished() const
+    {
+       return finished_;
+    }
+
+    dvd_contents::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
+       {
+           page_queue_.push(uri);
+           contents_.menus.resize(contents_.menus.size() + 1);
+           return next_menu;
+       }
+    }
+
+    dvd_contents::pgc_ref videolink_window::add_title(const std::string & uri)
+    {
+       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)));
+
+       if (!insert_result.second)
+       {
+           return insert_result.first->second;
+       }
+       else
+       {
+           Glib::ustring hostname;
+           std::string path(Glib::filename_from_uri(uri, hostname));
+           // FIXME: Should check the hostname
+
+           vob_list list;
+
+           // 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 (!Glib::file_test(path, Glib::FILE_TEST_IS_REGULAR))
+                   throw std::runtime_error(
+                       path + " is missing or not a regular file");
+               vob_ref ref;
+               ref.file = path;
+               list.push_back(ref);
+           }
+           else
+           {
+               assert(path.compare(path.size() - 8, 8, ".voblist") == 0);
+               read_vob_list(path).swap(list);
+           }
+
+           contents_.titles.resize(contents_.titles.size() + 1);
+           contents_.titles.back().swap(list);
+           return next_title;
+       }
+    }
+
+    void videolink_window::load_next_page()
+    {
+       assert(!page_queue_.empty());
+       const std::string & uri = page_queue_.front();
+       std::cout << "loading " << uri << std::endl;
+
+       browser_widget_.load_uri(uri);
+    }
+
+    bool videolink_window::on_idle()
+    {
+       load_next_page();
+       return false; // don't call again thankyou
+    }
+
+    void videolink_window::on_net_state_change(const char * uri,
+                                          gint flags, guint status)
+    {
+#       ifdef DEBUG_ON_NET_STATE_CHANGE
+       std::cout << "videolink_window::on_net_state_change(";
+       if (uri)
+           std::cout << '"' << uri << '"';
+       else
+           std::cout << "NULL";
+       std::cout << ", ";
+       {
+           gint flags_left = flags;
+           static const struct {
+               gint value;
+               const char * name;
+           } flag_names[] = {
+               { GTK_MOZ_EMBED_FLAG_START, "STATE_START" },
+               { GTK_MOZ_EMBED_FLAG_REDIRECTING, "STATE_REDIRECTING" },
+               { GTK_MOZ_EMBED_FLAG_TRANSFERRING, "STATE_TRANSFERRING" },
+               { GTK_MOZ_EMBED_FLAG_NEGOTIATING, "STATE_NEGOTIATING" },
+               { GTK_MOZ_EMBED_FLAG_STOP, "STATE_STOP" },
+               { GTK_MOZ_EMBED_FLAG_IS_REQUEST, "STATE_IS_REQUEST" },
+               { GTK_MOZ_EMBED_FLAG_IS_DOCUMENT, "STATE_IS_DOCUMENT" },
+               { GTK_MOZ_EMBED_FLAG_IS_NETWORK, "STATE_IS_NETWORK" },
+               { GTK_MOZ_EMBED_FLAG_IS_WINDOW, "STATE_IS_WINDOW" }
+           };
+           for (int i = 0; i != sizeof(flag_names)/sizeof(flag_names[0]); ++i)
+           {
+               if (flags & flag_names[i].value)
+               {
+                   std::cout << flag_names[i].name;
+                   flags_left -= flag_names[i].value;
+                   if (flags_left)
+                       std::cout << " | ";
+               }
+           }
+           if (flags_left)
+               std::cout << "0x" << std::setbase(16) << flags_left;
+       }
+       std::cout << ", " << "0x" << std::setbase(16) << status << ")\n";
+#       endif // DEBUG_ON_NET_STATE_CHANGE
+
+       if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
+       {
+           if (flags & GTK_MOZ_EMBED_FLAG_START)
+               ++pending_req_count_;
+
+           if (flags & GTK_MOZ_EMBED_FLAG_STOP)
+           {
+               assert(pending_req_count_ != 0);
+               --pending_req_count_;
+           }
+       }
+           
+       if (flags & GTK_MOZ_EMBED_FLAG_IS_DOCUMENT
+           && flags & GTK_MOZ_EMBED_FLAG_START)
+       {
+           pending_window_update_ = true;
+           have_tweaked_page_ = false;
+       }
+
+       if (flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW
+           && flags & GTK_MOZ_EMBED_FLAG_STOP)
+       {
+           // Check whether the load was successful, ignoring this
+           // pseudo-error.
+           if (status != NS_IMAGELIB_ERROR_LOAD_ABORTED)
+               check(status);
+
+           pending_window_update_ = false;
+       }
+
+       if (!browser_is_busy())
+       {
+           try
+           {
+               if (!process_page())
+               {
+                   finished_ = true;
+                   Gtk::Main::quit();
+               }
+           }
+           catch (std::exception & e)
+           {
+               std::cerr << "Fatal error";
+               if (!page_queue_.empty())
+                   std::cerr << " while processing <" << page_queue_.front()
+                             << ">";
+               std::cerr << ": " << e.what() << "\n";
+               Gtk::Main::quit();
+           }
+           catch (Glib::Exception & e)
+           {
+               std::cerr << "Fatal error";
+               if (!page_queue_.empty())
+                   std::cerr << " while processing <" << page_queue_.front()
+                             << ">";
+               std::cerr << ": " << e.what() << "\n";
+               Gtk::Main::quit();
+           }
+       }
+    }
+
+    bool videolink_window::process_page()
+    {
+       assert(!page_queue_.empty());
+
+       nsCOMPtr<nsIWebBrowser> browser(browser_widget_.get_browser());
+       nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
+       assert(doc_shell);
+       nsCOMPtr<nsIPresShell> pres_shell;
+       check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
+       nsCOMPtr<nsIPresContext> pres_context;
+       check(doc_shell->GetPresContext(getter_AddRefs(pres_context)));
+       nsCOMPtr<nsIDOMWindow> dom_window;
+       check(browser->GetContentDOMWindow(getter_AddRefs(dom_window)));
+
+       // If we haven't done so already, apply the stylesheet and
+       // disable scrollbars.
+       if (!have_tweaked_page_)
+       {
+           apply_style_sheet(stylesheet_, pres_shell);
+
+           // This actually only needs to be done once.
+           nsCOMPtr<nsIDOMBarProp> dom_bar_prop;
+           check(dom_window->GetScrollbars(getter_AddRefs(dom_bar_prop)));
+           check(dom_bar_prop->SetVisible(false));
+
+           have_tweaked_page_ = true;
+
+           // Might need to wait a while for things to load or more
+           // likely for a re-layout.
+           if (browser_is_busy())
+               return true;
+       }
+
+       // 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();
+
+           // 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())
+           {
+               page_queue_.pop();
+               if (page_queue_.empty())
+               {
+                   generate_dvd(contents_, output_dir_);
+                   return false;
+               }
+               else
+               {
+                   load_next_page();
+               }
+           }
+       }
+
+       return true;
+    }
+
+    void videolink_window::save_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");
+    }
+
+    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,
+                                    nsIPresContext * 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(
+           pres_context->EventStateManager()); // does not AddRef
+       assert(event_state_man);
+       nsCOMPtr<nsIDOMDocumentEvent> event_factory(
+           do_QueryInterface(basic_doc));
+       assert(event_factory);
+       nsCOMPtr<nsIDOMDocumentView> doc_view(do_QueryInterface(basic_doc));
+       assert(doc_view);
+       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;
+
+       for (/* no initialisation */;
+            state->links_it != state->links_end;
+            ++state->links_it)
+       {
+           nsCOMPtr<nsIDOMNode> node(*state->links_it);
+
+           // Find the link URI and separate any fragment from it.
+           nsCOMPtr<nsILink> link(do_QueryInterface(node));
+           assert(link);
+           nsCOMPtr<nsIURI> uri_iface;
+           check(link->GetHrefURI(getter_AddRefs(uri_iface)));
+           std::string uri_and_fragment, uri, fragment;
+           {
+               nsCString uri_and_fragment_ns;
+               check(uri_iface->GetSpec(uri_and_fragment_ns));
+               uri_and_fragment.assign(uri_and_fragment_ns.BeginReading(),
+                                       uri_and_fragment_ns.EndReading());
+
+               std::size_t hash_pos = uri_and_fragment.find('#');
+               uri.assign(uri_and_fragment, 0, hash_pos);
+               if (hash_pos != std::string::npos)
+                   fragment.assign(uri_and_fragment,
+                                   hash_pos + 1, std::string::npos);
+           }
+
+           // Is this a new link?
+           if (!state->link_changing)
+           {
+               // Find a rectangle enclosing the link and clip it to the
+               // window.
+               nsCOMPtr<nsIDOMElement> elem(do_QueryInterface(node));
+               assert(elem);
+               state->link_rect = get_elem_rect(ns_doc, elem);
+               state->link_rect &= window_rect;
+
+               if (state->link_rect.empty())
+               {
+                   std::cerr << "Ignoring invisible link to "
+                             << uri_and_fragment << "\n";
+                   continue;
+               }
+
+               ++state->link_num;
+
+               if (state->link_num >= unsigned(dvd::menu_buttons_max))
+               {
+                   if (state->link_num == unsigned(dvd::menu_buttons_max))
+                       std::cerr << "No more than " << dvd::menu_buttons_max
+                                 << " buttons can be placed on a menu\n";
+                   std::cerr << "Ignoring link to " << uri_and_fragment
+                             << "\n";
+                   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))
+               {
+                   PRBool is_file;
+                   check(uri_iface->SchemeIs("file", &is_file));
+                   if (!is_file)
+                   {
+                       std::cerr << "Links to video must use the file:"
+                                 << " scheme\n";
+                       continue;
+                   }
+                   target = add_title(uri);
+                   target.sub_index =
+                       std::strtoul(fragment.c_str(), NULL, 10);
+               }
+               else
+               {
+                   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);
+
+               nsCOMPtr<nsIContent> content(do_QueryInterface(node));
+               assert(content);
+               nsCOMPtr<nsIDOMEventTarget> event_target(
+                   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"),
+                         getter_AddRefs(event)));
+               nsCOMPtr<nsIDOMMouseEvent> mouse_event(
+                   do_QueryInterface(event));
+               assert(mouse_event);
+               check(mouse_event->InitMouseEvent(
+                         NS_ConvertASCIItoUTF16("mouseover"),
+                         true,  // can bubble
+                         true,  // cancelable
+                         view,
+                         0,     // detail: mouse click count
+                         state->link_rect.left, // screenX
+                         state->link_rect.top,  // screenY
+                         state->link_rect.left, // clientX
+                         state->link_rect.top,  // clientY
+                         false, false, false, false, // qualifiers
+                         0,     // button: left (or primary)
+                         0));   // related target
+               PRBool dummy;
+               check(event_target->DispatchEvent(mouse_event,
+                                                 &dummy));
+               check(event_state_man->SetContentState(content,
+                                                      NS_EVENT_STATE_HOVER));
+
+               pres_shell->FlushPendingNotifications(true);
+
+               // We may have to exit and wait for image loading
+               // to complete, at which point we will be called
+               // again.
+               if (browser_is_busy())
+               {
+                   state->link_changing = true;
+                   page_state_ = state;
+                   return;
+               }
+           }
+
+           window->process_updates(true);
+
+           Glib::RefPtr<Gdk::Pixbuf> changed_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));
+           diff_rgb_pixbufs(
+               state->norm_pixbuf,
+               changed_pixbuf,
+               state->diff_pixbuf,
+               state->link_rect.left,
+               state->link_rect.top,
+               state->link_rect.right - state->link_rect.left,
+               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");
+       }
+    }
+
+    const video::frame_params & lookup_frame_params(const char * str)
+    {
+       assert(str);
+       static const char * const known_strings[] = {
+           "525",    "625",
+           "525/60", "625/50",
+           "NTSC",   "PAL",
+           "ntsc",   "pal"
+       };
+       for (std::size_t i = 0;
+            i != sizeof(known_strings)/sizeof(known_strings[0]);
+            ++i)
+           if (std::strcmp(str, known_strings[i]) == 0)
+               return (i & 1)
+                   ? video::frame_params_625
+                   : video::frame_params_525;
+       throw std::runtime_error(
+           std::string("Invalid video standard: ").append(str));
+    }
+
+    void print_usage(std::ostream & stream, const char * command_name)
+    {
+       stream <<
+           "Usage: " << command_name << " [gtk-options] [--preview]\n"
+           "           [--video-std {525|525/60|NTSC|ntsc"
+           " | 625|625/50|PAL|pal}]\n"
+           "           [--encoder {mjpegtools|mjpegtools-old}]\n"
+           "           menu-url [output-dir]\n";
+    }
+    
+    void set_browser_preferences()
+    {
+       nsCOMPtr<nsIPrefService> pref_service;
+       static const nsCID pref_service_cid = NS_PREFSERVICE_CID;
+       check(CallGetService<nsIPrefService>(pref_service_cid,
+                                            getter_AddRefs(pref_service)));
+       nsCOMPtr<nsIPrefBranch> pref_branch;
+
+       // Disable IE-compatibility kluge that causes backgrounds to
+       // sometimes/usually be missing from snapshots.  This is only
+       // effective from Mozilla 1.8 onward.
+#      if MOZ_VERSION_MAJOR > 1                                 \
+           || (MOZ_VERSION_MAJOR == 1 && MOZ_VERSION_MINOR >= 8)
+       check(pref_service->GetDefaultBranch("layout",
+                                            getter_AddRefs(pref_branch)));
+       check(pref_branch->SetBoolPref(
+                 "fire_onload_after_image_background_loads",
+                 true));
+#      endif
+
+       // Set display resolution.  With standard-definition video we
+       // will be fitting ~600 pixels across a screen typically
+       // ranging from 10 to 25 inches wide, for a resolution of
+       // 24-60 dpi.  I therefore declare the average horizontal
+       // resolution to be 40 dpi.  The vertical resolution will be
+       // slightly different but unfortunately Mozilla doesn't
+       // support non-square pixels (and neither do fontconfig or Xft
+       // anyway).
+       check(pref_service->GetDefaultBranch("browser.display",
+                                            getter_AddRefs(pref_branch)));
+       check(pref_branch->SetIntPref("screen_resolution", 40));
+    }
+
+} // namespace
+
+void fatal_error(const std::string & message)
+{
+    std::cerr << "Fatal error: " << message << "\n";
+    Gtk::Main::quit();
+}
+
+int main(int argc, char ** argv)
+{
+    try
+    {
+       video::frame_params frame_params = video::frame_params_625;
+       bool preview_mode = false;
+       std::string menu_url;
+       std::string output_dir;
+       mpeg_encoder encoder = mpeg_encoder_ffmpeg;
+
+       // Do initial option parsing.  We have to do this before
+       // letting Gtk parse the arguments since we may need to spawn
+       // Xvfb first.
+       int argi = 1;
+       while (argi != argc)
+       {
+           if (std::strcmp(argv[argi], "--") == 0)
+           {
+               break;
+           }
+           else if (std::strcmp(argv[argi], "--help") == 0)
+           {
+               print_usage(std::cout, argv[0]);
+               return EXIT_SUCCESS;
+           }
+           else if (std::strcmp(argv[argi], "--preview") == 0)
+           {
+               preview_mode = true;
+               argi += 1;
+           }
+           else if (std::strcmp(argv[argi], "--video-std") == 0)
+           {
+               if (argi + 1 == argc)
+               {
+                   std::cerr << "Missing argument to --video-std\n";
+                   print_usage(std::cerr, argv[0]);
+                   return EXIT_FAILURE;
+               }
+               frame_params = lookup_frame_params(argv[argi + 1]);
+               argi += 2;
+           }
+           else
+           {
+               argi += 1;
+           }
+       }
+
+       std::auto_ptr<x_frame_buffer> fb;
+       if (!preview_mode)
+       {
+           // Spawn Xvfb and set env variables so that Xlib will use it
+           // Use 8 bits each for RGB components, which should translate into
+           // "enough" bits for YUV components.
+           fb.reset(new x_frame_buffer(frame_params.width,
+                                       frame_params.height,
+                                       3 * 8));
+           setenv("XAUTHORITY", fb->get_authority().c_str(), true);
+           setenv("DISPLAY", fb->get_display().c_str(), true);
+       }
+
+       // Initialise Gtk
+       Gtk::Main kit(argc, argv);
+
+       // Complete option parsing with Gtk's options out of the way.
+       argi = 1;
+       while (argi != argc)
+       {
+           if (std::strcmp(argv[argi], "--") == 0)
+           {
+               argi += 1;
+               break;
+           }
+           else if (std::strcmp(argv[argi], "--preview") == 0)
+           {
+               argi += 1;
+           }
+           else if (std::strcmp(argv[argi], "--video-std") == 0)
+           {
+               argi += 2;
+           }
+           else if (std::strcmp(argv[argi], "--save-temps") == 0)
+           {
+               temp_file::keep_all(true);
+               argi += 1;
+           }
+           else if (std::strcmp(argv[argi], "--encoder") == 0)
+           {
+               if (argi + 1 == argc)
+               {
+                   std::cerr << "Missing argument to --encoder\n";
+                   print_usage(std::cerr, argv[0]);
+                   return EXIT_FAILURE;
+               }
+               if (std::strcmp(argv[argi + 1], "ffmpeg") == 0)
+               {
+                   encoder = mpeg_encoder_ffmpeg;
+               }
+               else if (std::strcmp(argv[argi + 1], "mjpegtools-old") == 0)
+               {
+                   encoder = 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;
+               }
+               else
+               {
+                   std::cerr << "Invalid argument to --encoder\n";
+                   print_usage(std::cerr, argv[0]);
+                   return EXIT_FAILURE;
+               }
+               argi += 2;
+           }
+           else if (argv[argi][0] == '-')
+           {
+               std::cerr << "Invalid option: " << argv[argi] << "\n";
+               print_usage(std::cerr, argv[0]);
+               return EXIT_FAILURE;
+           }
+           else
+           {
+               break;
+           }
+       }
+
+       // Look for a starting URL or filename and (except in preview
+       // mode) an output directory after the options.
+        if (argc - argi != (preview_mode ? 1 : 2))
+       {
+           print_usage(std::cerr, argv[0]);
+           return EXIT_FAILURE;
+       }
+       if (std::strstr(argv[argi], "://"))
+       {
+           // It appears to be an absolute URL, so use it as-is.
+           menu_url = argv[argi];
+       }
+       else
+       {
+           // Assume it's a filename.  Resolve it to an absolute URL.
+           std::string path(argv[argi]);
+           if (!Glib::path_is_absolute(path))
+               path = Glib::build_filename(Glib::get_current_dir(), path);
+           menu_url = Glib::filename_to_uri(path);             
+       }
+       if (!preview_mode)
+           output_dir = argv[argi + 1];
+
+       // Initialise Mozilla
+       browser_widget::initialiser browser_init;
+       set_browser_preferences();
+       if (!preview_mode)
+           null_prompt_service::install();
+
+       // Run the browser/converter
+       videolink_window window(frame_params, menu_url, output_dir, encoder);
+       window.show();
+       window.signal_hide().connect(SigC::slot(&Gtk::Main::quit));
+       Gtk::Main::run();
+
+       return ((preview_mode || window.is_finished())
+               ? EXIT_SUCCESS
+               : EXIT_FAILURE);
+    }
+    catch (std::exception & e)
+    {
+       std::cerr << "Fatal error: " << e.what() << "\n";
+       return EXIT_FAILURE;
+    }
+}
diff --git a/videolink.css b/videolink.css
new file mode 100644 (file)
index 0000000..b441499
--- /dev/null
@@ -0,0 +1,27 @@
+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;
+}
+a:link, a:visited {
+  color: blue;
+  /* Don't underline links because underlining tends to flicker on TVs. */
+  text-decoration: none;
+  /* Buttons have to be rectangular (AFAIK). */
+  white-space: nowrap;
+}
+/* The hover state must be made obvious since DVD players have no pointer. */
+a:hover {
+  color: red;
+}
+/* The active state should provide visual feedback, but is not so critical. */
+a:active {
+  color: purple;
+}
+/* Don't show focus rectangles. */
+*|*:-moz-any-link:focus {
+  -moz-outline: none !important;
+}
diff --git a/videolink.hpp b/videolink.hpp
new file mode 100644 (file)
index 0000000..db3eede
--- /dev/null
@@ -0,0 +1,8 @@
+#ifndef INC_VIDEOLINK_HPP
+#define INC_VIDEOLINK_HPP
+
+#include <string>
+
+void fatal_error(const std::string & message);
+
+#endif // !INC_VIDEOLINK_HPP
diff --git a/webdvd.cpp b/webdvd.cpp
deleted file mode 100644 (file)
index 9063bce..0000000
+++ /dev/null
@@ -1,1057 +0,0 @@
-// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
-// See the file "COPYING" for licence details.
-
-#include <cassert>
-#include <cstring>
-#include <exception>
-#include <fstream>
-#include <iomanip>
-#include <iostream>
-#include <memory>
-#include <queue>
-#include <set>
-#include <sstream>
-#include <string>
-
-#include <stdlib.h>
-
-#include <boost/shared_ptr.hpp>
-
-#include <gdkmm/pixbuf.h>
-#include <glibmm/convert.h>
-#include <glibmm/spawn.h>
-#include <gtkmm/main.h>
-#include <gtkmm/window.h>
-
-#include <imglib2/ImageErrors.h>
-#include <nsGUIEvent.h>
-#include <nsIBoxObject.h>
-#include <nsIContent.h>
-#include <nsIDocShell.h>
-#include <nsIDOMAbstractView.h>
-#include <nsIDOMBarProp.h>
-#include <nsIDOMDocumentEvent.h>
-#include <nsIDOMDocumentView.h>
-#include <nsIDOMElement.h>
-#include <nsIDOMEventTarget.h>
-#include <nsIDOMHTMLDocument.h>
-#include <nsIDOMMouseEvent.h>
-#include <nsIDOMNSDocument.h>
-#include <nsIDOMWindow.h>
-#include <nsIEventStateManager.h>
-#include <nsIInterfaceRequestorUtils.h>
-#include <nsIURI.h> // required before nsILink.h
-#include <nsILink.h>
-#include <nsIPrefBranch.h>
-#include <nsIPrefService.h>
-#include <nsIPresContext.h>
-#include <nsIPresShell.h>
-#include <nsIServiceManagerUtils.h>
-#include <nsIWebBrowser.h>
-#include <nsString.h>
-
-#include "browser_widget.hpp"
-#include "child_iterator.hpp"
-#include "dvd.hpp"
-#include "generate_dvd.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 "x_frame_buffer.hpp"
-#include "xml_utils.hpp"
-#include "xpcom_support.hpp"
-
-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)
-    {
-       rectangle result;
-
-       // Start with this element's bounding box
-       nsCOMPtr<nsIBoxObject> box;
-       check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
-       int width, height;
-       check(box->GetScreenX(&result.left));
-       check(box->GetScreenY(&result.top));
-       check(box->GetWidth(&width));
-       check(box->GetHeight(&height));
-       result.right = result.left + width;
-       result.bottom = result.top + height;
-
-       // Merge bounding boxes of all child elements
-       for (child_iterator it = child_iterator(elem), end; it != end; ++it)
-       {
-           nsCOMPtr<nsIDOMNode> child_node(*it);
-           PRUint16 child_type;
-           if (check(child_node->GetNodeType(&child_type)),
-               child_type == nsIDOMNode::ELEMENT_NODE)
-           {
-               nsCOMPtr<nsIDOMElement> child_elem(
-                   do_QueryInterface(child_node));
-               result |= get_elem_rect(ns_doc, child_elem);
-           }
-       }
-
-       return result;
-    }
-
-
-    class webdvd_window : public Gtk::Window
-    {
-    public:
-       webdvd_window(
-           const video::frame_params & frame_params,
-           const std::string & main_page_uri,
-           const std::string & output_dir,
-           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);
-       void load_next_page();
-       bool on_idle();
-       void on_net_state_change(const char * uri, gint flags, guint status);
-       bool browser_is_busy() const
-           {
-               return pending_window_update_ || pending_req_count_;
-           }
-       bool process_page();
-       void save_screenshot();
-       void process_links(nsIPresShell * pres_shell,
-                          nsIPresContext * pres_context,
-                          nsIDOMWindow * dom_window);
-
-       video::frame_params frame_params_;
-       std::string output_dir_;
-       mpeg_encoder encoder_;
-       browser_widget browser_widget_;
-       nsCOMPtr<nsIStyleSheet> stylesheet_;
-
-       dvd_contents contents_;
-       typedef std::map<std::string, dvd_contents::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_;
-    };
-
-    webdvd_window::webdvd_window(
-       const video::frame_params & frame_params,
-       const std::string & main_page_uri,
-       const std::string & output_dir,
-       mpeg_encoder encoder)
-           : frame_params_(frame_params),
-             output_dir_(output_dir),
-             encoder_(encoder),
-             stylesheet_(load_css("file://" WEBDVD_LIB_DIR "/webdvd.css")),
-             pending_window_update_(false),
-             pending_req_count_(0),
-             have_tweaked_page_(false),
-             finished_(false)
-    {
-       set_size_request(frame_params_.width, frame_params_.height);
-       set_resizable(false);
-
-       add(browser_widget_);
-       browser_widget_.show();
-       Glib::signal_idle().connect(
-           SigC::slot(*this, &webdvd_window::on_idle));
-       browser_widget_.signal_net_state().connect(
-           SigC::slot(*this, &webdvd_window::on_net_state_change));
-
-       add_menu(main_page_uri);
-    }
-
-    bool webdvd_window::is_finished() const
-    {
-       return finished_;
-    }
-
-    dvd_contents::pgc_ref webdvd_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
-       {
-           page_queue_.push(uri);
-           contents_.menus.resize(contents_.menus.size() + 1);
-           return next_menu;
-       }
-    }
-
-    dvd_contents::pgc_ref webdvd_window::add_title(const std::string & uri)
-    {
-       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)));
-
-       if (!insert_result.second)
-       {
-           return insert_result.first->second;
-       }
-       else
-       {
-           Glib::ustring hostname;
-           std::string path(Glib::filename_from_uri(uri, hostname));
-           // FIXME: Should check the hostname
-
-           vob_list list;
-
-           // 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 (!Glib::file_test(path, Glib::FILE_TEST_IS_REGULAR))
-                   throw std::runtime_error(
-                       path + " is missing or not a regular file");
-               vob_ref ref;
-               ref.file = path;
-               list.push_back(ref);
-           }
-           else
-           {
-               assert(path.compare(path.size() - 8, 8, ".voblist") == 0);
-               read_vob_list(path).swap(list);
-           }
-
-           contents_.titles.resize(contents_.titles.size() + 1);
-           contents_.titles.back().swap(list);
-           return next_title;
-       }
-    }
-
-    void webdvd_window::load_next_page()
-    {
-       assert(!page_queue_.empty());
-       const std::string & uri = page_queue_.front();
-       std::cout << "loading " << uri << std::endl;
-
-       browser_widget_.load_uri(uri);
-    }
-
-    bool webdvd_window::on_idle()
-    {
-       load_next_page();
-       return false; // don't call again thankyou
-    }
-
-    void webdvd_window::on_net_state_change(const char * uri,
-                                          gint flags, guint status)
-    {
-#       ifdef DEBUG_ON_NET_STATE_CHANGE
-       std::cout << "webdvd_window::on_net_state_change(";
-       if (uri)
-           std::cout << '"' << uri << '"';
-       else
-           std::cout << "NULL";
-       std::cout << ", ";
-       {
-           gint flags_left = flags;
-           static const struct {
-               gint value;
-               const char * name;
-           } flag_names[] = {
-               { GTK_MOZ_EMBED_FLAG_START, "STATE_START" },
-               { GTK_MOZ_EMBED_FLAG_REDIRECTING, "STATE_REDIRECTING" },
-               { GTK_MOZ_EMBED_FLAG_TRANSFERRING, "STATE_TRANSFERRING" },
-               { GTK_MOZ_EMBED_FLAG_NEGOTIATING, "STATE_NEGOTIATING" },
-               { GTK_MOZ_EMBED_FLAG_STOP, "STATE_STOP" },
-               { GTK_MOZ_EMBED_FLAG_IS_REQUEST, "STATE_IS_REQUEST" },
-               { GTK_MOZ_EMBED_FLAG_IS_DOCUMENT, "STATE_IS_DOCUMENT" },
-               { GTK_MOZ_EMBED_FLAG_IS_NETWORK, "STATE_IS_NETWORK" },
-               { GTK_MOZ_EMBED_FLAG_IS_WINDOW, "STATE_IS_WINDOW" }
-           };
-           for (int i = 0; i != sizeof(flag_names)/sizeof(flag_names[0]); ++i)
-           {
-               if (flags & flag_names[i].value)
-               {
-                   std::cout << flag_names[i].name;
-                   flags_left -= flag_names[i].value;
-                   if (flags_left)
-                       std::cout << " | ";
-               }
-           }
-           if (flags_left)
-               std::cout << "0x" << std::setbase(16) << flags_left;
-       }
-       std::cout << ", " << "0x" << std::setbase(16) << status << ")\n";
-#       endif // DEBUG_ON_NET_STATE_CHANGE
-
-       if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
-       {
-           if (flags & GTK_MOZ_EMBED_FLAG_START)
-               ++pending_req_count_;
-
-           if (flags & GTK_MOZ_EMBED_FLAG_STOP)
-           {
-               assert(pending_req_count_ != 0);
-               --pending_req_count_;
-           }
-       }
-           
-       if (flags & GTK_MOZ_EMBED_FLAG_IS_DOCUMENT
-           && flags & GTK_MOZ_EMBED_FLAG_START)
-       {
-           pending_window_update_ = true;
-           have_tweaked_page_ = false;
-       }
-
-       if (flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW
-           && flags & GTK_MOZ_EMBED_FLAG_STOP)
-       {
-           // Check whether the load was successful, ignoring this
-           // pseudo-error.
-           if (status != NS_IMAGELIB_ERROR_LOAD_ABORTED)
-               check(status);
-
-           pending_window_update_ = false;
-       }
-
-       if (!browser_is_busy())
-       {
-           try
-           {
-               if (!process_page())
-               {
-                   finished_ = true;
-                   Gtk::Main::quit();
-               }
-           }
-           catch (std::exception & e)
-           {
-               std::cerr << "Fatal error";
-               if (!page_queue_.empty())
-                   std::cerr << " while processing <" << page_queue_.front()
-                             << ">";
-               std::cerr << ": " << e.what() << "\n";
-               Gtk::Main::quit();
-           }
-           catch (Glib::Exception & e)
-           {
-               std::cerr << "Fatal error";
-               if (!page_queue_.empty())
-                   std::cerr << " while processing <" << page_queue_.front()
-                             << ">";
-               std::cerr << ": " << e.what() << "\n";
-               Gtk::Main::quit();
-           }
-       }
-    }
-
-    bool webdvd_window::process_page()
-    {
-       assert(!page_queue_.empty());
-
-       nsCOMPtr<nsIWebBrowser> browser(browser_widget_.get_browser());
-       nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
-       assert(doc_shell);
-       nsCOMPtr<nsIPresShell> pres_shell;
-       check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
-       nsCOMPtr<nsIPresContext> pres_context;
-       check(doc_shell->GetPresContext(getter_AddRefs(pres_context)));
-       nsCOMPtr<nsIDOMWindow> dom_window;
-       check(browser->GetContentDOMWindow(getter_AddRefs(dom_window)));
-
-       // If we haven't done so already, apply the stylesheet and
-       // disable scrollbars.
-       if (!have_tweaked_page_)
-       {
-           apply_style_sheet(stylesheet_, pres_shell);
-
-           // This actually only needs to be done once.
-           nsCOMPtr<nsIDOMBarProp> dom_bar_prop;
-           check(dom_window->GetScrollbars(getter_AddRefs(dom_bar_prop)));
-           check(dom_bar_prop->SetVisible(false));
-
-           have_tweaked_page_ = true;
-
-           // Might need to wait a while for things to load or more
-           // likely for a re-layout.
-           if (browser_is_busy())
-               return true;
-       }
-
-       // 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();
-
-           // 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())
-           {
-               page_queue_.pop();
-               if (page_queue_.empty())
-               {
-                   generate_dvd(contents_, output_dir_);
-                   return false;
-               }
-               else
-               {
-                   load_next_page();
-               }
-           }
-       }
-
-       return true;
-    }
-
-    void webdvd_window::save_screenshot()
-    {
-       Glib::RefPtr<Gdk::Window> window(get_window());
-       assert(window);
-       window->process_updates(true);
-
-       background_temp_.reset(new temp_file("webdvd-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");
-    }
-
-    struct webdvd_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("webdvd-spumux-"),
-                 links_temp("webdvd-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 webdvd_window::process_links(nsIPresShell * pres_shell,
-                                    nsIPresContext * 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(
-           pres_context->EventStateManager()); // does not AddRef
-       assert(event_state_man);
-       nsCOMPtr<nsIDOMDocumentEvent> event_factory(
-           do_QueryInterface(basic_doc));
-       assert(event_factory);
-       nsCOMPtr<nsIDOMDocumentView> doc_view(do_QueryInterface(basic_doc));
-       assert(doc_view);
-       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;
-
-       for (/* no initialisation */;
-            state->links_it != state->links_end;
-            ++state->links_it)
-       {
-           nsCOMPtr<nsIDOMNode> node(*state->links_it);
-
-           // Find the link URI and separate any fragment from it.
-           nsCOMPtr<nsILink> link(do_QueryInterface(node));
-           assert(link);
-           nsCOMPtr<nsIURI> uri_iface;
-           check(link->GetHrefURI(getter_AddRefs(uri_iface)));
-           std::string uri_and_fragment, uri, fragment;
-           {
-               nsCString uri_and_fragment_ns;
-               check(uri_iface->GetSpec(uri_and_fragment_ns));
-               uri_and_fragment.assign(uri_and_fragment_ns.BeginReading(),
-                                       uri_and_fragment_ns.EndReading());
-
-               std::size_t hash_pos = uri_and_fragment.find('#');
-               uri.assign(uri_and_fragment, 0, hash_pos);
-               if (hash_pos != std::string::npos)
-                   fragment.assign(uri_and_fragment,
-                                   hash_pos + 1, std::string::npos);
-           }
-
-           // Is this a new link?
-           if (!state->link_changing)
-           {
-               // Find a rectangle enclosing the link and clip it to the
-               // window.
-               nsCOMPtr<nsIDOMElement> elem(do_QueryInterface(node));
-               assert(elem);
-               state->link_rect = get_elem_rect(ns_doc, elem);
-               state->link_rect &= window_rect;
-
-               if (state->link_rect.empty())
-               {
-                   std::cerr << "Ignoring invisible link to "
-                             << uri_and_fragment << "\n";
-                   continue;
-               }
-
-               ++state->link_num;
-
-               if (state->link_num >= unsigned(dvd::menu_buttons_max))
-               {
-                   if (state->link_num == unsigned(dvd::menu_buttons_max))
-                       std::cerr << "No more than " << dvd::menu_buttons_max
-                                 << " buttons can be placed on a menu\n";
-                   std::cerr << "Ignoring link to " << uri_and_fragment
-                             << "\n";
-                   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))
-               {
-                   PRBool is_file;
-                   check(uri_iface->SchemeIs("file", &is_file));
-                   if (!is_file)
-                   {
-                       std::cerr << "Links to video must use the file:"
-                                 << " scheme\n";
-                       continue;
-                   }
-                   target = add_title(uri);
-                   target.sub_index =
-                       std::strtoul(fragment.c_str(), NULL, 10);
-               }
-               else
-               {
-                   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);
-
-               nsCOMPtr<nsIContent> content(do_QueryInterface(node));
-               assert(content);
-               nsCOMPtr<nsIDOMEventTarget> event_target(
-                   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"),
-                         getter_AddRefs(event)));
-               nsCOMPtr<nsIDOMMouseEvent> mouse_event(
-                   do_QueryInterface(event));
-               assert(mouse_event);
-               check(mouse_event->InitMouseEvent(
-                         NS_ConvertASCIItoUTF16("mouseover"),
-                         true,  // can bubble
-                         true,  // cancelable
-                         view,
-                         0,     // detail: mouse click count
-                         state->link_rect.left, // screenX
-                         state->link_rect.top,  // screenY
-                         state->link_rect.left, // clientX
-                         state->link_rect.top,  // clientY
-                         false, false, false, false, // qualifiers
-                         0,     // button: left (or primary)
-                         0));   // related target
-               PRBool dummy;
-               check(event_target->DispatchEvent(mouse_event,
-                                                 &dummy));
-               check(event_state_man->SetContentState(content,
-                                                      NS_EVENT_STATE_HOVER));
-
-               pres_shell->FlushPendingNotifications(true);
-
-               // We may have to exit and wait for image loading
-               // to complete, at which point we will be called
-               // again.
-               if (browser_is_busy())
-               {
-                   state->link_changing = true;
-                   page_state_ = state;
-                   return;
-               }
-           }
-
-           window->process_updates(true);
-
-           Glib::RefPtr<Gdk::Pixbuf> changed_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));
-           diff_rgb_pixbufs(
-               state->norm_pixbuf,
-               changed_pixbuf,
-               state->diff_pixbuf,
-               state->link_rect.left,
-               state->link_rect.top,
-               state->link_rect.right - state->link_rect.left,
-               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");
-       }
-    }
-
-    const video::frame_params & lookup_frame_params(const char * str)
-    {
-       assert(str);
-       static const char * const known_strings[] = {
-           "525",    "625",
-           "525/60", "625/50",
-           "NTSC",   "PAL",
-           "ntsc",   "pal"
-       };
-       for (std::size_t i = 0;
-            i != sizeof(known_strings)/sizeof(known_strings[0]);
-            ++i)
-           if (std::strcmp(str, known_strings[i]) == 0)
-               return (i & 1)
-                   ? video::frame_params_625
-                   : video::frame_params_525;
-       throw std::runtime_error(
-           std::string("Invalid video standard: ").append(str));
-    }
-
-    void print_usage(std::ostream & stream, const char * command_name)
-    {
-       stream <<
-           "Usage: " << command_name << " [gtk-options] [--preview]\n"
-           "           [--video-std {525|525/60|NTSC|ntsc"
-           " | 625|625/50|PAL|pal}]\n"
-           "           [--encoder {mjpegtools|mjpegtools-old}]\n"
-           "           menu-url [output-dir]\n";
-    }
-    
-    void set_browser_preferences()
-    {
-       nsCOMPtr<nsIPrefService> pref_service;
-       static const nsCID pref_service_cid = NS_PREFSERVICE_CID;
-       check(CallGetService<nsIPrefService>(pref_service_cid,
-                                            getter_AddRefs(pref_service)));
-       nsCOMPtr<nsIPrefBranch> pref_branch;
-
-       // Disable IE-compatibility kluge that causes backgrounds to
-       // sometimes/usually be missing from snapshots.  This is only
-       // effective from Mozilla 1.8 onward.
-#      if MOZ_VERSION_MAJOR > 1                                 \
-           || (MOZ_VERSION_MAJOR == 1 && MOZ_VERSION_MINOR >= 8)
-       check(pref_service->GetDefaultBranch("layout",
-                                            getter_AddRefs(pref_branch)));
-       check(pref_branch->SetBoolPref(
-                 "fire_onload_after_image_background_loads",
-                 true));
-#      endif
-
-       // Set display resolution.  With standard-definition video we
-       // will be fitting ~600 pixels across a screen typically
-       // ranging from 10 to 25 inches wide, for a resolution of
-       // 24-60 dpi.  I therefore declare the average horizontal
-       // resolution to be 40 dpi.  The vertical resolution will be
-       // slightly different but unfortunately Mozilla doesn't
-       // support non-square pixels (and neither do fontconfig or Xft
-       // anyway).
-       check(pref_service->GetDefaultBranch("browser.display",
-                                            getter_AddRefs(pref_branch)));
-       check(pref_branch->SetIntPref("screen_resolution", 40));
-    }
-
-} // namespace
-
-void fatal_error(const std::string & message)
-{
-    std::cerr << "Fatal error: " << message << "\n";
-    Gtk::Main::quit();
-}
-
-int main(int argc, char ** argv)
-{
-    try
-    {
-       video::frame_params frame_params = video::frame_params_625;
-       bool preview_mode = false;
-       std::string menu_url;
-       std::string output_dir;
-       mpeg_encoder encoder = mpeg_encoder_ffmpeg;
-
-       // Do initial option parsing.  We have to do this before
-       // letting Gtk parse the arguments since we may need to spawn
-       // Xvfb first.
-       int argi = 1;
-       while (argi != argc)
-       {
-           if (std::strcmp(argv[argi], "--") == 0)
-           {
-               break;
-           }
-           else if (std::strcmp(argv[argi], "--help") == 0)
-           {
-               print_usage(std::cout, argv[0]);
-               return EXIT_SUCCESS;
-           }
-           else if (std::strcmp(argv[argi], "--preview") == 0)
-           {
-               preview_mode = true;
-               argi += 1;
-           }
-           else if (std::strcmp(argv[argi], "--video-std") == 0)
-           {
-               if (argi + 1 == argc)
-               {
-                   std::cerr << "Missing argument to --video-std\n";
-                   print_usage(std::cerr, argv[0]);
-                   return EXIT_FAILURE;
-               }
-               frame_params = lookup_frame_params(argv[argi + 1]);
-               argi += 2;
-           }
-           else
-           {
-               argi += 1;
-           }
-       }
-
-       std::auto_ptr<x_frame_buffer> fb;
-       if (!preview_mode)
-       {
-           // Spawn Xvfb and set env variables so that Xlib will use it
-           // Use 8 bits each for RGB components, which should translate into
-           // "enough" bits for YUV components.
-           fb.reset(new x_frame_buffer(frame_params.width,
-                                       frame_params.height,
-                                       3 * 8));
-           setenv("XAUTHORITY", fb->get_authority().c_str(), true);
-           setenv("DISPLAY", fb->get_display().c_str(), true);
-       }
-
-       // Initialise Gtk
-       Gtk::Main kit(argc, argv);
-
-       // Complete option parsing with Gtk's options out of the way.
-       argi = 1;
-       while (argi != argc)
-       {
-           if (std::strcmp(argv[argi], "--") == 0)
-           {
-               argi += 1;
-               break;
-           }
-           else if (std::strcmp(argv[argi], "--preview") == 0)
-           {
-               argi += 1;
-           }
-           else if (std::strcmp(argv[argi], "--video-std") == 0)
-           {
-               argi += 2;
-           }
-           else if (std::strcmp(argv[argi], "--save-temps") == 0)
-           {
-               temp_file::keep_all(true);
-               argi += 1;
-           }
-           else if (std::strcmp(argv[argi], "--encoder") == 0)
-           {
-               if (argi + 1 == argc)
-               {
-                   std::cerr << "Missing argument to --encoder\n";
-                   print_usage(std::cerr, argv[0]);
-                   return EXIT_FAILURE;
-               }
-               if (std::strcmp(argv[argi + 1], "ffmpeg") == 0)
-               {
-                   encoder = mpeg_encoder_ffmpeg;
-               }
-               else if (std::strcmp(argv[argi + 1], "mjpegtools-old") == 0)
-               {
-                   encoder = 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;
-               }
-               else
-               {
-                   std::cerr << "Invalid argument to --encoder\n";
-                   print_usage(std::cerr, argv[0]);
-                   return EXIT_FAILURE;
-               }
-               argi += 2;
-           }
-           else if (argv[argi][0] == '-')
-           {
-               std::cerr << "Invalid option: " << argv[argi] << "\n";
-               print_usage(std::cerr, argv[0]);
-               return EXIT_FAILURE;
-           }
-           else
-           {
-               break;
-           }
-       }
-
-       // Look for a starting URL or filename and (except in preview
-       // mode) an output directory after the options.
-        if (argc - argi != (preview_mode ? 1 : 2))
-       {
-           print_usage(std::cerr, argv[0]);
-           return EXIT_FAILURE;
-       }
-       if (std::strstr(argv[argi], "://"))
-       {
-           // It appears to be an absolute URL, so use it as-is.
-           menu_url = argv[argi];
-       }
-       else
-       {
-           // Assume it's a filename.  Resolve it to an absolute URL.
-           std::string path(argv[argi]);
-           if (!Glib::path_is_absolute(path))
-               path = Glib::build_filename(Glib::get_current_dir(), path);
-           menu_url = Glib::filename_to_uri(path);             
-       }
-       if (!preview_mode)
-           output_dir = argv[argi + 1];
-
-       // Initialise Mozilla
-       browser_widget::initialiser browser_init;
-       set_browser_preferences();
-       if (!preview_mode)
-           null_prompt_service::install();
-
-       // Run the browser/converter
-       webdvd_window window(frame_params, menu_url, output_dir, encoder);
-       window.show();
-       window.signal_hide().connect(SigC::slot(&Gtk::Main::quit));
-       Gtk::Main::run();
-
-       return ((preview_mode || window.is_finished())
-               ? EXIT_SUCCESS
-               : EXIT_FAILURE);
-    }
-    catch (std::exception & e)
-    {
-       std::cerr << "Fatal error: " << e.what() << "\n";
-       return EXIT_FAILURE;
-    }
-}
diff --git a/webdvd.css b/webdvd.css
deleted file mode 100644 (file)
index b441499..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-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;
-}
-a:link, a:visited {
-  color: blue;
-  /* Don't underline links because underlining tends to flicker on TVs. */
-  text-decoration: none;
-  /* Buttons have to be rectangular (AFAIK). */
-  white-space: nowrap;
-}
-/* The hover state must be made obvious since DVD players have no pointer. */
-a:hover {
-  color: red;
-}
-/* The active state should provide visual feedback, but is not so critical. */
-a:active {
-  color: purple;
-}
-/* Don't show focus rectangles. */
-*|*:-moz-any-link:focus {
-  -moz-outline: none !important;
-}
diff --git a/webdvd.hpp b/webdvd.hpp
deleted file mode 100644 (file)
index d281814..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-#ifndef INC_WEBDVD_HPP
-#define INC_WEBDVD_HPP
-
-#include <string>
-
-void fatal_error(const std::string & message);
-
-#endif // !INC_WEBDVD_HPP