]> git.decadent.org.uk Git - videolink.git/blobdiff - webdvd.cpp
Added comments.
[videolink.git] / webdvd.cpp
index 8e586e2d230fbc4d20aa6a1cca9fc99b5dbb87c3..6c6419e70afd8cf27ff686f8dcfeb5abfc9dbf22 100644 (file)
@@ -9,15 +9,21 @@
 #include <memory>
 #include <queue>
 #include <set>
+#include <sstream>
 #include <string>
 
 #include <stdlib.h>
 #include <unistd.h>
 
+#include <boost/shared_ptr.hpp>
+
 #include <gdkmm/pixbuf.h>
+#include <glibmm/convert.h>
+#include <glibmm/spawn.h>
 #include <gtkmm/main.h>
 #include <gtkmm/window.h>
 
+#include <imglib2/ImageErrors.h>
 #include <nsGUIEvent.h>
 #include <nsIBoxObject.h>
 #include <nsIContent.h>
@@ -47,6 +53,7 @@
 #include "linkiterator.hpp"
 #include "pixbufs.hpp"
 #include "stylesheets.hpp"
+#include "temp_file.hpp"
 #include "video.hpp"
 #include "xpcom_support.hpp"
 
@@ -103,6 +110,7 @@ namespace
     {
        rectangle result;
 
+       // Start with this element's bounding box
        nsCOMPtr<nsIBoxObject> box;
        check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
        int width, height;
@@ -113,6 +121,7 @@ namespace
        result.right = result.left + width;
        result.bottom = result.top + height;
 
+       // Merge bounding boxes of all child elements
        for (ChildIterator it = ChildIterator(elem), end; it != end; ++it)
        {
            nsCOMPtr<nsIDOMNode> child_node(*it);
@@ -132,45 +141,59 @@ namespace
     class WebDvdWindow : public Gtk::Window
     {
     public:
-       WebDvdWindow(int width, int height);
-       void add_page(const std::string & uri);
+       WebDvdWindow(
+           const video::frame_params & frame_params,
+           const std::string & main_page_uri,
+           const std::string & output_dir);
 
     private:
+       void add_page(const std::string & uri);
        void add_video(const std::string & uri);
        void load_next_page();
        void on_net_state_change(const char * uri, gint flags, guint status);
+       bool process_page();
        void save_screenshot();
        void process_links(nsIPresShell * pres_shell,
                           nsIPresContext * pres_context,
                           nsIDOMWindow * dom_window);
-       void generate_dvdauthor_file();
+       void generate_dvd();
 
        enum ResourceType { page_resource, video_resource };
        typedef std::pair<ResourceType, int> ResourceEntry;
-       int width_, height_;
+       video::frame_params frame_params_;
+       std::string output_dir_;
        BrowserWidget browser_widget_;
        nsCOMPtr<nsIStyleSheet> stylesheet_;
        std::queue<std::string> page_queue_;
        std::map<std::string, ResourceEntry> resource_map_;
        std::vector<std::vector<std::string> > page_links_;
        std::vector<std::string> video_paths_;
-       bool loading_;
+       bool pending_window_update_;
        int pending_req_count_;
-       struct link_state;
-       std::auto_ptr<link_state> link_state_;
+       std::auto_ptr<temp_file> background_temp_;
+       struct page_state;
+       std::auto_ptr<page_state> page_state_;
+       std::vector<boost::shared_ptr<temp_file> > page_temp_files_;
     };
 
-    WebDvdWindow::WebDvdWindow(int width, int height)
-           : width_(width), height_(height),
+    WebDvdWindow::WebDvdWindow(
+       const video::frame_params & frame_params,
+       const std::string & main_page_uri,
+       const std::string & output_dir)
+           : frame_params_(frame_params),
+             output_dir_(output_dir),
              stylesheet_(load_css("file://" WEBDVD_LIB_DIR "/webdvd.css")),
-             loading_(false),
+             pending_window_update_(false),
              pending_req_count_(0)
     {
-       set_default_size(width, height);
+       set_default_size(frame_params_.width, frame_params_.height);
        add(browser_widget_);
        browser_widget_.show();
        browser_widget_.signal_net_state().connect(
            SigC::slot(*this, &WebDvdWindow::on_net_state_change));
+
+       add_page(main_page_uri);
+       load_next_page();
     }
 
     void WebDvdWindow::add_page(const std::string & uri)
@@ -180,8 +203,6 @@ namespace
            .second)
        {
            page_queue_.push(uri);
-           if (!loading_)
-               load_next_page();
        }
     }
 
