]> git.decadent.org.uk Git - videolink.git/blob - generate_dvd.cpp
Corrected formula in menu_duration_seconds. It looks like any sufficiently large...
[videolink.git] / generate_dvd.cpp
1 // Copyright 2005-6 Ben Hutchings <ben@decadent.org.uk>.
2 // See the file "COPYING" for licence details.
3
4 #include <cerrno>
5 #include <cstring>
6 #include <fstream>
7 #include <iomanip>
8 #include <iostream>
9 #include <ostream>
10 #include <sstream>
11 #include <stdexcept>
12
13 #include <gdkmm/pixbuf.h>
14 #include <glibmm/miscutils.h>
15 #include <glibmm/spawn.h>
16
17 #include "dvd.hpp"
18 #include "generate_dvd.hpp"
19 #include "xml_utils.hpp"
20
21 namespace
22 {
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,
29                               int y_dir)
30     {
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
40         // "start" rectangle.
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;
44         int end_x;
45         if (end.right < start_x)
46             end_x = end.right;
47         else if (end.left > start_x)
48             end_x = end.left;
49         else
50             end_x = start_x;
51
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)
57             return 0.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;
62     }
63
64     std::string temp_file_name(const temp_dir & dir,
65                                std::string base_name,
66                                unsigned index=0)
67     {
68         if (index != 0)
69         {
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;
75         }
76
77         return Glib::build_filename(dir.get_name(), base_name);
78     }
79
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)
85     {
86         return 12;
87     }
88     double menu_duration_seconds(const video::frame_params & params)
89     {
90         return double(menu_duration_frames(params))
91             / double(params.rate_numer)
92             * double(params.rate_denom);
93     }
94
95     void throw_length_error(const char * limit_type, std::size_t limit)
96     {
97         std::ostringstream oss;
98         oss << "exceeded DVD limit: " << limit_type << " > " << limit;
99         throw std::length_error(oss.str());
100     }
101
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;
108 }
109
110 dvd_generator::dvd_generator(const video::frame_params & frame_params,
111                              mpeg_encoder encoder)
112         : temp_dir_("videolink-"),
113           frame_params_(frame_params),
114           encoder_(encoder)
115 {}
116
117 dvd_generator::pgc_ref dvd_generator::add_menu()
118 {
119     pgc_ref next_menu(menu_pgc, menus_.size());
120
121     if (next_menu.index == dvdauthor_anonymous_menus_max)
122         throw_length_error("number of menus", dvdauthor_anonymous_menus_max);
123
124     menus_.resize(next_menu.index + 1);
125     return next_menu;
126 }
127
128 void dvd_generator::add_menu_entry(unsigned index,
129                                    const rectangle & area,
130                                    const pgc_ref & target)
131 {
132     assert(index < menus_.size());
133     assert(target.type == menu_pgc && target.index < menus_.size()
134            || target.type == title_pgc && target.index < titles_.size());
135
136     if (menus_[index].entries.size() == dvd::menu_buttons_max)
137         throw_length_error("number of buttons", dvd::menu_buttons_max);
138
139     menu_entry new_entry = { area, target };
140     menus_[index].entries.push_back(new_entry);
141 }
142
143 void dvd_generator::generate_menu_vob(unsigned index,
144                                       Glib::RefPtr<Gdk::Pixbuf> background,
145                                       Glib::RefPtr<Gdk::Pixbuf> highlights)
146     const
147 {
148     assert(index < menus_.size());
149     const menu & this_menu = menus_[index];
150
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");
155
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");
160
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());
164     spumux_file <<
165         "<subpictures>\n"
166         "  <stream>\n"
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)
172     {
173         const menu_entry & this_entry = this_menu.entries[i];
174
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)
183         {
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)
188             {
189                 up_button = j;
190                 up_closeness = closeness;
191             }
192             else
193             {
194                 closeness = directed_closeness(
195                     this_entry.area, other_entry.area, 1);
196                 if (closeness > down_closeness)
197                 {
198                     down_button = j;
199                     down_closeness = closeness;
200                 }
201             }
202         }
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 << "'"
212             "/>\n";
213     }
214     spumux_file <<
215         "    </spu>\n"
216         "  </stream>\n"
217         "</subpictures>\n";
218     spumux_file.close();
219     if (!spumux_file)
220         throw std::runtime_error("Failed to write control file for spumux");
221
222     std::ostringstream command_stream;
223     unsigned frame_count(menu_duration_frames(frame_params_));
224     if (encoder_ == mpeg_encoder_ffmpeg)
225     {
226         for (unsigned i = 0; i != frame_count; ++i)
227         {
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)));
235         }
236         command_stream <<
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";
243     }
244     else
245     {
246         assert(encoder_ == mpeg_encoder_mjpegtools_old
247                || encoder_ == mpeg_encoder_mjpegtools_new);
248         command_stream
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
254             << " -Ip ";
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";
260         else
261             command_stream << "-S420mpeg2";
262         command_stream <<
263             " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
264             " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
265     }
266     command_stream
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
272     };
273     std::cout << "running " << command << std::endl;
274     int command_result;
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,
280                      SigC::Slot0<void>(),
281                      0, 0,
282                      &command_result);
283     if (command_result != 0)
284         throw std::runtime_error("spumux pipeline failed");
285 }
286
287 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
288 {
289     pgc_ref next_title(title_pgc, titles_.size());
290
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);
294
295     titles_.resize(next_title.index + 1);
296     titles_[next_title.index].swap(content);
297     return next_title;
298 }
299
300 void dvd_generator::generate(const std::string & output_dir) const
301 {
302     std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
303     std::ofstream file(name.c_str());
304
305     // We generate code that uses registers in the following way:
306     //
307     // g0:     scratch
308     // g1:     target menu location
309     // g2:     source/return menu location for title
310     // g3:     target chapter number
311     //
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.
317     //
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;
321
322     file <<
323         "<dvdauthor>\n"
324         "  <vmgm>\n"
325         "    <menus>\n";
326             
327     for (unsigned menu_index = 0; menu_index != menus_.size(); ++menu_index)
328     {
329         const menu & this_menu = menus_[menu_index];
330
331         if (menu_index == 0)
332         {
333             // This is the first (title) menu, displayed when the
334             // disc is first played.
335             file <<
336                 "      <pgc entry='title'>\n"
337                 "        <pre>\n"
338                 // Set a default target location if none is set.
339                 // This covers first play and use of the "top menu"
340                 // button.
341                 "          if (g1 eq 0)\n"
342                 "            g1 = " << 1 + button_mult << ";\n";
343         }
344         else
345         {
346             file <<
347                 "      <pgc>\n"
348                 "        <pre>\n";
349         }
350
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.
358
359         file << "          g0 = g1 &amp; " << menu_mask << ";\n";
360
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.
371             
372         for (std::size_t menu_incr = (menu_mask + 1) / 2;
373              menu_incr != 0;
374              menu_incr /= 2)
375         {
376             if (menu_index + menu_incr < menus_.size()
377                 && (menu_index & (menu_incr * 2 - 1)) == 0)
378             {
379                 file <<
380                     "          if (g0 ge " << 1 + menu_index + menu_incr
381                                            << ")\n"
382                     "            jump menu " << 1 + menu_index + menu_incr
383                                            << ";\n";
384             }
385         }
386
387         file <<
388             // Highlight the appropriate button.
389             "          s8 = g1 &amp; " << 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
392             // this same menu!
393             "          g1 = 0;\n"
394             "        </pre>\n"
395             "        <vob file='"
396              << temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + menu_index)
397              << "'>\n"
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"
405             "        </vob>\n";
406
407         for (unsigned button_index = 0;
408              button_index != this_menu.entries.size();
409              ++button_index)
410         {
411             const pgc_ref & target = this_menu.entries[button_index].target;
412
413             file << "        <button> ";
414
415             if (target.type == menu_pgc)
416             {
417                 unsigned target_button_num;
418
419                 if (target.sub_index)
420                 {
421                     target_button_num = target.sub_index;
422                 }
423                 else
424                 {
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
428                     // first button.
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
435                                != this_pgc))
436                         --target_button_num;
437                 }
438                          
439                 file <<
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 << "; ";
445             }
446             else
447             {
448                 assert(target.type == title_pgc);
449
450                 file <<
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 << "; ";
458             }
459
460             file <<  "</button>\n";
461         }
462
463         file <<
464             "      </pgc>\n";
465     }
466
467     file <<
468         "    </menus>\n"
469         "  </vmgm>\n";
470  
471     // Generate a titleset for each title.  This appears to make
472     // jumping to titles a whole lot simpler (but limits us to 99
473     // titles).
474     for (unsigned title_index = 0;
475          title_index != titles_.size();
476          ++title_index)
477     {
478         file <<
479             "  <titleset>\n"
480             // Generate a dummy menu so that the "menu" button will
481             // work.  This returns to the source menu via the title
482             // menu.
483             "    <menus>\n"
484             "      <pgc entry='root'>\n"
485             "        <pre> g1 = g2; jump vmgm menu; </pre>\n"
486             "      </pgc>\n"
487             "    </menus>\n"
488             "    <titles>\n"
489             "      <pgc>\n"
490             "        <pre>\n";
491
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();
497              it != end;
498              ++it)
499         {
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.
505             ++n_chapters;
506             std::size_t pos = 0;
507             while ((pos = it->chapters.find(',', pos)) != std::string::npos)
508             {
509                 ++n_chapters;
510                 ++pos;
511             }
512         }
513
514         // Generate jump "table" for chapters.
515         for (unsigned chapter_num = 1;
516              chapter_num <= n_chapters;
517              ++chapter_num)
518             file <<
519                 "          if (g3 == " << chapter_num << ")\n"
520                 "            jump chapter " << chapter_num << ";\n";
521
522         file <<
523             "        </pre>\n";
524
525         for (vob_list::const_iterator
526                  it = titles_[title_index].begin(),
527                  end = titles_[title_index].end();
528              it != end;
529              ++it)
530         {
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) << "'";
536             file << "/>\n";
537         }
538
539         file <<
540             "        <post>\n"
541             // Return to the source menu, but highlight the next button.
542             "          g2 = g2 + " << button_mult << ";\n"
543             "          call menu;\n"
544             "        </post>\n"
545             "      </pgc>\n"
546             "    </titles>\n"
547             "  </titleset>\n";
548     }
549
550     file <<
551         "</dvdauthor>\n";
552
553     file.close();
554
555     {
556         const char * argv[] = {
557             "dvdauthor",
558             "-o", output_dir.c_str(),
559             "-x", name.c_str(),
560             0
561         };
562         int command_result;
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,
569                          SigC::Slot0<void>(),
570                          0, 0,
571                          &command_result);
572         if (command_result != 0)
573             throw std::runtime_error("dvdauthor failed");
574     }
575 }