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