// See the file "COPYING" for licence details.
#include <cassert>
+#include <cstring>
#include <exception>
#include <fstream>
#include <iomanip>
#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 "linkiterator.hpp"
#include "pixbufs.hpp"
#include "stylesheets.hpp"
+#include "temp_file.hpp"
#include "video.hpp"
#include "xpcom_support.hpp"
{
rectangle result;
+ // Start with this element's bounding box
nsCOMPtr<nsIBoxObject> box;
check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
int width, height;
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);
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_size_request(frame_params_.width, frame_params_.height);
+ set_resizable(false);
+
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)
.second)
{
page_queue_.push(uri);
- if (!loading_)
- load_next_page();
}
}
video_paths_.size() + 1)))
.second)
{
- // FIXME: Should accept some slightly different URI prefixes
- // (e.g. file://localhost/) and decode any URI-escaped
- // characters in the path.
- assert(uri.compare(0, 8, "file:///") == 0);
- video_paths_.push_back(uri.substr(7));
+ Glib::ustring hostname;
+ video_paths_.push_back(Glib::filename_from_uri(uri, hostname));
+ // FIXME: Should check the hostname
}
}
void WebDvdWindow::load_next_page()
{
- loading_ = true;
-
assert(!page_queue_.empty());
const std::string & uri = page_queue_.front();
std::cout << "loading " << uri << std::endl;
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 whether the load was successful, ignoring this
- // pseudo-error.
- if (status != NS_IMAGELIB_ERROR_LOAD_ABORTED)
- 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)
{
}
}
+ 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[25];
- 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;
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;
if (pending_req_count_ > 0)
{
state->link_changing = true;
- link_state_ = state;
+ page_state_ = state;
return;
}
}
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:
//
// appropriate link/button.
" g0 = 0; s8 = g1 & " << 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();
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,
}
}
+ 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;
+ std::string menu_url;
+ std::string output_dir;
+
+ // Do initial option 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 option 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;
+ }
+ }
+
+ // Look for a starting URL or filename and (except in preview
+ // mode) an output directory after the options.
+ if (argc - argi != (preview_mode ? 1 : 2))
+ {
+ print_usage(std::cerr, argv[0]);
+ return EXIT_FAILURE;
+ }
+ if (std::strstr(argv[argi], "://"))
+ {
+ // It appears to be an absolute URL, so use it as-is.
+ menu_url = argv[argi];
+ }
+ else
+ {
+ // Assume it's a filename. Resolve it to an absolute URL.
+ std::string path(argv[argi]);
+ if (!Glib::path_is_absolute(path))
+ path = Glib::build_filename(Glib::get_current_dir(), path);
+ menu_url = Glib::filename_to_uri(path);
+ }
+ if (!preview_mode)
+ output_dir = argv[argi + 1];
+
+ // 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:");
+ // Run the browser/converter
+ WebDvdWindow window(frame_params, menu_url, output_dir);
Gtk::Main::run(window);
}
catch (std::exception & e)