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 unsigned frame_count(menu_duration_frames(frame_params_));
246 if (encoder_ == mpeg_encoder_ffmpeg)
248 for (unsigned i = 0; i != frame_count; ++i)
250 std::string frame_name(background_name);
251 frame_name.push_back('-');
252 frame_name.push_back('0' + i / 10);
253 frame_name.push_back('0' + i % 10);
254 if (symlink(background_name.c_str(), frame_name.c_str()) != 0)
255 throw std::runtime_error(
256 std::string("symlink: ").append(std::strerror(errno)));
259 "ffmpeg -f image2 -vcodec png"
260 " -r " << frame_params_.rate_numer <<
261 "/" << frame_params_.rate_denom <<
262 " -i " << background_name << "-%02d"
263 " -target " << frame_params_.common_name << "-dvd"
264 " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout";
268 assert(encoder_ == mpeg_encoder_mjpegtools_old
269 || encoder_ == mpeg_encoder_mjpegtools_new);
271 << "pngtopnm " << background_name
272 << " | ppmtoy4m -v0 -n" << frame_count << " -F"
273 << frame_params_.rate_numer << ":" << frame_params_.rate_denom
274 << " -A" << frame_params_.pixel_ratio_width
275 << ":" << frame_params_.pixel_ratio_height
277 // The chroma subsampling keywords changed between
278 // versions 1.6.2 and 1.8 of mjpegtools. There is no
279 // keyword that works with both.
280 if (encoder_ == mpeg_encoder_mjpegtools_old)
281 command_stream << "-S420_mpeg2";
283 command_stream << "-S420mpeg2";
285 " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
286 " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
289 << " | spumux -v0 -mdvd " << spumux_name << " > " << output_name;
290 std::string command(command_stream.str());
291 const char * argv[] = {
292 "/bin/sh", "-c", command.c_str(), 0
294 std::cout << "INFO: Running " << command << std::endl;
296 Glib::spawn_sync(".",
297 Glib::ArrayHandle<std::string>(
298 argv, sizeof(argv)/sizeof(argv[0]),
299 Glib::OWNERSHIP_NONE),
300 Glib::SPAWN_STDOUT_TO_DEV_NULL,
304 struct stat stat_buf;
305 if (command_result != 0 || stat(output_name.c_str(), &stat_buf) != 0
306 || stat_buf.st_size == 0)
307 throw std::runtime_error("spumux pipeline failed");
310 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
312 pgc_ref next_title(title_pgc, titles_.size());
314 // Check against maximum number of titles.
315 if (next_title.index == dvd::titles_max)
316 throw_length_error("number of titles", dvd::titles_max);
318 titles_.resize(next_title.index + 1);
319 titles_[next_title.index].swap(content);
323 void dvd_generator::generate(const std::string & output_dir) const
325 // This function uses a mixture of 0-based and 1-based numbering,
326 // due to the differing conventions of the language and the DVD
327 // format. Variable names ending in "_index" indicate 0-based
328 // indices and variable names ending in "_num" indicate 1-based
331 std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
332 std::ofstream file(name.c_str());
333 file << "<dvdauthor>\n";
335 // We generate code that uses registers in the following way:
338 // g1: Target location when jumping between menus. Top 6 bits are
339 // the button number (like s8) and bottom 10 bits are the menu
340 // number. This is used for selecting the appropriate button
341 // when entering a menu, for completing indirect jumps between
342 // domains, and for jumping to the correct menu after exiting a
343 // title. This is set to 0 in the pre-routine of the target
345 // g2: Current location in menus. This is used for jumping to the
346 // correct menu when the player exits a title.
347 // g3: Target chapter number plus 1 when jumping to a title.
348 // This is used to jump to the correct chapter and to
349 // distinguish between indirect jumps to menus and titles.
350 // This is set to 0 in the pre-routine of the target title.
351 // g4: Source menu location used to jump to a title. This is
352 // compared with g2 to determine whether to increment the
353 // button number if the title is played to the end.
355 static const unsigned button_mult = dvd::reg_s8_button_mult;
356 static const unsigned menu_mask = button_mult - 1;
357 static const unsigned button_mask = (1U << dvd::reg_bits) - button_mult;
359 // Iterate over VMGM and titlesets. For these purposes, we
360 // consider the VMGM to be titleset 0.
362 // We need a titleset for each title, and we may also need titlesets to
363 // hold extra menus if we have too many for the VMGM.
364 // Also, we need at least one titleset.
365 const unsigned titleset_end = std::max<unsigned>(
366 1U + std::max<unsigned>(1U, titles_.size()),
367 (menus_.size() + dvdauthor_anonymous_menus_max - 1)
368 / dvdauthor_anonymous_menus_max);
370 for (unsigned titleset_num = 0;
371 titleset_num != titleset_end;
374 const char * const outer_element_name =
375 titleset_num == 0 ? "vmgm" : "titleset";
376 const bool have_real_title =
377 titleset_num != 0 && titleset_num <= titles_.size();
378 const bool have_real_menus =
379 titleset_num * dvdauthor_anonymous_menus_max < menus_.size();
381 file << " <" << outer_element_name << ">\n"
384 const unsigned menu_begin = titleset_num * dvdauthor_anonymous_menus_max;
385 const unsigned menu_end =
387 ? std::min<unsigned>(
388 (titleset_num + 1) * dvdauthor_anonymous_menus_max,
392 for (unsigned menu_index = menu_begin;
393 menu_index != menu_end;
396 // There are various cases in which menus may be called:
398 // 1. The user follows a direct link to the menu.
399 // 2. The user follows an indirect link to some other menu
400 // and that goes via this menu. This is distinguished
401 // from case 1 by the value of g1. We must jump to or
402 // at least toward the other menu.
403 // 3. The title menu is called when the disc is first
404 // played or the user presses the "top menu" button.
405 // This is distinguished from cases 2 and 3 by g1 == 0.
406 // We make this look like case 1.
407 // 4. The root menu of a titleset is called when the user
408 // follows an indirect link to the title. This is
409 // distinguished from all other cases by g3 != 0. We
410 // must jump to the title.
411 // 5. The root menu of a titleset is called when the title
412 // ends or the user presses the "menu" button during
413 // the title. This is distinguished from cases 1, 2
414 // and 4 by g1 == 0 and g3 == 0. We must jump to the
415 // latest menu (which can turn into case 1 or 2).
417 // Cases 3 and 5 do not apply to the same menus so they
418 // do not need to be distinguished.
424 " <pgc entry='title'>\n"
426 " if (g1 eq 0)\n" // case 3
427 " g1 = " << 1 + button_mult << ";\n";
429 else if (menu_index == titleset_num * dvdauthor_anonymous_menus_max)
433 " <pgc entry='root'>\n"
438 " if (g3 ne 0)\n" // case 4
440 " if (g1 eq 0) {\n" // case 5
442 " jump vmgm menu entry title;\n"
454 if (!have_real_menus)
456 // This is a root menu only reachable from the title.
463 const menu & this_menu = menus_[menu_index];
465 // Detect and handle case 2.
467 // There is a limit of 128 VM instructions in each PGC.
468 // Also, we can't jump to an arbitrary menu in another
469 // domain. Finally, we can't do computed jumps.
470 // Therefore we statically expand and distribute a binary
471 // search across the menus, resulting in a code size of
472 // O(log(menu_count)) in each menu. In practice there are
473 // at most 11 conditional jumps needed in any menu.
475 // The initial bounds of the binary search are strange
476 // because we must ensure that any jump between titlesets
477 // is to the first menu of the titleset, marked as the
480 // Mask target location to get the target menu.
481 file << " g0 = g1 & " << menu_mask << ";\n";
485 top = 16 * dvdauthor_anonymous_menus_max;
488 unsigned middle = (bottom + top) / 2;
489 if (menu_index == bottom && middle < menus_.size())
491 file << " if (g0 ge " << 1 + middle << ")\n"
493 unsigned target_titleset_num =
494 middle / dvdauthor_anonymous_menus_max;
495 if (target_titleset_num != titleset_num)
497 assert(middle % dvdauthor_anonymous_menus_max == 0);
498 file << "titleset " << target_titleset_num
499 << " menu entry root";
504 << 1 + middle % dvdauthor_anonymous_menus_max;
508 if (menu_index >= middle)
516 // Highlight the appropriate button.
517 file << " s8 = g1 & " << button_mask << ";\n";
519 // Copy the target location to the current location and
520 // then clear the target location so that the title menu
521 // can distinguish cases 2 and 3.
529 << temp_file_name(temp_dir_, "menu-%3d.mpeg",
532 // Define a cell covering the whole menu and set a still
533 // time at the end of that, since it seems all players
534 // support that but some ignore a still time set on a PGC.
535 " <cell start='0' end='"
536 << std::fixed << std::setprecision(4)
537 << menu_duration_seconds(frame_params_) << "'"
538 " chapter='yes' pause='inf'/>\n"
541 for (unsigned button_index = 0;
542 button_index != this_menu.entries.size();
545 const pgc_ref & target =
546 this_menu.entries[button_index].target;
548 file << " <button> ";
550 if (target.type == menu_pgc)
552 unsigned target_button_num;
554 if (target.sub_index)
556 target_button_num = target.sub_index;
560 // Look for a button on the new menu that links
561 // back to this one. If there is one, set that to
562 // be the highlighted button; otherwise, use the
564 const std::vector<menu_entry> & target_menu_entries =
565 menus_[target.index].entries;
566 pgc_ref this_pgc(menu_pgc, menu_index);
567 target_button_num = target_menu_entries.size();
568 while (target_button_num != 1
569 && (target_menu_entries[target_button_num - 1].target
574 // Set new menu location.
576 << (1 + target.index + target_button_num * button_mult)
578 // Jump to the target menu.
579 unsigned target_titleset_num =
580 target.index / dvdauthor_anonymous_menus_max;
581 if (target_titleset_num == titleset_num)
584 % dvdauthor_anonymous_menus_max)
586 else if (target_titleset_num == 0)
587 file << "jump vmgm menu entry title; ";
589 file << "jump titleset " << target_titleset_num
590 << " menu entry root; ";
594 assert(target.type == title_pgc);
596 // Record current menu location and set target chapter
599 "g2 = " << (1 + menu_index
600 + (1 + button_index) * button_mult) << "; "
601 "g3 = " << 1 + target.sub_index << "; ";
602 // Jump to the target title, possibly via its titleset's
604 unsigned target_titleset_num = 1 + target.index;
605 if (titleset_num == 0)
606 file << "jump title " << target_titleset_num << "; ";
607 else if (target_titleset_num == titleset_num)
608 file << "jump title 1; ";
610 file << "jump titleset " << target_titleset_num
611 << " menu entry root; ";
614 file << "</button>\n";
621 file << " </menus>\n";
631 // Count chapters in the title.
632 unsigned n_chapters = 0;
633 for (vob_list::const_iterator
634 it = titles_[titleset_num - 1].begin(),
635 end = titles_[titleset_num - 1].end();
639 // Chapter start times may be specified in the "chapters"
640 // attribute as a comma-separated list. If this is not
641 // specified then the beginning of each file starts a new
642 // chapter. Thus the number of chapters in each file is
643 // the number of commas in the chapter attribute, plus 1.
646 while ((pos = it->chapters.find(',', pos)) != std::string::npos)
653 // Move the chapter number to scratch so the root menu can
654 // distinguish cases 4 and 5.
655 file << " g0 = g3; g3 = 0;\n";
657 // Copy the latest menu location for use by the post-routine.
658 file << " g4 = g2;\n";
660 // Jump to the correct chapter.
661 for (unsigned chapter_num = 1;
662 chapter_num <= n_chapters;
665 " if (g0 eq " << 1 + chapter_num << ")\n"
666 " jump chapter " << chapter_num << ";\n";
670 for (vob_list::const_iterator
671 it = titles_[titleset_num - 1].begin(),
672 end = titles_[titleset_num - 1].end();
676 file << " <vob file='" << xml_escape(it->file) << "'";
677 if (!it->chapters.empty())
678 file << " chapters='" << xml_escape(it->chapters) << "'";
679 if (!it->pause.empty())
680 file << " pause='" << xml_escape(it->pause) << "'";
684 // If the user has not exited to the menus and then
685 // resumed the title, set the latest menu location to be
686 // the button after the one that linked to this title.
687 // In any case, return to the (root) menu which will
688 // then jump to the correct menu.
692 " g2 = g2 + " << button_mult << ";\n"
698 else if (titleset_num != 0) // && !have_real_title
700 file << " <titles><pgc/></titles>\n";
703 file << " </" << outer_element_name << ">\n";
706 file << "</dvdauthor>\n";
710 const char * argv[] = {
712 "-o", output_dir.c_str(),
717 Glib::spawn_sync(".",
718 Glib::ArrayHandle<std::string>(
719 argv, sizeof(argv)/sizeof(argv[0]),
720 Glib::OWNERSHIP_NONE),
721 Glib::SPAWN_SEARCH_PATH
722 | Glib::SPAWN_STDOUT_TO_DEV_NULL,
726 if (command_result != 0)
727 throw std::runtime_error("dvdauthor failed");