1 // Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
2 // See the file "COPYING" for licence details.
9 #include <gdkmm/pixbuf.h>
10 #include <glibmm/spawn.h>
13 #include "generate_dvd.hpp"
14 #include "xml_utils.hpp"
18 // Return a closeness metric of an "end" rectangle to a "start"
19 // rectangle in the upward (-1) or downward (+1) direction. Given
20 // several possible "end" rectangles, the one that seems visually
21 // closest in the given direction should have the highest value of
22 // this metric. This is necessarily a heuristic function!
23 double directed_closeness(const rectangle & start, const rectangle & end,
26 // The obvious approach is to use the centres of the
27 // rectangles. However, for the "end" rectangle, using the
28 // horizontal position nearest the centre of the "start"
29 // rectangle seems to produce more reasonable results. For
30 // example, if there are two "end" rectangles equally near to
31 // the "start" rectangle in terms of vertical distance and one
32 // of them horizontally overlaps the centre of the "start"
33 // rectangle, we want to pick that one even if the centre of
34 // that rectangle is further away from the centre of the
36 int start_x = (start.left + start.right) / 2;
37 int start_y = (start.top + start.bottom) / 2;
38 int end_y = (end.top + end.bottom) / 2;
40 if (end.right < start_x)
42 else if (end.left > start_x)
47 // Return cosine of angle between the line between these points
48 // and the vertical, divided by the distance between the points
49 // if that is defined and positive; otherwise return 0.
50 int vertical_distance = (end_y - start_y) * y_dir;
51 if (vertical_distance <= 0)
53 double distance_squared =
54 (end_x - start_x) * (end_x - start_x)
55 + (end_y - start_y) * (end_y - start_y);
56 return vertical_distance / distance_squared;
60 dvd_generator::menu::menu()
61 : vob_temp(new temp_file("videolink-vob-"))
66 dvd_generator::pgc_ref dvd_generator::add_menu()
68 pgc_ref next_menu(menu_pgc, menus_.size());
70 // Check against maximum number of menus. It appears that no more
71 // than 128 menus are reachable through LinkPGCN instructions, and
72 // dvdauthor uses some menu numbers for special purposes, resulting
73 // in a practical limit of 119 per domain. We can work around this
74 // later by spreading some menus across titlesets.
75 if (next_menu.index == 119)
76 throw std::runtime_error("No more than 119 menus can be used");
78 menus_.resize(next_menu.index + 1);
82 void dvd_generator::add_menu_entry(unsigned index,
83 const rectangle & area,
84 const pgc_ref & target)
86 assert(index < menus_.size());
87 assert(target.type == menu_pgc && target.index < menus_.size()
88 || target.type == title_pgc && target.index < titles_.size());
89 menu_entry new_entry = { area, target };
90 menus_[index].entries.push_back(new_entry);
93 void dvd_generator::generate_menu_vob(unsigned index,
94 Glib::RefPtr<Gdk::Pixbuf> background,
95 Glib::RefPtr<Gdk::Pixbuf> highlights)
98 assert(index < menus_.size());
99 const menu & this_menu = menus_[index];
101 temp_file background_temp("videolink-back-");
102 background_temp.close();
103 std::cout << "saving " << background_temp.get_name() << std::endl;
104 background->save(background_temp.get_name(), "png");
106 temp_file highlights_temp("videolink-links-");
107 highlights_temp.close();
108 std::cout << "saving " << highlights_temp.get_name() << std::endl;
109 highlights->save(highlights_temp.get_name(), "png");
111 temp_file spumux_temp("videolink-spumux-");
113 std::ofstream spumux_file(spumux_temp.get_name().c_str());
117 " <spu force='yes' start='00:00:00.00'\n"
118 " highlight='" << highlights_temp.get_name() << "'\n"
119 " select='" << highlights_temp.get_name() << "'>\n";
120 int button_count = this_menu.entries.size();
121 for (int i = 0; i != button_count; ++i)
123 const menu_entry & this_entry = this_menu.entries[i];
125 // We program left and right to cycle through the buttons in
126 // the order the entries were added. This should result in
127 // left and right behaving like the tab and shift-tab keys
128 // would in the browser. Hopefully that's a sensible order.
129 // We program up and down to behave geometrically.
130 int up_button = i, down_button = i;
131 double up_closeness = 0.0, down_closeness = 0.0;
132 for (int j = 0; j != button_count; ++j)
134 const menu_entry & other_entry = this_menu.entries[j];
135 double closeness = directed_closeness(
136 this_entry.area, other_entry.area, -1);
137 if (closeness > up_closeness)
140 up_closeness = closeness;
144 closeness = directed_closeness(
145 this_entry.area, other_entry.area, 1);
146 if (closeness > down_closeness)
149 down_closeness = closeness;
153 spumux_file << " <button"
154 " x0='" << this_entry.area.left << "'"
155 " y0='" << this_entry.area.top << "'"
156 " x1='" << this_entry.area.right << "'"
157 " y1='" << this_entry.area.bottom << "'"
158 " left='" << (i == 0 ? button_count : i) << "'"
159 " right='" << 1 + (i + 1) % button_count << "'"
160 " up='" << 1 + up_button << "'"
161 " down='" << 1 + down_button << "'"
170 throw std::runtime_error("Failed to write control file for spumux");
172 std::ostringstream command_stream;
173 if (encoder_ == mpeg_encoder_ffmpeg)
177 << " -f image2 -vcodec png -i "
178 << background_temp.get_name()
179 << " -target " << frame_params_.common_name << "-dvd"
180 << " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout";
184 assert(encoder_ == mpeg_encoder_mjpegtools_old
185 || encoder_ == mpeg_encoder_mjpegtools_new);
188 << background_temp.get_name()
189 << " | ppmtoy4m -v0 -n1 -F"
190 << frame_params_.rate_numer << ":" << frame_params_.rate_denom
191 << " -A" << frame_params_.pixel_ratio_width
192 << ":" << frame_params_.pixel_ratio_height
194 // The chroma subsampling keywords changed between
195 // versions 1.6.2 and 1.8 of mjpegtools. There is no
196 // keyword that works with both.
197 if (encoder_ == mpeg_encoder_mjpegtools_old)
198 command_stream << "-S420_mpeg2";
200 command_stream << "-S420mpeg2";
202 " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
203 " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
206 << " | spumux -v0 -mdvd " << spumux_temp.get_name()
207 << " > " << this_menu.vob_temp->get_name();
208 std::string command(command_stream.str());
209 const char * argv[] = {
210 "/bin/sh", "-c", command.c_str(), 0
212 std::cout << "running " << command << std::endl;
214 Glib::spawn_sync(".",
215 Glib::ArrayHandle<std::string>(
216 argv, sizeof(argv)/sizeof(argv[0]),
217 Glib::OWNERSHIP_NONE),
218 Glib::SPAWN_STDOUT_TO_DEV_NULL,
222 if (command_result != 0)
223 throw std::runtime_error("spumux pipeline failed");
226 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
228 pgc_ref next_title(title_pgc, titles_.size());
230 // Check against maximum number of titles.
231 if (next_title.index == 99)
232 throw std::runtime_error("No more than 99 titles can be used");
234 titles_.resize(next_title.index + 1);
235 titles_[next_title.index].swap(content);
239 void dvd_generator::generate(const std::string & output_dir) const
241 temp_file temp("videolink-dvdauthor-");
243 std::ofstream file(temp.get_name().c_str());
245 // We generate code that uses registers in the following way:
248 // g1: target menu location
249 // g2: source/return menu location for title
250 // g3: target chapter number
252 // All locations are divided into two bitfields: the least
253 // significant 10 bits are a page/menu number and the most
254 // significant 6 bits are a link/button number, and numbering
255 // starts at 1, not 0. This is done for compatibility with
256 // the encoding of the s8 (button) register.
258 static const int button_mult = dvd::reg_s8_button_mult;
259 static const int menu_mask = button_mult - 1;
260 static const int button_mask = (1 << dvd::reg_bits) - button_mult;
267 for (unsigned menu_index = 0; menu_index != menus_.size(); ++menu_index)
269 const menu & this_menu = menus_[menu_index];
273 // This is the first (title) menu, displayed when the
274 // disc is first played.
276 " <pgc entry='title' pause='inf'>\n"
278 // Set a default target location if none is set.
279 // This covers first play and use of the "top menu"
282 " g1 = " << 1 + button_mult << ";\n";
287 " <pgc pause='inf'>\n"
291 // When a title finishes or the user presses the "menu"
292 // button, this always jumps to the titleset's root menu.
293 // We want to return the user to the last menu they used.
294 // So we arrange for each titleset's root menu to return
295 // to the vmgm title menu and then dispatch from there to
296 // whatever the correct menu is. We determine the correct
297 // menu by looking at the menu part of g1.
299 file << " g0 = g1 & " << menu_mask << ";\n";
301 // There is a limit of 128 VM instructions in each PGC.
302 // Therefore in each menu's <pre> section we generate
303 // jumps to menus with numbers greater by 512, 256, 128,
304 // ..., 1 where (a) such a menu exists, (b) this menu
305 // number is divisible by twice that increment and (c) the
306 // correct menu is that or a later menu. Thus each menu
307 // has at most 10 such conditional jumps and is reachable
308 // by at most 10 jumps from the title menu. This chain of
309 // jumps might take too long on some players; this has yet
310 // to be investigated.
312 for (std::size_t menu_incr = (menu_mask + 1) / 2;
316 if (menu_index + menu_incr < menus_.size()
317 && (menu_index & (menu_incr * 2 - 1)) == 0)
320 " if (g0 ge " << 1 + menu_index + menu_incr
322 " jump menu " << 1 + menu_index + menu_incr
328 // Highlight the appropriate button.
329 " s8 = g1 & " << button_mask << ";\n"
330 // Forget the link target. If we don't do this, pressing
331 // the "top menu" button will result in jumping back to
335 " <vob file='" << this_menu.vob_temp->get_name() << "'/>\n";
337 for (unsigned button_index = 0;
338 button_index != this_menu.entries.size();
341 const pgc_ref & target = this_menu.entries[button_index].target;
343 file << " <button> ";
345 if (target.type == menu_pgc)
347 unsigned target_button_num;
349 if (target.sub_index)
351 target_button_num = target.sub_index;
355 // Look for a button on the new menu that links
356 // back to this one. If there is one, set that to
357 // be the highlighted button; otherwise, use the
359 const std::vector<menu_entry> & target_menu_entries =
360 menus_[target.index].entries;
361 pgc_ref this_pgc(menu_pgc, menu_index);
362 target_button_num = target_menu_entries.size();
363 while (target_button_num != 1
364 && (target_menu_entries[target_button_num - 1].target
370 // Set new menu location.
371 "g1 = " << (1 + target.index
372 + target_button_num * button_mult) << "; "
373 // Jump to the target menu.
374 "jump menu " << 1 + target.index << "; ";
378 assert(target.type == title_pgc);
381 // Record current menu location.
382 "g2 = " << (1 + menu_index
383 + (1 + button_index) * button_mult) << "; "
384 // Set target chapter number.
385 "g3 = " << target.sub_index << "; "
386 // Jump to the target title.
387 "jump title " << 1 + target.index << "; ";
390 file << "</button>\n";
394 // Some DVD players don't seem to obey pause='inf' so make
406 // Generate a titleset for each title. This appears to make
407 // jumping to titles a whole lot simpler (but limits us to 99
409 for (unsigned title_index = 0;
410 title_index != titles_.size();
415 // Generate a dummy menu so that the "menu" button will
416 // work. This returns to the source menu via the title
419 " <pgc entry='root'>\n"
420 " <pre> g1 = g2; jump vmgm menu; </pre>\n"
427 // Count chapters in the title.
428 unsigned n_chapters = 0;
429 for (vob_list::const_iterator
430 it = titles_[title_index].begin(),
431 end = titles_[title_index].end();
435 // Chapter start times may be specified in the "chapters"
436 // attribute as a comma-separated list. If this is not
437 // specified then the beginning of each file starts a new
438 // chapter. Thus the number of chapters in each file is
439 // the number of commas in the chapter attribute, plus 1.
442 while ((pos = it->chapters.find(',', pos)) != std::string::npos)
449 // Generate jump "table" for chapters.
450 for (unsigned chapter_num = 1;
451 chapter_num <= n_chapters;
454 " if (g3 == " << chapter_num << ")\n"
455 " jump chapter " << chapter_num << ";\n";
460 for (vob_list::const_iterator
461 it = titles_[title_index].begin(),
462 end = titles_[title_index].end();
466 file << " <vob file='" << xml_escape(it->file) << "'";
467 if (!it->chapters.empty())
468 file << " chapters='" << xml_escape(it->chapters) << "'";
469 if (!it->pause.empty())
470 file << " pause='" << xml_escape(it->pause) << "'";
476 // Return to the source menu, but highlight the next button.
477 " g2 = g2 + " << button_mult << ";\n"
491 const char * argv[] = {
493 "-o", output_dir.c_str(),
494 "-x", temp.get_name().c_str(),
498 Glib::spawn_sync(".",
499 Glib::ArrayHandle<std::string>(
500 argv, sizeof(argv)/sizeof(argv[0]),
501 Glib::OWNERSHIP_NONE),
502 Glib::SPAWN_SEARCH_PATH
503 | Glib::SPAWN_STDOUT_TO_DEV_NULL,
507 if (command_result != 0)
508 throw std::runtime_error("dvdauthor failed");