]> git.decadent.org.uk Git - videolink.git/blob - webdvd.cpp
Imported version 0.1
[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 <exception>
6 #include <fstream>
7 #include <iomanip>
8 #include <iostream>
9 #include <memory>
10 #include <queue>
11 #include <set>
12 #include <string>
13
14 #include <stdlib.h>
15 #include <unistd.h>
16
17 #include <gdkmm/pixbuf.h>
18 #include <gtkmm/main.h>
19 #include <gtkmm/window.h>
20
21 #include <nsGUIEvent.h>
22 #include <nsIBoxObject.h>
23 #include <nsIContent.h>
24 #include <nsIDocShell.h>
25 #include <nsIDOMAbstractView.h>
26 #include <nsIDOMDocumentEvent.h>
27 #include <nsIDOMDocumentView.h>
28 #include <nsIDOMElement.h>
29 #include <nsIDOMEventTarget.h>
30 #include <nsIDOMHTMLDocument.h>
31 #include <nsIDOMMouseEvent.h>
32 #include <nsIDOMNSDocument.h>
33 #include <nsIDOMWindow.h>
34 #include <nsIEventStateManager.h>
35 #include <nsIInterfaceRequestorUtils.h>
36 #include <nsIURI.h> // required before nsILink.h
37 #include <nsILink.h>
38 #include <nsIPresContext.h>
39 #include <nsIPresShell.h>
40 #include <nsIWebBrowser.h>
41 #include <nsString.h>
42
43 #include "browserwidget.hpp"
44 #include "childiterator.hpp"
45 #include "dvd.hpp"
46 #include "framebuffer.hpp"
47 #include "linkiterator.hpp"
48 #include "pixbufs.hpp"
49 #include "stylesheets.hpp"
50 #include "video.hpp"
51 #include "xpcom_support.hpp"
52
53 using xpcom_support::check;
54
55 namespace
56 {
57     struct rectangle
58     {
59         int left, top;     // inclusive
60         int right, bottom; // exclusive
61
62         rectangle operator|=(const rectangle & other)
63             {
64                 if (other.empty())
65                 {
66                     // use current extents unchanged
67                 }
68                 else if (empty())
69                 {
70                     // use other extents
71                     *this = other;
72                 }
73                 else
74                 {
75                     // find rectangle enclosing both extents
76                     left = std::min(left, other.left);
77                     top = std::min(top, other.top);
78                     right = std::max(right, other.right);
79                     bottom = std::max(bottom, other.bottom);
80                 }
81
82                 return *this;
83             }
84
85         rectangle operator&=(const rectangle & other)
86             {
87                 // find rectangle enclosed in both extents
88                 left = std::max(left, other.left);
89                 top = std::max(top, other.top);
90                 right = std::max(left, std::min(right, other.right));
91                 bottom = std::max(top, std::min(bottom, other.bottom));
92                 return *this;
93             }
94
95         bool empty() const
96             {
97                 return left == right || bottom == top;
98             }
99     };
100
101     rectangle get_elem_rect(nsIDOMNSDocument * ns_doc,
102                             nsIDOMElement * elem)
103     {
104         rectangle result;
105
106         nsCOMPtr<nsIBoxObject> box;
107         check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
108         int width, height;
109         check(box->GetScreenX(&result.left));
110         check(box->GetScreenY(&result.top));
111         check(box->GetWidth(&width));
112         check(box->GetHeight(&height));
113         result.right = result.left + width;
114         result.bottom = result.top + height;
115
116         for (ChildIterator it = ChildIterator(elem), end; it != end; ++it)
117         {
118             nsCOMPtr<nsIDOMNode> child_node(*it);
119             PRUint16 child_type;
120             if (check(child_node->GetNodeType(&child_type)),
121                 child_type == nsIDOMNode::ELEMENT_NODE)
122             {
123                 nsCOMPtr<nsIDOMElement> child_elem(
124                     do_QueryInterface(child_node));
125                 result |= get_elem_rect(ns_doc, child_elem);
126             }
127         }
128
129         return result;
130     }
131
132     class WebDvdWindow : public Gtk::Window
133     {
134     public:
135         WebDvdWindow(int width, int height);
136         void add_page(const std::string & uri);
137
138     private:
139         void add_video(const std::string & uri);
140         void load_next_page();
141         void on_net_state_change(const char * uri, gint flags, guint status);
142         void save_screenshot();
143         void process_links(nsIPresShell * pres_shell,
144                            nsIPresContext * pres_context,
145                            nsIDOMWindow * dom_window);
146         void generate_dvdauthor_file();
147
148         enum ResourceType { page_resource, video_resource };
149         typedef std::pair<ResourceType, int> ResourceEntry;
150         int width_, height_;
151         BrowserWidget browser_widget_;
152         nsCOMPtr<nsIStyleSheet> stylesheet_;
153         std::queue<std::string> page_queue_;
154         std::map<std::string, ResourceEntry> resource_map_;
155         std::vector<std::vector<std::string> > page_links_;
156         std::vector<std::string> video_paths_;
157         bool loading_;
158         int pending_req_count_;
159         struct link_state;
160         std::auto_ptr<link_state> link_state_;
161     };
162
163     WebDvdWindow::WebDvdWindow(int width, int height)
164             : width_(width), height_(height),
165               stylesheet_(load_css("file://" WEBDVD_LIB_DIR "/webdvd.css")),
166               loading_(false),
167               pending_req_count_(0)
168     {
169         set_default_size(width, height);
170         add(browser_widget_);
171         browser_widget_.show();
172         browser_widget_.signal_net_state().connect(
173             SigC::slot(*this, &WebDvdWindow::on_net_state_change));
174     }
175
176     void WebDvdWindow::add_page(const std::string & uri)
177     {
178         if (resource_map_.insert(
179                 std::make_pair(uri, ResourceEntry(page_resource, 0)))
180             .second)
181         {
182             page_queue_.push(uri);
183             if (!loading_)
184                 load_next_page();
185         }
186     }
187
188     void WebDvdWindow::add_video(const std::string & uri)
189     {
190         if (resource_map_.insert(
191                 std::make_pair(uri, ResourceEntry(video_resource,
192                                                   video_paths_.size() + 1)))
193             .second)
194         {
195             // FIXME: Should accept some slightly different URI prefixes
196             // (e.g. file://localhost/) and decode any URI-escaped
197             // characters in the path.
198             assert(uri.compare(0, 8, "file:///") == 0);
199             video_paths_.push_back(uri.substr(7));
200         }
201     }
202
203     void WebDvdWindow::load_next_page()
204     {
205         loading_ = true;
206
207         assert(!page_queue_.empty());
208         const std::string & uri = page_queue_.front();
209         std::cout << "loading " << uri << std::endl;
210
211         std::size_t page_count = page_links_.size();
212         resource_map_[uri].second = ++page_count;
213         page_links_.resize(page_count);
214         browser_widget_.load_uri(uri);
215     }
216
217     void WebDvdWindow::on_net_state_change(const char * uri,
218                                            gint flags, guint status)
219     {
220         enum {
221             process_nothing,
222             process_new_page,
223             process_current_link
224         } action = process_nothing;
225
226         if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
227         {
228             if (flags & GTK_MOZ_EMBED_FLAG_START)
229                 ++pending_req_count_;
230             if (flags & GTK_MOZ_EMBED_FLAG_STOP)
231             {
232                 assert(pending_req_count_ != 0);
233                 --pending_req_count_;
234             }
235             if (pending_req_count_ == 0 && link_state_.get())
236                 action = process_current_link;
237         }
238             
239         if (flags & GTK_MOZ_EMBED_FLAG_STOP
240             && flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW)
241             action = process_new_page;
242
243         if (action != process_nothing)
244         {
245             assert(loading_ && !page_queue_.empty());
246             assert(pending_req_count_ == 0);
247
248             try
249             {
250                 check(status);
251
252                 nsCOMPtr<nsIWebBrowser> browser(
253                     browser_widget_.get_browser());
254                 nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
255                 assert(doc_shell);
256                 nsCOMPtr<nsIPresShell> pres_shell;
257                 check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
258                 nsCOMPtr<nsIPresContext> pres_context;
259                 check(doc_shell->GetPresContext(
260                           getter_AddRefs(pres_context)));
261                 nsCOMPtr<nsIDOMWindow> dom_window;
262                 check(browser->GetContentDOMWindow(
263                           getter_AddRefs(dom_window)));
264
265                 if (action == process_new_page)
266                 {
267                     apply_style_sheet(stylesheet_, pres_shell);
268                     save_screenshot();
269                 }
270                 process_links(pres_shell, pres_context, dom_window);
271                 if (!link_state_.get())
272                 {
273                     page_queue_.pop();
274                     if (page_queue_.empty())
275                     {
276                         generate_dvdauthor_file();
277                         Gtk::Main::quit();
278                     }
279                     else
280                         load_next_page();
281                 }
282             }
283             catch (std::exception & e)
284             {
285                 std::cerr << "Fatal error";
286                 if (!page_queue_.empty())
287                     std::cerr << " while processing <" << page_queue_.front()
288                               << ">";
289                 std::cerr << ": " << e.what() << "\n";
290                 Gtk::Main::quit();
291             }
292         }
293     }
294
295     void WebDvdWindow::save_screenshot()
296     {
297         char filename[20];
298         std::sprintf(filename, "page_%06d_back.png", page_links_.size());
299         Glib::RefPtr<Gdk::Window> window(get_window());
300         assert(window);
301         window->process_updates(true);
302         std::cout << "saving " << filename << std::endl;
303         Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
304                             window->get_colormap(),
305                             0, 0, 0, 0, width_, height_)
306             ->save(filename, "png");
307     }
308
309     struct WebDvdWindow::link_state
310     {
311         Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
312
313         std::ofstream spumux_file;
314
315         int link_num;
316         LinkIterator links_it, links_end;
317
318         rectangle link_rect;
319         bool link_changing;
320         Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
321     };
322
323     void WebDvdWindow::process_links(nsIPresShell * pres_shell,
324                                      nsIPresContext * pres_context,
325                                      nsIDOMWindow * dom_window)
326     {
327         Glib::RefPtr<Gdk::Window> window(get_window());
328         assert(window);
329
330         nsCOMPtr<nsIDOMDocument> basic_doc;
331         check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
332         nsCOMPtr<nsIDOMNSDocument> ns_doc(do_QueryInterface(basic_doc));
333         assert(ns_doc);
334         nsCOMPtr<nsIEventStateManager> event_state_man(
335             pres_context->EventStateManager()); // does not AddRef
336         assert(event_state_man);
337         nsCOMPtr<nsIDOMDocumentEvent> event_factory(
338             do_QueryInterface(basic_doc));
339         assert(event_factory);
340         nsCOMPtr<nsIDOMDocumentView> doc_view(do_QueryInterface(basic_doc));
341         assert(doc_view);
342         nsCOMPtr<nsIDOMAbstractView> view;
343         check(doc_view->GetDefaultView(getter_AddRefs(view)));
344
345         // Set up or recover our iteration state.
346         std::auto_ptr<link_state> state(link_state_);
347         if (!state.get())
348         {
349             state.reset(new link_state);
350
351             state->diff_pixbuf = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB,
352                                                      true, // has_alpha
353                                                      8, // bits_per_sample
354                                                      width_, height_);
355
356             char spumux_filename[20];
357             std::sprintf(spumux_filename,
358                          "page_%06d.spumux", page_links_.size());
359             state->spumux_file.open(spumux_filename);
360             state->spumux_file <<
361                 "<subpictures>\n"
362                 "  <stream>\n"
363                 "    <spu force='yes' start='00:00:00.00'\n"
364                 "        highlight='page_" << std::setfill('0')
365                                << std::setw(6) << page_links_.size()
366                                << "_links.png'\n"
367                 "        select='page_" << std::setfill('0')
368                                << std::setw(6) << page_links_.size()
369                                << "_links.png'>\n";
370
371             state->link_num = 0;
372             state->links_it = LinkIterator(basic_doc);
373             state->link_changing = false;
374         }
375             
376         rectangle window_rect = { 0, 0, width_, height_ };
377
378         for (/* no initialisation */;
379              state->links_it != state->links_end;
380              ++state->links_it)
381         {
382             nsCOMPtr<nsIDOMNode> node(*state->links_it);
383
384             // Find the link URI.
385             nsCOMPtr<nsILink> link(do_QueryInterface(node));
386             assert(link);
387             nsCOMPtr<nsIURI> uri;
388             check(link->GetHrefURI(getter_AddRefs(uri)));
389             std::string uri_string;
390             {
391                 nsCString uri_ns_string;
392                 check(uri->GetSpec(uri_ns_string));
393                 uri_string.assign(uri_ns_string.BeginReading(),
394                                   uri_ns_string.EndReading());
395             }
396             std::string uri_sans_fragment(uri_string, 0, uri_string.find('#'));
397
398             // Is this a new link?
399             if (!state->link_changing)
400             {
401                 // Find a rectangle enclosing the link and clip it to the
402                 // window.
403                 nsCOMPtr<nsIDOMElement> elem(do_QueryInterface(node));
404                 assert(elem);
405                 state->link_rect = get_elem_rect(ns_doc, elem);
406                 state->link_rect &= window_rect;
407
408                 if (state->link_rect.empty())
409                 {
410                     std::cerr << "Ignoring invisible link to "
411                               << uri_string << "\n";
412                     continue;
413                 }
414
415                 ++state->link_num;
416
417                 if (state->link_num >= dvd::menu_buttons_max)
418                 {
419                     if (state->link_num == dvd::menu_buttons_max)
420                         std::cerr << "No more than " << dvd::menu_buttons_max
421                                   << " buttons can be placed on a page\n";
422                     std::cerr << "Ignoring link to " << uri_string << "\n";
423                     continue;
424                 }
425
426                 // Check whether this is a link to a video or a page then
427                 // add it to the known resources if not already seen.
428                 nsCString path;
429                 check(uri->GetPath(path));
430                 // FIXME: This is a bit of a hack.  Perhaps we could decide
431                 // later based on the MIME type determined by Mozilla?
432                 if (path.Length() > 4
433                     && std::strcmp(path.EndReading() - 4, ".vob") == 0)
434                 {
435                     PRBool is_file;
436                     check(uri->SchemeIs("file", &is_file));
437                     if (!is_file)
438                     {
439                         std::cerr << "Links to video must use the file:"
440                                   << " scheme\n";
441                         continue;
442                     }
443                     add_video(uri_sans_fragment);
444                 }
445                 else
446                 {
447                     add_page(uri_sans_fragment);
448                 }
449
450                 nsCOMPtr<nsIContent> content(do_QueryInterface(node));
451                 assert(content);
452                 nsCOMPtr<nsIDOMEventTarget> event_target(
453                     do_QueryInterface(node));
454                 assert(event_target);
455
456                 state->norm_pixbuf = Gdk::Pixbuf::create(
457                     Glib::RefPtr<Gdk::Drawable>(window),
458                     window->get_colormap(),
459                     state->link_rect.left,
460                     state->link_rect.top,
461                     0,
462                     0,
463                     state->link_rect.right - state->link_rect.left,
464                     state->link_rect.bottom - state->link_rect.top);
465
466                 nsCOMPtr<nsIDOMEvent> event;
467                 check(event_factory->CreateEvent(
468                           NS_ConvertASCIItoUTF16("MouseEvents"),
469                           getter_AddRefs(event)));
470                 nsCOMPtr<nsIDOMMouseEvent> mouse_event(
471                     do_QueryInterface(event));
472                 assert(mouse_event);
473                 check(mouse_event->InitMouseEvent(
474                           NS_ConvertASCIItoUTF16("mouseover"),
475                           true,  // can bubble
476                           true,  // cancelable
477                           view,
478                           0,     // detail: mouse click count
479                           state->link_rect.left, // screenX
480                           state->link_rect.top,  // screenY
481                           state->link_rect.left, // clientX
482                           state->link_rect.top,  // clientY
483                           false, false, false, false, // qualifiers
484                           0,     // button: left (or primary)
485                           0));   // related target
486                 PRBool dummy;
487                 check(event_target->DispatchEvent(mouse_event,
488                                                   &dummy));
489                 check(event_state_man->SetContentState(content,
490                                                        NS_EVENT_STATE_HOVER));
491
492                 pres_shell->FlushPendingNotifications(true);
493
494                 // We may have to exit and wait for image loading
495                 // to complete, at which point we will be called
496                 // again.
497                 if (pending_req_count_ > 0)
498                 {
499                     state->link_changing = true;
500                     link_state_ = state;
501                     return;
502                 }
503             }
504
505             window->process_updates(true);
506
507             Glib::RefPtr<Gdk::Pixbuf> changed_pixbuf(
508                 Gdk::Pixbuf::create(
509                     Glib::RefPtr<Gdk::Drawable>(window),
510                     window->get_colormap(),
511                     state->link_rect.left,
512                     state->link_rect.top,
513                     0,
514                     0,
515                     state->link_rect.right - state->link_rect.left,
516                     state->link_rect.bottom - state->link_rect.top));
517             diff_rgb_pixbufs(
518                 state->norm_pixbuf,
519                 changed_pixbuf,
520                 state->diff_pixbuf,
521                 state->link_rect.left,
522                 state->link_rect.top,
523                 state->link_rect.right - state->link_rect.left,
524                 state->link_rect.bottom - state->link_rect.top);
525
526             state->spumux_file <<
527                 "      <button x0='" << state->link_rect.left << "'"
528                 " y0='" << state->link_rect.top << "'"
529                 " x1='" << state->link_rect.right - 1 << "'"
530                 " y1='" << state->link_rect.bottom - 1 << "'/>\n";
531
532             // Add to the page's links, ignoring any fragment (for now).
533             page_links_.back().push_back(uri_sans_fragment);
534         }
535
536         quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
537
538         char filename[25];
539         std::sprintf(filename, "page_%06d_links.png", page_links_.size());
540         std::cout << "saving " << filename << std::endl;
541         state->diff_pixbuf->save(filename, "png");
542
543         state->spumux_file <<
544             "    </spu>\n"
545             "  </stream>\n"
546             "</subpictures>\n";
547     }
548
549     void generate_page_dispatch(std::ostream &, int indent,
550                                 int first_page, int last_page);
551
552     void WebDvdWindow::generate_dvdauthor_file()
553     {
554         std::ofstream file("webdvd.dvdauthor");
555
556         // We generate code that uses registers in the following way:
557         //
558         // g0:     link destination (when jumping to menu 1), then scratch
559         // g1:     current location
560         // g2-g11: location history (g2 = most recent)
561         // g12:    location that last linked to a video
562         //
563         // All locations are divided into two bitfields: the least
564         // significant 10 bits are a page/menu number and the most
565         // significant 6 bits are a link/button number.  This is
566         // chosen for compatibility with the encoding of the s8
567         // (button) register.
568         //
569         static const int link_mult = dvd::reg_s8_button_mult;
570         static const int page_mask = link_mult - 1;
571         static const int link_mask = (1 << dvd::reg_bits) - link_mult;
572
573         file <<
574             "<dvdauthor>\n"
575             "  <vmgm>\n"
576             "    <menus>\n";
577             
578         for (std::size_t page_num = 1;
579              page_num <= page_links_.size();
580              ++page_num)
581         {
582             std::vector<std::string> & page_links =
583                 page_links_[page_num - 1];
584
585             if (page_num == 1)
586             {
587                 // This is the first page (root menu) which needs to
588                 // include initialisation and dispatch code.
589         
590                 file <<
591                     "      <pgc entry='title'>\n"
592                     "        <pre>\n"
593                     // Has the location been set yet?
594                     "          if (g1 eq 0)\n"
595                     "          {\n"
596                     // Initialise the current location to first link on
597                     // this page.
598                     "            g1 = " << 1 * link_mult + 1 << ";\n"
599                     "          }\n"
600                     "          else\n"
601                     "          {\n"
602                     // Has the user selected a link?
603                     "            if (g0 ne 0)\n"
604                     "            {\n"
605                     // First update the history.
606                     // Does link go to the last page in the history?
607                     "              if (((g0 ^ g2) &amp; " << page_mask
608                      << ") == 0)\n"
609                     // It does; we treat this as going back and pop the old
610                     // location off the history stack into the current
611                     // location.  Clear the free stack slot.
612                     "              {\n"
613                     "                g1 = g2; g2 = g3; g3 = g4; g4 = g5;\n"
614                     "                g5 = g6; g6 = g7; g7 = g8; g8 = g9;\n"
615                     "                g9 = g10; g10 = g11; g11 = 0;\n"
616                     "              }\n"
617                     "              else\n"
618                     // Link goes to some other page, so push current
619                     // location onto the history stack and set the current
620                     // location to be exactly the target location.
621                     "              {\n"
622                     "                g11 = g10; g10 = g9; g9 = g8; g8 = g7;\n"
623                     "                g7 = g6; g6 = g5; g5 = g4; g4 = g3;\n"
624                     "                g3 = g2; g2 = g1; g1 = g0;\n"
625                     "              }\n"
626                     "            }\n"
627                     // Find the target page number.
628                     "            g0 = g1 &amp; " << page_mask << ";\n";
629                 // There seems to be no way to perform a computed jump,
630                 // so we generate all possible jumps and a binary search
631                 // to select the correct one.
632                 generate_page_dispatch(file, 12, 1, page_links_.size());
633                 file <<
634                     "          }\n";
635             }
636             else // page_num != 1
637             {
638                 file <<
639                     "      <pgc>\n"
640                     "        <pre>\n";
641             }
642
643             file <<
644                 // Clear link indicator and highlight the
645                 // appropriate link/button.
646                 "          g0 = 0; s8 = g1 &amp; " << link_mask << ";\n"
647                 "        </pre>\n"
648                 "        <vob file='page_"
649                  << std::setfill('0') << std::setw(6) << page_num
650                  << ".vob'/>\n";
651
652             for (std::size_t link_num = 1;
653                  link_num <= page_links.size();
654                  ++link_num)
655             {
656                 file <<
657                     "        <button>"
658                     // Update current location.
659                     " g1 = " << link_num * link_mult + page_num << ";";
660
661                 // Jump to appropriate resource.
662                 const ResourceEntry & resource_loc =
663                     resource_map_[page_links[link_num - 1]];
664                 if (resource_loc.first == page_resource)
665                     file <<
666                         " g0 = " << 1 * link_mult + resource_loc.second << ";"
667                         " jump menu 1;";
668                 else if (resource_loc.first == video_resource)
669                     file << " jump title " << resource_loc.second << ";";
670
671                 file <<  " </button>\n";
672             }
673
674             file << "      </pgc>\n";
675         }
676
677         file <<
678             "    </menus>\n"
679             "  </vmgm>\n";
680
681         // Generate a titleset for each video.  This appears to make
682         // jumping to titles a whole lot simpler.
683         for (std::size_t video_num = 1;
684              video_num <= video_paths_.size();
685              ++video_num)
686         {
687             file <<
688                 "  <titleset>\n"
689                 // Generate a dummy menu so that the menu button on the
690                 // remote control will work.
691                 "    <menus>\n"
692                 "      <pgc entry='root'>\n"
693                 "        <pre> jump vmgm menu; </pre>\n"
694                 "      </pgc>\n"
695                 "    </menus>\n"
696                 "    <titles>\n"
697                 "      <pgc>\n"
698                 // Record calling page/menu.
699                 "        <pre> g12 = g1; </pre>\n"
700                 // FIXME: Should XML-escape the path
701                 "        <vob file='" << video_paths_[video_num - 1]
702                  << "'/>\n"
703                 // If page/menu location has not been changed during the
704                 // video, change the location to be the following
705                 // link/button when returning to it.  In any case,
706                 // return to a page/menu.
707                 "        <post> if (g1 eq g12) g1 = g1 + " << link_mult
708                  << "; call menu; </post>\n"
709                 "      </pgc>\n"
710                 "    </titles>\n"
711                 "  </titleset>\n";
712         }
713
714         file <<
715             "</dvdauthor>\n";
716     }
717
718     void generate_page_dispatch(std::ostream & file, int indent,
719                                 int first_page, int last_page)
720     {
721         if (first_page == 1 && last_page == 1)
722         {
723             // The dispatch code is *on* page 1 so we must not dispatch to
724             // page 1 since that would cause an infinite loop.  This case
725             // should be unreachable if there is more than one page due
726             // to the following case.
727         }
728         else if (first_page == 1 && last_page == 2)
729         {
730             // dvdauthor doesn't allow empty blocks or null statements so
731             // when selecting between pages 1 and 2 we don't use an "else"
732             // part.  We must use braces so that a following "else" will
733             // match the right "if".
734             file << std::setw(indent) << "" << "{\n"
735                  << std::setw(indent) << "" << "if (g0 eq 2)\n"
736                  << std::setw(indent + 2) << "" << "jump menu 2;\n"
737                  << std::setw(indent) << "" << "}\n";
738         }
739         else if (first_page == last_page)
740         {
741             file << std::setw(indent) << ""
742                  << "jump menu " << first_page << ";\n";
743         }
744         else
745         {
746             int middle = (first_page + last_page) / 2;
747             file << std::setw(indent) << "" << "if (g0 le " << middle << ")\n";
748             generate_page_dispatch(file, indent + 2, first_page, middle);
749             file << std::setw(indent) << "" << "else\n";
750             generate_page_dispatch(file, indent + 2, middle + 1, last_page);
751         }
752     }
753
754 } // namespace
755
756 int main(int argc, char ** argv)
757 {
758     // Get dimensions
759     int width = video::pal_oscan_width, height = video::pal_oscan_height;
760     for (int i = 1; i < argc - 1; ++i)
761         if (std::strcmp(argv[i], "-geometry") == 0)
762         {
763             std::sscanf(argv[i + 1], "%dx%d", &width, &height);
764             break;
765         }
766     // A depth of 24 results in 8 bits each for RGB components, which
767     // translates into "enough" bits for YUV components.
768     const int depth = 24;
769
770     try
771     {
772         // Spawn Xvfb and set env variables so that Xlib will use it
773         FrameBuffer fb(width, height, depth);
774         setenv("XAUTHORITY", fb.get_x_authority().c_str(), true);
775         setenv("DISPLAY", fb.get_x_display().c_str(), true);
776
777         // Initialise Gtk and Mozilla
778         Gtk::Main kit(argc, argv);
779         BrowserWidget::init();
780
781         WebDvdWindow window(width, height);
782         for (int argi = 1; argi < argc; ++argi)
783             window.add_page(argv[argi]);
784         if (argc < 2)
785             window.add_page("about:");
786         Gtk::Main::run(window);
787     }
788     catch (std::exception & e)
789     {
790         std::cerr << "Fatal error: " << e.what() << "\n";
791         return EXIT_FAILURE;
792     }
793
794     return EXIT_SUCCESS;
795 }