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