@@ -202,8 +223,6 @@ namespace
 
     void WebDvdWindow::load_next_page()
     {
-       loading_ = true;
-
        assert(!page_queue_.empty());
        const std::string & uri = page_queue_.front();
        std::cout << "loading " << uri << std::endl;
@@ -211,74 +230,43 @@ namespace
        std::size_t page_count = page_links_.size();
        resource_map_[uri].second = ++page_count;
        page_links_.resize(page_count);
+
+       pending_window_update_ = true;
        browser_widget_.load_uri(uri);
     }
 
     void WebDvdWindow::on_net_state_change(const char * uri,
                                           gint flags, guint status)
     {
-       enum {
-           process_nothing,
-           process_new_page,
-           process_current_link
-       } action = process_nothing;
-
        if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
        {
            if (flags & GTK_MOZ_EMBED_FLAG_START)
                ++pending_req_count_;
+
            if (flags & GTK_MOZ_EMBED_FLAG_STOP)
            {
                assert(pending_req_count_ != 0);
                --pending_req_count_;
            }
-           if (pending_req_count_ == 0 && link_state_.get())
-               action = process_current_link;
        }
            
        if (flags & GTK_MOZ_EMBED_FLAG_STOP
            && flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW)
-           action = process_new_page;
-
-       if (action != process_nothing)
        {
-           assert(loading_ && !page_queue_.empty());
-           assert(pending_req_count_ == 0);
+           // Check whether the load was successful, ignoring this
+           // pseudo-error.
+           if (status != NS_IMAGELIB_ERROR_LOAD_ABORTED)
+               check(status);
 
+           pending_window_update_ = false;
+       }
+
+       if (pending_req_count_ == 0 && !pending_window_update_)
+       {
            try
            {
-               check(status);
-
-               nsCOMPtr<nsIWebBrowser> browser(
-                   browser_widget_.get_browser());
-               nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
-               assert(doc_shell);
-               nsCOMPtr<nsIPresShell> pres_shell;
-               check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
-               nsCOMPtr<nsIPresContext> pres_context;
-               check(doc_shell->GetPresContext(
-                         getter_AddRefs(pres_context)));
-               nsCOMPtr<nsIDOMWindow> dom_window;
-               check(browser->GetContentDOMWindow(
-                         getter_AddRefs(dom_window)));
-
-               if (action == process_new_page)
-               {
-                   apply_style_sheet(stylesheet_, pres_shell);
-                   save_screenshot();
-               }
-               process_links(pres_shell, pres_context, dom_window);
-               if (!link_state_.get())
-               {
-                   page_queue_.pop();
-                   if (page_queue_.empty())
-                   {
-                       generate_dvdauthor_file();
-                       Gtk::Main::quit();
-                   }
-                   else
-                       load_next_page();
-               }
+               if (!process_page())
+                   Gtk::Main::quit();
            }
            catch (std::exception & e)
            {
@@ -292,26 +280,100 @@ namespace
        }
     }
 
+    bool WebDvdWindow::process_page()
+    {
+       assert(!page_queue_.empty());
+
+       nsCOMPtr<nsIWebBrowser> browser(browser_widget_.get_browser());
+       nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
+       assert(doc_shell);
+       nsCOMPtr<nsIPresShell> pres_shell;
+       check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
+       nsCOMPtr<nsIPresContext> pres_context;
+       check(doc_shell->GetPresContext(getter_AddRefs(pres_context)));
+       nsCOMPtr<nsIDOMWindow> dom_window;
+       check(browser->GetContentDOMWindow(getter_AddRefs(dom_window)));
+
+       if (output_dir_.empty())
+       {
+           // In preview mode, just apply the stylesheet and let the
+           // user select links.
+           apply_style_sheet(stylesheet_, pres_shell);
+       }
+       else
+       {
+           // If we haven't already started work on this page, apply
+           // the stylesheet and save a screenshot of its normal
+           // appearance.
+           if (!page_state_.get())
+           {
+               apply_style_sheet(stylesheet_, pres_shell);
+               save_screenshot();
+           }
+
+           // Start or continue processing links.
+           process_links(pres_shell, pres_context, dom_window);
+
+           // If we've finished work on the links, move on to the
+           // next page, if any, or else generate the DVD filesystem.
+           if (!page_state_.get())
+           {
+               page_queue_.pop();
+               if (page_queue_.empty())
+               {
+                   generate_dvd();
+                   return false;
+               }
+               else
+               {
+                   load_next_page();
+               }
+           }
+       }
+
+       return true;
+    }
+
     void WebDvdWindow::save_screenshot()
     {
-       char filename[20];
-       std::sprintf(filename, "page_%06d_back.png", page_links_.size());
        Glib::RefPtr<Gdk::Window> window(get_window());
        assert(window);
        window->process_updates(true);
-       std::cout << "saving " << filename << std::endl;
+
+       background_temp_.reset(new temp_file("webdvd-back-"));
+       background_temp_->close();
+       std::cout << "saving " << background_temp_->get_name() << std::endl;
        Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
                            window->get_colormap(),
-                           0, 0, 0, 0, width_, height_)
-           ->save(filename, "png");
+                           0, 0, 0, 0,
+                           frame_params_.width, frame_params_.height)
+           ->save(background_temp_->get_name(), "png");
     }
 
