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