1 // Copyright 2005-8 Ben Hutchings <ben@decadent.org.uk>.
2 // See the file "COPYING" for licence details.
14 #include <sys/types.h>
18 #include <gdkmm/pixbuf.h>
19 #include <glibmm/miscutils.h>
20 #include <glibmm/spawn.h>
23 #include "generate_dvd.hpp"
24 #include "xml_utils.hpp"
28 // Return a closeness metric of an "end" rectangle to a "start"
29 // rectangle in the upward (-1) or downward (+1) direction. Given
30 // several possible "end" rectangles, the one that seems visually
31 // closest in the given direction should have the highest value of
32 // this metric. This is necessarily a heuristic function!
33 double directed_closeness(const rectangle & start, const rectangle & end,
36 // The obvious approach is to use the centres of the
37 // rectangles. However, for the "end" rectangle, using the
38 // horizontal position nearest the centre of the "start"
39 // rectangle seems to produce more reasonable results. For
40 // example, if there are two "end" rectangles equally near to
41 // the "start" rectangle in terms of vertical distance and one
42 // of them horizontally overlaps the centre of the "start"
43 // rectangle, we want to pick that one even if the centre of
44 // that rectangle is further away from the centre of the
46 int start_x = (start.left + start.right) / 2;
47 int start_y = (start.top + start.bottom) / 2;
48 int end_y = (end.top + end.bottom) / 2;
50 if (end.right < start_x)
52 else if (end.left > start_x)
57 // Return cosine of angle between the line between these points
58 // and the vertical, divided by the distance between the points
59 // if that is defined and positive; otherwise return 0.
60 int vertical_distance = (end_y - start_y) * y_dir;
61 if (vertical_distance <= 0)
63 double distance_squared =
64 (end_x - start_x) * (end_x - start_x)
65 + (end_y - start_y) * (end_y - start_y);
66 return vertical_distance / distance_squared;
69 std::string temp_file_name(const temp_dir & dir,
70 std::string base_name,
75 std::size_t index_pos = base_name.find("%3d");
76 assert(index_pos != std::string::npos);
77 base_name[index_pos] = '0' + index / 100;
78 base_name[index_pos + 1] = '0' + (index / 10) % 10;
79 base_name[index_pos + 2] = '0' + index % 10;
82 return Glib::build_filename(dir.get_name(), base_name);
85 // We would like to use just a single frame for the menu but this
86 // seems not to be legal or compatible. The minimum length of a
87 // cell is 0.4 seconds but I've seen a static menu using 12 frames
88 // on a commercial "PAL" disc so let's use 12 frames regardless.
89 unsigned menu_duration_frames(const video::frame_params & params)
93 double menu_duration_seconds(const video::frame_params & params)
95 return double(menu_duration_frames(params))
96 / double(params.rate_numer)
97 * double(params.rate_denom);
100 void throw_length_error(const char * limit_type, std::size_t limit)
102 std::ostringstream oss;
103 oss << "exceeded DVD limit: " << limit_type << " > " << limit;
104 throw std::length_error(oss.str());
107 // dvdauthor uses some menu numbers to represent entry points -
108 // distinct from the actual numbers of the menus assigned as those
109 // entry points - resulting in a practical limit of 119 per
110 // domain. This seems to be an oddity of the parser that could be
111 // fixed, but for now we'll have to work with it.
112 const unsigned dvdauthor_anonymous_menus_max = dvd::domain_pgcs_max - 8;
114 // The current navigation code packs menu and button number into a
115 // single register, so the number of menus is limited to
116 // dvd::reg_s8_button_mult - 1 == 1023. However temp_file_name()
117 // is limited to 999 numbered files and it seems pointless to
118 // change it to get another 24.
119 // If people really need more we could use separate menu and
120 // button number registers, possibly allowing up to 11900 menus
121 // (the size of the indirect jump tables might become a problem
123 const unsigned menus_max = 999;
126 dvd_generator::dvd_generator(const video::frame_params & frame_params,
127 mpeg_encoder encoder)
128 : temp_dir_("videolink-"),
129 frame_params_(frame_params),
133 dvd_generator::pgc_ref dvd_generator::add_menu()
135 pgc_ref next_menu(menu_pgc, menus_.size());
137 if (next_menu.index == menus_max)
138 throw_length_error("number of menus", menus_max);
140 menus_.resize(next_menu.index + 1);
144 void dvd_generator::add_menu_entry(unsigned index,
145 const rectangle & area,
146 const pgc_ref & target)
148 assert(index < menus_.size());
149 assert(target.type == menu_pgc && target.index < menus_.size()
150 || target.type == title_pgc && target.index < titles_.size());
152 if (menus_[index].entries.size() == dvd::menu_buttons_max)
153 throw_length_error("number of buttons", dvd::menu_buttons_max);
155 menu_entry new_entry = { area, target };
156 menus_[index].entries.push_back(new_entry);
159 void dvd_generator::generate_menu_vob(unsigned index,
160 Glib::RefPtr<Gdk::Pixbuf> background,
161 Glib::RefPtr<Gdk::Pixbuf> highlights)
164 assert(index < menus_.size());
165 const menu & this_menu = menus_[index];
167 std::string background_name(
168 temp_file_name(temp_dir_, "menu-%3d-back.png", 1 + index));
169 std::cout << "INFO: Saving " << background_name << std::endl;
170 background->save(background_name, "png");
172 std::string highlights_name(
173 temp_file_name(temp_dir_, "menu-%3d-links.png", 1 + index));
174 std::cout << "INFO: Saving " << highlights_name << std::endl;
175 highlights->save(highlights_name, "png");
177 std::string spumux_name(
178 temp_file_name(temp_dir_, "menu-%3d.subpictures", 1 + index));
179 std::ofstream spumux_file(spumux_name.c_str());
183 " <spu force='yes' start='00:00:00.00'\n"
184 " highlight='" << highlights_name << "'\n"
185 " select='" << highlights_name << "'>\n";
186 int button_count = this_menu.entries.size();
187 for (int i = 0; i != button_count; ++i)
189 const menu_entry & this_entry = this_menu.entries[i];
191 // We program left and right to cycle through the buttons in
192 // the order the entries were added. This should result in
193 // left and right behaving like the tab and shift-tab keys
194 // would in the browser. Hopefully that's a sensible order.
195 // We program up and down to behave geometrically.
196 int up_button = i, down_button = i;
197 double up_closeness = 0.0, down_closeness = 0.0;
198 for (int j = 0; j != button_count; ++j)
200 const menu_entry & other_entry = this_menu.entries[j];
201 double closeness = directed_closeness(
202 this_entry.area, other_entry.area, -1);
203 if (closeness > up_closeness)
206 up_closeness = closeness;
210 closeness = directed_closeness(
211 this_entry.area, other_entry.area, 1);
212 if (closeness > down_closeness)
215 down_closeness = closeness;
219 // Pad vertically to even y coordinates since dvdauthor claims
220 // odd values may result in incorrect display.
221 // XXX This may cause overlappping where it wasn't previously
223 spumux_file << " <button"
224 " x0='" << this_entry.area.left << "'"
225 " y0='" << (this_entry.area.top & ~1) << "'"
226 " x1='" << this_entry.area.right << "'"
227 " y1='" << ((this_entry.area.bottom + 1) & ~1) << "'"
228 " left='" << (i == 0 ? button_count : i) << "'"
229 " right='" << 1 + (i + 1) % button_count << "'"
230 " up='" << 1 + up_button << "'"
231 " down='" << 1 + down_button << "'"
240 throw std::runtime_error("Failed to write control file for spumux");
242 std::string output_name(
243 temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index));
245 std::ostringstream command_stream;
246 if (encoder_ == mpeg_encoder_ffmpeg)
249 "ffmpeg -f image2 -vcodec png"
250 " -r " << frame_params_.rate_numer <<
251 "/" << frame_params_.rate_denom <<
252 " -loop_input -i " << background_name <<
253 " -t " << menu_duration_seconds(frame_params_) <<
254 " -target " << frame_params_.common_name << "-dvd"
255 " -aspect 4:3 -an -y /dev/stdout";
259 assert(encoder_ == mpeg_encoder_mjpegtools);
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 " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
268 " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
271 << " | spumux -v0 -mdvd " << spumux_name << " > " << output_name;
272 std::string command(command_stream.str());
273 const char * argv[] = {
274 "/bin/sh", "-c", command.c_str(), 0
276 std::cout << "INFO: Running " << command << std::endl;
278 Glib::spawn_sync(".",
279 Glib::ArrayHandle<std::string>(
280 argv, sizeof(argv)/sizeof(argv[0]),
281 Glib::OWNERSHIP_NONE),
282 Glib::SPAWN_STDOUT_TO_DEV_NULL,
286 struct stat stat_buf;
287 if (command_result != 0 || stat(output_name.c_str(), &stat_buf) != 0
288 || stat_buf.st_size == 0)
289 throw std::runtime_error("spumux pipeline failed");
292 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
294 pgc_ref next_title(title_pgc, titles_.size());
296 // Check against maximum number of titles.
297 if (next_title.index == dvd::titles_max)
298 throw_length_error("number of titles", dvd::titles_max);
300 titles_.resize(next_title.index + 1);
301 titles_[next_title.index].swap(content);
305 void dvd_generator::generate(const std::string & output_dir) const
307 // This function uses a mixture of 0-based and 1-based numbering,
308 // due to the differing conventions of the language and the DVD
309 // format. Variable names ending in "_index" indicate 0-based
310 // indices and variable names ending in "_num" indicate 1-based
313 std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
314 std::ofstream file(name.c_str());
315 file << "<dvdauthor>\n";
317 // We generate code that uses registers in the following way:
320 // g1: Target location when jumping between menus. Top 6 bits are
321 // the button number (like s8) and bottom 10 bits are the menu
322 // number. This is used for selecting the appropriate button
323 // when entering a menu, for completing indirect jumps between
324 // domains, and for jumping to the correct menu after exiting a
325 // title. This is set to 0 in the pre-routine of the target
327 // g2: Current location in menus. This is used for jumping to the
328 // correct menu when the player exits a title.
329 // g3: Target chapter number plus 1 when jumping to a title.
330 // This is used to jump to the correct chapter and to
331 // distinguish between indirect jumps to menus and titles.
332 // This is set to 0 in the pre-routine of the target title.
333 // g4: Source menu location used to jump to a title. This is
334 // compared with g2 to determine whether to increment the
335 // button number if the title is played to the end.
337 static const unsigned button_mult = dvd::reg_s8_button_mult;
338 static const unsigned menu_mask = button_mult - 1;
339 static const unsigned button_mask = (1U << dvd::reg_bits) - button_mult;
341 // Iterate over VMGM and titlesets. For these purposes, we
342 // consider the VMGM to be titleset 0.
344 // We need a titleset for each title, and we may also need titlesets to
345 // hold extra menus if we have too many for the VMGM.
346 // Also, we need at least one titleset.
347 const unsigned titleset_end = std::max<unsigned>(
348 1U + std::max<unsigned>(1U, titles_.size()),
349 (menus_.size() + dvdauthor_anonymous_menus_max - 1)
350 / dvdauthor_anonymous_menus_max);
352 for (unsigned titleset_num = 0;
353 titleset_num != titleset_end;
356 const char * const outer_element_name =
357 titleset_num == 0 ? "vmgm" : "titleset";
358 const bool have_real_title =
359 titleset_num != 0 && titleset_num <= titles_.size();
360 const bool have_real_menus =
361 titleset_num * dvdauthor_anonymous_menus_max < menus_.size();
363 file << " <" << outer_element_name << ">\n"
366 const unsigned menu_begin = titleset_num * dvdauthor_anonymous_menus_max;
367 const unsigned menu_end =
369 ? std::min<unsigned>(
370 (titleset_num + 1) * dvdauthor_anonymous_menus_max,
374 for (unsigned menu_index = menu_begin;
375 menu_index != menu_end;
378 // There are various cases in which menus may be called:
380 // 1. The user follows a direct link to the menu.
381 // 2. The user follows an indirect link to some other menu
382 // and that goes via this menu. This is distinguished
383 // from case 1 by the value of g1. We must jump to or
384 // at least toward the other menu.
385 // 3. The title menu is called when the disc is first
386 // played or the user presses the "top menu" button.
387 // This is distinguished from cases 2 and 3 by g1 == 0.
388 // We make this look like case 1.
389 // 4. The root menu of a titleset is called when the user
390 // follows an indirect link to the title. This is
391 // distinguished from all other cases by g3 != 0. We
392 // must jump to the title.
393 // 5. The root menu of a titleset is called when the title
394 // ends or the user presses the "menu" button during
395 // the title. This is distinguished from cases 1, 2
396 // and 4 by g1 == 0 and g3 == 0. We must jump to the
397 // latest menu (which can turn into case 1 or 2).
399 // Cases 3 and 5 do not apply to the same menus so they
400 // do not need to be distinguished.
406 " <pgc entry='title'>\n"
408 " if (g1 eq 0)\n" // case 3
409 " g1 = " << 1 + button_mult << ";\n";
411 else if (menu_index == titleset_num * dvdauthor_anonymous_menus_max)
415 " <pgc entry='root'>\n"
420 " if (g3 ne 0)\n" // case 4
422 " if (g1 eq 0) {\n" // case 5
424 " jump vmgm menu entry title;\n"
436 if (!have_real_menus)
438 // This is a root menu only reachable from the title.
445 const menu & this_menu = menus_[menu_index];
447 // Detect and handle case 2.
449 // There is a limit of 128 VM instructions in each PGC.
450 // Also, we can't jump to an arbitrary menu in another
451 // domain. Finally, we can't do computed jumps.
452 // Therefore we statically expand and distribute a binary
453 // search across the menus, resulting in a code size of
454 // O(log(menu_count)) in each menu. In practice there are
455 // at most 11 conditional jumps needed in any menu.
457 // The initial bounds of the binary search are strange
458 // because we must ensure that any jump between titlesets
459 // is to the first menu of the titleset, marked as the
462 // Mask target location to get the target menu.
463 file << " g0 = g1 & " << menu_mask << ";\n";
467 top = 16 * dvdauthor_anonymous_menus_max;
470 unsigned middle = (bottom + top) / 2;
471 if (menu_index == bottom && middle < menus_.size())
473 file << " if (g0 ge " << 1 + middle << ")\n"
475 unsigned target_titleset_num =
476 middle / dvdauthor_anonymous_menus_max;
477 if (target_titleset_num != titleset_num)
479 assert(middle % dvdauthor_anonymous_menus_max == 0);
480 file << "titleset " << target_titleset_num
481 << " menu entry root";
486 << 1 + middle % dvdauthor_anonymous_menus_max;
490 if (menu_index >= middle)
498 // Highlight the appropriate button.
499 file << " s8 = g1 & " << button_mask << ";\n";
501 // Copy the target location to the current location and
502 // then clear the target location so that the title menu
503 // can distinguish cases 2 and 3.
511 << temp_file_name(temp_dir_, "menu-%3d.mpeg",
514 // Define a cell covering the whole menu and set a still
515 // time at the end of that, since it seems all players
516 // support that but some ignore a still time set on a PGC.
517 " <cell start='0' end='"
518 << std::fixed << std::setprecision(4)
519 << menu_duration_seconds(frame_params_) << "'"
520 " chapter='yes' pause='inf'/>\n"
523 for (unsigned button_index = 0;
524 button_index != this_menu.entries.size();
527 const pgc_ref & target =
528 this_menu.entries[button_index].target;
530 file << " <button> ";
532 if (target.type == menu_pgc)
534 unsigned target_button_num;
536 if (target.sub_index)
538 target_button_num = target.sub_index;
542 // Look for a button on the new menu that links
543 // back to this one. If there is one, set that to
544 // be the highlighted button; otherwise, use the
546 const std::vector<menu_entry> & target_menu_entries =
547 menus_[target.index].entries;
548 pgc_ref this_pgc(menu_pgc, menu_index);
549 target_button_num = target_menu_entries.size();
550 while (target_button_num != 1
551 && (target_menu_entries[target_button_num - 1].target
556 // Set new menu location.
558 << (1 + target.index + target_button_num * button_mult)
560 // Jump to the target menu.
561 unsigned target_titleset_num =
562 target.index / dvdauthor_anonymous_menus_max;
563 if (target_titleset_num == titleset_num)
566 % dvdauthor_anonymous_menus_max)
568 else if (target_titleset_num == 0)
569 file << "jump vmgm menu entry title; ";
571 file << "jump titleset " << target_titleset_num
572 << " menu entry root; ";
576 assert(target.type == title_pgc);
578 // Record current menu location and set target chapter
581 "g2 = " << (1 + menu_index
582 + (1 + button_index) * button_mult) << "; "
583 "g3 = " << 1 + target.sub_index << "; ";
584 // Jump to the target title, possibly via its titleset's
586 unsigned target_titleset_num = 1 + target.index;
587 if (titleset_num == 0)
588 file << "jump title " << target_titleset_num << "; ";
589 else if (target_titleset_num == titleset_num)
590 file << "jump title 1; ";
592 file << "jump titleset " << target_titleset_num
593 << " menu entry root; ";
596 file << "</button>\n";
603 file << " </menus>\n";
613 // Count chapters in the title.
614 unsigned n_chapters = 0;
615 for (vob_list::const_iterator
616 it = titles_[titleset_num - 1].begin(),
617 end = titles_[titleset_num - 1].end();
621 // Chapter start times may be specified in the "chapters"
622 // attribute as a comma-separated list. If this is not
623 // specified then the beginning of each file starts a new
624 // chapter. Thus the number of chapters in each file is
625 // the number of commas in the chapter attribute, plus 1.
628 while ((pos = it->chapters.find(',', pos)) != std::string::npos)
635 // Move the chapter number to scratch so the root menu can
636 // distinguish cases 4 and 5.
637 file << " g0 = g3; g3 = 0;\n";
639 // Copy the latest menu location for use by the post-routine.
640 file << " g4 = g2;\n";
642 // Jump to the correct chapter.
643 for (unsigned chapter_num = 1;
644 chapter_num <= n_chapters;
647 " if (g0 eq " << 1 + chapter_num << ")\n"
648 " jump chapter " << chapter_num << ";\n";
652 for (vob_list::const_iterator
653 it = titles_[titleset_num - 1].begin(),
654 end = titles_[titleset_num - 1].end();
658 file << " <vob file='" << xml_escape(it->file) << "'";
659 if (!it->chapters.empty())
660 file << " chapters='" << xml_escape(it->chapters) << "'";
661 if (!it->pause.empty())
662 file << " pause='" << xml_escape(it->pause) << "'";
666 // If the user has not exited to the menus and then
667 // resumed the title, set the latest menu location to be
668 // the button after the one that linked to this title.
669 // In any case, return to the (root) menu which will
670 // then jump to the correct menu.
674 " g2 = g2 + " << button_mult << ";\n"
680 else if (titleset_num != 0) // && !have_real_title
682 file << " <titles><pgc/></titles>\n";
685 file << " </" << outer_element_name << ">\n";
688 file << "</dvdauthor>\n";
692 const char * argv[] = {
694 "-o", output_dir.c_str(),
699 Glib::spawn_sync(".",
700 Glib::ArrayHandle<std::string>(
701 argv, sizeof(argv)/sizeof(argv[0]),
702 Glib::OWNERSHIP_NONE),
703 Glib::SPAWN_SEARCH_PATH
704 | Glib::SPAWN_STDOUT_TO_DEV_NULL,
708 if (command_result != 0)
709 throw std::runtime_error("dvdauthor failed");