]> git.decadent.org.uk Git - videolink.git/blob - webdvd.cpp
e2d8cb87425867a969e293e08c95e53a306ad598
[videolink.git] / webdvd.cpp
1 // Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
2 // See the file "COPYING" for licence details.
3
4 #include <cassert>
5 #include <cstring>
6 #include <exception>
7 #include <fstream>
8 #include <iomanip>
9 #include <iostream>
10 #include <memory>
11 #include <queue>
12 #include <set>
13 #include <sstream>
14 #include <string>
15
16 #include <stdlib.h>
17
18 #include <boost/shared_ptr.hpp>
19
20 #include <gdkmm/pixbuf.h>
21 #include <glibmm/convert.h>
22 #include <glibmm/spawn.h>
23 #include <gtkmm/main.h>
24 #include <gtkmm/window.h>
25
26 #include <imglib2/ImageErrors.h>
27 #include <nsGUIEvent.h>
28 #include <nsIBoxObject.h>
29 #include <nsIContent.h>
30 #include <nsIDocShell.h>
31 #include <nsIDOMAbstractView.h>
32 #include <nsIDOMBarProp.h>
33 #include <nsIDOMDocumentEvent.h>
34 #include <nsIDOMDocumentView.h>
35 #include <nsIDOMElement.h>
36 #include <nsIDOMEventTarget.h>
37 #include <nsIDOMHTMLDocument.h>
38 #include <nsIDOMMouseEvent.h>
39 #include <nsIDOMNSDocument.h>
40 #include <nsIDOMWindow.h>
41 #include <nsIEventStateManager.h>
42 #include <nsIInterfaceRequestorUtils.h>
43 #include <nsIURI.h> // required before nsILink.h
44 #include <nsILink.h>
45 #include <nsIPrefBranch.h>
46 #include <nsIPrefService.h>
47 #include <nsIPresContext.h>
48 #include <nsIPresShell.h>
49 #include <nsIServiceManagerUtils.h>
50 #include <nsIWebBrowser.h>
51 #include <nsString.h>
52
53 #include "browser_widget.hpp"
54 #include "child_iterator.hpp"
55 #include "dvd.hpp"
56 #include "link_iterator.hpp"
57 #include "pixbufs.hpp"
58 #include "style_sheets.hpp"
59 #include "temp_file.hpp"
60 #include "video.hpp"
61 #include "x_frame_buffer.hpp"
62 #include "xpcom_support.hpp"
63
64 using xpcom_support::check;
65
66 namespace
67 {
68     struct rectangle
69     {
70         int left, top;     // inclusive
71         int right, bottom; // exclusive
72
73         rectangle operator|=(const rectangle & other)
74             {
75                 if (other.empty())
76                 {
77                     // use current extents unchanged
78                 }
79                 else if (empty())
80                 {
81                     // use other extents
82                     *this = other;
83                 }
84                 else
85                 {
86                     // find rectangle enclosing both extents
87                     left = std::min(left, other.left);
88                     top = std::min(top, other.top);
89                     right = std::max(right, other.right);
90                     bottom = std::max(bottom, other.bottom);
91                 }
92
93                 return *this;
94             }
95
96         rectangle operator&=(const rectangle & other)
97             {
98                 // find rectangle enclosed in both extents
99                 left = std::max(left, other.left);
100                 top = std::max(top, other.top);
101                 right = std::max(left, std::min(right, other.right));
102                 bottom = std::max(top, std::min(bottom, other.bottom));
103                 return *this;
104             }
105
106         bool empty() const
107             {
108                 return left == right || bottom == top;
109             }
110     };
111
112     rectangle get_elem_rect(nsIDOMNSDocument * ns_doc,
113                             nsIDOMElement * elem)
114     {
115         rectangle result;
116
117         // Start with this element's bounding box
118         nsCOMPtr<nsIBoxObject> box;
119         check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
120         int width, height;
121         check(box->GetScreenX(&result.left));
122         check(box->GetScreenY(&result.top));
123         check(box->GetWidth(&width));
124         check(box->GetHeight(&height));
125         result.right = result.left + width;
126         result.bottom = result.top + height;
127
128         // Merge bounding boxes of all child elements
129         for (child_iterator it = child_iterator(elem), end; it != end; ++it)
130         {
131             nsCOMPtr<nsIDOMNode> child_node(*it);
132             PRUint16 child_type;
133             if (check(child_node->GetNodeType(&child_type)),
134                 child_type == nsIDOMNode::ELEMENT_NODE)
135             {
136                 nsCOMPtr<nsIDOMElement> child_elem(
137                     do_QueryInterface(child_node));
138                 result |= get_elem_rect(ns_doc, child_elem);
139             }
140         }
141
142         return result;
143     }
144
145
146     std::string xml_escape(const std::string & str)
147     {
148         std::string result;
149         std::size_t begin = 0;
150
151         for (;;)
152         {
153             std::size_t end = str.find_first_of("\"&'<>", begin);
154             result.append(str, begin, end - begin);
155             if (end == std::string::npos)
156                 return result;
157
158             const char * entity = NULL;
159             switch (str[end])
160             {
161             case '"':  entity = "&quot;"; break;
162             case '&':  entity = "&amp;";  break;
163             case '\'': entity = "&apos;"; break;
164             case '<':  entity = "&lt;";   break;
165             case '>':  entity = "&gt;";   break;
166             }
167             assert(entity);
168             result.append(entity);
169
170             begin = end + 1;
171         }
172     }
173
174     
175     struct dvd_contents
176     {
177         enum pgc_type { unknown_pgc,  menu_pgc, title_pgc };
178
179         struct pgc_ref
180         {
181             explicit pgc_ref(pgc_type type = unknown_pgc,
182                              int index = -1,
183                              int sub_index = 0)
184                     : type(type), index(index), sub_index(sub_index)
185                 {}
186             bool operator==(const pgc_ref & other) const
187                 {
188                     return type == other.type && index == other.index;
189                 }
190             bool operator!=(const pgc_ref & other) const
191                 {
192                     return !(*this == other);
193                 }
194             
195             pgc_type type;      // Menu or title reference?
196             unsigned index;     // Menu or title index (within resp. vector)
197             unsigned sub_index; // Button or chapter number (1-based; 0 if
198                                 // unspecified; not compared!)
199         };
200
201         struct menu
202         {
203             menu()
204                     : vob_temp(new temp_file("webdvd-vob-"))
205                 {
206                     vob_temp->close();
207                 }
208
209             boost::shared_ptr<temp_file> vob_temp;
210             std::vector<pgc_ref> entries;
211         };
212
213         struct title
214         {
215             explicit title(const std::string & vob_list)
216                     : vob_list(vob_list)
217                 {}
218
219             std::string vob_list;
220         };
221
222         std::vector<menu> menus;
223         std::vector<title> titles;
224     };
225
226     void generate_dvd(const dvd_contents & contents,
227                       const std::string & output_dir);
228
229     class webdvd_window : public Gtk::Window
230     {
231     public:
232         webdvd_window(
233             const video::frame_params & frame_params,
234             const std::string & main_page_uri,
235             const std::string & output_dir);
236
237     private:
238         dvd_contents::pgc_ref add_menu(const std::string & uri);
239         dvd_contents::pgc_ref add_title(const std::string & uri);
240         void load_next_page();
241         void on_net_state_change(const char * uri, gint flags, guint status);
242         bool browser_is_busy() const
243             {
244                 return pending_window_update_ || pending_req_count_;
245             }
246         bool process_page();
247         void save_screenshot();
248         void process_links(nsIPresShell * pres_shell,
249                            nsIPresContext * pres_context,
250                            nsIDOMWindow * dom_window);
251
252         video::frame_params frame_params_;
253         std::string output_dir_;
254         browser_widget browser_widget_;
255         nsCOMPtr<nsIStyleSheet> stylesheet_;
256
257         dvd_contents contents_;
258         typedef std::map<std::string, dvd_contents::pgc_ref> resource_map_type;
259         resource_map_type resource_map_;
260
261         std::queue<std::string> page_queue_;
262         bool pending_window_update_;
263         int pending_req_count_;
264         bool have_tweaked_page_;
265         std::auto_ptr<temp_file> background_temp_;
266         struct page_state;
267         std::auto_ptr<page_state> page_state_;
268     };
269
270     webdvd_window::webdvd_window(
271         const video::frame_params & frame_params,
272         const std::string & main_page_uri,
273         const std::string & output_dir)
274             : frame_params_(frame_params),
275               output_dir_(output_dir),
276               stylesheet_(load_css("file://" WEBDVD_LIB_DIR "/webdvd.css")),
277               pending_window_update_(false),
278               pending_req_count_(0),
279               have_tweaked_page_(false)
280     {
281         set_size_request(frame_params_.width, frame_params_.height);
282         set_resizable(false);
283
284         add(browser_widget_);
285         browser_widget_.show();
286         browser_widget_.signal_net_state().connect(
287             SigC::slot(*this, &webdvd_window::on_net_state_change));
288
289         add_menu(main_page_uri);
290         load_next_page();
291     }
292
293     dvd_contents::pgc_ref webdvd_window::add_menu(const std::string & uri)
294     {
295         dvd_contents::pgc_ref next_menu(dvd_contents::menu_pgc,
296                                         contents_.menus.size());
297         std::pair<resource_map_type::iterator, bool> insert_result(
298             resource_map_.insert(std::make_pair(uri, next_menu)));
299
300         if (!insert_result.second)
301         {
302             return insert_result.first->second;
303         }
304         else
305         {
306             page_queue_.push(uri);
307             contents_.menus.resize(contents_.menus.size() + 1);
308             return next_menu;
309         }
310     }
311
312     dvd_contents::pgc_ref webdvd_window::add_title(const std::string & uri)
313     {
314         dvd_contents::pgc_ref next_title(dvd_contents::title_pgc,
315                                          contents_.titles.size());
316         std::pair<resource_map_type::iterator, bool> insert_result(
317             resource_map_.insert(std::make_pair(uri, next_title)));
318
319         if (!insert_result.second)
320         {
321             return insert_result.first->second;
322         }
323         else
324         {
325             Glib::ustring hostname;
326             std::string filename(Glib::filename_from_uri(uri, hostname));
327             // FIXME: Should check the hostname
328
329             std::string vob_list;
330
331             // Store a reference to a linked VOB file, or the contents
332             // of a linked VOB list file.
333             if (filename.compare(filename.size() - 4, 4, ".vob") == 0)
334             {
335                 if (!Glib::file_test(filename, Glib::FILE_TEST_IS_REGULAR))
336                     throw std::runtime_error(
337                         filename + " is missing or not a regular file");
338                 vob_list
339                     .append("<vob file='")
340                     .append(xml_escape(filename))
341                     .append("'/>\n");
342             }
343             else
344             {
345                 assert(filename.compare(filename.size() - 8, 8, ".voblist")
346                        == 0);
347                 // TODO: Validate the file contents
348                 vob_list.assign(Glib::file_get_contents(filename));
349             }
350
351             contents_.titles.push_back(dvd_contents::title(vob_list));
352             return next_title;
353         }
354     }
355
356     void webdvd_window::load_next_page()
357     {
358         assert(!page_queue_.empty());
359         const std::string & uri = page_queue_.front();
360         std::cout << "loading " << uri << std::endl;
361
362         browser_widget_.load_uri(uri);
363     }
364
365     void webdvd_window::on_net_state_change(const char * uri,
366                                            gint flags, guint status)
367     {
368 #       ifdef DEBUG_ON_NET_STATE_CHANGE
369         std::cout << "webdvd_window::on_net_state_change(";
370         if (uri)
371             std::cout << '"' << uri << '"';
372         else
373             std::cout << "NULL";
374         std::cout << ", ";
375         {
376             gint flags_left = flags;
377             static const struct {
378                 gint value;
379                 const char * name;
380             } flag_names[] = {
381                 { GTK_MOZ_EMBED_FLAG_START, "STATE_START" },
382                 { GTK_MOZ_EMBED_FLAG_REDIRECTING, "STATE_REDIRECTING" },
383                 { GTK_MOZ_EMBED_FLAG_TRANSFERRING, "STATE_TRANSFERRING" },
384                 { GTK_MOZ_EMBED_FLAG_NEGOTIATING, "STATE_NEGOTIATING" },
385                 { GTK_MOZ_EMBED_FLAG_STOP, "STATE_STOP" },
386                 { GTK_MOZ_EMBED_FLAG_IS_REQUEST, "STATE_IS_REQUEST" },
387                 { GTK_MOZ_EMBED_FLAG_IS_DOCUMENT, "STATE_IS_DOCUMENT" },
388                 { GTK_MOZ_EMBED_FLAG_IS_NETWORK, "STATE_IS_NETWORK" },
389                 { GTK_MOZ_EMBED_FLAG_IS_WINDOW, "STATE_IS_WINDOW" }
390             };
391             for (int i = 0; i != sizeof(flag_names)/sizeof(flag_names[0]); ++i)
392             {
393                 if (flags & flag_names[i].value)
394                 {
395                     std::cout << flag_names[i].name;
396                     flags_left -= flag_names[i].value;
397                     if (flags_left)
398                         std::cout << " | ";
399                 }
400             }
401             if (flags_left)
402                 std::cout << "0x" << std::setbase(16) << flags_left;
403         }
404         std::cout << ", " << "0x" << std::setbase(16) << status << ")\n";
405 #       endif // DEBUG_ON_NET_STATE_CHANGE
406
407         if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
408         {
409             if (flags & GTK_MOZ_EMBED_FLAG_START)
410                 ++pending_req_count_;
411
412             if (flags & GTK_MOZ_EMBED_FLAG_STOP)
413             {
414                 assert(pending_req_count_ != 0);
415                 --pending_req_count_;
416             }
417         }
418             
419         if (flags & GTK_MOZ_EMBED_FLAG_IS_DOCUMENT
420             && flags & GTK_MOZ_EMBED_FLAG_START)
421         {
422             pending_window_update_ = true;
423             have_tweaked_page_ = false;
424         }
425
426         if (flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW
427             && flags & GTK_MOZ_EMBED_FLAG_STOP)
428         {
429             // Check whether the load was successful, ignoring this
430             // pseudo-error.
431             if (status != NS_IMAGELIB_ERROR_LOAD_ABORTED)
432                 check(status);
433
434             pending_window_update_ = false;
435         }
436
437         if (!browser_is_busy())
438         {
439             try
440             {
441                 if (!process_page())
442                     Gtk::Main::quit();
443             }
444             catch (std::exception & e)
445             {
446                 std::cerr << "Fatal error";
447                 if (!page_queue_.empty())
448                     std::cerr << " while processing <" << page_queue_.front()
449                               << ">";
450                 std::cerr << ": " << e.what() << "\n";
451                 Gtk::Main::quit();
452             }
453             catch (Glib::Exception & e)
454             {
455                 std::cerr << "Fatal error";
456                 if (!page_queue_.empty())
457                     std::cerr << " while processing <" << page_queue_.front()
458                               << ">";
459                 std::cerr << ": " << e.what() << "\n";
460                 Gtk::Main::quit();
461             }
462         }
463     }
464
465     bool webdvd_window::process_page()
466     {
467         assert(!page_queue_.empty());
468
469         nsCOMPtr<nsIWebBrowser> browser(browser_widget_.get_browser());
470         nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
471         assert(doc_shell);
472         nsCOMPtr<nsIPresShell> pres_shell;
473         check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
474         nsCOMPtr<nsIPresContext> pres_context;
475         check(doc_shell->GetPresContext(getter_AddRefs(pres_context)));
476         nsCOMPtr<nsIDOMWindow> dom_window;
477         check(browser->GetContentDOMWindow(getter_AddRefs(dom_window)));
478
479         // If we haven't done so already, apply the stylesheet and
480         // disable scrollbars.
481         if (!have_tweaked_page_)
482         {
483             apply_style_sheet(stylesheet_, pres_shell);
484
485             // This actually only needs to be done once.
486             nsCOMPtr<nsIDOMBarProp> dom_bar_prop;
487             check(dom_window->GetScrollbars(getter_AddRefs(dom_bar_prop)));
488             check(dom_bar_prop->SetVisible(false));
489
490             have_tweaked_page_ = true;
491
492             // Might need to wait a while for things to load or more
493             // likely for a re-layout.
494             if (browser_is_busy())
495                 return true;
496         }
497
498         // All further work should only be done if we're not in preview mode.
499         if (!output_dir_.empty())
500         {
501             // If we haven't already started work on this menu, save a
502             // screenshot of its normal appearance.
503             if (!page_state_.get())
504                 save_screenshot();
505
506             // Start or continue processing links.
507             process_links(pres_shell, pres_context, dom_window);
508
509             // If we've finished work on the links, move on to the
510             // next page, if any, or else generate the DVD filesystem.
511             if (!page_state_.get())
512             {
513                 page_queue_.pop();
514                 if (page_queue_.empty())
515                 {
516                     generate_dvd(contents_, output_dir_);
517                     return false;
518                 }
519                 else
520                 {
521                     load_next_page();
522                 }
523             }
524         }
525
526         return true;
527     }
528
529     void webdvd_window::save_screenshot()
530     {
531         Glib::RefPtr<Gdk::Window> window(get_window());
532         assert(window);
533         window->process_updates(true);
534
535         background_temp_.reset(new temp_file("webdvd-back-"));
536         background_temp_->close();
537         std::cout << "saving " << background_temp_->get_name() << std::endl;
538         Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
539                             window->get_colormap(),
540                             0, 0, 0, 0,
541                             frame_params_.width, frame_params_.height)
542             ->save(background_temp_->get_name(), "png");
543     }
544
545     struct webdvd_window::page_state
546     {
547         page_state(nsIDOMDocument * doc, int width, int height)
548                 : diff_pixbuf(Gdk::Pixbuf::create(
549                                   Gdk::COLORSPACE_RGB,
550                                   true, 8, // has_alpha, bits_per_sample
551                                   width, height)),
552                   spumux_temp("webdvd-spumux-"),
553                   links_temp("webdvd-links-"),
554                   link_num(0),
555                   links_it(doc),
556                   link_changing(false)
557             {
558                 spumux_temp.close();
559                 links_temp.close();
560             }
561
562         Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
563
564         temp_file spumux_temp;
565         std::ofstream spumux_file;
566
567         temp_file links_temp;
568
569         unsigned link_num;
570         link_iterator links_it, links_end;
571
572         rectangle link_rect;
573         bool link_changing;
574         Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
575     };
576
577     void webdvd_window::process_links(nsIPresShell * pres_shell,
578                                      nsIPresContext * pres_context,
579                                      nsIDOMWindow * dom_window)
580     {
581         Glib::RefPtr<Gdk::Window> window(get_window());
582         assert(window);
583
584         nsCOMPtr<nsIDOMDocument> basic_doc;
585         check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
586         nsCOMPtr<nsIDOMNSDocument> ns_doc(do_QueryInterface(basic_doc));
587         assert(ns_doc);
588         nsCOMPtr<nsIEventStateManager> event_state_man(
589             pres_context->EventStateManager()); // does not AddRef
590         assert(event_state_man);
591         nsCOMPtr<nsIDOMDocumentEvent> event_factory(
592             do_QueryInterface(basic_doc));
593         assert(event_factory);
594         nsCOMPtr<nsIDOMDocumentView> doc_view(do_QueryInterface(basic_doc));
595         assert(doc_view);
596         nsCOMPtr<nsIDOMAbstractView> view;
597         check(doc_view->GetDefaultView(getter_AddRefs(view)));
598
599         // Set up or recover our iteration state.
600         std::auto_ptr<page_state> state(page_state_);
601         if (!state.get())
602         {
603             state.reset(
604                 new page_state(
605                     basic_doc, frame_params_.width, frame_params_.height));
606             
607             state->spumux_file.open(state->spumux_temp.get_name().c_str());
608             state->spumux_file <<
609                 "<subpictures>\n"
610                 "  <stream>\n"
611                 "    <spu force='yes' start='00:00:00.00'\n"
612                 "        highlight='" << state->links_temp.get_name() << "'\n"
613                 "        select='" << state->links_temp.get_name() << "'>\n";
614         }
615
616         rectangle window_rect = {
617             0, 0, frame_params_.width, frame_params_.height
618         };
619
620         unsigned menu_num = resource_map_[page_queue_.front()].index;
621
622         for (/* no initialisation */;
623              state->links_it != state->links_end;
624              ++state->links_it)
625         {
626             nsCOMPtr<nsIDOMNode> node(*state->links_it);
627
628             // Find the link URI and separate any fragment from it.
629             nsCOMPtr<nsILink> link(do_QueryInterface(node));
630             assert(link);
631             nsCOMPtr<nsIURI> uri_iface;
632             check(link->GetHrefURI(getter_AddRefs(uri_iface)));
633             std::string uri_and_fragment, uri, fragment;
634             {
635                 nsCString uri_and_fragment_ns;
636                 check(uri_iface->GetSpec(uri_and_fragment_ns));
637                 uri_and_fragment.assign(uri_and_fragment_ns.BeginReading(),
638                                         uri_and_fragment_ns.EndReading());
639
640                 std::size_t hash_pos = uri_and_fragment.find('#');
641                 uri.assign(uri_and_fragment, 0, hash_pos);
642                 if (hash_pos != std::string::npos)
643                     fragment.assign(uri_and_fragment,
644                                     hash_pos + 1, std::string::npos);
645             }
646
647             // Is this a new link?
648             if (!state->link_changing)
649             {
650                 // Find a rectangle enclosing the link and clip it to the
651                 // window.
652                 nsCOMPtr<nsIDOMElement> elem(do_QueryInterface(node));
653                 assert(elem);
654                 state->link_rect = get_elem_rect(ns_doc, elem);
655                 state->link_rect &= window_rect;
656
657                 if (state->link_rect.empty())
658                 {
659                     std::cerr << "Ignoring invisible link to "
660                               << uri_and_fragment << "\n";
661                     continue;
662                 }
663
664                 ++state->link_num;
665
666                 if (state->link_num >= unsigned(dvd::menu_buttons_max))
667                 {
668                     if (state->link_num == unsigned(dvd::menu_buttons_max))
669                         std::cerr << "No more than " << dvd::menu_buttons_max
670                                   << " buttons can be placed on a menu\n";
671                     std::cerr << "Ignoring link to " << uri_and_fragment
672                               << "\n";
673                     continue;
674                 }
675
676                 state->spumux_file <<
677                     "      <button x0='" << state->link_rect.left << "'"
678                     " y0='" << state->link_rect.top << "'"
679                     " x1='" << state->link_rect.right - 1 << "'"
680                     " y1='" << state->link_rect.bottom - 1 << "'/>\n";
681
682                 // Check whether this is a link to a video or a page then
683                 // add it to the known resources if not already seen; then
684                 // add it to the menu entries.
685                 dvd_contents::pgc_ref target;
686                 // FIXME: This is a bit of a hack.  Perhaps we could decide
687                 // later based on the MIME type determined by Mozilla?
688                 if ((uri.size() > 4
689                      && uri.compare(uri.size() - 4, 4, ".vob") == 0)
690                     || (uri.size() > 8
691                         && uri.compare(uri.size() - 8, 8, ".voblist") == 0))
692                 {
693                     PRBool is_file;
694                     check(uri_iface->SchemeIs("file", &is_file));
695                     if (!is_file)
696                     {
697                         std::cerr << "Links to video must use the file:"
698                                   << " scheme\n";
699                         continue;
700                     }
701                     target = add_title(uri);
702                     target.sub_index =
703                         std::strtoul(fragment.c_str(), NULL, 10);
704                 }
705                 else
706                 {
707                     target = add_menu(uri);
708                     // TODO: If there's a fragment, work out which button
709                     // is closest and set target.sub_index.
710                 }
711                 contents_.menus[menu_num].entries.push_back(target);
712
713                 nsCOMPtr<nsIContent> content(do_QueryInterface(node));
714                 assert(content);
715                 nsCOMPtr<nsIDOMEventTarget> event_target(
716                     do_QueryInterface(node));
717                 assert(event_target);
718
719                 state->norm_pixbuf = Gdk::Pixbuf::create(
720                     Glib::RefPtr<Gdk::Drawable>(window),
721                     window->get_colormap(),
722                     state->link_rect.left,
723                     state->link_rect.top,
724                     0,
725                     0,
726                     state->link_rect.right - state->link_rect.left,
727                     state->link_rect.bottom - state->link_rect.top);
728
729                 nsCOMPtr<nsIDOMEvent> event;
730                 check(event_factory->CreateEvent(
731                           NS_ConvertASCIItoUTF16("MouseEvents"),
732                           getter_AddRefs(event)));
733                 nsCOMPtr<nsIDOMMouseEvent> mouse_event(
734                     do_QueryInterface(event));
735                 assert(mouse_event);
736                 check(mouse_event->InitMouseEvent(
737                           NS_ConvertASCIItoUTF16("mouseover"),
738                           true,  // can bubble
739                           true,  // cancelable
740                           view,
741                           0,     // detail: mouse click count
742                           state->link_rect.left, // screenX
743                           state->link_rect.top,  // screenY
744                           state->link_rect.left, // clientX
745                           state->link_rect.top,  // clientY
746                           false, false, false, false, // qualifiers
747                           0,     // button: left (or primary)
748                           0));   // related target
749                 PRBool dummy;
750                 check(event_target->DispatchEvent(mouse_event,
751                                                   &dummy));
752                 check(event_state_man->SetContentState(content,
753                                                        NS_EVENT_STATE_HOVER));
754
755                 pres_shell->FlushPendingNotifications(true);
756
757                 // We may have to exit and wait for image loading
758                 // to complete, at which point we will be called
759                 // again.
760                 if (browser_is_busy())
761                 {
762                     state->link_changing = true;
763                     page_state_ = state;
764                     return;
765                 }
766             }
767
768             window->process_updates(true);
769
770             Glib::RefPtr<Gdk::Pixbuf> changed_pixbuf(
771                 Gdk::Pixbuf::create(
772                     Glib::RefPtr<Gdk::Drawable>(window),
773                     window->get_colormap(),
774                     state->link_rect.left,
775                     state->link_rect.top,
776                     0,
777                     0,
778                     state->link_rect.right - state->link_rect.left,
779                     state->link_rect.bottom - state->link_rect.top));
780             diff_rgb_pixbufs(
781                 state->norm_pixbuf,
782                 changed_pixbuf,
783                 state->diff_pixbuf,
784                 state->link_rect.left,
785                 state->link_rect.top,
786                 state->link_rect.right - state->link_rect.left,
787                 state->link_rect.bottom - state->link_rect.top);
788         }
789
790         quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
791
792         std::cout << "saving " << state->links_temp.get_name()
793                   << std::endl;
794         state->diff_pixbuf->save(state->links_temp.get_name(), "png");
795
796         state->spumux_file <<
797             "    </spu>\n"
798             "  </stream>\n"
799             "</subpictures>\n";
800
801         state->spumux_file.close();
802
803         // TODO: if (!state->spumux_file) throw ...
804
805         {
806             std::ostringstream command_stream;
807             command_stream << "pngtopnm "
808                            << background_temp_->get_name()
809                            << " | ppmtoy4m -v0 -n1 -F"
810                            << frame_params_.rate_numer
811                            << ":" << frame_params_.rate_denom
812                            << " -A" << frame_params_.pixel_ratio_width
813                            << ":" << frame_params_.pixel_ratio_height
814                            << (" -Ip -S420_mpeg2"
815                                " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
816                                " | mplex -v0 -f8 -o/dev/stdout /dev/stdin"
817                                " | spumux -v0 -mdvd ")
818                            << state->spumux_temp.get_name()
819                            << " > "
820                            << contents_.menus[menu_num].vob_temp->get_name();
821             std::string command(command_stream.str());
822             const char * argv[] = {
823                 "/bin/sh", "-c", command.c_str(), 0
824             };
825             std::cout << "running " << argv[2] << std::endl;
826             int command_result;
827             Glib::spawn_sync(".",
828                              Glib::ArrayHandle<std::string>(
829                                  argv, sizeof(argv)/sizeof(argv[0]),
830                                  Glib::OWNERSHIP_NONE),
831                              Glib::SPAWN_STDOUT_TO_DEV_NULL,
832                              SigC::Slot0<void>(),
833                              0, 0,
834                              &command_result);
835             if (command_result != 0)
836                 throw std::runtime_error("spumux pipeline failed");
837         }
838     }
839
840     void generate_dvd(const dvd_contents & contents,
841                       const std::string & output_dir)
842     {
843         temp_file temp("webdvd-dvdauthor-");
844         temp.close();
845         std::ofstream file(temp.get_name().c_str());
846
847         // We generate code that uses registers in the following way:
848         //
849         // g0:     scratch
850         // g1:     current location
851         // g12:    location that last jumped to a video
852         //
853         // All locations are divided into two bitfields: the least
854         // significant 10 bits are a page/menu number and the most
855         // significant 6 bits are a link/button number, and numbering
856         // starts at 1, not 0.  This is done for compatibility with
857         // the encoding of the s8 (button) register.
858         //
859         static const int button_mult = dvd::reg_s8_button_mult;
860         static const int menu_mask = button_mult - 1;
861         static const int button_mask = (1 << dvd::reg_bits) - button_mult;
862
863         file <<
864             "<dvdauthor>\n"
865             "  <vmgm>\n"
866             "    <menus>\n";
867             
868         for (unsigned menu_num = 0;
869              menu_num != contents.menus.size();
870              ++menu_num)
871         {
872             const dvd_contents::menu & menu = contents.menus[menu_num];
873
874             if (menu_num == 0)
875             {
876                 // This is the first (title) menu, displayed when the
877                 // disc is first played.
878                 file <<
879                     "      <pgc entry='title'>\n"
880                     "        <pre>\n"
881                     // Initialise the current location if it is not set
882                     // (all general registers are initially 0).
883                     "          if (g1 eq 0)\n"
884                     "            g1 = " << 1 + button_mult << ";\n";
885             }
886             else
887             {
888                 file <<
889                     "      <pgc>\n"
890                     "        <pre>\n";
891             }
892
893             // When a title finishes or the user presses the menu
894             // button, this always jumps to the titleset's root menu.
895             // We want to return the user to the last menu they used.
896             // So we arrange for each titleset's root menu to return
897             // to the vmgm title menu and then dispatch from there to
898             // whatever the correct menu is.  We determine the correct
899             // menu by looking at the menu part of g1.
900
901             file << "          g0 = g1 &amp; " << menu_mask << ";\n";
902
903             // There is a limit of 128 VM instructions in each PGC.
904             // Therefore in each menu's <pre> section we generate
905             // jumps to menus with numbers greater by 512, 256, 128,
906             // ..., 1 where (a) such a menu exists, (b) this menu
907             // number is divisible by twice that increment and (c) the
908             // correct menu is that or a later menu.  Thus each menu
909             // has at most 10 such conditional jumps and is reachable
910             // by at most 10 jumps from the title menu.  This chain of
911             // jumps might take too long on some players; this has yet
912             // to be investigated.
913             
914             for (std::size_t menu_incr = (menu_mask + 1) / 2;
915                  menu_incr != 0;
916                  menu_incr /= 2)
917             {
918                 if (menu_num + menu_incr < contents.menus.size()
919                     && (menu_num & (menu_incr * 2 - 1)) == 0)
920                 {
921                     file <<
922                         "          if (g0 ge " << 1 + menu_num + menu_incr
923                                                << ")\n"
924                         "            jump menu " << 1 + menu_num + menu_incr
925                                                << ";\n";
926                 }
927             }
928
929             file <<
930                 // Highlight the appropriate button.
931                 "          s8 = g1 &amp; " << button_mask << ";\n"
932                 "        </pre>\n"
933                 "        <vob file='" << menu.vob_temp->get_name() << "'/>\n";
934
935             for (unsigned button_num = 0;
936                  button_num != menu.entries.size();
937                  ++button_num)
938             {
939                 const dvd_contents::pgc_ref & target =
940                     menu.entries[button_num];
941
942                 file << "        <button> ";
943
944                 if (target.type == dvd_contents::menu_pgc)
945                 {
946                     unsigned target_button_num;
947
948                     if (target.sub_index)
949                     {
950                         target_button_num = target.sub_index;
951                     }
952                     else
953                     {
954                         // Look for a button on the new menu that links
955                         // back to this one.  If there is one, set that to
956                         // be the highlighted button; otherwise, use the
957                         // first button.
958                         const std::vector<dvd_contents::pgc_ref> &
959                             target_menu_entries =
960                             contents.menus[target.index].entries;
961                         dvd_contents::pgc_ref this_pgc(dvd_contents::menu_pgc,
962                                                        menu_num);
963                         target_button_num = target_menu_entries.size();
964                         while (target_button_num != 0
965                                && (target_menu_entries[--target_button_num]
966                                    != this_pgc))
967                             ;
968                     }
969                          
970                     file << "g1 = "
971                          << (1 + target.index
972                              + (1 + target_button_num) * button_mult)
973                          << "; jump menu " << 1 + target.index << ";";
974                 }
975                 else
976                 {
977                     assert(target.type == dvd_contents::title_pgc);
978
979                     file << "g1 = "
980                          << 1 + menu_num + (1 + button_num) * button_mult
981                          << "; jump title "
982                          << 1 + target.index;
983                     if (target.sub_index)
984                         file << " chapter " << target.sub_index;
985                     file << ";";
986                 }
987
988                 file <<  " </button>\n";
989             }
990
991             file << "      </pgc>\n";
992         }
993
994         file <<
995             "    </menus>\n"
996             "  </vmgm>\n";
997
998         // Generate a titleset for each title.  This appears to make
999         // jumping to titles a whole lot simpler (but limits us to 99
1000         // titles).
1001         for (unsigned title_num = 0;
1002              title_num != contents.titles.size();
1003              ++title_num)
1004         {
1005             file <<
1006                 "  <titleset>\n"
1007                 // Generate a dummy menu so that the menu button on the
1008                 // remote control will work.
1009                 "    <menus>\n"
1010                 "      <pgc entry='root'>\n"
1011                 "        <pre> jump vmgm menu; </pre>\n"
1012                 "      </pgc>\n"
1013                 "    </menus>\n"
1014                 "    <titles>\n"
1015                 "      <pgc>\n"
1016                 // Record calling location.
1017                 "        <pre> g12 = g1; </pre>\n"
1018                  << contents.titles[title_num].vob_list <<
1019                 // If the menu location has not been changed during
1020                 // the title, set the location to be the following
1021                 // button in the menu.  In any case, return to some
1022                 // menu.
1023                 "        <post> if (g1 eq g12) g1 = g1 + " << button_mult
1024                  << "; call menu; </post>\n"
1025                 "      </pgc>\n"
1026                 "    </titles>\n"
1027                 "  </titleset>\n";
1028         }
1029
1030         file <<
1031             "</dvdauthor>\n";
1032
1033         file.close();
1034
1035         {
1036             const char * argv[] = {
1037                 "dvdauthor",
1038                 "-o", output_dir.c_str(),
1039                 "-x", temp.get_name().c_str(),
1040                 0
1041             };
1042             int command_result;
1043             Glib::spawn_sync(".",
1044                              Glib::ArrayHandle<std::string>(
1045                                  argv, sizeof(argv)/sizeof(argv[0]),
1046                                  Glib::OWNERSHIP_NONE),
1047                              Glib::SPAWN_SEARCH_PATH
1048                              | Glib::SPAWN_STDOUT_TO_DEV_NULL,
1049                              SigC::Slot0<void>(),
1050                              0, 0,
1051                              &command_result);
1052             if (command_result != 0)
1053                 throw std::runtime_error("dvdauthor failed");
1054         }
1055     }
1056
1057     const video::frame_params & lookup_frame_params(const char * str)
1058     {
1059         assert(str);
1060         static const struct { const char * str; bool is_ntsc; }
1061         known_strings[] = {
1062             { "NTSC",  true },
1063             { "ntsc",  true },
1064             { "PAL",   false },
1065             { "pal",   false },
1066             // For DVD purposes, SECAM can be treated identically to PAL.
1067             { "SECAM", false },
1068             { "secam", false }
1069         };
1070         for (std::size_t i = 0;
1071              i != sizeof(known_strings)/sizeof(known_strings[0]);
1072              ++i)
1073             if (std::strcmp(str, known_strings[i].str) == 0)
1074                 return known_strings[i].is_ntsc ?
1075                     video::ntsc_params : video::pal_params;
1076         throw std::runtime_error(
1077             std::string("Invalid video standard: ").append(str));
1078     }
1079
1080     void print_usage(std::ostream & stream, const char * command_name)
1081     {
1082         stream << "Usage: " << command_name
1083                << (" [gtk-options] [--video-std std-name]"
1084                    " [--preview] menu-url [output-dir]\n");
1085     }
1086     
1087     void set_browser_preferences()
1088     {
1089         nsCOMPtr<nsIPrefService> pref_service;
1090         static const nsCID pref_service_cid = NS_PREFSERVICE_CID;
1091         check(CallGetService<nsIPrefService>(pref_service_cid,
1092                                              getter_AddRefs(pref_service)));
1093         nsCOMPtr<nsIPrefBranch> pref_branch;
1094
1095         // Disable IE-compatibility kluge that causes backgrounds to
1096         // sometimes/usually be missing from snapshots.  This is only
1097         // effective from Mozilla 1.8 onward.
1098 #       if MOZ_VERSION_MAJOR > 1                                 \
1099            || (MOZ_VERSION_MAJOR == 1 && MOZ_VERSION_MINOR >= 8)
1100         check(pref_service->GetDefaultBranch("layout",
1101                                              getter_AddRefs(pref_branch)));
1102         check(pref_branch->SetBoolPref(
1103                   "fire_onload_after_image_background_loads",
1104                   true));
1105 #       endif
1106
1107         // Set display resolution.  With standard-definition video we
1108         // will be fitting ~600 pixels across a screen typically
1109         // ranging from 10 to 25 inches wide, for a resolution of
1110         // 24-60 dpi.  I therefore declare the average horizontal
1111         // resolution to be 40 dpi.  The vertical resolution will be
1112         // slightly higher (PAL/SECAM) or lower (NTSC), but
1113         // unfortunately Mozilla doesn't support non-square pixels
1114         // (and neither do fontconfig or Xft anyway).
1115         check(pref_service->GetDefaultBranch("browser.display",
1116                                              getter_AddRefs(pref_branch)));
1117         check(pref_branch->SetIntPref("screen_resolution", 40));
1118     }
1119
1120 } // namespace
1121
1122 int main(int argc, char ** argv)
1123 {
1124     try
1125     {
1126         video::frame_params frame_params = video::pal_params;
1127         bool preview_mode = false;
1128         std::string menu_url;
1129         std::string output_dir;
1130
1131         // Do initial option parsing.  We have to do this before
1132         // letting Gtk parse the arguments since we may need to spawn
1133         // Xvfb first.
1134         int argi = 1;
1135         while (argi != argc)
1136         {
1137             if (std::strcmp(argv[argi], "--") == 0)
1138             {
1139                 break;
1140             }
1141             else if (std::strcmp(argv[argi], "--help") == 0)
1142             {
1143                 print_usage(std::cout, argv[0]);
1144                 return EXIT_SUCCESS;
1145             }
1146             else if (std::strcmp(argv[argi], "--preview") == 0)
1147             {
1148                 preview_mode = true;
1149                 argi += 1;
1150             }
1151             else if (std::strcmp(argv[argi], "--video-std") == 0)
1152             {
1153                 if (argi + 1 == argc)
1154                 {
1155                     std::cerr << "Missing argument to --video-std\n";
1156                     print_usage(std::cerr, argv[0]);
1157                     return EXIT_FAILURE;
1158                 }
1159                 frame_params = lookup_frame_params(argv[argi + 1]);
1160                 argi += 2;
1161             }
1162             else
1163             {
1164                 argi += 1;
1165             }
1166         }
1167
1168         std::auto_ptr<x_frame_buffer> fb;
1169         if (!preview_mode)
1170         {
1171             // Spawn Xvfb and set env variables so that Xlib will use it
1172             // Use 8 bits each for RGB components, which should translate into
1173             // "enough" bits for YUV components.
1174             fb.reset(new x_frame_buffer(frame_params.width,
1175                                         frame_params.height,
1176                                         3 * 8));
1177             setenv("XAUTHORITY", fb->get_authority().c_str(), true);
1178             setenv("DISPLAY", fb->get_display().c_str(), true);
1179         }
1180
1181         // Initialise Gtk
1182         Gtk::Main kit(argc, argv);
1183
1184         // Complete option parsing with Gtk's options out of the way.
1185         argi = 1;
1186         while (argi != argc)
1187         {
1188             if (std::strcmp(argv[argi], "--") == 0)
1189             {
1190                 argi += 1;
1191                 break;
1192             }
1193             else if (std::strcmp(argv[argi], "--preview") == 0)
1194             {
1195                 argi += 1;
1196             }
1197             else if (std::strcmp(argv[argi], "--video-std") == 0)
1198             {
1199                 argi += 2;
1200             }
1201             else if (argv[argi][0] == '-')
1202             {
1203                 std::cerr << "Invalid option: " << argv[argi] << "\n";
1204                 print_usage(std::cerr, argv[0]);
1205                 return EXIT_FAILURE;
1206             }
1207             else
1208             {
1209                 break;
1210             }
1211         }
1212
1213         // Look for a starting URL or filename and (except in preview
1214         // mode) an output directory after the options.
1215         if (argc - argi != (preview_mode ? 1 : 2))
1216         {
1217             print_usage(std::cerr, argv[0]);
1218             return EXIT_FAILURE;
1219         }
1220         if (std::strstr(argv[argi], "://"))
1221         {
1222             // It appears to be an absolute URL, so use it as-is.
1223             menu_url = argv[argi];
1224         }
1225         else
1226         {
1227             // Assume it's a filename.  Resolve it to an absolute URL.
1228             std::string path(argv[argi]);
1229             if (!Glib::path_is_absolute(path))
1230                 path = Glib::build_filename(Glib::get_current_dir(), path);
1231             menu_url = Glib::filename_to_uri(path);             
1232         }
1233         if (!preview_mode)
1234             output_dir = argv[argi + 1];
1235
1236         // Initialise Mozilla
1237         browser_widget::initialiser browser_init;
1238         set_browser_preferences();
1239
1240         // Run the browser/converter
1241         webdvd_window window(frame_params, menu_url, output_dir);
1242         Gtk::Main::run(window);
1243     }
1244     catch (std::exception & e)
1245     {
1246         std::cerr << "Fatal error: " << e.what() << "\n";
1247         return EXIT_FAILURE;
1248     }
1249
1250     return EXIT_SUCCESS;
1251 }