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;
110 dvd_generator::dvd_generator(const video::frame_params & frame_params,
111 mpeg_encoder encoder)
112 : temp_dir_("videolink-"),
113 frame_params_(frame_params),
117 dvd_generator::pgc_ref dvd_generator::add_menu()
119 pgc_ref next_menu(menu_pgc, menus_.size());
121 if (next_menu.index == dvdauthor_anonymous_menus_max)
122 throw_length_error("number of menus", dvdauthor_anonymous_menus_max);
124 menus_.resize(next_menu.index + 1);
128 void dvd_generator::add_menu_entry(unsigned index,
129 const rectangle & area,
130 const pgc_ref & target)
132 assert(index < menus_.size());
133 assert(target.type == menu_pgc && target.index < menus_.size()
134 || target.type == title_pgc && target.index < titles_.size());
136 if (menus_[index].entries.size() == dvd::menu_buttons_max)
137 throw_length_error("number of buttons", dvd::menu_buttons_max);
139 menu_entry new_entry = { area, target };
140 menus_[index].entries.push_back(new_entry);
143 void dvd_generator::generate_menu_vob(unsigned index,
144 Glib::RefPtr<Gdk::Pixbuf> background,
145 Glib::RefPtr<Gdk::Pixbuf> highlights)
148 assert(index < menus_.size());
149 const menu & this_menu = menus_[index];
151 std::string background_name(
152 temp_file_name(temp_dir_, "menu-%3d-back.png", 1 + index));
153 std::cout << "saving " << background_name << std::endl;
154 background->save(background_name, "png");
156 std::string highlights_name(
157 temp_file_name(temp_dir_, "menu-%3d-links.png", 1 + index));
158 std::cout << "saving " << highlights_name << std::endl;
159 highlights->save(highlights_name, "png");
161 std::string spumux_name(
162 temp_file_name(temp_dir_, "menu-%3d.subpictures", 1 + index));
163 std::ofstream spumux_file(spumux_name.c_str());
167 " <spu force='yes' start='00:00:00.00'\n"
168 " highlight='" << highlights_name << "'\n"
169 " select='" << highlights_name << "'>\n";
170 int button_count = this_menu.entries.size();
171 for (int i = 0; i != button_count; ++i)
173 const menu_entry & this_entry = this_menu.entries[i];
175 // We program left and right to cycle through the buttons in
176 // the order the entries were added. This should result in
177 // left and right behaving like the tab and shift-tab keys
178 // would in the browser. Hopefully that's a sensible order.
179 // We program up and down to behave geometrically.
180 int up_button = i, down_button = i;
181 double up_closeness = 0.0, down_closeness = 0.0;
182 for (int j = 0; j != button_count; ++j)
184 const menu_entry & other_entry = this_menu.entries[j];
185 double closeness = directed_closeness(
186 this_entry.area, other_entry.area, -1);
187 if (closeness > up_closeness)
190 up_closeness = closeness;
194 closeness = directed_closeness(
195 this_entry.area, other_entry.area, 1);
196 if (closeness > down_closeness)
199 down_closeness = closeness;
203 spumux_file << " <button"
204 " x0='" << this_entry.area.left << "'"
205 " y0='" << this_entry.area.top << "'"
206 " x1='" << this_entry.area.right << "'"
207 " y1='" << this_entry.area.bottom << "'"
208 " left='" << (i == 0 ? button_count : i) << "'"
209 " right='" << 1 + (i + 1) % button_count << "'"
210 " up='" << 1 + up_button << "'"
211 " down='" << 1 + down_button << "'"
220 throw std::runtime_error("Failed to write control file for spumux");
222 std::ostringstream command_stream;
223 unsigned frame_count(menu_duration_frames(frame_params_));
224 if (encoder_ == mpeg_encoder_ffmpeg)
226 for (unsigned i = 0; i != frame_count; ++i)
228 std::string frame_name(background_name);
229 frame_name.push_back('-');
230 frame_name.push_back('0' + i / 10);
231 frame_name.push_back('0' + i % 10);
232 if (symlink(background_name.c_str(), frame_name.c_str()) != 0)
233 throw std::runtime_error(
234 std::string("symlink: ").append(std::strerror(errno)));
237 "ffmpeg -f image2 -vcodec png"
238 " -r " << frame_params_.rate_numer <<
239 "/" << frame_params_.rate_denom <<
240 " -i " << background_name << "-%02d"
241 " -target " << frame_params_.common_name << "-dvd"
242 " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout";
246 assert(encoder_ == mpeg_encoder_mjpegtools_old
247 || encoder_ == mpeg_encoder_mjpegtools_new);
249 << "pngtopnm " << background_name
250 << " | ppmtoy4m -v0 -n" << frame_count << " -F"
251 << frame_params_.rate_numer << ":" << frame_params_.rate_denom
252 << " -A" << frame_params_.pixel_ratio_width
253 << ":" << frame_params_.pixel_ratio_height
255 // The chroma subsampling keywords changed between
256 // versions 1.6.2 and 1.8 of mjpegtools. There is no
257 // keyword that works with both.
258 if (encoder_ == mpeg_encoder_mjpegtools_old)
259 command_stream << "-S420_mpeg2";
261 command_stream << "-S420mpeg2";
263 " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
264 " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
267 << " | spumux -v0 -mdvd " << spumux_name
268 << " > " << temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index);
269 std::string command(command_stream.str());
270 const char * argv[] = {
271 "/bin/sh", "-c", command.c_str(), 0
273 std::cout << "running " << command << std::endl;
275 Glib::spawn_sync(".",
276 Glib::ArrayHandle<std::string>(
277 argv, sizeof(argv)/sizeof(argv[0]),
278 Glib::OWNERSHIP_NONE),
279 Glib::SPAWN_STDOUT_TO_DEV_NULL,
283 if (command_result != 0)
284 throw std::runtime_error("spumux pipeline failed");
287 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
289 pgc_ref next_title(title_pgc, titles_.size());
291 // Check against maximum number of titles.
292 if (next_title.index == dvd::titles_max)
293 throw_length_error("number of titles", dvd::titles_max);
295 titles_.resize(next_title.index + 1);
296 titles_[next_title.index].swap(content);
300 void dvd_generator::generate(const std::string & output_dir) const
302 std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
303 std::ofstream file(name.c_str());
305 // We generate code that uses registers in the following way:
308 // g1: target menu location
309 // g2: source/return menu location for title
310 // g3: target chapter number
312 // All locations are divided into two bitfields: the least
313 // significant 10 bits are a page/menu number and the most
314 // significant 6 bits are a link/button number, and numbering
315 // starts at 1, not 0. This is done for compatibility with
316 // the encoding of the s8 (button) register.
318 static const int button_mult = dvd::reg_s8_button_mult;
319 static const int menu_mask = button_mult - 1;
320 static const int button_mask = (1 << dvd::reg_bits) - button_mult;
327 for (unsigned menu_index = 0; menu_index != menus_.size(); ++menu_index)
329 const menu & this_menu = menus_[menu_index];
333 // This is the first (title) menu, displayed when the
334 // disc is first played.
336 " <pgc entry='title'>\n"
338 // Set a default target location if none is set.
339 // This covers first play and use of the "top menu"
342 " g1 = " << 1 + button_mult << ";\n";
351 // When a title finishes or the user presses the "menu"
352 // button, this always jumps to the titleset's root menu.
353 // We want to return the user to the last menu they used.
354 // So we arrange for each titleset's root menu to return
355 // to the vmgm title menu and then dispatch from there to
356 // whatever the correct menu is. We determine the correct
357 // menu by looking at the menu part of g1.
359 file << " g0 = g1 & " << menu_mask << ";\n";
361 // There is a limit of 128 VM instructions in each PGC.
362 // Therefore in each menu's <pre> section we generate
363 // jumps to menus with numbers greater by 512, 256, 128,
364 // ..., 1 where (a) such a menu exists, (b) this menu
365 // number is divisible by twice that increment and (c) the
366 // correct menu is that or a later menu. Thus each menu
367 // has at most 10 such conditional jumps and is reachable
368 // by at most 10 jumps from the title menu. This chain of
369 // jumps might take too long on some players; this has yet
370 // to be investigated.
372 for (std::size_t menu_incr = (menu_mask + 1) / 2;
376 if (menu_index + menu_incr < menus_.size()
377 && (menu_index & (menu_incr * 2 - 1)) == 0)
380 " if (g0 ge " << 1 + menu_index + menu_incr
382 " jump menu " << 1 + menu_index + menu_incr
388 // Highlight the appropriate button.
389 " s8 = g1 & " << button_mask << ";\n"
390 // Forget the link target. If we don't do this, pressing
391 // the "top menu" button will result in jumping back to
396 << temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + menu_index)
398 // Define a cell covering the whole menu and set a still
399 // time at the end of that, since it seems all players
400 // support that but some ignore a still time set on a PGC.
401 " <cell start='0' end='"
402 << std::fixed << std::setprecision(4)
403 << menu_duration_seconds(frame_params_) << "'"
404 " chapter='yes' pause='inf'/>\n"
407 for (unsigned button_index = 0;
408 button_index != this_menu.entries.size();
411 const pgc_ref & target = this_menu.entries[button_index].target;
413 file << " <button> ";
415 if (target.type == menu_pgc)
417 unsigned target_button_num;
419 if (target.sub_index)
421 target_button_num = target.sub_index;
425 // Look for a button on the new menu that links
426 // back to this one. If there is one, set that to
427 // be the highlighted button; otherwise, use the
429 const std::vector<menu_entry> & target_menu_entries =
430 menus_[target.index].entries;
431 pgc_ref this_pgc(menu_pgc, menu_index);
432 target_button_num = target_menu_entries.size();
433 while (target_button_num != 1
434 && (target_menu_entries[target_button_num - 1].target
440 // Set new menu location.
441 "g1 = " << (1 + target.index
442 + target_button_num * button_mult) << "; "
443 // Jump to the target menu.
444 "jump menu " << 1 + target.index << "; ";
448 assert(target.type == title_pgc);
451 // Record current menu location.
452 "g2 = " << (1 + menu_index
453 + (1 + button_index) * button_mult) << "; "
454 // Set target chapter number.
455 "g3 = " << target.sub_index << "; "
456 // Jump to the target title.
457 "jump title " << 1 + target.index << "; ";
460 file << "</button>\n";
471 // Generate a titleset for each title. This appears to make
472 // jumping to titles a whole lot simpler (but limits us to 99
474 for (unsigned title_index = 0;
475 title_index != titles_.size();
480 // Generate a dummy menu so that the "menu" button will
481 // work. This returns to the source menu via the title
484 " <pgc entry='root'>\n"
485 " <pre> g1 = g2; jump vmgm menu; </pre>\n"
492 // Count chapters in the title.
493 unsigned n_chapters = 0;
494 for (vob_list::const_iterator
495 it = titles_[title_index].begin(),
496 end = titles_[title_index].end();
500 // Chapter start times may be specified in the "chapters"
501 // attribute as a comma-separated list. If this is not
502 // specified then the beginning of each file starts a new
503 // chapter. Thus the number of chapters in each file is
504 // the number of commas in the chapter attribute, plus 1.
507 while ((pos = it->chapters.find(',', pos)) != std::string::npos)
514 // Generate jump "table" for chapters.
515 for (unsigned chapter_num = 1;
516 chapter_num <= n_chapters;
519 " if (g3 == " << chapter_num << ")\n"
520 " jump chapter " << chapter_num << ";\n";
525 for (vob_list::const_iterator
526 it = titles_[title_index].begin(),
527 end = titles_[title_index].end();
531 file << " <vob file='" << xml_escape(it->file) << "'";
532 if (!it->chapters.empty())
533 file << " chapters='" << xml_escape(it->chapters) << "'";
534 if (!it->pause.empty())
535 file << " pause='" << xml_escape(it->pause) << "'";
541 // Return to the source menu, but highlight the next button.
542 " g2 = g2 + " << button_mult << ";\n"
556 const char * argv[] = {
558 "-o", output_dir.c_str(),
563 Glib::spawn_sync(".",
564 Glib::ArrayHandle<std::string>(
565 argv, sizeof(argv)/sizeof(argv[0]),
566 Glib::OWNERSHIP_NONE),
567 Glib::SPAWN_SEARCH_PATH
568 | Glib::SPAWN_STDOUT_TO_DEV_NULL,
572 if (command_result != 0)
573 throw std::runtime_error("dvdauthor failed");