Fixed operation of "top menu" or "title menu" button.
Added explicit aspect ratio to menu streams encoded with ffmpeg, since some players will default to 16:9 otherwise.
Fixed off-by-one error in link coordinates passed to spumux.
Changed default padding for the body element to 10% of frame dimensions, a common rule for the title-safe area.
Added explicit assignment of actions to direction buttons since dvdauthor's policy doesn't seem to work that well.
Documented limit of 119 menus discovered by Mark Burton.
Fixed an occasional problem of links appearing in their hover state initially due to the pointer being in the middle of the virtual framebuffer.
+videolink (0.11) unstable; urgency=low
+
+ * Fixed operation of "top menu" or "title menu" button
+ * Added explicit aspect ratio to menu streams encoded with ffmpeg,
+ since some players will default to 16:9 otherwise
+ * Fixed off-by-one error in link coordinates passed to spumux
+ * Changed default padding for the body element to 10% of frame
+ dimensions, a common rule for the title-safe area
+ * Added explicit assignment of actions to direction buttons
+ since dvdauthor's policy doesn't seem to work that well
+ * Documented limit of 119 menus discovered by Mark Burton
+ * Fixed an occasional problem of links appearing in their hover state
+ initially due to the pointer being in the middle of the virtual
+ framebuffer
+
+ -- Ben Hutchings <ben@decadent.org.uk> Mon, 25 Sep 2006 18:11:07 +0100
+
videolink (0.10) unstable; urgency=low
* Changed style-sheet application in Mozila/XULRunner 1.8 to override
cxxsources := \
auto_proc.cpp browser_widget.cpp child_iterator.cpp generate_dvd.cpp \
link_iterator.cpp null_prompt_service.cpp pixbufs.cpp style_sheets.cpp \
- temp_file.cpp video.cpp vob_list.cpp videolink.cpp x_frame_buffer.cpp \
- xml_utils.cpp xpcom_support.cpp
+ temp_file.cpp video.cpp vob_list.cpp videolink.cpp warp_pointer.cpp \
+ x_frame_buffer.cpp xml_utils.cpp xpcom_support.cpp
csources := jquant2.c
videolink : $(cxxsources:%.cpp=.objs/%.o) $(csources:%.c=.objs/%.o)
gzip -c9 videolink.1 >$(DESTDIR)$(mandir)/man1/videolink.1.gz
chmod 644 $(DESTDIR)$(mandir)/man1/videolink.1.gz
mkdir -p -m 755 $(DESTDIR)$(sharedir)/videolink
- install -m 644 videolink.css $(DESTDIR)$(sharedir)/videolink
+ install -m 644 *.css $(DESTDIR)$(sharedir)/videolink
.PHONY : clean install
.objs/videolink.% \
: CPPFLAGS += -DVIDEOLINK_SHARE_DIR='"$(sharedir)/videolink"'
-.objs/browser_widget.% .objs/generate_dvd.% .objs/pixbufs.% \
-.objs/temp_file.% .objs/vob_list.% .objs/videolink.% \
+.objs/browser_widget.% .objs/generate_dvd.% .objs/pixbufs.% \
+.objs/temp_file.% .objs/vob_list.% .objs/videolink.% .objs/warp_pointer.% \
: CPPFLAGS += $(shell pkg-config --cflags gtkmm-2.0)
.objs/browser_widget.% .objs/child_iterator.% .objs/link_iterator.% \
Linking to video
You can link directly to local MPEG video files whose names end in
-".vob". If you wish to combine multiple files into a single video
-sequence ("title" in DVD terminology) or to add chapter marks to a
-video sequence, create and link to a VOB-list file (explained below)
-whose name ends in ".voblist".
+".mpeg", ".mpeg2" or ".vob". If you wish to combine multiple files
+into a single video sequence ("title" in DVD terminology) or to add
+chapter marks to a video sequence, create and link to a VOB-list file
+(explained below) whose name ends in ".voblist".
VOB-lists
above. The exact visible area varies between TVs so the background
should cover all or very nearly all the frame whereas the important
content such as text must not be placed near the edge. For this
-reason VideoLink applies a stylesheet to all pages that adds 60 pixels of
-padding on all sides of the body; this doesn't apply to the
-background.
+reason VideoLink applies a stylesheet to all pages that adds padding
+equal to 10% of the frame dimension on each side of the body; this
+doesn't apply to the background.
Prior to version 1.8, Mozilla may signal that a page is completely
loaded before any background images are loaded and displayed. This
next. If this is a real problem, let me know, and I may be able to
provide a better solution in a later version of VideoLink.
+There is a limit of 128 menus in each "domain" of a DVD, which is
+further reduced by dvdauthor to a practical limit of 119 menus.
+Currently VideoLink generates all menus in a single domain (the VMGM
+domain). Later versions of VideoLink should support larger numbers of
+menus by using multiple domains.
+
Bugs
----
Use ffmpeg to convert unsuitable video files (how do we check this?).
Add keyboard command for "top menu" in preview mode.
Add support for videos in preview mode.
-Support more than 99 videos somehow (grouping them into titlesets won't help)
-Allow explicit ordering of videos.
+Support more than 99 titles somehow (grouping them into titlesets won't help)
+Support more than 119 menus somehow.
+Track down and fix/suppress the NS_BINDING_ABORTED (0x804b002) error that occasionally appears in preview mode.
Priority 3 (lowest)
Provide a means to specify menu & audio language.
-May need to override default menu navigation actions because dvdauthor seems to cause rather undesirable behaviour when buttons are not arranged in a grid.
For any page with no links, add a "back" link (this raises the issue of localisation, of course).
If a page is too large, split it and add "next" and "previous" links (seems very difficult).
Set Mozilla's idea of the pixel aspect ratio to match video pixels. (For extra credit, scale preview mode to correct for the differing aspect ratio.) This seems to be impossible.
Avoid running Xvfb if we're only going to display an error message.
+Allow explicit ordering of titles.
+videolink (0.11-1) unstable; urgency=low
+
+ * New upstream version
+
+ -- Ben Hutchings <ben@decadent.org.uk> Tue, 26 Sep 2006 02:21:15 +0100
+
videolink (0.10-1) unstable; urgency=low
* New upstream version - closes: #386832
// See the file "COPYING" for licence details.
#include <fstream>
+#include <iostream>
+#include <sstream>
#include <stdexcept>
+#include <gdkmm/pixbuf.h>
#include <glibmm/spawn.h>
#include "dvd.hpp"
#include "generate_dvd.hpp"
#include "xml_utils.hpp"
-dvd_contents::menu::menu()
+namespace
+{
+ // Return a closeness metric of an "end" rectangle to a "start"
+ // rectangle in the upward (-1) or downward (+1) direction. Given
+ // several possible "end" rectangles, the one that seems visually
+ // closest in the given direction should have the highest value of
+ // this metric. This is necessarily a heuristic function!
+ double directed_closeness(const rectangle & start, const rectangle & end,
+ int y_dir)
+ {
+ // The obvious approach is to use the centres of the
+ // rectangles. However, for the "end" rectangle, using the
+ // horizontal position nearest the centre of the "start"
+ // rectangle seems to produce more reasonable results. For
+ // example, if there are two "end" rectangles equally near to
+ // the "start" rectangle in terms of vertical distance and one
+ // of them horizontally overlaps the centre of the "start"
+ // rectangle, we want to pick that one even if the centre of
+ // that rectangle is further away from the centre of the
+ // "start" rectangle.
+ int start_x = (start.left + start.right) / 2;
+ int start_y = (start.top + start.bottom) / 2;
+ int end_y = (end.top + end.bottom) / 2;
+ int end_x;
+ if (end.right < start_x)
+ end_x = end.right;
+ else if (end.left > start_x)
+ end_x = end.left;
+ else
+ end_x = start_x;
+
+ // Return cosine of angle between the line between these points
+ // and the vertical, divided by the distance between the points
+ // if that is defined and positive; otherwise return 0.
+ int vertical_distance = (end_y - start_y) * y_dir;
+ if (vertical_distance <= 0)
+ return 0.0;
+ double distance_squared =
+ (end_x - start_x) * (end_x - start_x)
+ + (end_y - start_y) * (end_y - start_y);
+ return vertical_distance / distance_squared;
+ }
+}
+
+dvd_generator::menu::menu()
: vob_temp(new temp_file("videolink-vob-"))
{
vob_temp->close();
}
-void generate_dvd(const dvd_contents & contents,
- const std::string & output_dir)
+dvd_generator::pgc_ref dvd_generator::add_menu()
+{
+ pgc_ref next_menu(menu_pgc, menus_.size());
+
+ // Check against maximum number of menus. It appears that no more
+ // than 128 menus are reachable through LinkPGCN instructions, and
+ // dvdauthor uses some menu numbers for special purposes, resulting
+ // in a practical limit of 119 per domain. We can work around this
+ // later by spreading some menus across titlesets.
+ if (next_menu.index == 119)
+ throw std::runtime_error("No more than 119 menus can be used");
+
+ menus_.resize(next_menu.index + 1);
+ return next_menu;
+}
+
+void dvd_generator::add_menu_entry(unsigned index,
+ const rectangle & area,
+ const pgc_ref & target)
+{
+ assert(index < menus_.size());
+ assert(target.type == menu_pgc && target.index < menus_.size()
+ || target.type == title_pgc && target.index < titles_.size());
+ menu_entry new_entry = { area, target };
+ menus_[index].entries.push_back(new_entry);
+}
+
+void dvd_generator::generate_menu_vob(unsigned index,
+ Glib::RefPtr<Gdk::Pixbuf> background,
+ Glib::RefPtr<Gdk::Pixbuf> highlights)
+ const
+{
+ assert(index < menus_.size());
+ const menu & this_menu = menus_[index];
+
+ temp_file background_temp("videolink-back-");
+ background_temp.close();
+ std::cout << "saving " << background_temp.get_name() << std::endl;
+ background->save(background_temp.get_name(), "png");
+
+ temp_file highlights_temp("videolink-links-");
+ highlights_temp.close();
+ std::cout << "saving " << highlights_temp.get_name() << std::endl;
+ highlights->save(highlights_temp.get_name(), "png");
+
+ temp_file spumux_temp("videolink-spumux-");
+ spumux_temp.close();
+ std::ofstream spumux_file(spumux_temp.get_name().c_str());
+ spumux_file <<
+ "<subpictures>\n"
+ " <stream>\n"
+ " <spu force='yes' start='00:00:00.00'\n"
+ " highlight='" << highlights_temp.get_name() << "'\n"
+ " select='" << highlights_temp.get_name() << "'>\n";
+ int button_count = this_menu.entries.size();
+ for (int i = 0; i != button_count; ++i)
+ {
+ const menu_entry & this_entry = this_menu.entries[i];
+
+ // We program left and right to cycle through the buttons in
+ // the order the entries were added. This should result in
+ // left and right behaving like the tab and shift-tab keys
+ // would in the browser. Hopefully that's a sensible order.
+ // We program up and down to behave geometrically.
+ int up_button = i, down_button = i;
+ double up_closeness = 0.0, down_closeness = 0.0;
+ for (int j = 0; j != button_count; ++j)
+ {
+ const menu_entry & other_entry = this_menu.entries[j];
+ double closeness = directed_closeness(
+ this_entry.area, other_entry.area, -1);
+ if (closeness > up_closeness)
+ {
+ up_button = j;
+ up_closeness = closeness;
+ }
+ else
+ {
+ closeness = directed_closeness(
+ this_entry.area, other_entry.area, 1);
+ if (closeness > down_closeness)
+ {
+ down_button = j;
+ down_closeness = closeness;
+ }
+ }
+ }
+ spumux_file << " <button"
+ " x0='" << this_entry.area.left << "'"
+ " y0='" << this_entry.area.top << "'"
+ " x1='" << this_entry.area.right << "'"
+ " y1='" << this_entry.area.bottom << "'"
+ " left='" << (i == 0 ? button_count : i) << "'"
+ " right='" << 1 + (i + 1) % button_count << "'"
+ " up='" << 1 + up_button << "'"
+ " down='" << 1 + down_button << "'"
+ "/>\n";
+ }
+ spumux_file <<
+ " </spu>\n"
+ " </stream>\n"
+ "</subpictures>\n";
+ spumux_file.close();
+ if (!spumux_file)
+ throw std::runtime_error("Failed to write control file for spumux");
+
+ std::ostringstream command_stream;
+ if (encoder_ == mpeg_encoder_ffmpeg)
+ {
+ command_stream
+ << "ffmpeg"
+ << " -f image2 -vcodec png -i "
+ << background_temp.get_name()
+ << " -target " << frame_params_.common_name << "-dvd"
+ << " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout";
+ }
+ else
+ {
+ assert(encoder_ == mpeg_encoder_mjpegtools_old
+ || encoder_ == mpeg_encoder_mjpegtools_new);
+ 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 ";
+ // The chroma subsampling keywords changed between
+ // versions 1.6.2 and 1.8 of mjpegtools. There is no
+ // keyword that works with both.
+ if (encoder_ == mpeg_encoder_mjpegtools_old)
+ command_stream << "-S420_mpeg2";
+ else
+ command_stream << "-S420mpeg2";
+ command_stream <<
+ " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
+ " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
+ }
+ command_stream
+ << " | spumux -v0 -mdvd " << spumux_temp.get_name()
+ << " > " << this_menu.vob_temp->get_name();
+ std::string command(command_stream.str());
+ const char * argv[] = {
+ "/bin/sh", "-c", command.c_str(), 0
+ };
+ std::cout << "running " << command << 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");
+}
+
+dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
+{
+ pgc_ref next_title(title_pgc, titles_.size());
+
+ // Check against maximum number of titles.
+ if (next_title.index == 99)
+ throw std::runtime_error("No more than 99 titles can be used");
+
+ titles_.resize(next_title.index + 1);
+ titles_[next_title.index].swap(content);
+ return next_title;
+}
+
+void dvd_generator::generate(const std::string & output_dir) const
{
temp_file temp("videolink-dvdauthor-");
temp.close();
// We generate code that uses registers in the following way:
//
// g0: scratch
- // g1: current location
- // g2: location that last jumped to a title
+ // g1: target menu location
+ // g2: source/return menu location for title
// g3: target chapter number
//
// All locations are divided into two bitfields: the least
" <vmgm>\n"
" <menus>\n";
- for (unsigned menu_index = 0;
- menu_index != contents.menus.size();
- ++menu_index)
+ for (unsigned menu_index = 0; menu_index != menus_.size(); ++menu_index)
{
- const dvd_contents::menu & menu = contents.menus[menu_index];
+ const menu & this_menu = menus_[menu_index];
if (menu_index == 0)
{
file <<
" <pgc entry='title' pause='inf'>\n"
" <pre>\n"
- // Initialise the current location if it is not set
- // (all general registers are initially 0).
+ // Set a default target location if none is set.
+ // This covers first play and use of the "top menu"
+ // button.
" if (g1 eq 0)\n"
" g1 = " << 1 + button_mult << ";\n";
}
" <pre>\n";
}
- // When a title finishes or the user presses the menu
+ // When a title finishes or the user presses the "menu"
// button, this always jumps to the titleset's root menu.
// We want to return the user to the last menu they used.
// So we arrange for each titleset's root menu to return
menu_incr != 0;
menu_incr /= 2)
{
- if (menu_index + menu_incr < contents.menus.size()
+ if (menu_index + menu_incr < menus_.size()
&& (menu_index & (menu_incr * 2 - 1)) == 0)
{
file <<
file <<
// Highlight the appropriate button.
" s8 = g1 & " << button_mask << ";\n"
+ // Forget the link target. If we don't do this, pressing
+ // the "top menu" button will result in jumping back to
+ // this same menu!
+ " g1 = 0;\n"
" </pre>\n"
- " <vob file='" << menu.vob_temp->get_name() << "'/>\n";
+ " <vob file='" << this_menu.vob_temp->get_name() << "'/>\n";
for (unsigned button_index = 0;
- button_index != menu.entries.size();
+ button_index != this_menu.entries.size();
++button_index)
{
- const dvd_contents::pgc_ref & target =
- menu.entries[button_index];
+ const pgc_ref & target = this_menu.entries[button_index].target;
file << " <button> ";
- if (target.type == dvd_contents::menu_pgc)
+ if (target.type == menu_pgc)
{
unsigned target_button_num;
// back to this one. If there is one, set that to
// be the highlighted button; otherwise, use the
// first button.
- const std::vector<dvd_contents::pgc_ref> &
- target_menu_entries =
- contents.menus[target.index].entries;
- dvd_contents::pgc_ref this_pgc(dvd_contents::menu_pgc,
- menu_index);
+ const std::vector<menu_entry> & target_menu_entries =
+ menus_[target.index].entries;
+ pgc_ref this_pgc(menu_pgc, menu_index);
target_button_num = target_menu_entries.size();
- while (target_button_num != 0
- && (target_menu_entries[target_button_num - 1]
+ while (target_button_num != 1
+ && (target_menu_entries[target_button_num - 1].target
!= this_pgc))
--target_button_num;
}
}
else
{
- assert(target.type == dvd_contents::title_pgc);
+ assert(target.type == title_pgc);
file <<
- // Record current menu location (g1 specifies this
- // menu but not necessarily this button).
- "g1 = " << (1 + menu_index
+ // Record current menu location.
+ "g2 = " << (1 + menu_index
+ (1 + button_index) * button_mult) << "; "
// Set target chapter number.
"g3 = " << target.sub_index << "; "
file <<
" </menus>\n"
- " </vmgm>\n";
-
+ " </vmgm>\n";
+
// Generate a titleset for each title. This appears to make
// jumping to titles a whole lot simpler (but limits us to 99
// titles).
for (unsigned title_index = 0;
- title_index != contents.titles.size();
- ++title_index)
+ title_index != titles_.size();
+ ++title_index)
{
- file <<
+ file <<
" <titleset>\n"
- // Generate a dummy menu so that the menu button on the
- // remote control will work.
+ // Generate a dummy menu so that the "menu" button will
+ // work. This returns to the source menu via the title
+ // menu.
" <menus>\n"
" <pgc entry='root'>\n"
- " <pre> jump vmgm menu; </pre>\n"
+ " <pre> g1 = g2; jump vmgm menu; </pre>\n"
" </pgc>\n"
" </menus>\n"
" <titles>\n"
" <pgc>\n"
- " <pre>\n"
- // Record calling location.
- " g2 = g1;\n";
+ " <pre>\n";
// Count chapters in the title.
unsigned n_chapters = 0;
for (vob_list::const_iterator
- it = contents.titles[title_index].begin(),
- end = contents.titles[title_index].end();
+ it = titles_[title_index].begin(),
+ end = titles_[title_index].end();
it != end;
++it)
{
" </pre>\n";
for (vob_list::const_iterator
- it = contents.titles[title_index].begin(),
- end = contents.titles[title_index].end();
+ it = titles_[title_index].begin(),
+ end = titles_[title_index].end();
it != end;
++it)
{
file <<
" <post>\n"
- // If the menu location has not been changed during
- // the title, set the location to be the following
- // button in the menu.
- " if (g1 eq g2)\n"
- " g1 = g1 + " << button_mult << ";\n"
- // In any case, return to some menu.
+ // Return to the source menu, but highlight the next button.
+ " g2 = g2 + " << button_mult << ";\n"
" call menu;\n"
" </post>\n"
" </pgc>\n"
#include <boost/shared_ptr.hpp>
+#include <glibmm/refptr.h>
+
+#include "geometry.hpp"
#include "temp_file.hpp"
+#include "video.hpp"
#include "vob_list.hpp"
+namespace Gdk
+{
+ class Pixbuf;
+}
+
// Description of menus and titles to go on a DVD.
-struct dvd_contents
+class dvd_generator
{
+public:
enum pgc_type { unknown_pgc, menu_pgc, title_pgc };
// Reference to some PGC (program chain).
// unspecified; not compared!)
};
+ // We can try using any of these encoders to convert PNG to MPEG.
+ enum mpeg_encoder
+ {
+ mpeg_encoder_ffmpeg, // ffmpeg
+ mpeg_encoder_mjpegtools_old, // mjpegtools before version 1.8
+ mpeg_encoder_mjpegtools_new // mjpegtools from version 1.8
+ };
+
+ dvd_generator(const video::frame_params & frame_params,
+ mpeg_encoder encoder)
+ : frame_params_(frame_params),
+ encoder_(encoder)
+ {}
+
+ // Create a new empty menu; return a reference to it.
+ // The client must call generate_menu_vob() for each menu before
+ // calling generate().
+ pgc_ref add_menu();
+ // Add a menu entry (link) to an existing menu.
+ void add_menu_entry(unsigned index,
+ const rectangle & area,
+ const pgc_ref & target);
+ // Generate the menu VOB from a background image and button
+ // highlight image.
+ void generate_menu_vob(unsigned index,
+ Glib::RefPtr<Gdk::Pixbuf> background,
+ Glib::RefPtr<Gdk::Pixbuf> highlights) const;
+
+ // Create a new title using the given vob_list; return a reference
+ // to it. The argument will be pilfered (i.e. emptied).
+ pgc_ref add_title(vob_list & list);
+
+ // Use dvdauthor to generate a DVD filesystem.
+ void generate(const std::string & output_dir) const;
+
+private:
+ struct menu_entry
+ {
+ rectangle area;
+ pgc_ref target;
+ };
+
// Menu definition.
struct menu
{
// References to the menus and titles that the menu buttons
// are meant to link to, in the same order as the buttons.
- std::vector<pgc_ref> entries;
+ std::vector<menu_entry> entries;
};
- std::vector<menu> menus;
- std::vector<vob_list> titles;
+ video::frame_params frame_params_;
+ mpeg_encoder encoder_;
+ std::vector<menu> menus_;
+ std::vector<vob_list> titles_;
};
-// Use dvdauthor to generate a DVD filesystem with the given contents.
-void generate_dvd(const dvd_contents & contents,
- const std::string & output_dir);
-
#endif // !INC_GENERATE_DVD_HPP
--- /dev/null
+// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
+// See the file "COPYING" for licence details.
+
+#ifndef INC_GEOMETRY_HPP
+#define INC_GEOMETRY_HPP
+
+struct rectangle
+{
+ int left, top; // inclusive
+ int right, bottom; // exclusive
+
+ rectangle operator|=(const rectangle & other)
+ {
+ if (other.empty())
+ {
+ // use current extents unchanged
+ }
+ else if (empty())
+ {
+ // use other extents
+ *this = other;
+ }
+ else
+ {
+ // find rectangle enclosing both extents
+ left = std::min(left, other.left);
+ top = std::min(top, other.top);
+ right = std::max(right, other.right);
+ bottom = std::max(bottom, other.bottom);
+ }
+
+ return *this;
+ }
+
+ rectangle operator&=(const rectangle & other)
+ {
+ // find rectangle enclosed in both extents
+ left = std::max(left, other.left);
+ top = std::max(top, other.top);
+ right = std::max(left, std::min(right, other.right));
+ bottom = std::max(top, std::min(bottom, other.bottom));
+ return *this;
+ }
+
+ bool empty() const
+ {
+ return left == right || bottom == top;
+ }
+};
+
+#endif // !INC_GEOMETRY_HPP
--- /dev/null
+body {
+ /* Let the background overscan, but restrict the content to title-safe. */
+ padding: 48px 72px;
+}
--- /dev/null
+body {
+ /* Let the background overscan, but restrict the content to title-safe. */
+ padding: 58px 72px;
+}
-// Copyright 2005 Ben Hutchings <ben@decadent.org.uk>.
+// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
// See the file "COPYING" for licence details.
#include "pixbufs.hpp"
// (or RGBA, but the alpha component will be ignored) and copy the
// differing pixels from the new one to a third RGBA Pixbuf at the
// specified offset with full opacity.
-// The width and height of the old and new Pixbufs must be equal
-// and match the specified dimensions. The width and height of
-// the third Pixbuf must be large enough to store a rectangle of
+// The width and height of the new Pixbufs must be equal and match
+// the specified dimensions. The width and height of the old and
+// third Pixbuf must be large enough to store a rectangle of
// those dimensions at the specified offset.
void diff_rgb_pixbufs(Glib::RefPtr<Gdk::Pixbuf> old_buf,
Glib::RefPtr<Gdk::Pixbuf> new_buf,
int old_bpr = old_buf->get_rowstride();
int old_bpp = old_buf->get_n_channels();
assert(old_bpp >= 3);
- assert(old_buf->get_width() == width);
- assert(old_buf->get_height() == height);
+ assert(old_buf->get_width() >= offset_x + width);
+ assert(old_buf->get_height() >= offset_y + height);
int new_bpr = new_buf->get_rowstride();
int new_bpp = new_buf->get_n_channels();
assert(new_bpp >= 3);
assert(diff_buf->get_width() >= offset_x + width);
assert(diff_buf->get_height() >= offset_y + height);
- const guint8 * old_p = old_buf->get_pixels();
+ const guint8 * old_p = (old_buf->get_pixels()
+ + old_bpr * offset_y
+ + old_bpp * offset_x);
const guint8 * new_p = new_buf->get_pixels();
guint8 * diff_p = (diff_buf->get_pixels()
+ diff_bpr * offset_y
-// Copyright 2005 Ben Hutchings <ben@decadent.org.uk>.
+// Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
// See the file "COPYING" for licence details.
#ifndef INC_PIXBUFS_HPP
// (or RGBA, but the alpha component will be ignored) and copy the
// differing pixels from the new one to a third RGBA Pixbuf at the
// specified offset with full opacity.
-// The width and height of the old and new Pixbufs must be equal
-// and match the specified dimensions. The width and height of
-// the third Pixbuf must be large enough to store a rectangle of
+// The width and height of the new Pixbufs must be equal and match
+// the specified dimensions. The width and height of the old and
+// third Pixbuf must be large enough to store a rectangle of
// those dimensions at the specified offset.
void diff_rgb_pixbufs(Glib::RefPtr<Gdk::Pixbuf> old_buf,
Glib::RefPtr<Gdk::Pixbuf> new_buf,
{
struct frame_params
{
- const char * ffmpeg_name;
+ const char * common_name;
unsigned int width, height;
unsigned int rate_numer, rate_denom;
unsigned int pixel_ratio_width, pixel_ratio_height;
#include <stdlib.h>
-#include <boost/shared_ptr.hpp>
-
#include <gdkmm/pixbuf.h>
#include <glibmm/convert.h>
#include <glibmm/spawn.h>
#include "child_iterator.hpp"
#include "dvd.hpp"
#include "generate_dvd.hpp"
+#include "geometry.hpp"
#include "link_iterator.hpp"
#include "null_prompt_service.hpp"
#include "pixbufs.hpp"
#include "style_sheets.hpp"
#include "temp_file.hpp"
#include "video.hpp"
+#include "warp_pointer.hpp"
#include "x_frame_buffer.hpp"
#include "xml_utils.hpp"
#include "xpcom_support.hpp"
namespace
{
- // We can try using any of these encoders to convert PNG to MPEG.
- enum mpeg_encoder
- {
- mpeg_encoder_ffmpeg, // ffmpeg
- mpeg_encoder_mjpegtools_old, // mjpegtools before version 1.8
- mpeg_encoder_mjpegtools_new // mjpegtools from version 1.8
- };
-
- struct rectangle
- {
- int left, top; // inclusive
- int right, bottom; // exclusive
-
- rectangle operator|=(const rectangle & other)
- {
- if (other.empty())
- {
- // use current extents unchanged
- }
- else if (empty())
- {
- // use other extents
- *this = other;
- }
- else
- {
- // find rectangle enclosing both extents
- left = std::min(left, other.left);
- top = std::min(top, other.top);
- right = std::max(right, other.right);
- bottom = std::max(bottom, other.bottom);
- }
-
- return *this;
- }
-
- rectangle operator&=(const rectangle & other)
- {
- // find rectangle enclosed in both extents
- left = std::max(left, other.left);
- top = std::max(top, other.top);
- right = std::max(left, std::min(right, other.right));
- bottom = std::max(top, std::min(bottom, other.bottom));
- return *this;
- }
-
- bool empty() const
- {
- return left == right || bottom == top;
- }
- };
-
rectangle get_elem_rect(nsIDOMNSDocument * ns_doc,
nsIDOMElement * elem)
{
}
+ enum video_format
+ {
+ video_format_none,
+ video_format_mpeg2_ps,
+ video_format_vob_list
+ };
+
+ video_format video_format_from_uri(const std::string & uri)
+ {
+ // FIXME: This is a bit of a hack. Perhaps we could decide
+ // later based on the MIME type determined by Mozilla?
+ static struct {
+ const char * extension;
+ video_format format;
+ } const mapping[] = {
+ {".vob", video_format_mpeg2_ps},
+ {".mpeg", video_format_mpeg2_ps},
+ {".mpeg2", video_format_mpeg2_ps},
+ {".voblist", video_format_vob_list}
+ };
+ for (std::size_t i = 0;
+ i != sizeof(mapping) / sizeof(mapping[0]);
+ ++i)
+ {
+ std::size_t ext_len = std::strlen(mapping[i].extension);
+ if (uri.size() > ext_len
+ && uri.compare(uri.size() - ext_len, ext_len,
+ mapping[i].extension) == 0)
+ return mapping[i].format;
+ }
+ return video_format_none;
+ }
+
+
class videolink_window : public Gtk::Window
{
public:
const video::frame_params & frame_params,
const std::string & main_page_uri,
const std::string & output_dir,
- mpeg_encoder encoder);
+ dvd_generator::mpeg_encoder encoder);
bool is_finished() const;
private:
- dvd_contents::pgc_ref add_menu(const std::string & uri);
- dvd_contents::pgc_ref add_title(const std::string & uri);
+ struct page_state;
+
+ dvd_generator::pgc_ref add_menu(const std::string & uri);
+ dvd_generator::pgc_ref add_title(const std::string & uri,
+ video_format format);
void load_next_page();
bool on_idle();
void on_net_state_change(const char * uri, gint flags, guint status);
{
return pending_window_update_ || pending_req_count_;
}
- bool process_page();
- void save_screenshot();
- void process_links(nsIPresShell * pres_shell,
- nsPresContext * pres_context,
- nsIDOMWindow * dom_window);
+ // Do as much processing as possible. Return a flag indicating
+ // whether to call again once the browser is idle.
+ bool process();
+ // Return a Pixbuf containing a copy of the window contents.
+ Glib::RefPtr<Gdk::Pixbuf> get_screenshot();
+ // Do as much processing as possible on the page links. Return
+ // a flag indicating whether to call again once the browser is
+ // idle.
+ bool process_links(
+ page_state * state,
+ nsIDOMDocument * basic_doc,
+ nsIPresShell * pres_shell,
+ nsPresContext * pres_context,
+ nsIDOMWindow * dom_window);
video::frame_params frame_params_;
std::string output_dir_;
- mpeg_encoder encoder_;
browser_widget browser_widget_;
- agent_style_sheet_holder style_sheet_;
+ agent_style_sheet_holder main_style_sheet_, frame_style_sheet_;
- dvd_contents contents_;
- typedef std::map<std::string, dvd_contents::pgc_ref> resource_map_type;
+ dvd_generator generator_;
+ typedef std::map<std::string, dvd_generator::pgc_ref>
+ resource_map_type;
resource_map_type resource_map_;
std::queue<std::string> page_queue_;
bool pending_window_update_;
int pending_req_count_;
bool have_tweaked_page_;
- std::auto_ptr<temp_file> background_temp_;
- struct page_state;
std::auto_ptr<page_state> page_state_;
bool finished_;
const video::frame_params & frame_params,
const std::string & main_page_uri,
const std::string & output_dir,
- mpeg_encoder encoder)
+ dvd_generator::mpeg_encoder encoder)
: frame_params_(frame_params),
output_dir_(output_dir),
- encoder_(encoder),
- style_sheet_(init_agent_style_sheet(
- "file://"VIDEOLINK_SHARE_DIR"/videolink.css")),
+ main_style_sheet_(
+ init_agent_style_sheet(
+ "file://" VIDEOLINK_SHARE_DIR "/videolink.css")),
+ frame_style_sheet_(
+ init_agent_style_sheet(
+ std::string("file://" VIDEOLINK_SHARE_DIR "/")
+ .append(frame_params.common_name)
+ .append(".css")
+ .c_str())),
+ generator_(frame_params, encoder),
pending_window_update_(false),
pending_req_count_(0),
have_tweaked_page_(false),
return finished_;
}
- dvd_contents::pgc_ref videolink_window::add_menu(const std::string & uri)
+ dvd_generator::pgc_ref videolink_window::add_menu(const std::string & uri)
{
- dvd_contents::pgc_ref next_menu(dvd_contents::menu_pgc,
- contents_.menus.size());
- std::pair<resource_map_type::iterator, bool> insert_result(
- resource_map_.insert(std::make_pair(uri, next_menu)));
-
- if (!insert_result.second)
- {
- return insert_result.first->second;
- }
- else
+ dvd_generator::pgc_ref & pgc_ref = resource_map_[uri];
+ if (pgc_ref.type == dvd_generator::unknown_pgc)
{
+ pgc_ref = generator_.add_menu();
page_queue_.push(uri);
- contents_.menus.resize(contents_.menus.size() + 1);
- return next_menu;
}
+ return pgc_ref;
}
- dvd_contents::pgc_ref videolink_window::add_title(const std::string & uri)
+ dvd_generator::pgc_ref videolink_window::add_title(const std::string & uri,
+ video_format format)
{
- dvd_contents::pgc_ref next_title(dvd_contents::title_pgc,
- contents_.titles.size());
- std::pair<resource_map_type::iterator, bool> insert_result(
- resource_map_.insert(std::make_pair(uri, next_title)));
+ dvd_generator::pgc_ref & pgc_ref = resource_map_[uri];
- if (!insert_result.second)
- {
- return insert_result.first->second;
- }
- else
+ if (pgc_ref.type == dvd_generator::unknown_pgc)
{
Glib::ustring hostname;
std::string path(Glib::filename_from_uri(uri, hostname));
// Store a reference to a linked VOB file, or the contents
// of a linked VOB list file.
- if (path.compare(path.size() - 4, 4, ".vob") == 0)
+ if (format == video_format_mpeg2_ps)
{
if (!Glib::file_test(path, Glib::FILE_TEST_IS_REGULAR))
throw std::runtime_error(
ref.file = path;
list.push_back(ref);
}
- else
+ else if (format == video_format_vob_list)
{
- assert(path.compare(path.size() - 8, 8, ".voblist") == 0);
read_vob_list(path).swap(list);
}
+ else
+ {
+ assert(!"unrecognised format in add_title");
+ }
- contents_.titles.resize(contents_.titles.size() + 1);
- contents_.titles.back().swap(list);
- return next_title;
+ pgc_ref = generator_.add_title(list);
}
+
+ return pgc_ref;
}
void videolink_window::load_next_page()
bool videolink_window::on_idle()
{
+ if (!output_dir_.empty())
+ {
+ // Put pointer in the top-left so that no links appear in
+ // the hover state when we take a screenshot.
+ warp_pointer(get_window(),
+ -frame_params_.width, -frame_params_.height);
+ }
+
load_next_page();
return false; // don't call again thankyou
}
{
try
{
- if (!process_page())
+ if (!process())
{
finished_ = true;
Gtk::Main::quit();
}
}
- bool videolink_window::process_page()
+ struct videolink_window::page_state
+ {
+ page_state(Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf,
+ nsIDOMDocument * doc, int width, int height)
+ : norm_pixbuf(norm_pixbuf),
+ diff_pixbuf(Gdk::Pixbuf::create(
+ Gdk::COLORSPACE_RGB,
+ true, 8, // has_alpha, bits_per_sample
+ width, height)),
+ link_num(0),
+ links_it(doc),
+ link_changing(false)
+ {
+ }
+
+ Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
+ Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
+
+ unsigned link_num;
+ link_iterator links_it, links_end;
+
+ rectangle link_rect;
+ bool link_changing;
+ };
+
+ bool videolink_window::process()
{
assert(!page_queue_.empty());
// disable scrollbars.
if (!have_tweaked_page_)
{
- apply_agent_style_sheet(style_sheet_, pres_shell);
+ apply_agent_style_sheet(main_style_sheet_, pres_shell);
+ apply_agent_style_sheet(frame_style_sheet_, pres_shell);
// This actually only needs to be done once.
nsCOMPtr<nsIDOMBarProp> dom_bar_prop;
// All further work should only be done if we're not in preview mode.
if (!output_dir_.empty())
{
- // If we haven't already started work on this menu, save a
- // screenshot of its normal appearance.
- if (!page_state_.get())
- save_screenshot();
+ nsCOMPtr<nsIDOMDocument> basic_doc;
+ check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
// 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())
+ std::auto_ptr<page_state> state(page_state_);
+ if (!state.get())
+ state.reset(
+ new page_state(
+ get_screenshot(),
+ basic_doc, frame_params_.width, frame_params_.height));
+ if (process_links(
+ state.get(),
+ basic_doc, pres_shell, pres_context, dom_window))
{
+ // Save iteration state for later.
+ page_state_ = state;
+ }
+ else
+ {
+ // We've finished work on the links so generate the
+ // menu VOB.
+ quantise_rgba_pixbuf(state->diff_pixbuf,
+ dvd::button_n_colours);
+ generator_.generate_menu_vob(
+ resource_map_[page_queue_.front()].index,
+ state->norm_pixbuf, state->diff_pixbuf);
+
+ // Move on to the next page, if any, or else generate
+ // the DVD filesystem.
page_queue_.pop();
- if (page_queue_.empty())
+ if (!page_queue_.empty())
{
- generate_dvd(contents_, output_dir_);
- return false;
+ load_next_page();
}
else
{
- load_next_page();
+ generator_.generate(output_dir_);
+ return false;
}
}
}
return true;
}
- void videolink_window::save_screenshot()
+ Glib::RefPtr<Gdk::Pixbuf> videolink_window::get_screenshot()
{
Glib::RefPtr<Gdk::Window> window(get_window());
assert(window);
window->process_updates(true);
- background_temp_.reset(new temp_file("videolink-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,
- frame_params_.width, frame_params_.height)
- ->save(background_temp_->get_name(), "png");
+ return Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
+ window->get_colormap(),
+ 0, 0, 0, 0,
+ frame_params_.width, frame_params_.height);
}
- struct videolink_window::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("videolink-spumux-"),
- links_temp("videolink-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;
-
- unsigned link_num;
- link_iterator links_it, links_end;
-
- rectangle link_rect;
- bool link_changing;
- Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
- };
-
- void videolink_window::process_links(nsIPresShell * pres_shell,
- nsPresContext * pres_context,
- nsIDOMWindow * dom_window)
+ bool videolink_window::process_links(
+ page_state * state,
+ nsIDOMDocument * basic_doc,
+ nsIPresShell * pres_shell,
+ nsPresContext * pres_context,
+ nsIDOMWindow * dom_window)
{
Glib::RefPtr<Gdk::Window> window(get_window());
assert(window);
- nsCOMPtr<nsIDOMDocument> basic_doc;
- check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
nsCOMPtr<nsIDOMNSDocument> ns_doc(do_QueryInterface(basic_doc));
assert(ns_doc);
nsCOMPtr<nsIEventStateManager> event_state_man(
nsCOMPtr<nsIDOMAbstractView> view;
check(doc_view->GetDefaultView(getter_AddRefs(view)));
- // Set up or recover our iteration state.
- std::auto_ptr<page_state> state(page_state_);
- if (!state.get())
- {
- 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='" << state->links_temp.get_name() << "'\n"
- " select='" << state->links_temp.get_name() << "'>\n";
- }
-
rectangle window_rect = {
0, 0, frame_params_.width, frame_params_.height
};
- unsigned menu_num = resource_map_[page_queue_.front()].index;
+ unsigned menu_index = resource_map_[page_queue_.front()].index;
for (/* no initialisation */;
state->links_it != state->links_end;
continue;
}
- state->spumux_file <<
- " <button x0='" << state->link_rect.left << "'"
- " y0='" << state->link_rect.top << "'"
- " x1='" << state->link_rect.right - 1 << "'"
- " y1='" << state->link_rect.bottom - 1 << "'/>\n";
-
// Check whether this is a link to a video or a page then
// add it to the known resources if not already seen; then
// add it to the menu entries.
- dvd_contents::pgc_ref target;
- // FIXME: This is a bit of a hack. Perhaps we could decide
- // later based on the MIME type determined by Mozilla?
- if ((uri.size() > 4
- && uri.compare(uri.size() - 4, 4, ".vob") == 0)
- || (uri.size() > 8
- && uri.compare(uri.size() - 8, 8, ".voblist") == 0))
+ dvd_generator::pgc_ref target;
+ video_format format = video_format_from_uri(uri);
+ if (format != video_format_none)
{
PRBool is_file;
check(uri_iface->SchemeIs("file", &is_file));
<< " scheme\n";
continue;
}
- target = add_title(uri);
+ target = add_title(uri, format);
target.sub_index =
std::strtoul(fragment.c_str(), NULL, 10);
}
- else
+ else // video_format == video_format_none
{
target = add_menu(uri);
// TODO: If there's a fragment, work out which button
// is closest and set target.sub_index.
}
- contents_.menus[menu_num].entries.push_back(target);
+
+ generator_.add_menu_entry(menu_index,
+ state->link_rect, target);
nsCOMPtr<nsIContent> content(do_QueryInterface(node));
assert(content);
do_QueryInterface(node));
assert(event_target);
- state->norm_pixbuf = Gdk::Pixbuf::create(
- Glib::RefPtr<Gdk::Drawable>(window),
- window->get_colormap(),
- state->link_rect.left,
- state->link_rect.top,
- 0,
- 0,
- state->link_rect.right - state->link_rect.left,
- state->link_rect.bottom - state->link_rect.top);
-
nsCOMPtr<nsIDOMEvent> event;
check(event_factory->CreateEvent(
NS_ConvertASCIItoUTF16("MouseEvents"),
if (browser_is_busy())
{
state->link_changing = true;
- page_state_ = state;
- return;
+ return true;
}
}
state->link_rect.bottom - state->link_rect.top);
}
- quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
-
- 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 ...
-
- {
- std::ostringstream command_stream;
- if (encoder_ == mpeg_encoder_ffmpeg)
- {
- command_stream
- << "ffmpeg"
- << " -f image2 -vcodec png -i "
- << background_temp_->get_name()
- << " -target " << frame_params_.ffmpeg_name << "-dvd"
- << " -vcodec mpeg2video -an -y /dev/stdout"
- << " | spumux -v0 -mdvd " << state->spumux_temp.get_name()
- << " > " << contents_.menus[menu_num].vob_temp->get_name();
- }
- else
- {
- assert(encoder_ == mpeg_encoder_mjpegtools_old
- || encoder_ == mpeg_encoder_mjpegtools_new);
- 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 ";
- // The chroma subsampling keywords changed between
- // versions 1.6.2 and 1.8 of mjpegtools. There is no
- // keyword that works with both.
- if (encoder_ == mpeg_encoder_mjpegtools_old)
- command_stream << "-S420_mpeg2";
- else
- command_stream << "-S420mpeg2";
- command_stream
- << (" | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
- " | mplex -v0 -f8 -o/dev/stdout /dev/stdin"
- " | spumux -v0 -mdvd ")
- << state->spumux_temp.get_name()
- << " > "
- << contents_.menus[menu_num].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");
- }
+ return false;
}
const video::frame_params & lookup_frame_params(const char * str)
bool preview_mode = false;
std::string menu_url;
std::string output_dir;
- mpeg_encoder encoder = mpeg_encoder_ffmpeg;
+ dvd_generator::mpeg_encoder encoder =
+ dvd_generator::mpeg_encoder_ffmpeg;
// Do initial option parsing. We have to do this before
// letting Gtk parse the arguments since we may need to spawn
}
if (std::strcmp(argv[argi + 1], "ffmpeg") == 0)
{
- encoder = mpeg_encoder_ffmpeg;
+ encoder = dvd_generator::mpeg_encoder_ffmpeg;
}
else if (std::strcmp(argv[argi + 1], "mjpegtools-old") == 0)
{
- encoder = mpeg_encoder_mjpegtools_old;
+ encoder = dvd_generator::mpeg_encoder_mjpegtools_old;
}
else if (std::strcmp(argv[argi + 1], "mjpegtools") == 0
|| std::strcmp(argv[argi + 1], "mjpegtools-new") == 0)
{
- encoder = mpeg_encoder_mjpegtools_new;
+ encoder = dvd_generator::mpeg_encoder_mjpegtools_new;
}
else
{
body {
/* Sans-serif fonts will be much more readable than serif on a TV. */
font-family: sans-serif;
- /* Let the background overscan, but not the content. */
- padding: 60px;
/* No scroll bars. */
overflow: hidden;
}
--- /dev/null
+// Copyright 2006 Ben Hutchings <ben@decadent.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include "warp_pointer.hpp"
+
+#include <gdk/gdkx.h>
+
+// Move the pointer by (dx, dy) relative to its current position on
+// the display which window appears on.
+// This is separated from videolink.cpp solely because it uses Xlib
+// and Xlib.h defines many macros we don't want.
+void warp_pointer(const Glib::RefPtr<Gdk::Window> & window, int dx, int dy)
+{
+ XWarpPointer(gdk_x11_drawable_get_xdisplay(window->gobj()), None, None,
+ 0, 0, 0, 0, dx, dy);
+}
--- /dev/null
+// Copyright 2006 Ben Hutchings <ben@decadent.org.uk>.
+// See the file "COPYING" for licence details.
+
+#include <gdkmm/window.h>
+
+// Move the pointer by (dx, dy) relative to its current position on
+// the display which window appears on.
+// This is separated from videolink.cpp solely because it uses Xlib
+// and Xlib.h defines many macros we don't want.
+void warp_pointer(const Glib::RefPtr<Gdk::Window> & window, int dx, int dy);