1 // Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
2 // See the file "COPYING" for licence details.
13 #include <gdkmm/pixbuf.h>
14 #include <glibmm/miscutils.h>
15 #include <glibmm/spawn.h>
18 #include "generate_dvd.hpp"
19 #include "xml_utils.hpp"
23 // Return a closeness metric of an "end" rectangle to a "start"
24 // rectangle in the upward (-1) or downward (+1) direction. Given
25 // several possible "end" rectangles, the one that seems visually
26 // closest in the given direction should have the highest value of
27 // this metric. This is necessarily a heuristic function!
28 double directed_closeness(const rectangle & start, const rectangle & end,
31 // The obvious approach is to use the centres of the
32 // rectangles. However, for the "end" rectangle, using the
33 // horizontal position nearest the centre of the "start"
34 // rectangle seems to produce more reasonable results. For
35 // example, if there are two "end" rectangles equally near to
36 // the "start" rectangle in terms of vertical distance and one
37 // of them horizontally overlaps the centre of the "start"
38 // rectangle, we want to pick that one even if the centre of
39 // that rectangle is further away from the centre of the
41 int start_x = (start.left + start.right) / 2;
42 int start_y = (start.top + start.bottom) / 2;
43 int end_y = (end.top + end.bottom) / 2;
45 if (end.right < start_x)
47 else if (end.left > start_x)
52 // Return cosine of angle between the line between these points
53 // and the vertical, divided by the distance between the points
54 // if that is defined and positive; otherwise return 0.
55 int vertical_distance = (end_y - start_y) * y_dir;
56 if (vertical_distance <= 0)
58 double distance_squared =
59 (end_x - start_x) * (end_x - start_x)
60 + (end_y - start_y) * (end_y - start_y);
61 return vertical_distance / distance_squared;
64 std::string temp_file_name(const temp_dir & dir,
65 std::string base_name,
70 std::size_t index_pos = base_name.find("%3d");
71 assert(index_pos != std::string::npos);
72 base_name[index_pos] = '0' + index / 100;
73 base_name[index_pos + 1] = '0' + (index / 10) % 10;
74 base_name[index_pos + 2] = '0' + index % 10;
77 return Glib::build_filename(dir.get_name(), base_name);
80 // We would like to use just a single frame for the menu but this
81 // seems not to be legal or compatible. The minimum length of a
82 // cell is 0.4 seconds but I've seen a static menu using 12 frames
83 // on a commercial "PAL" disc so let's use 12 frames regardless.
84 unsigned menu_duration_frames(const video::frame_params & params)
88 double menu_duration_seconds(const video::frame_params & params)
90 return double(menu_duration_frames(params))
91 / double(params.rate_numer)
92 * double(params.rate_denom);
95 void throw_length_error(const char * limit_type, std::size_t limit)
97 std::ostringstream oss;
98 oss << "exceeded DVD limit: " << limit_type << " > " << limit;
99 throw std::length_error(oss.str());
102 // dvdauthor uses some menu numbers to represent entry points -
103 // distinct from the actual numbers of the menus assigned as those
104 // entry points - resulting in a practical limit of 119 per
105 // domain. This seems to be an oddity of the parser that could be
106 // fixed, but for now we'll have to work with it.
107 const unsigned dvdauthor_anonymous_menus_max = dvd::domain_pgcs_max - 8;
109 // The current navigation code packs menu and button number into a
110 // single register, so the number of menus is limited to
111 // dvd::reg_s8_button_mult - 1 == 1023. However temp_file_name()
112 // is limited to 999 numbered files and it seems pointless to
113 // change it to get another 24.
114 // If people really need more we could use separate menu and
115 // button number registers, possibly allowing up to 11900 menus
116 // (the size of the indirect jump tables might become a problem
118 const unsigned menus_max = 999;
121 dvd_generator::dvd_generator(const video::frame_params & frame_params,
122 mpeg_encoder encoder)
123 : temp_dir_("videolink-"),
124 frame_params_(frame_params),
128 dvd_generator::pgc_ref dvd_generator::add_menu()
130 pgc_ref next_menu(menu_pgc, menus_.size());
132 if (next_menu.index == menus_max)
133 throw_length_error("number of menus", menus_max);
135 menus_.resize(next_menu.index + 1);
139 void dvd_generator::add_menu_entry(unsigned index,
140 const rectangle & area,
141 const pgc_ref & target)
143 assert(index < menus_.size());
144 assert(target.type == menu_pgc && target.index < menus_.size()
145 || target.type == title_pgc && target.index < titles_.size());
147 if (menus_[index].entries.size() == dvd::menu_buttons_max)
148 throw_length_error("number of buttons", dvd::menu_buttons_max);
150 menu_entry new_entry = { area, target };
151 menus_[index].entries.push_back(new_entry);
154 void dvd_generator::generate_menu_vob(unsigned index,
155 Glib::RefPtr<Gdk::Pixbuf> background,
156 Glib::RefPtr<Gdk::Pixbuf> highlights)
159 assert(index < menus_.size());
160 const menu & this_menu = menus_[index];
162 std::string background_name(
163 temp_file_name(temp_dir_, "menu-%3d-back.png", 1 + index));
164 std::cout << "INFO: Saving " << background_name << std::endl;
165 background->save(background_name, "png");
167 std::string highlights_name(
168 temp_file_name(temp_dir_, "menu-%3d-links.png", 1 + index));
169 std::cout << "INFO: Saving " << highlights_name << std::endl;
170 highlights->save(highlights_name, "png");
172 std::string spumux_name(
173 temp_file_name(temp_dir_, "menu-%3d.subpictures", 1 + index));
174 std::ofstream spumux_file(spumux_name.c_str());
178 " <spu force='yes' start='00:00:00.00'\n"
179 " highlight='" << highlights_name << "'\n"
180 " select='" << highlights_name << "'>\n";
181 int button_count = this_menu.entries.size();
182 for (int i = 0; i != button_count; ++i)
184 const menu_entry & this_entry = this_menu.entries[i];
186 // We program left and right to cycle through the buttons in
187 // the order the entries were added. This should result in
188 // left and right behaving like the tab and shift-tab keys
189 // would in the browser. Hopefully that's a sensible order.
190 // We program up and down to behave geometrically.
191 int up_button = i, down_button = i;
192 double up_closeness = 0.0, down_closeness = 0.0;
193 for (int j = 0; j != button_count; ++j)
195 const menu_entry & other_entry = this_menu.entries[j];
196 double closeness = directed_closeness(
197 this_entry.area, other_entry.area, -1);
198 if (closeness > up_closeness)
201 up_closeness = closeness;
205 closeness = directed_closeness(
206 this_entry.area, other_entry.area, 1);
207 if (closeness > down_closeness)
210 down_closeness = closeness;
214 // Pad vertically to even y coordinates since dvdauthor claims
215 // odd values may result in incorrect display.
216 // XXX This may cause overlappping where it wasn't previously
218 spumux_file << " <button"
219 " x0='" << this_entry.area.left << "'"
220 " y0='" << (this_entry.area.top & ~1) << "'"
221 " x1='" << this_entry.area.right << "'"
222 " y1='" << ((this_entry.area.bottom + 1) & ~1) << "'"
223 " left='" << (i == 0 ? button_count : i) << "'"
224 " right='" << 1 + (i + 1) % button_count << "'"
225 " up='" << 1 + up_button << "'"
226 " down='" << 1 + down_button << "'"
235 throw std::runtime_error("Failed to write control file for spumux");
237 std::ostringstream command_stream;
238 unsigned frame_count(menu_duration_frames(frame_params_));
239 if (encoder_ == mpeg_encoder_ffmpeg)
241 for (unsigned i = 0; i != frame_count; ++i)
243 std::string frame_name(background_name);
244 frame_name.push_back('-');
245 frame_name.push_back('0' + i / 10);
246 frame_name.push_back('0' + i % 10);
247 if (symlink(background_name.c_str(), frame_name.c_str()) != 0)
248 throw std::runtime_error(
249 std::string("symlink: ").append(std::strerror(errno)));
252 "ffmpeg -f image2 -vcodec png"
253 " -r " << frame_params_.rate_numer <<
254 "/" << frame_params_.rate_denom <<
255 " -i " << background_name << "-%02d"
256 " -target " << frame_params_.common_name << "-dvd"
257 " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout";
261 assert(encoder_ == mpeg_encoder_mjpegtools_old
262 || encoder_ == mpeg_encoder_mjpegtools_new);
264 << "pngtopnm " << background_name
265 << " | ppmtoy4m -v0 -n" << frame_count << " -F"
266 << frame_params_.rate_numer << ":" << frame_params_.rate_denom
267 << " -A" << frame_params_.pixel_ratio_width
268 << ":" << frame_params_.pixel_ratio_height
270 // The chroma subsampling keywords changed between
271 // versions 1.6.2 and 1.8 of mjpegtools. There is no
272 // keyword that works with both.
273 if (encoder_ == mpeg_encoder_mjpegtools_old)
274 command_stream << "-S420_mpeg2";
276 command_stream << "-S420mpeg2";
278 " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
279 " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
282 << " | spumux -v0 -mdvd " << spumux_name
283 << " > " << temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index);
284 std::string command(command_stream.str());
285 const char * argv[] = {
286 "/bin/sh", "-c", command.c_str(), 0
288 std::cout << "INFO: Running " << command << std::endl;
290 Glib::spawn_sync(".",
291 Glib::ArrayHandle<std::string>(
292 argv, sizeof(argv)/sizeof(argv[0]),
293 Glib::OWNERSHIP_NONE),
294 Glib::SPAWN_STDOUT_TO_DEV_NULL,
298 if (command_result != 0)
299 throw std::runtime_error("spumux pipeline failed");
302 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
304 pgc_ref next_title(title_pgc, titles_.size());
306 // Check against maximum number of titles.
307 if (next_title.index == dvd::titles_max)
308 throw_length_error("number of titles", dvd::titles_max);
310 titles_.resize(next_title.index + 1);
311 titles_[next_title.index].swap(content);
315 void dvd_generator::generate(const std::string & output_dir) const
317 // This function uses a mixture of 0-based and 1-based numbering,
318 // due to the differing conventions of the language and the DVD
319 // format. Variable names ending in "_index" indicate 0-based
320 // indices and variable names ending in "_num" indicate 1-based
323 std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
324 std::ofstream file(name.c_str());
325 file << "<dvdauthor>\n";
327 // We generate code that uses registers in the following way:
330 // g1: Target location when jumping between menus. Top 6 bits are
331 // the button number (like s8) and bottom 10 bits are the menu
332 // number. This is used for selecting the appropriate button
333 // when entering a menu, for completing indirect jumps between
334 // domains, and for jumping to the correct menu after exiting a
335 // title. This is set to 0 in the pre-routine of the target
337 // g2: Current location in menus. This is used for jumping to the
338 // correct menu when the player exits a title.
339 // g3: Target chapter number plus 1 when jumping to a title.
340 // This is used to jump to the correct chapter and to
341 // distinguish between indirect jumps to menus and titles.
342 // This is set to 0 in the pre-routine of the target title.
343 // g4: Source menu location used to jump to a title. This is
344 // compared with g2 to determine whether to increment the
345 // button number if the title is played to the end.
347 static const unsigned button_mult = dvd::reg_s8_button_mult;
348 static const unsigned menu_mask = button_mult - 1;
349 static const unsigned button_mask = (1U << dvd::reg_bits) - button_mult;
351 // Iterate over VMGM and titlesets. For these purposes, we
352 // consider the VMGM to be titleset 0.
354 // We need a titleset for each title, and we may also need titlesets to
355 // hold extra menus if we have too many for the VMGM.
356 // Also, we need at least one titleset.
357 const unsigned titleset_end = std::max<unsigned>(
358 1U + std::max<unsigned>(1U, titles_.size()),
359 (menus_.size() + dvdauthor_anonymous_menus_max - 1)
360 / dvdauthor_anonymous_menus_max);
362 for (unsigned titleset_num = 0;
363 titleset_num != titleset_end;
366 const char * const outer_element_name =
367 titleset_num == 0 ? "vmgm" : "titleset";
368 const bool have_real_title =
369 titleset_num != 0 && titleset_num <= titles_.size();
370 const bool have_real_menus =
371 titleset_num * dvdauthor_anonymous_menus_max < menus_.size();
373 file << " <" << outer_element_name << ">\n"
376 const unsigned menu_begin = titleset_num * dvdauthor_anonymous_menus_max;
377 const unsigned menu_end =
379 ? std::min<unsigned>(
380 (titleset_num + 1) * dvdauthor_anonymous_menus_max,
384 for (unsigned menu_index = menu_begin;
385 menu_index != menu_end;
388 // There are various cases in which menus may be called:
390 // 1. The user follows a direct link to the menu.
391 // 2. The user follows an indirect link to some other menu
392 // and that goes via this menu. This is distinguished
393 // from case 1 by the value of g1. We must jump to or
394 // at least toward the other menu.
395 // 3. The title menu is called when the disc is first
396 // played or the user presses the "top menu" button.
397 // This is distinguished from cases 2 and 3 by g1 == 0.
398 // We make this look like case 1.
399 // 4. The root menu of a titleset is called when the user
400 // follows an indirect link to the title. This is
401 // distinguished from all other cases by g3 != 0. We
402 // must jump to the title.
403 // 5. The root menu of a titleset is called when the title
404 // ends or the user presses the "menu" button during
405 // the title. This is distinguished from cases 1, 2
406 // and 4 by g1 == 0 and g3 == 0. We must jump to the
407 // latest menu (which can turn into case 1 or 2).
409 // Cases 3 and 5 do not apply to the same menus so they
410 // do not need to be distinguished.
416 " <pgc entry='title'>\n"
418 " if (g1 eq 0)\n" // case 3
419 " g1 = " << 1 + button_mult << ";\n";
421 else if (menu_index == titleset_num * dvdauthor_anonymous_menus_max)
425 " <pgc entry='root'>\n"
430 " if (g3 ne 0)\n" // case 4
432 " if (g1 eq 0) {\n" // case 5
434 " jump vmgm menu entry title;\n"
446 if (!have_real_menus)
448 // This is a root menu only reachable from the title.
455 const menu & this_menu = menus_[menu_index];
457 // Detect and handle case 2.
459 // There is a limit of 128 VM instructions in each PGC.
460 // Also, we can't jump to an arbitrary menu in another
461 // domain. Finally, we can't do computed jumps.
462 // Therefore we statically expand and distribute a binary
463 // search across the menus, resulting in a code size of
464 // O(log(menu_count)) in each menu. In practice there are
465 // at most 11 conditional jumps needed in any menu.
467 // The initial bounds of the binary search are strange
468 // because we must ensure that any jump between titlesets
469 // is to the first menu of the titleset, marked as the
472 // Mask target location to get the target menu.
473 file << " g0 = g1 & " << menu_mask << ";\n";
477 top = 16 * dvdauthor_anonymous_menus_max;
480 unsigned middle = (bottom + top) / 2;
481 if (menu_index == bottom && middle < menus_.size())
483 file << " if (g0 ge " << 1 + middle << ")\n"
485 unsigned target_titleset_num =
486 middle / dvdauthor_anonymous_menus_max;
487 if (target_titleset_num != titleset_num)
489 assert(middle % dvdauthor_anonymous_menus_max == 0);
490 file << "titleset " << target_titleset_num
491 << " menu entry root";
496 << 1 + middle % dvdauthor_anonymous_menus_max;
500 if (menu_index >= middle)
508 // Highlight the appropriate button.
509 file << " s8 = g1 & " << button_mask << ";\n";
511 // Copy the target location to the current location and
512 // then clear the target location so that the title menu
513 // can distinguish cases 2 and 3.
521 << temp_file_name(temp_dir_, "menu-%3d.mpeg",
524 // Define a cell covering the whole menu and set a still
525 // time at the end of that, since it seems all players
526 // support that but some ignore a still time set on a PGC.
527 " <cell start='0' end='"
528 << std::fixed << std::setprecision(4)
529 << menu_duration_seconds(frame_params_) << "'"
530 " chapter='yes' pause='inf'/>\n"
533 for (unsigned button_index = 0;
534 button_index != this_menu.entries.size();
537 const pgc_ref & target =
538 this_menu.entries[button_index].target;
540 file << " <button> ";
542 if (target.type == menu_pgc)
544 unsigned target_button_num;
546 if (target.sub_index)
548 target_button_num = target.sub_index;
552 // Look for a button on the new menu that links
553 // back to this one. If there is one, set that to
554 // be the highlighted button; otherwise, use the
556 const std::vector<menu_entry> & target_menu_entries =
557 menus_[target.index].entries;
558 pgc_ref this_pgc(menu_pgc, menu_index);
559 target_button_num = target_menu_entries.size();
560 while (target_button_num != 1
561 && (target_menu_entries[target_button_num - 1].target
566 // Set new menu location.
568 << (1 + target.index + target_button_num * button_mult)
570 // Jump to the target menu.
571 unsigned target_titleset_num =
572 target.index / dvdauthor_anonymous_menus_max;
573 if (target_titleset_num == titleset_num)
576 % dvdauthor_anonymous_menus_max)
578 else if (target_titleset_num == 0)
579 file << "jump vmgm menu entry title; ";
581 file << "jump titleset " << target_titleset_num
582 << " menu entry root; ";
586 assert(target.type == title_pgc);
588 // Record current menu location and set target chapter
591 "g2 = " << (1 + menu_index
592 + (1 + button_index) * button_mult) << "; "
593 "g3 = " << 1 + target.sub_index << "; ";
594 // Jump to the target title, possibly via its titleset's
596 unsigned target_titleset_num = 1 + target.index;
597 if (titleset_num == 0)
598 file << "jump title " << target_titleset_num << "; ";
599 else if (target_titleset_num == titleset_num)
600 file << "jump title 1; ";
602 file << "jump titleset " << target_titleset_num
603 << " menu entry root; ";
606 file << "</button>\n";
613 file << " </menus>\n";
623 // Count chapters in the title.
624 unsigned n_chapters = 0;
625 for (vob_list::const_iterator
626 it = titles_[titleset_num - 1].begin(),
627 end = titles_[titleset_num - 1].end();
631 // Chapter start times may be specified in the "chapters"
632 // attribute as a comma-separated list. If this is not
633 // specified then the beginning of each file starts a new
634 // chapter. Thus the number of chapters in each file is
635 // the number of commas in the chapter attribute, plus 1.
638 while ((pos = it->chapters.find(',', pos)) != std::string::npos)
645 // Move the chapter number to scratch so the root menu can
646 // distinguish cases 4 and 5.
647 file << " g0 = g3; g3 = 0;\n";
649 // Copy the latest menu location for use by the post-routine.
650 file << " g4 = g2;\n";
652 // Jump to the correct chapter.
653 for (unsigned chapter_num = 1;
654 chapter_num <= n_chapters;
657 " if (g0 eq " << 1 + chapter_num << ")\n"
658 " jump chapter " << chapter_num << ";\n";
662 for (vob_list::const_iterator
663 it = titles_[titleset_num - 1].begin(),
664 end = titles_[titleset_num - 1].end();
668 file << " <vob file='" << xml_escape(it->file) << "'";
669 if (!it->chapters.empty())
670 file << " chapters='" << xml_escape(it->chapters) << "'";
671 if (!it->pause.empty())
672 file << " pause='" << xml_escape(it->pause) << "'";
676 // If the user has not exited to the menus and then
677 // resumed the title, set the latest menu location to be
678 // the button after the one that linked to this title.
679 // In any case, return to the (root) menu which will
680 // then jump to the correct menu.
684 " g2 = g2 + " << button_mult << ";\n"
690 else if (titleset_num != 0) // && !have_real_title
692 file << " <titles><pgc/></titles>\n";
695 file << " </" << outer_element_name << ">\n";
698 file << "</dvdauthor>\n";
702 const char * argv[] = {
704 "-o", output_dir.c_str(),
709 Glib::spawn_sync(".",
710 Glib::ArrayHandle<std::string>(
711 argv, sizeof(argv)/sizeof(argv[0]),
712 Glib::OWNERSHIP_NONE),
713 Glib::SPAWN_SEARCH_PATH
714 | Glib::SPAWN_STDOUT_TO_DEV_NULL,
718 if (command_result != 0)
719 throw std::runtime_error("dvdauthor failed");