]> git.decadent.org.uk Git - videolink.git/blob - webdvd.cpp
5b3cd9b998b65375f1139d58ad9fa0e1d92f269a
[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 <nsIPresContext.h>
46 #include <nsIPresShell.h>
47 #include <nsIWebBrowser.h>
48 #include <nsString.h>
49
50 #include "browserwidget.hpp"
51 #include "childiterator.hpp"
52 #include "dvd.hpp"
53 #include "framebuffer.hpp"
54 #include "linkiterator.hpp"
55 #include "pixbufs.hpp"
56 #include "stylesheets.hpp"
57 #include "temp_file.hpp"
58 #include "video.hpp"
59 #include "xpcom_support.hpp"
60
61 using xpcom_support::check;
62
63 namespace
64 {
65     struct rectangle
66     {
67         int left, top;     // inclusive
68         int right, bottom; // exclusive
69
70         rectangle operator|=(const rectangle & other)
71             {
72                 if (other.empty())
73                 {
74                     // use current extents unchanged
75                 }
76                 else if (empty())
77                 {
78                     // use other extents
79                     *this = other;
80                 }
81                 else
82                 {
83                     // find rectangle enclosing both extents
84                     left = std::min(left, other.left);
85                     top = std::min(top, other.top);
86                     right = std::max(right, other.right);
87                     bottom = std::max(bottom, other.bottom);
88                 }
89
90                 return *this;
91             }
92
93         rectangle operator&=(const rectangle & other)
94             {
95                 // find rectangle enclosed in both extents
96                 left = std::max(left, other.left);
97                 top = std::max(top, other.top);
98                 right = std::max(left, std::min(right, other.right));
99                 bottom = std::max(top, std::min(bottom, other.bottom));
100                 return *this;
101             }
102
103         bool empty() const
104             {
105                 return left == right || bottom == top;
106             }
107     };
108
109     rectangle get_elem_rect(nsIDOMNSDocument * ns_doc,
110                             nsIDOMElement * elem)
111     {
112         rectangle result;
113
114         // Start with this element's bounding box
115         nsCOMPtr<nsIBoxObject> box;
116         check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
117         int width, height;
118         check(box->GetScreenX(&result.left));
119         check(box->GetScreenY(&result.top));
120         check(box->GetWidth(&width));
121         check(box->GetHeight(&height));
122         result.right = result.left + width;
123         result.bottom = result.top + height;
124
125         // Merge bounding boxes of all child elements
126         for (ChildIterator it = ChildIterator(elem), end; it != end; ++it)
127         {
128             nsCOMPtr<nsIDOMNode> child_node(*it);
129             PRUint16 child_type;
130             if (check(child_node->GetNodeType(&child_type)),
131                 child_type == nsIDOMNode::ELEMENT_NODE)
132             {
133                 nsCOMPtr<nsIDOMElement> child_elem(
134                     do_QueryInterface(child_node));
135                 result |= get_elem_rect(ns_doc, child_elem);
136             }
137         }
138
139         return result;
140     }
141
142     class WebDvdWindow : public Gtk::Window
143     {
144     public:
145         WebDvdWindow(
146             const video::frame_params & frame_params,
147             const std::string & main_page_uri,
148             const std::string & output_dir);
149
150     private:
151         void add_page(const std::string & uri);
152         void add_video(const std::string & uri);
153         void load_next_page();
154         void on_net_state_change(const char * uri, gint flags, guint status);
155         bool process_page();
156         void save_screenshot();
157         void process_links(nsIPresShell * pres_shell,
158                            nsIPresContext * pres_context,
159                            nsIDOMWindow * dom_window);
160         void generate_dvd();
161
162         enum ResourceType { page_resource, video_resource };
163         typedef std::pair<ResourceType, int> ResourceEntry;
164         video::frame_params frame_params_;
165         std::string output_dir_;
166         BrowserWidget browser_widget_;
167         nsCOMPtr<nsIStyleSheet> stylesheet_;
168         std::queue<std::string> page_queue_;
169         std::map<std::string, ResourceEntry> resource_map_;
170         std::vector<std::vector<std::string> > page_links_;
171         std::vector<std::string> video_paths_;
172         bool pending_window_update_;
173         int pending_req_count_;
174         bool have_tweaked_page_;
175         std::auto_ptr<temp_file> background_temp_;
176         struct page_state;
177         std::auto_ptr<page_state> page_state_;
178         std::vector<boost::shared_ptr<temp_file> > page_temp_files_;
179     };
180
181     WebDvdWindow::WebDvdWindow(
182         const video::frame_params & frame_params,
183         const std::string & main_page_uri,
184         const std::string & output_dir)
185             : frame_params_(frame_params),
186               output_dir_(output_dir),
187               stylesheet_(load_css("file://" WEBDVD_LIB_DIR "/webdvd.css")),
188               pending_window_update_(false),
189               pending_req_count_(0),
190               have_tweaked_page_(false)
191     {
192         set_size_request(frame_params_.width, frame_params_.height);
193         set_resizable(false);
194
195         add(browser_widget_);
196         browser_widget_.show();
197         browser_widget_.signal_net_state().connect(
198             SigC::slot(*this, &WebDvdWindow::on_net_state_change));
199
200         add_page(main_page_uri);
201         load_next_page();
202     }
203
204     void WebDvdWindow::add_page(const std::string & uri)
205     {
206         if (resource_map_.insert(
207                 std::make_pair(uri, ResourceEntry(page_resource, 0)))
208             .second)
209         {
210             page_queue_.push(uri);
211         }
212     }
213
214     void WebDvdWindow::add_video(const std::string & uri)
215     {
216         if (resource_map_.insert(
217                 std::make_pair(uri, ResourceEntry(video_resource,
218                                                   video_paths_.size() + 1)))
219             .second)
220         {
221             Glib::ustring hostname;
222             video_paths_.push_back(Glib::filename_from_uri(uri, hostname));
223             // FIXME: Should check the hostname
224         }
225     }
226
227     void WebDvdWindow::load_next_page()
228     {
229         assert(!page_queue_.empty());
230         const std::string & uri = page_queue_.front();
231         std::cout << "loading " << uri << std::endl;
232
233         std::size_t page_count = page_links_.size();
234         resource_map_[uri].second = ++page_count;
235         page_links_.resize(page_count);
236
237         browser_widget_.load_uri(uri);
238     }
239
240     void WebDvdWindow::on_net_state_change(const char * uri,
241                                            gint flags, guint status)
242     {
243         if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
244         {
245             if (flags & GTK_MOZ_EMBED_FLAG_START)
246                 ++pending_req_count_;
247
248             if (flags & GTK_MOZ_EMBED_FLAG_STOP)
249             {
250                 assert(pending_req_count_ != 0);
251                 --pending_req_count_;
252             }
253         }
254             
255         if (flags & GTK_MOZ_EMBED_FLAG_IS_DOCUMENT
256             && flags & GTK_MOZ_EMBED_FLAG_START)
257         {
258             pending_window_update_ = true;
259             have_tweaked_page_ = false;
260         }
261
262         if (flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW
263             && flags & GTK_MOZ_EMBED_FLAG_STOP)
264         {
265             // Check whether the load was successful, ignoring this
266             // pseudo-error.
267             if (status != NS_IMAGELIB_ERROR_LOAD_ABORTED)
268                 check(status);
269
270             pending_window_update_ = false;
271         }
272
273         if (pending_req_count_ == 0 && !pending_window_update_)
274         {
275             try
276             {
277                 if (!process_page())
278                     Gtk::Main::quit();
279             }
280             catch (std::exception & e)
281             {
282                 std::cerr << "Fatal error";
283                 if (!page_queue_.empty())
284                     std::cerr << " while processing <" << page_queue_.front()
285                               << ">";
286                 std::cerr << ": " << e.what() << "\n";
287                 Gtk::Main::quit();
288             }
289         }
290     }
291
292     bool WebDvdWindow::process_page()
293     {
294         assert(!page_queue_.empty());
295
296         nsCOMPtr<nsIWebBrowser> browser(browser_widget_.get_browser());
297         nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
298         assert(doc_shell);
299         nsCOMPtr<nsIPresShell> pres_shell;
300         check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
301         nsCOMPtr<nsIPresContext> pres_context;
302         check(doc_shell->GetPresContext(getter_AddRefs(pres_context)));
303         nsCOMPtr<nsIDOMWindow> dom_window;
304         check(browser->GetContentDOMWindow(getter_AddRefs(dom_window)));
305
306         // If we haven't done so already, apply the stylesheet and
307         // disable scrollbars.
308         if (!have_tweaked_page_)
309         {
310             apply_style_sheet(stylesheet_, pres_shell);
311
312             // This actually only needs to be done once.
313             nsCOMPtr<nsIDOMBarProp> dom_bar_prop;
314             check(dom_window->GetScrollbars(getter_AddRefs(dom_bar_prop)));
315             check(dom_bar_prop->SetVisible(false));
316
317             have_tweaked_page_ = true;
318
319             // Might need to wait a while for things to load or more
320             // likely for a re-layout.
321             if (pending_req_count_ > 0)
322                 return true;
323         }
324
325         // All further work should only be done if we're not in preview mode.
326         if (!output_dir_.empty())
327         {
328             // If we haven't already started work on this page, save a
329             // screenshot of its normal appearance.
330             if (!page_state_.get())
331                 save_screenshot();
332
333             // Start or continue processing links.
334             process_links(pres_shell, pres_context, dom_window);
335
336             // If we've finished work on the links, move on to the
337             // next page, if any, or else generate the DVD filesystem.
338             if (!page_state_.get())
339             {
340                 page_queue_.pop();
341                 if (page_queue_.empty())
342                 {
343                     generate_dvd();
344                     return false;
345                 }
346                 else
347                 {
348                     load_next_page();
349                 }
350             }
351         }
352
353         return true;
354     }
355
356     void WebDvdWindow::save_screenshot()
357     {
358         Glib::RefPtr<Gdk::Window> window(get_window());
359         assert(window);
360         window->process_updates(true);
361
362         background_temp_.reset(new temp_file("webdvd-back-"));
363         background_temp_->close();
364         std::cout << "saving " << background_temp_->get_name() << std::endl;
365         Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
366                             window->get_colormap(),
367                             0, 0, 0, 0,
368                             frame_params_.width, frame_params_.height)
369             ->save(background_temp_->get_name(), "png");
370     }
371
372     struct WebDvdWindow::page_state
373     {
374         page_state(nsIDOMDocument * doc, int width, int height)
375                 : diff_pixbuf(Gdk::Pixbuf::create(
376                                   Gdk::COLORSPACE_RGB,
377                                   true, 8, // has_alpha, bits_per_sample
378                                   width, height)),
379                   spumux_temp("webdvd-spumux-"),
380                   links_temp("webdvd-links-"),
381                   link_num(0),
382                   links_it(doc),
383                   link_changing(false)
384             {
385                 spumux_temp.close();
386                 links_temp.close();
387             }
388
389         Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
390
391         temp_file spumux_temp;
392         std::ofstream spumux_file;
393
394         temp_file links_temp;
395
396         int link_num;
397         LinkIterator links_it, links_end;
398
399         rectangle link_rect;
400         bool link_changing;
401         Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
402     };
403
404     void WebDvdWindow::process_links(nsIPresShell * pres_shell,
405                                      nsIPresContext * pres_context,
406                                      nsIDOMWindow * dom_window)
407     {
408         Glib::RefPtr<Gdk::Window> window(get_window());
409         assert(window);
410
411         nsCOMPtr<nsIDOMDocument> basic_doc;
412         check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
413         nsCOMPtr<nsIDOMNSDocument> ns_doc(do_QueryInterface(basic_doc));
414         assert(ns_doc);
415         nsCOMPtr<nsIEventStateManager> event_state_man(
416             pres_context->EventStateManager()); // does not AddRef
417         assert(event_state_man);
418         nsCOMPtr<nsIDOMDocumentEvent> event_factory(
419             do_QueryInterface(basic_doc));
420         assert(event_factory);
421         nsCOMPtr<nsIDOMDocumentView> doc_view(do_QueryInterface(basic_doc));
422         assert(doc_view);
423         nsCOMPtr<nsIDOMAbstractView> view;
424         check(doc_view->GetDefaultView(getter_AddRefs(view)));
425
426         // Set up or recover our iteration state.
427         std::auto_ptr<page_state> state(page_state_);
428         if (!state.get())
429         {
430             state.reset(
431                 new page_state(
432                     basic_doc, frame_params_.width, frame_params_.height));
433             
434             state->spumux_file.open(state->spumux_temp.get_name().c_str());
435             state->spumux_file <<
436                 "<subpictures>\n"
437                 "  <stream>\n"
438                 "    <spu force='yes' start='00:00:00.00'\n"
439                 "        highlight='" << state->links_temp.get_name() << "'\n"
440                 "        select='" << state->links_temp.get_name() << "'>\n";
441         }
442
443         rectangle window_rect = {
444             0, 0, frame_params_.width, frame_params_.height
445         };
446
447         for (/* no initialisation */;
448              state->links_it != state->links_end;
449              ++state->links_it)
450         {
451             nsCOMPtr<nsIDOMNode> node(*state->links_it);
452
453             // Find the link URI.
454             nsCOMPtr<nsILink> link(do_QueryInterface(node));
455             assert(link);
456             nsCOMPtr<nsIURI> uri;
457             check(link->GetHrefURI(getter_AddRefs(uri)));
458             std::string uri_string;
459             {
460                 nsCString uri_ns_string;
461                 check(uri->GetSpec(uri_ns_string));
462                 uri_string.assign(uri_ns_string.BeginReading(),
463                                   uri_ns_string.EndReading());
464             }
465             std::string uri_sans_fragment(uri_string, 0, uri_string.find('#'));
466
467             // Is this a new link?
468             if (!state->link_changing)
469             {
470                 // Find a rectangle enclosing the link and clip it to the
471                 // window.
472                 nsCOMPtr<nsIDOMElement> elem(do_QueryInterface(node));
473                 assert(elem);
474                 state->link_rect = get_elem_rect(ns_doc, elem);
475                 state->link_rect &= window_rect;
476
477                 if (state->link_rect.empty())
478                 {
479                     std::cerr << "Ignoring invisible link to "
480                               << uri_string << "\n";
481                     continue;
482                 }
483
484                 ++state->link_num;
485
486                 if (state->link_num >= dvd::menu_buttons_max)
487                 {
488                     if (state->link_num == dvd::menu_buttons_max)
489                         std::cerr << "No more than " << dvd::menu_buttons_max
490                                   << " buttons can be placed on a page\n";
491                     std::cerr << "Ignoring link to " << uri_string << "\n";
492                     continue;
493                 }
494
495                 // Check whether this is a link to a video or a page then
496                 // add it to the known resources if not already seen.
497                 nsCString path;
498                 check(uri->GetPath(path));
499                 // FIXME: This is a bit of a hack.  Perhaps we could decide
500                 // later based on the MIME type determined by Mozilla?
501                 if (path.Length() > 4
502                     && std::strcmp(path.EndReading() - 4, ".vob") == 0)
503                 {
504                     PRBool is_file;
505                     check(uri->SchemeIs("file", &is_file));
506                     if (!is_file)
507                     {
508                         std::cerr << "Links to video must use the file:"
509                                   << " scheme\n";
510                         continue;
511                     }
512                     add_video(uri_sans_fragment);
513                 }
514                 else
515                 {
516                     add_page(uri_sans_fragment);
517                 }
518
519                 nsCOMPtr<nsIContent> content(do_QueryInterface(node));
520                 assert(content);
521                 nsCOMPtr<nsIDOMEventTarget> event_target(
522                     do_QueryInterface(node));
523                 assert(event_target);
524
525                 state->norm_pixbuf = Gdk::Pixbuf::create(
526                     Glib::RefPtr<Gdk::Drawable>(window),
527                     window->get_colormap(),
528                     state->link_rect.left,
529                     state->link_rect.top,
530                     0,
531                     0,
532                     state->link_rect.right - state->link_rect.left,
533                     state->link_rect.bottom - state->link_rect.top);
534
535                 nsCOMPtr<nsIDOMEvent> event;
536                 check(event_factory->CreateEvent(
537                           NS_ConvertASCIItoUTF16("MouseEvents"),
538                           getter_AddRefs(event)));
539                 nsCOMPtr<nsIDOMMouseEvent> mouse_event(
540                     do_QueryInterface(event));
541                 assert(mouse_event);
542                 check(mouse_event->InitMouseEvent(
543                           NS_ConvertASCIItoUTF16("mouseover"),
544                           true,  // can bubble
545                           true,  // cancelable
546                           view,
547                           0,     // detail: mouse click count
548                           state->link_rect.left, // screenX
549                           state->link_rect.top,  // screenY
550                           state->link_rect.left, // clientX
551                           state->link_rect.top,  // clientY
552                           false, false, false, false, // qualifiers
553                           0,     // button: left (or primary)
554                           0));   // related target
555                 PRBool dummy;
556                 check(event_target->DispatchEvent(mouse_event,
557                                                   &dummy));
558                 check(event_state_man->SetContentState(content,
559                                                        NS_EVENT_STATE_HOVER));
560
561                 pres_shell->FlushPendingNotifications(true);
562
563                 // We may have to exit and wait for image loading
564                 // to complete, at which point we will be called
565                 // again.
566                 if (pending_req_count_ > 0)
567                 {
568                     state->link_changing = true;
569                     page_state_ = state;
570                     return;
571                 }
572             }
573
574             window->process_updates(true);
575
576             Glib::RefPtr<Gdk::Pixbuf> changed_pixbuf(
577                 Gdk::Pixbuf::create(
578                     Glib::RefPtr<Gdk::Drawable>(window),
579                     window->get_colormap(),
580                     state->link_rect.left,
581                     state->link_rect.top,
582                     0,
583                     0,
584                     state->link_rect.right - state->link_rect.left,
585                     state->link_rect.bottom - state->link_rect.top));
586             diff_rgb_pixbufs(
587                 state->norm_pixbuf,
588                 changed_pixbuf,
589                 state->diff_pixbuf,
590                 state->link_rect.left,
591                 state->link_rect.top,
592                 state->link_rect.right - state->link_rect.left,
593                 state->link_rect.bottom - state->link_rect.top);
594
595             state->spumux_file <<
596                 "      <button x0='" << state->link_rect.left << "'"
597                 " y0='" << state->link_rect.top << "'"
598                 " x1='" << state->link_rect.right - 1 << "'"
599                 " y1='" << state->link_rect.bottom - 1 << "'/>\n";
600
601             // Add to the page's links, ignoring any fragment (for now).
602             page_links_.back().push_back(uri_sans_fragment);
603         }
604
605         quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
606
607         std::cout << "saving " << state->links_temp.get_name()
608                   << std::endl;
609         state->diff_pixbuf->save(state->links_temp.get_name(), "png");
610
611         state->spumux_file <<
612             "    </spu>\n"
613             "  </stream>\n"
614             "</subpictures>\n";
615
616         state->spumux_file.close();
617
618         // TODO: if (!state->spumux_file) throw ...
619
620         {
621             boost::shared_ptr<temp_file> vob_temp(
622                 new temp_file("webdvd-vob-"));
623             std::ostringstream command_stream;
624             command_stream << "pngtopnm "
625                            << background_temp_->get_name()
626                            << " | ppmtoy4m -v0 -n1 -F"
627                            << frame_params_.rate_numer
628                            << ":" << frame_params_.rate_denom
629                            << " -A" << frame_params_.pixel_ratio_width
630                            << ":" << frame_params_.pixel_ratio_height
631                            << (" -Ip -S420_mpeg2"
632                                " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
633                                " | mplex -v0 -f8 -o/dev/stdout /dev/stdin"
634                                " | spumux -v0 -mdvd ")
635                            << state->spumux_temp.get_name()
636                            << " > " << vob_temp->get_name();
637             std::string command(command_stream.str());
638             const char * argv[] = {
639                 "/bin/sh", "-c", command.c_str(), 0
640             };
641             std::cout << "running " << argv[2] << std::endl;
642             int command_result;
643             Glib::spawn_sync(".",
644                              Glib::ArrayHandle<std::string>(
645                                  argv, sizeof(argv)/sizeof(argv[0]),
646                                  Glib::OWNERSHIP_NONE),
647                              Glib::SPAWN_STDOUT_TO_DEV_NULL,
648                              SigC::Slot0<void>(),
649                              0, 0,
650                              &command_result);
651             if (command_result != 0)
652                 throw std::runtime_error("spumux pipeline failed");
653
654             page_temp_files_.push_back(vob_temp);
655         }
656     }
657
658     void generate_page_dispatch(std::ostream &, int indent,
659                                 int first_page, int last_page);
660
661     void WebDvdWindow::generate_dvd()
662     {
663         temp_file temp("webdvd-dvdauthor-");
664         temp.close();
665         std::ofstream file(temp.get_name().c_str());
666
667         // We generate code that uses registers in the following way:
668         //
669         // g0:     link destination (when jumping to menu 1), then scratch
670         // g1:     current location
671         // g2-g11: location history (g2 = most recent)
672         // g12:    location that last linked to a video
673         //
674         // All locations are divided into two bitfields: the least
675         // significant 10 bits are a page/menu number and the most
676         // significant 6 bits are a link/button number.  This is
677         // chosen for compatibility with the encoding of the s8
678         // (button) register.
679         //
680         static const int link_mult = dvd::reg_s8_button_mult;
681         static const int page_mask = link_mult - 1;
682         static const int link_mask = (1 << dvd::reg_bits) - link_mult;
683
684         file <<
685             "<dvdauthor>\n"
686             "  <vmgm>\n"
687             "    <menus>\n";
688             
689         for (std::size_t page_num = 1;
690              page_num <= page_links_.size();
691              ++page_num)
692         {
693             std::vector<std::string> & page_links =
694                 page_links_[page_num - 1];
695
696             if (page_num == 1)
697             {
698                 // This is the first page (root menu) which needs to
699                 // include initialisation and dispatch code.
700         
701                 file <<
702                     "      <pgc entry='title'>\n"
703                     "        <pre>\n"
704                     // Has the location been set yet?
705                     "          if (g1 eq 0)\n"
706                     "          {\n"
707                     // Initialise the current location to first link on
708                     // this page.
709                     "            g1 = " << 1 * link_mult + 1 << ";\n"
710                     "          }\n"
711                     "          else\n"
712                     "          {\n"
713                     // Has the user selected a link?
714                     "            if (g0 ne 0)\n"
715                     "            {\n"
716                     // First update the history.
717                     // Does link go to the last page in the history?
718                     "              if (((g0 ^ g2) &amp; " << page_mask
719                      << ") == 0)\n"
720                     // It does; we treat this as going back and pop the old
721                     // location off the history stack into the current
722                     // location.  Clear the free stack slot.
723                     "              {\n"
724                     "                g1 = g2; g2 = g3; g3 = g4; g4 = g5;\n"
725                     "                g5 = g6; g6 = g7; g7 = g8; g8 = g9;\n"
726                     "                g9 = g10; g10 = g11; g11 = 0;\n"
727                     "              }\n"
728                     "              else\n"
729                     // Link goes to some other page, so push current
730                     // location onto the history stack and set the current
731                     // location to be exactly the target location.
732                     "              {\n"
733                     "                g11 = g10; g10 = g9; g9 = g8; g8 = g7;\n"
734                     "                g7 = g6; g6 = g5; g5 = g4; g4 = g3;\n"
735                     "                g3 = g2; g2 = g1; g1 = g0;\n"
736                     "              }\n"
737                     "            }\n"
738                     // Find the target page number.
739                     "            g0 = g1 &amp; " << page_mask << ";\n";
740                 // There seems to be no way to perform a computed jump,
741                 // so we generate all possible jumps and a binary search
742                 // to select the correct one.
743                 generate_page_dispatch(file, 12, 1, page_links_.size());
744                 file <<
745                     "          }\n";
746             }
747             else // page_num != 1
748             {
749                 file <<
750                     "      <pgc>\n"
751                     "        <pre>\n";
752             }
753
754             file <<
755                 // Clear link indicator and highlight the
756                 // appropriate link/button.
757                 "          g0 = 0; s8 = g1 &amp; " << link_mask << ";\n"
758                 "        </pre>\n"
759                 "        <vob file='"
760                  << page_temp_files_[page_num - 1]->get_name() << "'/>\n";
761
762             for (std::size_t link_num = 1;
763                  link_num <= page_links.size();
764                  ++link_num)
765             {
766                 file <<
767                     "        <button>"
768                     // Update current location.
769                     " g1 = " << link_num * link_mult + page_num << ";";
770
771                 // Jump to appropriate resource.
772                 const ResourceEntry & resource_loc =
773                     resource_map_[page_links[link_num - 1]];
774                 if (resource_loc.first == page_resource)
775                     file <<
776                         " g0 = " << 1 * link_mult + resource_loc.second << ";"
777                         " jump menu 1;";
778                 else if (resource_loc.first == video_resource)
779                     file << " jump title " << resource_loc.second << ";";
780
781                 file <<  " </button>\n";
782             }
783
784             file << "      </pgc>\n";
785         }
786
787         file <<
788             "    </menus>\n"
789             "  </vmgm>\n";
790
791         // Generate a titleset for each video.  This appears to make
792         // jumping to titles a whole lot simpler.
793         for (std::size_t video_num = 1;
794              video_num <= video_paths_.size();
795              ++video_num)
796         {
797             file <<
798                 "  <titleset>\n"
799                 // Generate a dummy menu so that the menu button on the
800                 // remote control will work.
801                 "    <menus>\n"
802                 "      <pgc entry='root'>\n"
803                 "        <pre> jump vmgm menu; </pre>\n"
804                 "      </pgc>\n"
805                 "    </menus>\n"
806                 "    <titles>\n"
807                 "      <pgc>\n"
808                 // Record calling page/menu.
809                 "        <pre> g12 = g1; </pre>\n"
810                 // FIXME: Should XML-escape the path
811                 "        <vob file='" << video_paths_[video_num - 1]
812                  << "'/>\n"
813                 // If page/menu location has not been changed during the
814                 // video, change the location to be the following
815                 // link/button when returning to it.  In any case,
816                 // return to a page/menu.
817                 "        <post> if (g1 eq g12) g1 = g1 + " << link_mult
818                  << "; call menu; </post>\n"
819                 "      </pgc>\n"
820                 "    </titles>\n"
821                 "  </titleset>\n";
822         }
823
824         file <<
825             "</dvdauthor>\n";
826
827         file.close();
828
829         {
830             const char * argv[] = {
831                 "dvdauthor",
832                 "-o", output_dir_.c_str(),
833                 "-x", temp.get_name().c_str(),
834                 0
835             };
836             int command_result;
837             Glib::spawn_sync(".",
838                              Glib::ArrayHandle<std::string>(
839                                  argv, sizeof(argv)/sizeof(argv[0]),
840                                  Glib::OWNERSHIP_NONE),
841                              Glib::SPAWN_SEARCH_PATH
842                              | Glib::SPAWN_STDOUT_TO_DEV_NULL,
843                              SigC::Slot0<void>(),
844                              0, 0,
845                              &command_result);
846             if (command_result != 0)
847                 throw std::runtime_error("dvdauthor failed");
848         }
849     }
850
851     void generate_page_dispatch(std::ostream & file, int indent,
852                                 int first_page, int last_page)
853     {
854         if (first_page == 1 && last_page == 1)
855         {
856             // The dispatch code is *on* page 1 so we must not dispatch to
857             // page 1 since that would cause an infinite loop.  This case
858             // should be unreachable if there is more than one page due
859             // to the following case.
860         }
861         else if (first_page == 1 && last_page == 2)
862         {
863             // dvdauthor doesn't allow empty blocks or null statements so
864             // when selecting between pages 1 and 2 we don't use an "else"
865             // part.  We must use braces so that a following "else" will
866             // match the right "if".
867             file << std::setw(indent) << "" << "{\n"
868                  << std::setw(indent) << "" << "if (g0 eq 2)\n"
869                  << std::setw(indent + 2) << "" << "jump menu 2;\n"
870                  << std::setw(indent) << "" << "}\n";
871         }
872         else if (first_page == last_page)
873         {
874             file << std::setw(indent) << ""
875                  << "jump menu " << first_page << ";\n";
876         }
877         else
878         {
879             int middle = (first_page + last_page) / 2;
880             file << std::setw(indent) << "" << "if (g0 le " << middle << ")\n";
881             generate_page_dispatch(file, indent + 2, first_page, middle);
882             file << std::setw(indent) << "" << "else\n";
883             generate_page_dispatch(file, indent + 2, middle + 1, last_page);
884         }
885     }
886
887     const video::frame_params & lookup_frame_params(const char * str)
888     {
889         assert(str);
890         static const struct { const char * str; bool is_ntsc; }
891         known_strings[] = {
892             { "NTSC",  true },
893             { "ntsc",  true },
894             { "PAL",   false },
895             { "pal",   false },
896             // For DVD purposes, SECAM can be treated identically to PAL.
897             { "SECAM", false },
898             { "secam", false }
899         };
900         for (std::size_t i = 0;
901              i != sizeof(known_strings)/sizeof(known_strings[0]);
902              ++i)
903             if (std::strcmp(str, known_strings[i].str) == 0)
904                 return known_strings[i].is_ntsc ?
905                     video::ntsc_params : video::pal_params;
906         throw std::runtime_error(
907             std::string("Invalid video standard: ").append(str));
908     }
909
910     void print_usage(std::ostream & stream, const char * command_name)
911     {
912         stream << "Usage: " << command_name
913                << (" [gtk-options] [--video-std std-name]"
914                    " [--preview] menu-url [output-dir]\n");
915     }
916     
917 } // namespace
918
919 int main(int argc, char ** argv)
920 {
921     try
922     {
923         video::frame_params frame_params = video::pal_params;
924         bool preview_mode = false;
925         std::string menu_url;
926         std::string output_dir;
927
928         // Do initial option parsing.  We have to do this before
929         // letting Gtk parse the arguments since we may need to spawn
930         // Xvfb first.
931         int argi = 1;
932         while (argi != argc)
933         {
934             if (std::strcmp(argv[argi], "--") == 0)
935             {
936                 break;
937             }
938             else if (std::strcmp(argv[argi], "--help") == 0)
939             {
940                 print_usage(std::cout, argv[0]);
941                 return EXIT_SUCCESS;
942             }
943             else if (std::strcmp(argv[argi], "--preview") == 0)
944             {
945                 preview_mode = true;
946                 argi += 1;
947             }
948             else if (std::strcmp(argv[argi], "--video-std") == 0)
949             {
950                 if (argi + 1 == argc)
951                 {
952                     std::cerr << "Missing argument to --video-std\n";
953                     print_usage(std::cerr, argv[0]);
954                     return EXIT_FAILURE;
955                 }
956                 frame_params = lookup_frame_params(argv[argi + 1]);
957                 argi += 2;
958             }
959             else
960             {
961                 argi += 1;
962             }
963         }
964
965         std::auto_ptr<FrameBuffer> fb;
966         if (!preview_mode)
967         {
968             // Spawn Xvfb and set env variables so that Xlib will use it
969             // Use 8 bits each for RGB components, which should translate into
970             // "enough" bits for YUV components.
971             fb.reset(new FrameBuffer(frame_params.width, frame_params.height,
972                                      3 * 8));
973             setenv("XAUTHORITY", fb->get_x_authority().c_str(), true);
974             setenv("DISPLAY", fb->get_x_display().c_str(), true);
975         }
976
977         // Initialise Gtk
978         Gtk::Main kit(argc, argv);
979
980         // Complete option parsing with Gtk's options out of the way.
981         argi = 1;
982         while (argi != argc)
983         {
984             if (std::strcmp(argv[argi], "--") == 0)
985             {
986                 argi += 1;
987                 break;
988             }
989             else if (std::strcmp(argv[argi], "--preview") == 0)
990             {
991                 argi += 1;
992             }
993             else if (std::strcmp(argv[argi], "--video-std") == 0)
994             {
995                 argi += 2;
996             }
997             else if (argv[argi][0] == '-')
998             {
999                 std::cerr << "Invalid option: " << argv[argi] << "\n";
1000                 print_usage(std::cerr, argv[0]);
1001                 return EXIT_FAILURE;
1002             }
1003             else
1004             {
1005                 break;
1006             }
1007         }
1008
1009         // Look for a starting URL or filename and (except in preview
1010         // mode) an output directory after the options.
1011         if (argc - argi != (preview_mode ? 1 : 2))
1012         {
1013             print_usage(std::cerr, argv[0]);
1014             return EXIT_FAILURE;
1015         }
1016         if (std::strstr(argv[argi], "://"))
1017         {
1018             // It appears to be an absolute URL, so use it as-is.
1019             menu_url = argv[argi];
1020         }
1021         else
1022         {
1023             // Assume it's a filename.  Resolve it to an absolute URL.
1024             std::string path(argv[argi]);
1025             if (!Glib::path_is_absolute(path))
1026                 path = Glib::build_filename(Glib::get_current_dir(), path);
1027             menu_url = Glib::filename_to_uri(path);             
1028         }
1029         if (!preview_mode)
1030             output_dir = argv[argi + 1];
1031
1032         // Initialise Mozilla
1033         BrowserWidget::init();
1034
1035         // Run the browser/converter
1036         WebDvdWindow window(frame_params, menu_url, output_dir);
1037         Gtk::Main::run(window);
1038     }
1039     catch (std::exception & e)
1040     {
1041         std::cerr << "Fatal error: " << e.what() << "\n";
1042         return EXIT_FAILURE;
1043     }
1044
1045     return EXIT_SUCCESS;
1046 }