]> git.decadent.org.uk Git - videolink.git/blob - generate_dvd.cpp
Moved generation of menu VOBs from videolink_window to dvd_generator.
[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 <fstream>
5 #include <iostream>
6 #include <sstream>
7 #include <stdexcept>
8
9 #include <gdkmm/pixbuf.h>
10 #include <glibmm/spawn.h>
11
12 #include "dvd.hpp"
13 #include "generate_dvd.hpp"
14 #include "xml_utils.hpp"
15
16 namespace
17 {
18     // Return a closeness metric of an "end" rectangle to a "start"
19     // rectangle in the upward (-1) or downward (+1) direction.  Given
20     // several possible "end" rectangles, the one that seems visually
21     // closest in the given direction should have the highest value of
22     // this metric.  This is necessarily a heuristic function!    
23     double directed_closeness(const rectangle & start, const rectangle & end,
24                               int y_dir)
25     {
26         // The obvious approach is to use the centres of the
27         // rectangles.  However, for the "end" rectangle, using the
28         // horizontal position nearest the centre of the "start"
29         // rectangle seems to produce more reasonable results.  For
30         // example, if there are two "end" rectangles equally near to
31         // the "start" rectangle in terms of vertical distance and one
32         // of them horizontally overlaps the centre of the "start"
33         // rectangle, we want to pick that one even if the centre of
34         // that rectangle is further away from the centre of the
35         // "start" rectangle.
36         int start_x = (start.left + start.right) / 2;
37         int start_y = (start.top + start.bottom) / 2;
38         int end_y = (end.top + end.bottom) / 2;
39         int end_x;
40         if (end.right < start_x)
41             end_x = end.right;
42         else if (end.left > start_x)
43             end_x = end.left;
44         else
45             end_x = start_x;
46
47         // Return cosine of angle between the line between these points
48         // and the vertical, divided by the distance between the points
49         // if that is defined and positive; otherwise return 0.
50         int vertical_distance = (end_y - start_y) * y_dir;
51         if (vertical_distance <= 0)
52             return 0.0;
53         double distance_squared =
54             (end_x - start_x) * (end_x - start_x)
55             + (end_y - start_y) * (end_y - start_y);
56         return vertical_distance / distance_squared;
57     }
58 }
59
60 dvd_generator::menu::menu()
61         : vob_temp(new temp_file("videolink-vob-"))
62 {
63     vob_temp->close();
64 }
65
66 dvd_generator::pgc_ref dvd_generator::add_menu()
67 {
68     pgc_ref next_menu(menu_pgc, menus_.size());
69
70     // Check against maximum number of menus.  It appears that no more
71     // than 128 menus are reachable through LinkPGCN instructions, and
72     // dvdauthor uses some menu numbers for special purposes, resulting
73     // in a practical limit of 119 per domain.  We can work around this
74     // later by spreading some menus across titlesets.
75     if (next_menu.index == 119)
76         throw std::runtime_error("No more than 119 menus can be used");
77
78     menus_.resize(next_menu.index + 1);
79     return next_menu;
80 }
81
82 void dvd_generator::add_menu_entry(unsigned index,
83                                    const rectangle & area,
84                                    const pgc_ref & target)
85 {
86     assert(index < menus_.size());
87     assert(target.type == menu_pgc && target.index < menus_.size()
88            || target.type == title_pgc && target.index < titles_.size());
89     menu_entry new_entry = { area, target };
90     menus_[index].entries.push_back(new_entry);
91 }
92
93 void dvd_generator::generate_menu_vob(unsigned index,
94                                       Glib::RefPtr<Gdk::Pixbuf> background,
95                                       Glib::RefPtr<Gdk::Pixbuf> highlights)
96     const
97 {
98     assert(index < menus_.size());
99     const menu & this_menu = menus_[index];
100
101     temp_file background_temp("videolink-back-");
102     background_temp.close();
103     std::cout << "saving " << background_temp.get_name() << std::endl;
104     background->save(background_temp.get_name(), "png");
105
106     temp_file highlights_temp("videolink-links-");
107     highlights_temp.close();
108     std::cout << "saving " << highlights_temp.get_name() << std::endl;
109     highlights->save(highlights_temp.get_name(), "png");
110
111     temp_file spumux_temp("videolink-spumux-");
112     spumux_temp.close();
113     std::ofstream spumux_file(spumux_temp.get_name().c_str());
114     spumux_file <<
115         "<subpictures>\n"
116         "  <stream>\n"
117         "    <spu force='yes' start='00:00:00.00'\n"
118         "        highlight='" << highlights_temp.get_name() << "'\n"
119         "        select='" << highlights_temp.get_name() << "'>\n";
120     int button_count = this_menu.entries.size();
121     for (int i = 0; i != button_count; ++i)
122     {
123         const menu_entry & this_entry = this_menu.entries[i];
124
125         // We program left and right to cycle through the buttons in
126         // the order the entries were added.  This should result in
127         // left and right behaving like the tab and shift-tab keys
128         // would in the browser.  Hopefully that's a sensible order.
129         // We program up and down to behave geometrically.
130         int up_button = i, down_button = i;
131         double up_closeness = 0.0, down_closeness = 0.0;
132         for (int j = 0; j != button_count; ++j)
133         {
134             const menu_entry & other_entry = this_menu.entries[j];
135             double closeness = directed_closeness(
136                 this_entry.area, other_entry.area, -1);
137             if (closeness > up_closeness)
138             {
139                 up_button = j;
140                 up_closeness = closeness;
141             }
142             else
143             {
144                 closeness = directed_closeness(
145                     this_entry.area, other_entry.area, 1);
146                 if (closeness > down_closeness)
147                 {
148                     down_button = j;
149                     down_closeness = closeness;
150                 }
151             }
152         }
153         spumux_file << "      <button"
154             " x0='" << this_entry.area.left << "'"
155             " y0='" << this_entry.area.top << "'"
156             " x1='" << this_entry.area.right << "'"
157             " y1='" << this_entry.area.bottom << "'"
158             " left='" << (i == 0 ? button_count : i) << "'"
159             " right='" << 1 + (i + 1) % button_count << "'"
160             " up='" << 1 + up_button << "'"
161             " down='" << 1 + down_button << "'"
162             "/>\n";
163     }
164     spumux_file <<
165         "    </spu>\n"
166         "  </stream>\n"
167         "</subpictures>\n";
168     spumux_file.close();
169     if (!spumux_file)
170         throw std::runtime_error("Failed to write control file for spumux");
171
172     std::ostringstream command_stream;
173     if (encoder_ == mpeg_encoder_ffmpeg)
174     {
175         command_stream
176             << "ffmpeg"
177             << " -f image2 -vcodec png -i "
178             << background_temp.get_name()
179             << " -target " << frame_params_.common_name <<  "-dvd"
180             << " -vcodec mpeg2video -aspect 4:3 -an -y /dev/stdout";
181     }
182     else
183     {
184         assert(encoder_ == mpeg_encoder_mjpegtools_old
185                || encoder_ == mpeg_encoder_mjpegtools_new);
186         command_stream
187             << "pngtopnm "
188             << background_temp.get_name()
189             << " | ppmtoy4m -v0 -n1 -F"
190             << frame_params_.rate_numer << ":" << frame_params_.rate_denom
191             << " -A" << frame_params_.pixel_ratio_width
192             << ":" << frame_params_.pixel_ratio_height
193             << " -Ip ";
194         // The chroma subsampling keywords changed between
195         // versions 1.6.2 and 1.8 of mjpegtools.  There is no
196         // keyword that works with both.
197         if (encoder_ == mpeg_encoder_mjpegtools_old)
198             command_stream << "-S420_mpeg2";
199         else
200             command_stream << "-S420mpeg2";
201         command_stream <<
202             " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
203             " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
204     }
205     command_stream
206         << " | spumux -v0 -mdvd " << spumux_temp.get_name()
207         << " > " << this_menu.vob_temp->get_name();
208     std::string command(command_stream.str());
209     const char * argv[] = {
210         "/bin/sh", "-c", command.c_str(), 0
211     };
212     std::cout << "running " << command << std::endl;
213     int command_result;
214     Glib::spawn_sync(".",
215                      Glib::ArrayHandle<std::string>(
216                          argv, sizeof(argv)/sizeof(argv[0]),
217                          Glib::OWNERSHIP_NONE),
218                      Glib::SPAWN_STDOUT_TO_DEV_NULL,
219                      SigC::Slot0<void>(),
220                      0, 0,
221                      &command_result);
222     if (command_result != 0)
223         throw std::runtime_error("spumux pipeline failed");
224 }
225
226 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
227 {
228     pgc_ref next_title(title_pgc, titles_.size());
229
230     // Check against maximum number of titles.
231     if (next_title.index == 99)
232         throw std::runtime_error("No more than 99 titles can be used");
233
234     titles_.resize(next_title.index + 1);
235     titles_[next_title.index].swap(content);
236     return next_title;
237 }
238
239 void dvd_generator::generate(const std::string & output_dir) const
240 {
241     temp_file temp("videolink-dvdauthor-");
242     temp.close();
243     std::ofstream file(temp.get_name().c_str());
244
245     // We generate code that uses registers in the following way:
246     //
247     // g0:     scratch
248     // g1:     target menu location
249     // g2:     source/return menu location for title
250     // g3:     target chapter number
251     //
252     // All locations are divided into two bitfields: the least
253     // significant 10 bits are a page/menu number and the most
254     // significant 6 bits are a link/button number, and numbering
255     // starts at 1, not 0.  This is done for compatibility with
256     // the encoding of the s8 (button) register.
257     //
258     static const int button_mult = dvd::reg_s8_button_mult;
259     static const int menu_mask = button_mult - 1;
260     static const int button_mask = (1 << dvd::reg_bits) - button_mult;
261
262     file <<
263         "<dvdauthor>\n"
264         "  <vmgm>\n"
265         "    <menus>\n";
266             
267     for (unsigned menu_index = 0; menu_index != menus_.size(); ++menu_index)
268     {
269         const menu & this_menu = menus_[menu_index];
270
271         if (menu_index == 0)
272         {
273             // This is the first (title) menu, displayed when the
274             // disc is first played.
275             file <<
276                 "      <pgc entry='title' pause='inf'>\n"
277                 "        <pre>\n"
278                 // Set a default target location if none is set.
279                 // This covers first play and use of the "top menu"
280                 // button.
281                 "          if (g1 eq 0)\n"
282                 "            g1 = " << 1 + button_mult << ";\n";
283         }
284         else
285         {
286             file <<
287                 "      <pgc pause='inf'>\n"
288                 "        <pre>\n";
289         }
290
291         // When a title finishes or the user presses the "menu"
292         // button, this always jumps to the titleset's root menu.
293         // We want to return the user to the last menu they used.
294         // So we arrange for each titleset's root menu to return
295         // to the vmgm title menu and then dispatch from there to
296         // whatever the correct menu is.  We determine the correct
297         // menu by looking at the menu part of g1.
298
299         file << "          g0 = g1 &amp; " << menu_mask << ";\n";
300
301         // There is a limit of 128 VM instructions in each PGC.
302         // Therefore in each menu's <pre> section we generate
303         // jumps to menus with numbers greater by 512, 256, 128,
304         // ..., 1 where (a) such a menu exists, (b) this menu
305         // number is divisible by twice that increment and (c) the
306         // correct menu is that or a later menu.  Thus each menu
307         // has at most 10 such conditional jumps and is reachable
308         // by at most 10 jumps from the title menu.  This chain of
309         // jumps might take too long on some players; this has yet
310         // to be investigated.
311             
312         for (std::size_t menu_incr = (menu_mask + 1) / 2;
313              menu_incr != 0;
314              menu_incr /= 2)
315         {
316             if (menu_index + menu_incr < menus_.size()
317                 && (menu_index & (menu_incr * 2 - 1)) == 0)
318             {
319                 file <<
320                     "          if (g0 ge " << 1 + menu_index + menu_incr
321                                            << ")\n"
322                     "            jump menu " << 1 + menu_index + menu_incr
323                                            << ";\n";
324             }
325         }
326
327         file <<
328             // Highlight the appropriate button.
329             "          s8 = g1 &amp; " << button_mask << ";\n"
330             // Forget the link target.  If we don't do this, pressing
331             // the "top menu" button will result in jumping back to
332             // this same menu!
333             "          g1 = 0;\n"
334             "        </pre>\n"
335             "        <vob file='" << this_menu.vob_temp->get_name() << "'/>\n";
336
337         for (unsigned button_index = 0;
338              button_index != this_menu.entries.size();
339              ++button_index)
340         {
341             const pgc_ref & target = this_menu.entries[button_index].target;
342
343             file << "        <button> ";
344
345             if (target.type == menu_pgc)
346             {
347                 unsigned target_button_num;
348
349                 if (target.sub_index)
350                 {
351                     target_button_num = target.sub_index;
352                 }
353                 else
354                 {
355                     // Look for a button on the new menu that links
356                     // back to this one.  If there is one, set that to
357                     // be the highlighted button; otherwise, use the
358                     // first button.
359                     const std::vector<menu_entry> & target_menu_entries =
360                         menus_[target.index].entries;
361                     pgc_ref this_pgc(menu_pgc, menu_index);
362                     target_button_num = target_menu_entries.size();
363                     while (target_button_num != 1
364                            && (target_menu_entries[target_button_num - 1].target
365                                != this_pgc))
366                         --target_button_num;
367                 }
368                          
369                 file <<
370                     // Set new menu location.
371                     "g1 = " << (1 + target.index
372                                 + target_button_num * button_mult) << "; "
373                     // Jump to the target menu.
374                     "jump menu " << 1 + target.index << "; ";
375             }
376             else
377             {
378                 assert(target.type == title_pgc);
379
380                 file <<
381                     // Record current menu location.
382                     "g2 = " << (1 + menu_index
383                                 + (1 + button_index) * button_mult) << "; "
384                     // Set target chapter number.
385                     "g3 = " << target.sub_index << "; "
386                     // Jump to the target title.
387                     "jump title " << 1 + target.index << "; ";
388             }
389
390             file <<  "</button>\n";
391         }
392
393         file <<
394             // Some DVD players don't seem to obey pause='inf' so make
395             // them loop.
396             "        <post>\n"
397             "          jump cell 1;\n"
398             "        </post>\n"
399             "      </pgc>\n";
400     }
401
402     file <<
403         "    </menus>\n"
404         "  </vmgm>\n";
405  
406     // Generate a titleset for each title.  This appears to make
407     // jumping to titles a whole lot simpler (but limits us to 99
408     // titles).
409     for (unsigned title_index = 0;
410          title_index != titles_.size();
411          ++title_index)
412     {
413         file <<
414             "  <titleset>\n"
415             // Generate a dummy menu so that the "menu" button will
416             // work.  This returns to the source menu via the title
417             // menu.
418             "    <menus>\n"
419             "      <pgc entry='root'>\n"
420             "        <pre> g1 = g2; jump vmgm menu; </pre>\n"
421             "      </pgc>\n"
422             "    </menus>\n"
423             "    <titles>\n"
424             "      <pgc>\n"
425             "        <pre>\n";
426
427         // Count chapters in the title.
428         unsigned n_chapters = 0;
429         for (vob_list::const_iterator
430                  it = titles_[title_index].begin(),
431                  end = titles_[title_index].end();
432              it != end;
433              ++it)
434         {
435             // Chapter start times may be specified in the "chapters"
436             // attribute as a comma-separated list.  If this is not
437             // specified then the beginning of each file starts a new
438             // chapter.  Thus the number of chapters in each file is
439             // the number of commas in the chapter attribute, plus 1.
440             ++n_chapters;
441             std::size_t pos = 0;
442             while ((pos = it->chapters.find(',', pos)) != std::string::npos)
443             {
444                 ++n_chapters;
445                 ++pos;
446             }
447         }
448
449         // Generate jump "table" for chapters.
450         for (unsigned chapter_num = 1;
451              chapter_num <= n_chapters;
452              ++chapter_num)
453             file <<
454                 "          if (g3 == " << chapter_num << ")\n"
455                 "            jump chapter " << chapter_num << ";\n";
456
457         file <<
458             "        </pre>\n";
459
460         for (vob_list::const_iterator
461                  it = titles_[title_index].begin(),
462                  end = titles_[title_index].end();
463              it != end;
464              ++it)
465         {
466             file << "        <vob file='" << xml_escape(it->file) << "'";
467             if (!it->chapters.empty())
468                 file << " chapters='" << xml_escape(it->chapters) << "'";
469             if (!it->pause.empty())
470                 file << " pause='" << xml_escape(it->pause) << "'";
471             file << "/>\n";
472         }
473
474         file <<
475             "        <post>\n"
476             // Return to the source menu, but highlight the next button.
477             "          g2 = g2 + " << button_mult << ";\n"
478             "          call menu;\n"
479             "        </post>\n"
480             "      </pgc>\n"
481             "    </titles>\n"
482             "  </titleset>\n";
483     }
484
485     file <<
486         "</dvdauthor>\n";
487
488     file.close();
489
490     {
491         const char * argv[] = {
492             "dvdauthor",
493             "-o", output_dir.c_str(),
494             "-x", temp.get_name().c_str(),
495             0
496         };
497         int command_result;
498         Glib::spawn_sync(".",
499                          Glib::ArrayHandle<std::string>(
500                              argv, sizeof(argv)/sizeof(argv[0]),
501                              Glib::OWNERSHIP_NONE),
502                          Glib::SPAWN_SEARCH_PATH
503                          | Glib::SPAWN_STDOUT_TO_DEV_NULL,
504                          SigC::Slot0<void>(),
505                          0, 0,
506                          &command_result);
507         if (command_result != 0)
508             throw std::runtime_error("dvdauthor failed");
509     }
510 }