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