-    struct WebDvdWindow::link_state
+    struct WebDvdWindow::page_state
     {
+       page_state(nsIDOMDocument * doc, int width, int height)
+               : diff_pixbuf(Gdk::Pixbuf::create(
+                                 Gdk::COLORSPACE_RGB,
+                                 true, 8, // has_alpha, bits_per_sample
+                                 width, height)),
+                 spumux_temp("webdvd-spumux-"),
+                 links_temp("webdvd-links-"),
+                 link_num(0),
+                 links_it(doc),
+                 link_changing(false)
+           {
+               spumux_temp.close();
+               links_temp.close();
+           }
+
        Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
 
+       temp_file spumux_temp;
        std::ofstream spumux_file;
 
+       temp_file links_temp;
+
        int link_num;
        LinkIterator links_it, links_end;
 
@@ -343,37 +405,25 @@ namespace
        check(doc_view->GetDefaultView(getter_AddRefs(view)));
 
        // Set up or recover our iteration state.
-       std::auto_ptr<link_state> state(link_state_);
+       std::auto_ptr<page_state> state(page_state_);
        if (!state.get())
        {
-           state.reset(new link_state);
-
-           state->diff_pixbuf = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB,
-                                                    true, // has_alpha
-                                                    8, // bits_per_sample
-                                                    width_, height_);
-
-           char spumux_filename[20];
-           std::sprintf(spumux_filename,
-                        "page_%06d.spumux", page_links_.size());
-           state->spumux_file.open(spumux_filename);
+           state.reset(
+               new page_state(
+                   basic_doc, frame_params_.width, frame_params_.height));
+           
+           state->spumux_file.open(state->spumux_temp.get_name().c_str());
            state->spumux_file <<
                "<subpictures>\n"
                "  <stream>\n"
                "    <spu force='yes' start='00:00:00.00'\n"
-               "        highlight='page_" << std::setfill('0')
-                              << std::setw(6) << page_links_.size()
-                              << "_links.png'\n"
-               "        select='page_" << std::setfill('0')
-                              << std::setw(6) << page_links_.size()
-                              << "_links.png'>\n";
-
-           state->link_num = 0;
-           state->links_it = LinkIterator(basic_doc);
-           state->link_changing = false;
+               "        highlight='" << state->links_temp.get_name() << "'\n"
+               "        select='" << state->links_temp.get_name() << "'>\n";
        }
-           
-       rectangle window_rect = { 0, 0, width_, height_ };
+
+       rectangle window_rect = {
+           0, 0, frame_params_.width, frame_params_.height
+       };
 
        for (/* no initialisation */;
             state->links_it != state->links_end;
@@ -497,7 +547,7 @@ namespace
                if (pending_req_count_ > 0)
                {
                    state->link_changing = true;
-                   link_state_ = state;
+                   page_state_ = state;
                    return;
                }
            }
@@ -535,23 +585,65 @@ namespace
 
        quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
 
-       char filename[25];
-       std::sprintf(filename, "page_%06d_links.png", page_links_.size());
-       std::cout << "saving " << filename << std::endl;
-       state->diff_pixbuf->save(filename, "png");
+       std::cout << "saving " << state->links_temp.get_name()
+                 << std::endl;
+       state->diff_pixbuf->save(state->links_temp.get_name(), "png");
 
        state->spumux_file <<
            "    </spu>\n"
            "  </stream>\n"
            "</subpictures>\n";
