1 // Copyright 2005-8 Ben Hutchings <ben@decadent.org.uk>.
2 // See the file "COPYING" for licence details.
13 #include <sys/types.h>
17 #include <gdkmm/pixbuf.h>
18 #include <glibmm/miscutils.h>
19 #include <glibmm/spawn.h>
22 #include "generate_dvd.hpp"
23 #include "xml_utils.hpp"
27 // Return a closeness metric of an "end" rectangle to a "start"
28 // rectangle in the upward (-1) or downward (+1) direction. Given
29 // several possible "end" rectangles, the one that seems visually
30 // closest in the given direction should have the highest value of
31 // this metric. This is necessarily a heuristic function!
32 double directed_closeness(const rectangle & start, const rectangle & end,
35 // The obvious approach is to use the centres of the
36 // rectangles. However, for the "end" rectangle, using the
37 // horizontal position nearest the centre of the "start"
38 // rectangle seems to produce more reasonable results. For
39 // example, if there are two "end" rectangles equally near to
40 // the "start" rectangle in terms of vertical distance and one
41 // of them horizontally overlaps the centre of the "start"
42 // rectangle, we want to pick that one even if the centre of
43 // that rectangle is further away from the centre of the
45 int start_x = (start.left + start.right) / 2;
46 int start_y = (start.top + start.bottom) / 2;
47 int end_y = (end.top + end.bottom) / 2;
49 if (end.right < start_x)
51 else if (end.left > start_x)
56 // Return cosine of angle between the line between these points
57 // and the vertical, divided by the distance between the points
58 // if that is defined and positive; otherwise return 0.
59 int vertical_distance = (end_y - start_y) * y_dir;
60 if (vertical_distance <= 0)
62 double distance_squared =
63 (end_x - start_x) * (end_x - start_x)
64 + (end_y - start_y) * (end_y - start_y);
65 return vertical_distance / distance_squared;
68 std::string temp_file_name(const temp_dir & dir,
69 std::string base_name,
74 std::size_t index_pos = base_name.find("%3d");
75 assert(index_pos != std::string::npos);
76 base_name[index_pos] = '0' + index / 100;
77 base_name[index_pos + 1] = '0' + (index / 10) % 10;
78 base_name[index_pos + 2] = '0' + index % 10;
81 return Glib::build_filename(dir.get_name(), base_name);
84 // We would like to use just a single frame for the menu but this
85 // seems not to be legal or compatible. The minimum length of a
86 // cell is 0.4 seconds but I've seen a static menu using 12 frames
87 // on a commercial "PAL" disc so let's use 12 frames regardless.
88 unsigned menu_duration_frames(const video::frame_params & params)
92 double menu_duration_seconds(const video::frame_params & params)
94 return double(menu_duration_frames(params))
95 / double(params.rate_numer)
96 * double(params.rate_denom);
99 void throw_length_error(const char * limit_type, std::size_t limit)
101 std::ostringstream oss;
102 oss << "exceeded DVD limit: " << limit_type << " > " << limit;
103 throw std::length_error(oss.str());
106 // dvdauthor uses some menu numbers to represent entry points -
107 // distinct from the actual numbers of the menus assigned as those
108 // entry points - resulting in a practical limit of 119 per
109 // domain. This seems to be an oddity of the parser that could be
110 // fixed, but for now we'll have to work with it.
111 const unsigned dvdauthor_anonymous_menus_max = dvd::domain_pgcs_max - 8;
113 // The current navigation code packs menu and button number into a
114 // single register, so the number of menus is limited to
115 // dvd::reg_s8_button_mult - 1 == 1023. However temp_file_name()
116 // is limited to 999 numbered files and it seems pointless to
117 // change it to get another 24.
118 // If people really need more we could use separate menu and
119 // button number registers, possibly allowing up to 11900 menus
120 // (the size of the indirect jump tables might become a problem
122 const unsigned menus_max = 999;
125 dvd_generator::dvd_generator(const video::frame_params & frame_params,
126 mpeg_encoder encoder)
127 : temp_dir_("videolink-"),
128 frame_params_(frame_params),
132 dvd_generator::pgc_ref dvd_generator::add_menu()
134 pgc_ref next_menu(menu_pgc, menus_.size());
136 if (next_menu.index == menus_max)
137 throw_length_error("number of menus", menus_max);
139 menus_.resize(next_menu.index + 1);
143 void dvd_generator::add_menu_entry(unsigned index,
144 const rectangle & area,
145 const pgc_ref & target)
147 assert(index < menus_.size());
148 assert(target.type == menu_pgc && target.index < menus_.size()
149 || target.type == title_pgc && target.index < titles_.size());
151 if (menus_[index].entries.size() == dvd::menu_buttons_max)
152 throw_length_error("number of buttons", dvd::menu_buttons_max);
154 menu_entry new_entry = { area, target };
155 menus_[index].entries.push_back(new_entry);
158 void dvd_generator::generate_menu_vob(unsigned index,
159 Glib::RefPtr<Gdk::Pixbuf> background,
160 Glib::RefPtr<Gdk::Pixbuf> highlights)
163 assert(index < menus_.size());
164 const menu & this_menu = menus_[index];
166 std::string background_name(
167 temp_file_name(temp_dir_, "menu-%3d-back.png", 1 + index));
168 std::cout << "INFO: Saving " << background_name << std::endl;
169 background->save(background_name, "png");
171 std::string highlights_name(
172 temp_file_name(temp_dir_, "menu-%3d-links.png", 1 + index));
173 std::cout << "INFO: Saving " << highlights_name << std::endl;
174 highlights->save(highlights_name, "png");
176 std::string spumux_name(
177 temp_file_name(temp_dir_, "menu-%3d.subpictures", 1 + index));
178 std::ofstream spumux_file(spumux_name.c_str());
182 " <spu force='yes' start='00:00:00.00'\n"
183 " highlight='" << highlights_name << "'\n"
184 " select='" << highlights_name << "'>\n";
185 int button_count = this_menu.entries.size();
186 for (int i = 0; i != button_count; ++i)
188 const menu_entry & this_entry = this_menu.entries[i];
190 // We program left and right to cycle through the buttons in
191 // the order the entries were added. This should result in
192 // left and right behaving like the tab and shift-tab keys
193 // would in the browser. Hopefully that's a sensible order.
194 // We program up and down to behave geometrically.
195 int up_button = i, down_button = i;
196 double up_closeness = 0.0, down_closeness = 0.0;
197 for (int j = 0; j != button_count; ++j)
199 const menu_entry & other_entry = this_menu.entries[j];
200 double closeness = directed_closeness(
201 this_entry.area, other_entry.area, -1);
202 if (closeness > up_closeness)
205 up_closeness = closeness;
209 closeness = directed_closeness(
210 this_entry.area, other_entry.area, 1);
211 if (closeness > down_closeness)
214 down_closeness = closeness;
218 // Pad vertically to even y coordinates since dvdauthor claims
219 // odd values may result in incorrect display.
220 // XXX This may cause overlappping where it wasn't previously
222 spumux_file << " <button"
223 " x0='" << this_entry.area.left << "'"
224 " y0='" << (this_entry.area.top & ~1) << "'"
225 " x1='" << this_entry.area.right << "'"
226 " y1='" << ((this_entry.area.bottom + 1) & ~1) << "'"
227 " left='" << (i == 0 ? button_count : i) << "'"
228 " right='" << 1 + (i + 1) % button_count << "'"
229 " up='" << 1 + up_button << "'"
230 " down='" << 1 + down_button << "'"
239 throw std::runtime_error("Failed to write control file for spumux");
241 std::string output_name(
242 temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index));
244 std::ostringstream command_stream;
245 if (encoder_ == mpeg_encoder_ffmpeg)
248 "ffmpeg -f image2 -vcodec png"
249 " -r " << frame_params_.rate_numer <<
250 "/" << frame_params_.rate_denom <<
251 " -loop_input -i " << background_name <<
252 " -t " << menu_duration_seconds(frame_params_) <<
253 " -target " << frame_params_.common_name << "-dvd"
254 " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout";
258 assert(encoder_ == mpeg_encoder_mjpegtools);
260 << "pngtopnm " << background_name
261 << " | ppmtoy4m -v0 -n" << menu_duration_frames(frame_params_)
262 << " -F" << frame_params_.rate_numer << ":" << frame_params_.rate_denom
263 << " -A" << frame_params_.pixel_ratio_width
264 << ":" << frame_params_.pixel_ratio_height
266 " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
267 " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
270 << " | spumux -v0 -mdvd " << spumux_name << " > " << output_name;
271 std::string command(command_stream.str());
272 const char * argv[] = {
273 "/bin/sh", "-c", command.c_str(), 0
275 std::cout << "INFO: Running " << command << std::endl;
277 Glib::spawn_sync(".",
278 Glib::ArrayHandle<std::string>(
279 argv, sizeof(argv)/sizeof(argv[0]),
280 Glib::OWNERSHIP_NONE),
281 Glib::SPAWN_STDOUT_TO_DEV_NULL,
285 struct stat stat_buf;
286 if (command_result != 0 || stat(output_name.c_str(), &stat_buf) != 0
287 || stat_buf.st_size == 0)
288 throw std::runtime_error("spumux pipeline failed");
291 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
293 pgc_ref next_title(title_pgc, titles_.size());
295 // Check against maximum number of titles.
296 if (next_title.index == dvd::titles_max)
297 throw_length_error("number of titles", dvd::titles_max);
299 titles_.resize(next_title.index + 1);
300 titles_[next_title.index].swap(content);
304 void dvd_generator::generate(const std::string & output_dir) const
306 // This function uses a mixture of 0-based and 1-based numbering,
307 // due to the differing conventions of the language and the DVD
308 // format. Variable names ending in "_index" indicate 0-based
309 // indices and variable names ending in "_num" indicate 1-based
312 std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
313 std::ofstream file(name.c_str());
314 file << "<dvdauthor>\n";
316 // We generate code that uses registers in the following way:
319 // g1: Target location when jumping between menus. Top 6 bits are
320 // the button number (like s8) and bottom 10 bits are the menu
321 // number. This is used for selecting the appropriate button
322 // when entering a menu, for completing indirect jumps between
323 // domains, and for jumping to the correct menu after exiting a
324 // title. This is set to 0 in the pre-routine of the target
326 // g2: Current location in menus. This is used for jumping to the
327 // correct menu when the player exits a title.
328 // g3: Target chapter number plus 1 when jumping to a title.
329 // This is used to jump to the correct chapter and to
330 // distinguish between indirect jumps to menus and titles.
331 // This is set to 0 in the pre-routine of the target title.
332 // g4: Source menu location used to jump to a title. This is
333 // compared with g2 to determine whether to increment the
334 // button number if the title is played to the end.
336 static const unsigned button_mult = dvd::reg_s8_button_mult;
337 static const unsigned menu_mask = button_mult - 1;
338 static const unsigned button_mask = (1U << dvd::reg_bits) - button_mult;
340 // Iterate over VMGM and titlesets. For these purposes, we
341 // consider the VMGM to be titleset 0.
343 // We need a titleset for each title, and we may also need titlesets to
344 // hold extra menus if we have too many for the VMGM.
345 // Also, we need at least one titleset.
346 const unsigned titleset_end = std::max<unsigned>(
347 1U + std::max<unsigned>(1U, titles_.size()),
348 (menus_.size() + dvdauthor_anonymous_menus_max - 1)
349 / dvdauthor_anonymous_menus_max);
351 for (unsigned titleset_num = 0;
352 titleset_num != titleset_end;
355 const char * const outer_element_name =
356 titleset_num == 0 ? "vmgm" : "titleset";
357 const bool have_real_title =
358 titleset_num != 0 && titleset_num <= titles_.size();
359 const bool have_real_menus =
360 titleset_num * dvdauthor_anonymous_menus_max < menus_.size();
362 file << " <" << outer_element_name << ">\n"
365 const unsigned menu_begin = titleset_num * dvdauthor_anonymous_menus_max;
366 const unsigned menu_end =
368 ? std::min<unsigned>(
369 (titleset_num + 1) * dvdauthor_anonymous_menus_max,
373 for (unsigned menu_index = menu_begin;
374 menu_index != menu_end;
377 // There are various cases in which menus may be called:
379 // 1. The user follows a direct link to the menu.
380 // 2. The user follows an indirect link to some other menu
381 // and that goes via this menu. This is distinguished
382 // from case 1 by the value of g1. We must jump to or
383 // at least toward the other menu.
384 // 3. The title menu is called when the disc is first
385 // played or the user presses the "top menu" button.
386 // This is distinguished from cases 2 and 3 by g1 == 0.
387 // We make this look like case 1.
388 // 4. The root menu of a titleset is called when the user
389 // follows an indirect link to the title. This is
390 // distinguished from all other cases by g3 != 0. We
391 // must jump to the title.
392 // 5. The root menu of a titleset is called when the title
393 // ends or the user presses the "menu" button during
394 // the title. This is distinguished from cases 1, 2
395 // and 4 by g1 == 0 and g3 == 0. We must jump to the
396 // latest menu (which can turn into case 1 or 2).
398 // Cases 3 and 5 do not apply to the same menus so they
399 // do not need to be distinguished.
405 " <pgc entry='title'>\n"
407 " if (g1 eq 0)\n" // case 3
408 " g1 = " << 1 + button_mult << ";\n";
410 else if (menu_index == titleset_num * dvdauthor_anonymous_menus_max)
414 " <pgc entry='root'>\n"
419 " if (g3 ne 0)\n" // case 4
421 " if (g1 eq 0) {\n" // case 5
423 " jump vmgm menu entry title;\n"
435 if (!have_real_menus)
437 // This is a root menu only reachable from the title.
444 const menu & this_menu = menus_[menu_index];
446 // Detect and handle case 2.
448 // There is a limit of 128 VM instructions in each PGC.
449 // Also, we can't jump to an arbitrary menu in another
450 // domain. Finally, we can't do computed jumps.
451 // Therefore we statically expand and distribute a binary
452 // search across the menus, resulting in a code size of
453 // O(log(menu_count)) in each menu. In practice there are
454 // at most 11 conditional jumps needed in any menu.
456 // The initial bounds of the binary search are strange
457 // because we must ensure that any jump between titlesets
458 // is to the first menu of the titleset, marked as the
461 // Mask target location to get the target menu.
462 file << " g0 = g1 & " << menu_mask << ";\n";
466 top = 16 * dvdauthor_anonymous_menus_max;
469 unsigned middle = (bottom + top) / 2;
470 if (menu_index == bottom && middle < menus_.size())
472 file << " if (g0 ge " << 1 + middle << ")\n"
474 unsigned target_titleset_num =
475 middle / dvdauthor_anonymous_menus_max;
476 if (target_titleset_num != titleset_num)
478 assert(middle % dvdauthor_anonymous_menus_max == 0);
479 file << "titleset " << target_titleset_num
480 << " menu entry root";
485 << 1 + middle % dvdauthor_anonymous_menus_max;
489 if (menu_index >= middle)
497 // Highlight the appropriate button.
498 file << " s8 = g1 & " << button_mask << ";\n";
500 // Copy the target location to the current location and
501 // then clear the target location so that the title menu
502 // can distinguish cases 2 and 3.
510 << temp_file_name(temp_dir_, "menu-%3d.mpeg",
513 // Define a cell covering the whole menu and set a still
514 // time at the end of that, since it seems all players
515 // support that but some ignore a still time set on a PGC.
516 " <cell start='0' end='"
517 << std::fixed << std::setprecision(4)
518 << menu_duration_seconds(frame_params_) << "'"
519 " chapter='yes' pause='inf'/>\n"
522 for (unsigned button_index = 0;
523 button_index != this_menu.entries.size();
526 const pgc_ref & target =
527 this_menu.entries[button_index].target;
529 file << " <button> ";
531 if (target.type == menu_pgc)
533 unsigned target_button_num;
535 if (target.sub_index)
537 target_button_num = target.sub_index;
541 // Look for a button on the new menu that links
542 // back to this one. If there is one, set that to
543 // be the highlighted button; otherwise, use the
545 const std::vector<menu_entry> & target_menu_entries =
546 menus_[target.index].entries;
547 pgc_ref this_pgc(menu_pgc, menu_index);
548 target_button_num = target_menu_entries.size();
549 while (target_button_num != 1
550 && (target_menu_entries[target_button_num - 1].target
555 // Set new menu location.
557 << (1 + target.index + target_button_num * button_mult)
559 // Jump to the target menu.
560 unsigned target_titleset_num =
561 target.index / dvdauthor_anonymous_menus_max;
562 if (target_titleset_num == titleset_num)
565 % dvdauthor_anonymous_menus_max)
567 else if (target_titleset_num == 0)
568 file << "jump vmgm menu entry title; ";
570 file << "jump titleset " << target_titleset_num
571 << " menu entry root; ";
575 assert(target.type == title_pgc);
577 // Record current menu location and set target chapter
580 "g2 = " << (1 + menu_index
581 + (1 + button_index) * button_mult) << "; "
582 "g3 = " << 1 + target.sub_index << "; ";
583 // Jump to the target title, possibly via its titleset's
585 unsigned target_titleset_num = 1 + target.index;
586 if (titleset_num == 0)
587 file << "jump title " << target_titleset_num << "; ";
588 else if (target_titleset_num == titleset_num)
589 file << "jump title 1; ";
591 file << "jump titleset " << target_titleset_num
592 << " menu entry root; ";
595 file << "</button>\n";
602 file << " </menus>\n";
612 // Count chapters in the title.
613 unsigned n_chapters = 0;
614 for (vob_list::const_iterator
615 it = titles_[titleset_num - 1].begin(),
616 end = titles_[titleset_num - 1].end();
620 // Chapter start times may be specified in the "chapters"
621 // attribute as a comma-separated list. If this is not
622 // specified then the beginning of each file starts a new
623 // chapter. Thus the number of chapters in each file is
624 // the number of commas in the chapter attribute, plus 1.
627 while ((pos = it->chapters.find(',', pos)) != std::string::npos)
634 // Move the chapter number to scratch so the root menu can
635 // distinguish cases 4 and 5.
636 file << " g0 = g3; g3 = 0;\n";
638 // Copy the latest menu location for use by the post-routine.
639 file << " g4 = g2;\n";
641 // Jump to the correct chapter.
642 for (unsigned chapter_num = 1;
643 chapter_num <= n_chapters;
646 " if (g0 eq " << 1 + chapter_num << ")\n"
647 " jump chapter " << chapter_num << ";\n";
651 for (vob_list::const_iterator
652 it = titles_[titleset_num - 1].begin(),
653 end = titles_[titleset_num - 1].end();
657 file << " <vob file='" << xml_escape(it->file) << "'";
658 if (!it->chapters.empty())
659 file << " chapters='" << xml_escape(it->chapters) << "'";
660 if (!it->pause.empty())
661 file << " pause='" << xml_escape(it->pause) << "'";
665 // If the user has not exited to the menus and then
666 // resumed the title, set the latest menu location to be
667 // the button after the one that linked to this title.
668 // In any case, return to the (root) menu which will
669 // then jump to the correct menu.
673 " g2 = g2 + " << button_mult << ";\n"
679 else if (titleset_num != 0) // && !have_real_title
681 file << " <titles><pgc/></titles>\n";
684 file << " </" << outer_element_name << ">\n";
687 file << "</dvdauthor>\n";
691 const char * argv[] = {
693 "-o", output_dir.c_str(),
698 Glib::spawn_sync(".",
699 Glib::ArrayHandle<std::string>(
700 argv, sizeof(argv)/sizeof(argv[0]),
701 Glib::OWNERSHIP_NONE),
702 Glib::SPAWN_SEARCH_PATH
703 | Glib::SPAWN_STDOUT_TO_DEV_NULL,
707 if (command_result != 0)
708 throw std::runtime_error("dvdauthor failed");