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