+
+       state->spumux_file.close();
+
+       // TODO: if (!state->spumux_file) throw ...
+
+       {
+           boost::shared_ptr<temp_file> vob_temp(
+               new temp_file("webdvd-vob-"));
+           std::ostringstream command_stream;
+           command_stream << "pngtopnm "
+                          << background_temp_->get_name()
+                          << " | ppmtoy4m -v0 -n1 -F"
+                          << frame_params_.rate_numer
+                          << ":" << frame_params_.rate_denom
+                          << " -A" << frame_params_.pixel_ratio_width
+                          << ":" << frame_params_.pixel_ratio_height
+                          << (" -Ip -S420_mpeg2"
+                              " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
+                              " | mplex -v0 -f8 -o/dev/stdout /dev/stdin"
+                              " | spumux -v0 -mdvd ")
+                          << state->spumux_temp.get_name()
+                          << " > " << vob_temp->get_name();
+           std::string command(command_stream.str());
+           const char * argv[] = {
+               "/bin/sh", "-c", command.c_str(), 0
+           };
+           std::cout << "running " << argv[2] << std::endl;
+           int command_result;
+           Glib::spawn_sync(".",
+                            Glib::ArrayHandle<std::string>(
+                                argv, sizeof(argv)/sizeof(argv[0]),
+                                Glib::OWNERSHIP_NONE),
+                            Glib::SPAWN_STDOUT_TO_DEV_NULL,
+                            SigC::Slot0<void>(),
+                            0, 0,
+                            &command_result);
+           if (command_result != 0)
+               throw std::runtime_error("spumux pipeline failed");
+
+           page_temp_files_.push_back(vob_temp);
+       }
     }
 
     void generate_page_dispatch(std::ostream &, int indent,
                                int first_page, int last_page);
 
-    void WebDvdWindow::generate_dvdauthor_file()
+    void WebDvdWindow::generate_dvd()
     {
-       std::ofstream file("webdvd.dvdauthor");
+       temp_file temp("webdvd-dvdauthor-");
+       temp.close();
+       std::ofstream file(temp.get_name().c_str());
 
        // We generate code that uses registers in the following way:
        //
@@ -645,9 +737,8 @@ namespace
                // appropriate link/button.
                "          g0 = 0; s8 = g1 &amp; " << link_mask << ";\n"
                "        </pre>\n"
-               "        <vob file='page_"
-                << std::setfill('0') << std::setw(6) << page_num
-                << ".vob'/>\n";
+               "        <vob file='"
+                << page_temp_files_[page_num - 1]->get_name() << "'/>\n";
 
            for (std::size_t link_num = 1;
                 link_num <= page_links.size();
@@ -713,6 +804,29 @@ namespace
 
        file <<
            "</dvdauthor>\n";
+
+       file.close();
+
+       {
+           const char * argv[] = {
+               "dvdauthor",
+               "-o", output_dir_.c_str(),
+               "-x", temp.get_name().c_str(),
+               0
+           };
+           int command_result;
+           Glib::spawn_sync(".",
+                            Glib::ArrayHandle<std::string>(
+                                argv, sizeof(argv)/sizeof(argv[0]),
+                                Glib::OWNERSHIP_NONE),
+                            Glib::SPAWN_SEARCH_PATH
+                            | Glib::SPAWN_STDOUT_TO_DEV_NULL,
+                            SigC::Slot0<void>(),
+                            0, 0,
+                            &command_result);
+           if (command_result != 0)
+               throw std::runtime_error("dvdauthor failed");
+       }
     }
 
     void generate_page_dispatch(std::ostream & file, int indent,
@@ -751,38 +865,137 @@ namespace
        }
     }
 
