-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
-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
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
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)
-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
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".
-----------
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.
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.
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.
-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/
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
#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();
}
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());
#include <nsIUnicodeEncoder.h>
#include "null_prompt_service.hpp"
-#include "webdvd.hpp"
+#include "videolink.hpp"
#include "xpcom_support.hpp"
using xpcom_support::check;
--- /dev/null
+// 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;
+ }
+}
--- /dev/null
+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;
+}
--- /dev/null
+#ifndef INC_VIDEOLINK_HPP
+#define INC_VIDEOLINK_HPP
+
+#include <string>
+
+void fatal_error(const std::string & message);
+
+#endif // !INC_VIDEOLINK_HPP
+++ /dev/null
-// 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;
- }
-}
+++ /dev/null
-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;
-}
+++ /dev/null
-#ifndef INC_WEBDVD_HPP
-#define INC_WEBDVD_HPP
-
-#include <string>
-
-void fatal_error(const std::string & message);
-
-#endif // !INC_WEBDVD_HPP