]> git.decadent.org.uk Git - videolink.git/blob - generate_dvd.cpp
Release versions 1.2.11 and 1.2.11-1
[videolink.git] / generate_dvd.cpp
1 // Copyright 2005-8 Ben Hutchings <ben@decadent.org.uk>.
2 // See the file "COPYING" for licence details.
3
4 #include <cassert>
5 #include <cerrno>
6 #include <cstring>
7 #include <fstream>
8 #include <iomanip>
9 #include <iostream>
10 #include <ostream>
11 #include <sstream>
12 #include <stdexcept>
13
14 #include <sys/types.h>
15 #include <sys/stat.h>
16 #include <unistd.h>
17
18 #include <gdkmm/pixbuf.h>
19 #include <glibmm/miscutils.h>
20 #include <glibmm/spawn.h>
21
22 #include "dvd.hpp"
23 #include "generate_dvd.hpp"
24 #include "xml_utils.hpp"
25
26 namespace
27 {
28     // Return a closeness metric of an "end" rectangle to a "start"
29     // rectangle in the upward (-1) or downward (+1) direction.  Given
30     // several possible "end" rectangles, the one that seems visually
31     // closest in the given direction should have the highest value of
32     // this metric.  This is necessarily a heuristic function!    
33     double directed_closeness(const rectangle & start, const rectangle & end,
34                               int y_dir)
35     {
36         // The obvious approach is to use the centres of the
37         // rectangles.  However, for the "end" rectangle, using the
38         // horizontal position nearest the centre of the "start"
39         // rectangle seems to produce more reasonable results.  For
40         // example, if there are two "end" rectangles equally near to
41         // the "start" rectangle in terms of vertical distance and one
42         // of them horizontally overlaps the centre of the "start"
43         // rectangle, we want to pick that one even if the centre of
44         // that rectangle is further away from the centre of the
45         // "start" rectangle.
46         int start_x = (start.left + start.right) / 2;
47         int start_y = (start.top + start.bottom) / 2;
48         int end_y = (end.top + end.bottom) / 2;
49         int end_x;
50         if (end.right < start_x)
51             end_x = end.right;
52         else if (end.left > start_x)
53             end_x = end.left;
54         else
55             end_x = start_x;
56
57         // Return cosine of angle between the line between these points
58         // and the vertical, divided by the distance between the points
59         // if that is defined and positive; otherwise return 0.
60         int vertical_distance = (end_y - start_y) * y_dir;
61         if (vertical_distance <= 0)
62             return 0.0;
63         double distance_squared =
64             (end_x - start_x) * (end_x - start_x)
65             + (end_y - start_y) * (end_y - start_y);
66         return vertical_distance / distance_squared;
67     }
68
69     std::string temp_file_name(const temp_dir & dir,
70                                std::string base_name,
71                                unsigned index=0)
72     {
73         if (index != 0)
74         {
75             std::size_t index_pos = base_name.find("%3d");
76             assert(index_pos != std::string::npos);
77             base_name[index_pos] = '0' + index / 100;
78             base_name[index_pos + 1] = '0' + (index / 10) % 10;
79             base_name[index_pos + 2] = '0' + index % 10;
80         }
81
82         return Glib::build_filename(dir.get_name(), base_name);
83     }
84
85     // We would like to use just a single frame for the menu but this
86     // seems not to be legal or compatible.  The minimum length of a
87     // cell is 0.4 seconds but I've seen a static menu using 12 frames
88     // on a commercial "PAL" disc so let's use 12 frames regardless.
89     unsigned menu_duration_frames(const video::frame_params & params)
90     {
91         return 12;
92     }
93     double menu_duration_seconds(const video::frame_params & params)
94     {
95         return double(menu_duration_frames(params))
96             / double(params.rate_numer)
97             * double(params.rate_denom);
98     }
99
100     void throw_length_error(const char * limit_type, std::size_t limit)
101     {
102         std::ostringstream oss;
103         oss << "exceeded DVD limit: " << limit_type << " > " << limit;
104         throw std::length_error(oss.str());
105     }
106
107     // dvdauthor uses some menu numbers to represent entry points -
108     // distinct from the actual numbers of the menus assigned as those
109     // entry points - resulting in a practical limit of 119 per
110     // domain.  This seems to be an oddity of the parser that could be
111     // fixed, but for now we'll have to work with it.
112     const unsigned dvdauthor_anonymous_menus_max = dvd::domain_pgcs_max - 8;
113
114     // The current navigation code packs menu and button number into a
115     // single register, so the number of menus is limited to
116     // dvd::reg_s8_button_mult - 1 == 1023.  However temp_file_name()
117     // is limited to 999 numbered files and it seems pointless to
118     // change it to get another 24.
119     // If people really need more we could use separate menu and
120     // button number registers, possibly allowing up to 11900 menus
121     // (the size of the indirect jump tables might become a problem
122     // though).
123     const unsigned menus_max = 999;
124 }
125
126 dvd_generator::dvd_generator(const video::frame_params & frame_params,
127                              mpeg_encoder encoder)
128         : temp_dir_("videolink-"),
129           frame_params_(frame_params),
130           encoder_(encoder)
131 {}
132
133 dvd_generator::pgc_ref dvd_generator::add_menu()
134 {
135     pgc_ref next_menu(menu_pgc, menus_.size());
136
137     if (next_menu.index == menus_max)
138         throw_length_error("number of menus", menus_max);
139
140     menus_.resize(next_menu.index + 1);
141     return next_menu;
142 }
143
144 void dvd_generator::add_menu_entry(unsigned index,
145                                    const rectangle & area,
146                                    const pgc_ref & target)
147 {
148     assert(index < menus_.size());
149     assert(target.type == menu_pgc && target.index < menus_.size()
150            || target.type == title_pgc && target.index < titles_.size());
151
152     if (menus_[index].entries.size() == dvd::menu_buttons_max)
153         throw_length_error("number of buttons", dvd::menu_buttons_max);
154
155     menu_entry new_entry = { area, target };
156     menus_[index].entries.push_back(new_entry);
157 }
158
159 void dvd_generator::generate_menu_vob(unsigned index,
160                                       Glib::RefPtr<Gdk::Pixbuf> background,
161                                       Glib::RefPtr<Gdk::Pixbuf> highlights)
162     const
163 {
164     assert(index < menus_.size());
165     const menu & this_menu = menus_[index];
166
167     std::string background_name(
168         temp_file_name(temp_dir_, "menu-%3d-back.png", 1 + index));
169     std::cout << "INFO: Saving " << background_name << std::endl;
170     background->save(background_name, "png");
171
172     std::string highlights_name(
173         temp_file_name(temp_dir_, "menu-%3d-links.png", 1 + index));
174     std::cout << "INFO: Saving " << highlights_name << std::endl;
175     highlights->save(highlights_name, "png");
176
177     std::string spumux_name(
178         temp_file_name(temp_dir_, "menu-%3d.subpictures", 1 + index));
179     std::ofstream spumux_file(spumux_name.c_str());
180     spumux_file <<
181         "<subpictures>\n"
182         "  <stream>\n"
183         "    <spu force='yes' start='00:00:00.00'\n"
184         "        highlight='" << highlights_name << "'\n"
185         "        select='" << highlights_name << "'>\n";
186     int button_count = this_menu.entries.size();
187     for (int i = 0; i != button_count; ++i)
188     {
189         const menu_entry & this_entry = this_menu.entries[i];
190
191         // We program left and right to cycle through the buttons in
192         // the order the entries were added.  This should result in
193         // left and right behaving like the tab and shift-tab keys
194         // would in the browser.  Hopefully that's a sensible order.
195         // We program up and down to behave geometrically.
196         int up_button = i, down_button = i;
197         double up_closeness = 0.0, down_closeness = 0.0;
198         for (int j = 0; j != button_count; ++j)
199         {
200             const menu_entry & other_entry = this_menu.entries[j];
201             double closeness = directed_closeness(
202                 this_entry.area, other_entry.area, -1);
203             if (closeness > up_closeness)
204             {
205                 up_button = j;
206                 up_closeness = closeness;
207             }
208             else
209             {
210                 closeness = directed_closeness(
211                     this_entry.area, other_entry.area, 1);
212                 if (closeness > down_closeness)
213                 {
214                     down_button = j;
215                     down_closeness = closeness;
216                 }
217             }
218         }
219         // Pad vertically to even y coordinates since dvdauthor claims
220         // odd values may result in incorrect display.
221         // XXX This may cause overlappping where it wasn't previously
222         // a problem.
223         spumux_file << "      <button"
224             " x0='" << this_entry.area.left << "'"
225             " y0='" << (this_entry.area.top & ~1) << "'"
226             " x1='" << this_entry.area.right << "'"
227             " y1='" << ((this_entry.area.bottom + 1) & ~1) << "'"
228             " left='" << (i == 0 ? button_count : i) << "'"
229             " right='" << 1 + (i + 1) % button_count << "'"
230             " up='" << 1 + up_button << "'"
231             " down='" << 1 + down_button << "'"
232             "/>\n";
233     }
234     spumux_file <<
235         "    </spu>\n"
236         "  </stream>\n"
237         "</subpictures>\n";
238     spumux_file.close();
239     if (!spumux_file)
240         throw std::runtime_error("Failed to write control file for spumux");
241
242     std::string output_name(
243         temp_file_name(temp_dir_, "menu-%3d.mpeg", 1 + index));
244
245     std::ostringstream command_stream;
246     if (encoder_ == mpeg_encoder_ffmpeg)
247     {
248         command_stream <<
249             "ffmpeg -f image2 -vcodec png"
250             " -r " << frame_params_.rate_numer <<
251             "/" << frame_params_.rate_denom <<
252             " -loop_input -i " << background_name <<
253             " -t " << menu_duration_seconds(frame_params_) <<
254             " -target " << frame_params_.common_name <<  "-dvd"
255             " -aspect 4:3 -an -y /dev/stdout";
256     }
257     else
258     {
259         assert(encoder_ == mpeg_encoder_mjpegtools);
260         command_stream
261             << "pngtopnm " << background_name
262             << " | ppmtoy4m -v0 -n" << menu_duration_frames(frame_params_)
263             << " -F" << frame_params_.rate_numer << ":" << frame_params_.rate_denom
264             << " -A" << frame_params_.pixel_ratio_width
265             << ":" << frame_params_.pixel_ratio_height
266             << " -Ip -S420mpeg2"
267             " | mpeg2enc -v0 -f8 -a2 -o/dev/stdout"
268             " | mplex -v0 -f8 -o/dev/stdout /dev/stdin";
269     }
270     command_stream
271         << " | spumux -v0 -mdvd " << spumux_name << " > " << output_name;
272     std::string command(command_stream.str());
273     const char * argv[] = {
274         "/bin/sh", "-c", command.c_str(), 0
275     };
276     std::cout << "INFO: Running " << command << std::endl;
277     int command_result;
278     Glib::spawn_sync(".",
279                      Glib::ArrayHandle<std::string>(
280                          argv, sizeof(argv)/sizeof(argv[0]),
281                          Glib::OWNERSHIP_NONE),
282                      Glib::SPAWN_STDOUT_TO_DEV_NULL,
283                      sigc::slot<void>(),
284                      0, 0,
285                      &command_result);
286     struct stat stat_buf;
287     if (command_result != 0 || stat(output_name.c_str(), &stat_buf) != 0
288         || stat_buf.st_size == 0)
289         throw std::runtime_error("spumux pipeline failed");
290 }
291
292 dvd_generator::pgc_ref dvd_generator::add_title(vob_list & content)
293 {
294     pgc_ref next_title(title_pgc, titles_.size());
295
296     // Check against maximum number of titles.
297     if (next_title.index == dvd::titles_max)
298         throw_length_error("number of titles", dvd::titles_max);
299
300     titles_.resize(next_title.index + 1);
301     titles_[next_title.index].swap(content);
302     return next_title;
303 }
304
305 void dvd_generator::generate(const std::string & output_dir) const
306 {
307     // This function uses a mixture of 0-based and 1-based numbering,
308     // due to the differing conventions of the language and the DVD
309     // format.  Variable names ending in "_index" indicate 0-based
310     // indices and variable names ending in "_num" indicate 1-based
311     // numbers.
312
313     std::string name(temp_file_name(temp_dir_, "videolink.dvdauthor"));
314     std::ofstream file(name.c_str());
315     file << "<dvdauthor>\n";
316
317     // We generate code that uses registers in the following way:
318     //
319     // g0: Scratch.
320     // g1: Target location when jumping between menus.  Top 6 bits are
321     //     the button number (like s8) and bottom 10 bits are the menu
322     //     number.  This is used for selecting the appropriate button
323     //     when entering a menu, for completing indirect jumps between
324     //     domains, and for jumping to the correct menu after exiting a
325     //     title.  This is set to 0 in the pre-routine of the target
326     //     menu.
327     // g2: Current location in menus.  This is used for jumping to the
328     //     correct menu when the player exits a title.
329     // g3: Target chapter number plus 1 when jumping to a title.
330     //     This is used to jump to the correct chapter and to
331     //     distinguish between indirect jumps to menus and titles.
332     //     This is set to 0 in the pre-routine of the target title.
333     // g4: Source menu location used to jump to a title.  This is
334     //     compared with g2 to determine whether to increment the
335     //     button number if the title is played to the end.
336
337     static const unsigned button_mult = dvd::reg_s8_button_mult;
338     static const unsigned menu_mask = button_mult - 1;
339     static const unsigned button_mask = (1U << dvd::reg_bits) - button_mult;
340
341     // Iterate over VMGM and titlesets.  For these purposes, we
342     // consider the VMGM to be titleset 0.
343
344     // We need a titleset for each title, and we may also need titlesets to
345     // hold extra menus if we have too many for the VMGM.
346     // Also, we need at least one titleset.
347     const unsigned titleset_end = std::max<unsigned>(
348             1U + std::max<unsigned>(1U, titles_.size()),
349             (menus_.size() + dvdauthor_anonymous_menus_max - 1)
350                  / dvdauthor_anonymous_menus_max);
351
352     for (unsigned titleset_num = 0;
353          titleset_num != titleset_end;
354          ++titleset_num)
355     {
356         const char * const outer_element_name =
357             titleset_num == 0 ? "vmgm" : "titleset";
358         const bool have_real_title =
359             titleset_num != 0 && titleset_num <= titles_.size();
360         const bool have_real_menus =
361             titleset_num * dvdauthor_anonymous_menus_max < menus_.size();
362
363         file << "  <" << outer_element_name << ">\n" <<
364              "    <menus>\n"
365              "      <video format='" << frame_params_.common_name << "'/>\n";
366
367         const unsigned menu_begin = titleset_num * dvdauthor_anonymous_menus_max;
368         const unsigned menu_end =
369             have_real_menus
370             ? std::min<unsigned>(
371                 (titleset_num + 1) * dvdauthor_anonymous_menus_max,
372                 menus_.size())
373             : menu_begin + 1;
374
375         for (unsigned menu_index = menu_begin;
376              menu_index != menu_end;
377              ++menu_index)
378         {
379             // There are various cases in which menus may be called:
380             //
381             // 1. The user follows a direct link to the menu.
382             // 2. The user follows an indirect link to some other menu
383             //    and that goes via this menu.  This is distinguished
384             //    from case 1 by the value of g1.  We must jump to or
385             //    at least toward the other menu.
386             // 3. The title menu is called when the disc is first
387             //    played or the user presses the "top menu" button.
388             //    This is distinguished from cases 2 and 3 by g1 == 0.
389             //    We make this look like case 1.
390             // 4. The root menu of a titleset is called when the user
391             //    follows an indirect link to the title.  This is
392             //    distinguished from all other cases by g3 != 0.  We
393             //    must jump to the title.
394             // 5. The root menu of a titleset is called when the title
395             //    ends or the user presses the "menu" button during
396             //    the title.  This is distinguished from cases 1, 2
397             //    and 4 by g1 == 0 and g3 == 0.  We must jump to the
398             //    latest menu (which can turn into case 1 or 2).
399             //
400             // Cases 3 and 5 do not apply to the same menus so they
401             // do not need to be distinguished.
402
403             if (menu_index == 0)
404             {
405                 // Title menu.
406                 file <<
407                     "      <pgc entry='title'>\n"
408                     "        <pre>\n"
409                     "          if (g1 eq 0)\n" // case 3
410                     "            g1 = " << 1 + button_mult << ";\n";
411             }
412             else if (menu_index == titleset_num * dvdauthor_anonymous_menus_max)
413             {
414                 // Root menu.
415                 file <<
416                     "      <pgc entry='root'>\n"
417                     "        <pre>\n";
418                 if (have_real_title)
419                 {
420                     file <<
421                         "          if (g3 ne 0)\n" // case 4
422                         "            jump title 1;\n"
423                         "          if (g1 eq 0) {\n" // case 5
424                         "            g1 = g2;\n"
425                         "            jump vmgm menu entry title;\n"
426                         "          }\n";
427                 }
428             }
429             else
430             {
431                 // Some other menu.
432                 file <<
433                     "      <pgc>\n"
434                     "        <pre>\n";
435             }
436
437             if (!have_real_menus)
438             {
439                 // This is a root menu only reachable from the title.
440                 file <<
441                     "        </pre>\n"
442                     "      </pgc>\n";
443                 continue;
444             }
445
446             const menu & this_menu = menus_[menu_index];
447
448             // Detect and handle case 2.
449             //
450             // There is a limit of 128 VM instructions in each PGC.
451             // Also, we can't jump to an arbitrary menu in another
452             // domain.  Finally, we can't do computed jumps.
453             // Therefore we statically expand and distribute a binary
454             // search across the menus, resulting in a code size of
455             // O(log(menu_count)) in each menu.  In practice there are
456             // at most 11 conditional jumps needed in any menu.
457             //
458             // The initial bounds of the binary search are strange
459             // because we must ensure that any jump between titlesets
460             // is to the first menu of the titleset, marked as the
461             // root entry.
462
463             // Mask target location to get the target menu.
464             file << "          g0 = g1 &amp; " << menu_mask << ";\n";
465
466             for (unsigned
467                      bottom = 0,
468                      top = 16 * dvdauthor_anonymous_menus_max;
469                  top - bottom > 1;)
470             {
471                 unsigned middle = (bottom + top) / 2;
472                 if (menu_index == bottom && middle < menus_.size())
473                 {
474                     file << "          if (g0 ge " << 1 + middle << ")\n"
475                          << "            jump ";
476                     unsigned target_titleset_num =
477                         middle / dvdauthor_anonymous_menus_max;
478                     if (target_titleset_num != titleset_num)
479                     {
480                         assert(middle % dvdauthor_anonymous_menus_max == 0);
481                         file << "titleset " << target_titleset_num
482                              << " menu entry root";
483                     }
484                     else
485                     {
486                         file << "menu "
487                              << 1 + middle % dvdauthor_anonymous_menus_max;
488                     }
489                     file << ";\n";
490                 }
491                 if (menu_index >= middle)
492                     bottom = middle;
493                 else
494                     top = middle;
495             }
496
497             // Case 1.
498
499             // Highlight the appropriate button.
500             file << "          s8 = g1 &amp; " << button_mask << ";\n";
501
502             // Copy the target location to the current location and
503             // then clear the target location so that the title menu
504             // can distinguish cases 2 and 3.
505             file <<
506                 "          g2 = g1;\n"
507                 "          g1 = 0;\n";
508
509             file <<
510                 "        </pre>\n"
511                 "        <vob file='"
512                  << temp_file_name(temp_dir_, "menu-%3d.mpeg",
513                                    1 + menu_index)
514                  << "'>\n"
515                 // Define a cell covering the whole menu and set a still
516                 // time at the end of that, since it seems all players
517                 // support that but some ignore a still time set on a PGC.
518                 "          <cell start='0' end='"
519                  << std::fixed << std::setprecision(4)
520                  << menu_duration_seconds(frame_params_) << "'"
521                 " chapter='yes' pause='inf'/>\n"
522                 "        </vob>\n";
523
524             for (unsigned button_index = 0;
525                  button_index != this_menu.entries.size();
526                  ++button_index)
527             {
528                 const pgc_ref & target =
529                     this_menu.entries[button_index].target;
530
531                 file << "        <button> ";
532
533                 if (target.type == menu_pgc)
534                 {
535                     unsigned target_button_num;
536
537                     if (target.sub_index)
538                     {
539                         target_button_num = target.sub_index;
540                     }
541                     else
542                     {
543                         // Look for a button on the new menu that links
544                         // back to this one.  If there is one, set that to
545                         // be the highlighted button; otherwise, use the
546                         // first button.
547                         const std::vector<menu_entry> & target_menu_entries =
548                             menus_[target.index].entries;
549                         pgc_ref this_pgc(menu_pgc, menu_index);
550                         target_button_num = target_menu_entries.size();
551                         while (target_button_num != 1
552                                && (target_menu_entries[target_button_num - 1].target
553                                    != this_pgc))
554                             --target_button_num;
555                     }
556                          
557                     // Set new menu location.
558                     file << "g1 = "
559                          << (1 + target.index + target_button_num * button_mult)
560                          << "; ";
561                     // Jump to the target menu.
562                     unsigned target_titleset_num =
563                         target.index / dvdauthor_anonymous_menus_max;
564                     if (target_titleset_num == titleset_num)
565                         file << "jump menu "
566                              << 1 + (target.index
567                                      % dvdauthor_anonymous_menus_max)
568                              << "; ";
569                     else if (target_titleset_num == 0)
570                         file << "jump vmgm menu entry title; ";
571                     else
572                         file << "jump titleset " << target_titleset_num
573                              << " menu entry root; ";
574                 }
575                 else
576                 {
577                     assert(target.type == title_pgc);
578
579                     // Record current menu location and set target chapter
580                     // number.
581                     file <<
582                         "g2 = " << (1 + menu_index
583                                     + (1 + button_index) * button_mult) << "; "
584                         "g3 = " << 1 + target.sub_index << "; ";
585                     // Jump to the target title, possibly via its titleset's
586                     // root menu.
587                     unsigned target_titleset_num = 1 + target.index;
588                     if (titleset_num == 0)
589                         file << "jump title " << target_titleset_num << "; ";
590                     else if (target_titleset_num == titleset_num)
591                         file << "jump title 1; ";
592                     else
593                         file << "jump titleset " << target_titleset_num
594                              << " menu entry root; ";
595                 }
596
597                 file <<  "</button>\n";
598             }
599
600             file <<
601                 "      </pgc>\n";
602         }
603
604         file << "    </menus>\n";
605
606         if (have_real_title)
607         {
608             file <<
609                 "    <titles>\n"
610                 "      <video format='" << frame_params_.common_name << "'/>\n"
611                 "      <pgc>\n";
612
613             file << "        <pre>\n";
614
615             // Count chapters in the title.
616             unsigned n_chapters = 0;
617             for (vob_list::const_iterator
618                      it = titles_[titleset_num - 1].begin(),
619                      end = titles_[titleset_num - 1].end();
620                  it != end;
621                  ++it)
622             {
623                 // Chapter start times may be specified in the "chapters"
624                 // attribute as a comma-separated list.  If this is not
625                 // specified then the beginning of each file starts a new
626                 // chapter.  Thus the number of chapters in each file is
627                 // the number of commas in the chapter attribute, plus 1.
628                 ++n_chapters;
629                 std::size_t pos = 0;
630                 while ((pos = it->chapters.find(',', pos)) != std::string::npos)
631                 {
632                     ++n_chapters;
633                     ++pos;
634                 }
635             }
636
637             // Move the chapter number to scratch so the root menu can
638             // distinguish cases 4 and 5.
639             file << "          g0 = g3; g3 = 0;\n";
640
641             // Copy the latest menu location for use by the post-routine.
642             file << "          g4 = g2;\n";
643
644             // Jump to the correct chapter.
645             for (unsigned chapter_num = 1;
646                  chapter_num <= n_chapters;
647                  ++chapter_num)
648                 file <<
649                     "          if (g0 eq " << 1 + chapter_num << ")\n"
650                     "            jump chapter " << chapter_num << ";\n";
651
652             file << "        </pre>\n";
653
654             for (vob_list::const_iterator
655                      it = titles_[titleset_num - 1].begin(),
656                      end = titles_[titleset_num - 1].end();
657                  it != end;
658                  ++it)
659             {
660                 file << "        <vob file='" << xml_escape(it->file) << "'";
661                 if (!it->chapters.empty())
662                     file << " chapters='" << xml_escape(it->chapters) << "'";
663                 if (!it->pause.empty())
664                     file << " pause='" << xml_escape(it->pause) << "'";
665                 file << "/>\n";
666             }
667
668             // If the user has not exited to the menus and then
669             // resumed the title, set the latest menu location to be
670             // the button after the one that linked to this title.
671             // In any case, return to the (root) menu which will
672             // then jump to the correct menu.
673             file <<
674                 "        <post>\n"
675                 "          if (g2 eq g4)\n"
676                 "            g2 = g2 + " << button_mult << ";\n"
677                 "          call menu;\n"
678                 "        </post>\n"
679                 "      </pgc>\n"
680                 "    </titles>\n";
681         }
682         else if (titleset_num != 0) // && !have_real_title
683         {
684             file << "    <titles><pgc/></titles>\n";
685         }
686
687         file << "  </" << outer_element_name << ">\n";
688     }
689
690     file << "</dvdauthor>\n";
691     file.close();
692
693     {
694         const char * argv[] = {
695             "dvdauthor",
696             "-o", output_dir.c_str(),
697             "-x", name.c_str(),
698             0
699         };
700         int command_result;
701         Glib::spawn_sync(".",
702                          Glib::ArrayHandle<std::string>(
703                              argv, sizeof(argv)/sizeof(argv[0]),
704                              Glib::OWNERSHIP_NONE),
705                          Glib::SPAWN_SEARCH_PATH
706                          | Glib::SPAWN_STDOUT_TO_DEV_NULL,
707                          sigc::slot<void>(),
708                          0, 0,
709                          &command_result);
710         if (command_result != 0)
711             throw std::runtime_error("dvdauthor failed");
712     }
713 }