+    const video::frame_params & lookup_frame_params(const char * str)
+    {
+       assert(str);
+       static const struct { const char * str; bool is_ntsc; }
+       known_strings[] = {
+           { "NTSC",  true },
+           { "ntsc",  true },
+           { "PAL",   false },
+           { "pal",   false },
+           // For DVD purposes, SECAM can be treated identically to PAL.
+           { "SECAM", false },
+           { "secam", false }
+       };
+       for (std::size_t i = 0;
+            i != sizeof(known_strings)/sizeof(known_strings[0]);
+            ++i)
+           if (std::strcmp(str, known_strings[i].str) == 0)
+               return known_strings[i].is_ntsc ?
+                   video::ntsc_params : video::pal_params;
+       throw std::runtime_error(
+           std::string("Invalid video standard: ").append(str));
+    }
+
+    void print_usage(std::ostream & stream, const char * command_name)
+    {
+       stream << "Usage: " << command_name
+              << (" [gtk-options] [--video-std std-name]"
+                  " [--preview] menu-url [output-dir]\n");
+    }
+    
 } // namespace
 
 int main(int argc, char ** argv)
 {
-    // Get dimensions
-    int width = video::pal_oscan_width, height = video::pal_oscan_height;
-    for (int i = 1; i < argc - 1; ++i)
-       if (std::strcmp(argv[i], "-geometry") == 0)
+    try
+    {
+       video::frame_params frame_params = video::pal_params;
+       bool preview_mode = false;
+
+       // Do initial argument parsing.  We have to do this before
+       // letting Gtk parse the arguments since we may need to spawn
+       // Xvfb first.
+       int argi = 1;
+       while (argi != argc)
        {
-           std::sscanf(argv[i + 1], "%dx%d", &width, &height);
-           break;
+           if (std::strcmp(argv[argi], "--") == 0)
+           {
+               break;
+           }
+           else if (std::strcmp(argv[argi], "--help") == 0)
+           {
+               print_usage(std::cout, argv[0]);
+               return EXIT_SUCCESS;
+           }
+           else if (std::strcmp(argv[argi], "--preview") == 0)
+           {
+               preview_mode = true;
+               argi += 1;
+           }
+           else if (std::strcmp(argv[argi], "--video-std") == 0)
+           {
+               if (argi + 1 == argc)
+               {
+                   std::cerr << "Missing argument to --video-std\n";
+                   print_usage(std::cerr, argv[0]);
+                   return EXIT_FAILURE;
+               }
+               frame_params = lookup_frame_params(argv[argi + 1]);
+               argi += 2;
+           }
+           else
+           {
+               argi += 1;
+           }
        }
-    // A depth of 24 results in 8 bits each for RGB components, which
-    // translates into "enough" bits for YUV components.
-    const int depth = 24;
 
-    try
-    {
-       // Spawn Xvfb and set env variables so that Xlib will use it
-       FrameBuffer fb(width, height, depth);
-       setenv("XAUTHORITY", fb.get_x_authority().c_str(), true);
-       setenv("DISPLAY", fb.get_x_display().c_str(), true);
+       std::auto_ptr<FrameBuffer> fb;
+       if (!preview_mode)
+       {
+           // Spawn Xvfb and set env variables so that Xlib will use it
+           // Use 8 bits each for RGB components, which should translate into
+           // "enough" bits for YUV components.
+           fb.reset(new FrameBuffer(frame_params.width, frame_params.height,
+                                    3 * 8));
+           setenv("XAUTHORITY", fb->get_x_authority().c_str(), true);
+           setenv("DISPLAY", fb->get_x_display().c_str(), true);
+       }
 
-       // Initialise Gtk and Mozilla
+       // Initialise Gtk
        Gtk::Main kit(argc, argv);
+
+       // Complete argument parsing with Gtk's options out of the way.
+       argi = 1;
+       while (argi != argc)
+       {
+           if (std::strcmp(argv[argi], "--") == 0)
+           {
+               argi += 1;
+               break;
+           }
+           else if (std::strcmp(argv[argi], "--preview") == 0)
+           {
+               argi += 1;
+           }
+           else if (std::strcmp(argv[argi], "--video-std") == 0)
+           {
+               argi += 2;
+           }
+           else if (argv[argi][0] == '-')
+           {
+               std::cerr << "Invalid option: " << argv[argi] << "\n";
+               print_usage(std::cerr, argv[0]);
+               return EXIT_FAILURE;
+           }
+           else
+           {
+               break;
+           }
+       }
+       if (argc - argi != (preview_mode ? 1 : 2))
+       {
+           print_usage(std::cerr, argv[0]);
+           return EXIT_FAILURE;
+       }
+
+       // Initialise Mozilla
        BrowserWidget::init();
 
-       WebDvdWindow window(width, height);
-       for (int argi = 1; argi < argc; ++argi)
-           window.add_page(argv[argi]);
-       if (argc < 2)
-           window.add_page("about:");
+       WebDvdWindow window(frame_params,
+                           argv[argi],
+                           preview_mode ? "" : argv[argi + 1]);
        Gtk::Main::run(window);
     }
     catch (std::exception & e)