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 " -i " << background_name <<
252 " -loop_input -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_old
259 || encoder_ == mpeg_encoder_mjpegtools_new);
261 << "pngtopnm " << background_name
262 << " | ppmtoy4m -v0 -n" << menu_duration_frames(frame_params_)
263 << " -F" << frame_params_.rate_numer << ":" << frame_params_.rate_denom
264 << " -A" << frame_params_.pixel_ratio_width
265 << ":" << frame_params_.pixel_ratio_height
267 // The chroma subsampling keywords changed between
268 // versions 1.6.2 and 1.8 of mjpegtools. There is no
269 // keyword that works with both.
270 if (encoder_ == mpeg_encoder_mjpegtools_old)
271 command_stream << "-S420_mpeg2";
273 command_stream << "-S420mpeg2";
275 " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
276 " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
279 << " | spumux -v0 -mdvd " << spumux_name << " > " << output_name;
280 std::string command(command_stream.str());
281 const char * argv[] = {
282 "/bin/sh", "-c", command.c_str(), 0
284 std::cout << "INFO: Running " << command << std::endl;
286 Glib::spawn_sync(".",
287 Glib::ArrayHandle<std::string>(
288 argv, sizeof(argv)/sizeof(argv[0]),
289 Glib::OWNERSHIP_NONE),
290 Glib::SPAWN_STDOUT_TO_DEV_NULL,
294 struct stat stat_buf;
295 if (command_result != 0 || stat(output_name.c_str(), &stat_buf) != 0
296 || stat_buf.st_size == 0)
297 throw std::runtime_error("spumux pipeline failed");
300 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
302 pgc_ref next_title(title_pgc, titles_.size());
304 // Check against maximum number of titles.
305 if (next_title.index == dvd::titles_max)
306 throw_length_error("number of titles", dvd::titles_max);
308 titles_.resize(next_title.index + 1);
309 titles_[next_title.index].swap(content);
313 void dvd_generator::generate(const std::string & output_dir) const
315 // This function uses a mixture of 0-based and 1-based numbering,
316 // due to the differing conventions of the language and the DVD
317 // format. Variable names ending in "_index" indicate 0-based
318 // indices and variable names ending in "_num" indicate 1-based
321 std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
322 std::ofstream file(name.c_str());
323 file << "<dvdauthor>\n";
325 // We generate code that uses registers in the following way:
328 // g1: Target location when jumping between menus. Top 6 bits are
329 // the button number (like s8) and bottom 10 bits are the menu
330 // number. This is used for selecting the appropriate button
331 // when entering a menu, for completing indirect jumps between
332 // domains, and for jumping to the correct menu after exiting a
333 // title. This is set to 0 in the pre-routine of the target
335 // g2: Current location in menus. This is used for jumping to the
336 // correct menu when the player exits a title.
337 // g3: Target chapter number plus 1 when jumping to a title.
338 // This is used to jump to the correct chapter and to
339 // distinguish between indirect jumps to menus and titles.
340 // This is set to 0 in the pre-routine of the target title.
341 // g4: Source menu location used to jump to a title. This is
342 // compared with g2 to determine whether to increment the
343 // button number if the title is played to the end.
345 static const unsigned button_mult = dvd::reg_s8_button_mult;
346 static const unsigned menu_mask = button_mult - 1;
347 static const unsigned button_mask = (1U << dvd::reg_bits) - button_mult;
349 // Iterate over VMGM and titlesets. For these purposes, we
350 // consider the VMGM to be titleset 0.
352 // We need a titleset for each title, and we may also need titlesets to
353 // hold extra menus if we have too many for the VMGM.
354 // Also, we need at least one titleset.
355 const unsigned titleset_end = std::max<unsigned>(
356 1U + std::max<unsigned>(1U, titles_.size()),
357 (menus_.size() + dvdauthor_anonymous_menus_max - 1)
358 / dvdauthor_anonymous_menus_max);
360 for (unsigned titleset_num = 0;
361 titleset_num != titleset_end;
364 const char * const outer_element_name =
365 titleset_num == 0 ? "vmgm" : "titleset";
366 const bool have_real_title =
367 titleset_num != 0 && titleset_num <= titles_.size();
368 const bool have_real_menus =
369 titleset_num * dvdauthor_anonymous_menus_max < menus_.size();
371 file << " <" << outer_element_name << ">\n"
374 const unsigned menu_begin = titleset_num * dvdauthor_anonymous_menus_max;
375 const unsigned menu_end =
377 ? std::min<unsigned>(
378 (titleset_num + 1) * dvdauthor_anonymous_menus_max,
382 for (unsigned menu_index = menu_begin;
383 menu_index != menu_end;
386 // There are various cases in which menus may be called:
388 // 1. The user follows a direct link to the menu.
389 // 2. The user follows an indirect link to some other menu
390 // and that goes via this menu. This is distinguished
391 // from case 1 by the value of g1. We must jump to or
392 // at least toward the other menu.
393 // 3. The title menu is called when the disc is first
394 // played or the user presses the "top menu" button.
395 // This is distinguished from cases 2 and 3 by g1 == 0.
396 // We make this look like case 1.
397 // 4. The root menu of a titleset is called when the user
398 // follows an indirect link to the title. This is
399 // distinguished from all other cases by g3 != 0. We
400 // must jump to the title.
401 // 5. The root menu of a titleset is called when the title
402 // ends or the user presses the "menu" button during
403 // the title. This is distinguished from cases 1, 2
404 // and 4 by g1 == 0 and g3 == 0. We must jump to the
405 // latest menu (which can turn into case 1 or 2).
407 // Cases 3 and 5 do not apply to the same menus so they
408 // do not need to be distinguished.
414 " <pgc entry='title'>\n"
416 " if (g1 eq 0)\n" // case 3
417 " g1 = " << 1 + button_mult << ";\n";
419 else if (menu_index == titleset_num * dvdauthor_anonymous_menus_max)
423 " <pgc entry='root'>\n"
428 " if (g3 ne 0)\n" // case 4
430 " if (g1 eq 0) {\n" // case 5
432 " jump vmgm menu entry title;\n"
444 if (!have_real_menus)
446 // This is a root menu only reachable from the title.
453 const menu & this_menu = menus_[menu_index];
455 // Detect and handle case 2.
457 // There is a limit of 128 VM instructions in each PGC.
458 // Also, we can't jump to an arbitrary menu in another
459 // domain. Finally, we can't do computed jumps.
460 // Therefore we statically expand and distribute a binary
461 // search across the menus, resulting in a code size of
462 // O(log(menu_count)) in each menu. In practice there are
463 // at most 11 conditional jumps needed in any menu.
465 // The initial bounds of the binary search are strange
466 // because we must ensure that any jump between titlesets
467 // is to the first menu of the titleset, marked as the
470 // Mask target location to get the target menu.
471 file << " g0 = g1 & " << menu_mask << ";\n";
475 top = 16 * dvdauthor_anonymous_menus_max;
478 unsigned middle = (bottom + top) / 2;
479 if (menu_index == bottom && middle < menus_.size())
481 file << " if (g0 ge " << 1 + middle << ")\n"
483 unsigned target_titleset_num =
484 middle / dvdauthor_anonymous_menus_max;
485 if (target_titleset_num != titleset_num)
487 assert(middle % dvdauthor_anonymous_menus_max == 0);
488 file << "titleset " << target_titleset_num
489 << " menu entry root";
494 << 1 + middle % dvdauthor_anonymous_menus_max;
498 if (menu_index >= middle)
506 // Highlight the appropriate button.
507 file << " s8 = g1 & " << button_mask << ";\n";
509 // Copy the target location to the current location and
510 // then clear the target location so that the title menu
511 // can distinguish cases 2 and 3.
519 << temp_file_name(temp_dir_, "menu-%3d.mpeg",
522 // Define a cell covering the whole menu and set a still
523 // time at the end of that, since it seems all players
524 // support that but some ignore a still time set on a PGC.
525 " <cell start='0' end='"
526 << std::fixed << std::setprecision(4)
527 << menu_duration_seconds(frame_params_) << "'"
528 " chapter='yes' pause='inf'/>\n"
531 for (unsigned button_index = 0;
532 button_index != this_menu.entries.size();
535 const pgc_ref & target =
536 this_menu.entries[button_index].target;
538 file << " <button> ";
540 if (target.type == menu_pgc)
542 unsigned target_button_num;
544 if (target.sub_index)
546 target_button_num = target.sub_index;
550 // Look for a button on the new menu that links
551 // back to this one. If there is one, set that to
552 // be the highlighted button; otherwise, use the
554 const std::vector<menu_entry> & target_menu_entries =
555 menus_[target.index].entries;
556 pgc_ref this_pgc(menu_pgc, menu_index);
557 target_button_num = target_menu_entries.size();
558 while (target_button_num != 1
559 && (target_menu_entries[target_button_num - 1].target
564 // Set new menu location.
566 << (1 + target.index + target_button_num * button_mult)
568 // Jump to the target menu.
569 unsigned target_titleset_num =
570 target.index / dvdauthor_anonymous_menus_max;
571 if (target_titleset_num == titleset_num)
574 % dvdauthor_anonymous_menus_max)
576 else if (target_titleset_num == 0)
577 file << "jump vmgm menu entry title; ";
579 file << "jump titleset " << target_titleset_num
580 << " menu entry root; ";
584 assert(target.type == title_pgc);
586 // Record current menu location and set target chapter
589 "g2 = " << (1 + menu_index
590 + (1 + button_index) * button_mult) << "; "
591 "g3 = " << 1 + target.sub_index << "; ";
592 // Jump to the target title, possibly via its titleset's
594 unsigned target_titleset_num = 1 + target.index;
595 if (titleset_num == 0)
596 file << "jump title " << target_titleset_num << "; ";
597 else if (target_titleset_num == titleset_num)
598 file << "jump title 1; ";
600 file << "jump titleset " << target_titleset_num
601 << " menu entry root; ";
604 file << "</button>\n";
611 file << " </menus>\n";
621 // Count chapters in the title.
622 unsigned n_chapters = 0;
623 for (vob_list::const_iterator
624 it = titles_[titleset_num - 1].begin(),
625 end = titles_[titleset_num - 1].end();
629 // Chapter start times may be specified in the "chapters"
630 // attribute as a comma-separated list. If this is not
631 // specified then the beginning of each file starts a new
632 // chapter. Thus the number of chapters in each file is
633 // the number of commas in the chapter attribute, plus 1.
636 while ((pos = it->chapters.find(',', pos)) != std::string::npos)
643 // Move the chapter number to scratch so the root menu can
644 // distinguish cases 4 and 5.
645 file << " g0 = g3; g3 = 0;\n";
647 // Copy the latest menu location for use by the post-routine.
648 file << " g4 = g2;\n";
650 // Jump to the correct chapter.
651 for (unsigned chapter_num = 1;
652 chapter_num <= n_chapters;
655 " if (g0 eq " << 1 + chapter_num << ")\n"
656 " jump chapter " << chapter_num << ";\n";
660 for (vob_list::const_iterator
661 it = titles_[titleset_num - 1].begin(),
662 end = titles_[titleset_num - 1].end();
666 file << " <vob file='" << xml_escape(it->file) << "'";
667 if (!it->chapters.empty())
668 file << " chapters='" << xml_escape(it->chapters) << "'";
669 if (!it->pause.empty())
670 file << " pause='" << xml_escape(it->pause) << "'";
674 // If the user has not exited to the menus and then
675 // resumed the title, set the latest menu location to be
676 // the button after the one that linked to this title.
677 // In any case, return to the (root) menu which will
678 // then jump to the correct menu.
682 " g2 = g2 + " << button_mult << ";\n"
688 else if (titleset_num != 0) // && !have_real_title
690 file << " <titles><pgc/></titles>\n";
693 file << " </" << outer_element_name << ">\n";
696 file << "</dvdauthor>\n";
700 const char * argv[] = {
702 "-o", output_dir.c_str(),
707 Glib::spawn_sync(".",
708 Glib::ArrayHandle<std::string>(
709 argv, sizeof(argv)/sizeof(argv[0]),
710 Glib::OWNERSHIP_NONE),
711 Glib::SPAWN_SEARCH_PATH
712 | Glib::SPAWN_STDOUT_TO_DEV_NULL,
716 if (command_result != 0)
717 throw std::runtime_error("dvdauthor failed");