Fix potential crash if maybe_add_text() fails.
[dcpomatic.git] / src / lib / reel_writer.cc
1 /*
2     Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "reel_writer.h"
22 #include "film.h"
23 #include "cross.h"
24 #include "job.h"
25 #include "log.h"
26 #include "dcpomatic_log.h"
27 #include "digester.h"
28 #include "font.h"
29 #include "compose.hpp"
30 #include "config.h"
31 #include "audio_buffers.h"
32 #include "image.h"
33 #include <dcp/atmos_asset.h>
34 #include <dcp/atmos_asset_writer.h>
35 #include <dcp/mono_picture_asset.h>
36 #include <dcp/stereo_picture_asset.h>
37 #include <dcp/sound_asset.h>
38 #include <dcp/sound_asset_writer.h>
39 #include <dcp/reel.h>
40 #include <dcp/reel_atmos_asset.h>
41 #include <dcp/reel_mono_picture_asset.h>
42 #include <dcp/reel_stereo_picture_asset.h>
43 #include <dcp/reel_sound_asset.h>
44 #include <dcp/reel_subtitle_asset.h>
45 #include <dcp/reel_closed_caption_asset.h>
46 #include <dcp/reel_markers_asset.h>
47 #include <dcp/dcp.h>
48 #include <dcp/cpl.h>
49 #include <dcp/certificate_chain.h>
50 #include <dcp/interop_subtitle_asset.h>
51 #include <dcp/smpte_subtitle_asset.h>
52 #include <dcp/raw_convert.h>
53 #include <dcp/subtitle_image.h>
54 #include <boost/foreach.hpp>
55
56 #include "i18n.h"
57
58 using std::list;
59 using std::string;
60 using std::cout;
61 using std::exception;
62 using std::map;
63 using std::vector;
64 using boost::shared_ptr;
65 using boost::optional;
66 using boost::dynamic_pointer_cast;
67 #if BOOST_VERSION >= 106100
68 using namespace boost::placeholders;
69 #endif
70 using dcp::ArrayData;
71 using dcp::Data;
72 using dcp::raw_convert;
73 using namespace dcpomatic;
74
75 int const ReelWriter::_info_size = 48;
76
77 static dcp::MXFMetadata
78 mxf_metadata ()
79 {
80         dcp::MXFMetadata meta;
81         Config* config = Config::instance();
82         if (!config->dcp_company_name().empty()) {
83                 meta.company_name = config->dcp_company_name ();
84         }
85         if (!config->dcp_product_name().empty()) {
86                 meta.product_name = config->dcp_product_name ();
87         }
88         if (!config->dcp_product_version().empty()) {
89                 meta.product_version = config->dcp_product_version ();
90         }
91         return meta;
92 }
93
94 /** @param job Related job, or 0 */
95 ReelWriter::ReelWriter (
96         shared_ptr<const Film> film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, optional<string> content_summary
97         )
98         : _film (film)
99         , _period (period)
100         , _reel_index (reel_index)
101         , _reel_count (reel_count)
102         , _content_summary (content_summary)
103         , _job (job)
104 {
105         /* Create or find our picture asset in a subdirectory, named
106            according to those film's parameters which affect the video
107            output.  We will hard-link it into the DCP later.
108         */
109
110         dcp::Standard const standard = _film->interop() ? dcp::INTEROP : dcp::SMPTE;
111
112         boost::filesystem::path const asset =
113                 _film->internal_video_asset_dir() / _film->internal_video_asset_filename(_period);
114
115         _first_nonexistant_frame = check_existing_picture_asset (asset);
116
117         if (_first_nonexistant_frame < period.duration().frames_round(_film->video_frame_rate())) {
118                 /* We do not have a complete picture asset.  If there is an
119                    existing asset, break any hard links to it as we are about
120                    to change its contents (if only by changing the IDs); see
121                    #1126.
122                 */
123                 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
124                         if (job) {
125                                 job->sub (_("Copying old video file"));
126                                 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
127                         } else {
128                                 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
129                         }
130                         boost::filesystem::remove (asset);
131                         boost::filesystem::rename (asset.string() + ".tmp", asset);
132                 }
133
134
135                 if (_film->three_d ()) {
136                         _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
137                 } else {
138                         _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
139                 }
140
141                 _picture_asset->set_size (_film->frame_size());
142                 _picture_asset->set_metadata (mxf_metadata());
143
144                 if (_film->encrypted ()) {
145                         _picture_asset->set_key (_film->key());
146                         _picture_asset->set_context_id (_film->context_id());
147                 }
148
149                 _picture_asset->set_file (asset);
150                 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
151         } else {
152                 /* We already have a complete picture asset that we can just re-use */
153                 /* XXX: what about if the encryption key changes? */
154                 if (_film->three_d ()) {
155                         _picture_asset.reset (new dcp::StereoPictureAsset(asset));
156                 } else {
157                         _picture_asset.reset (new dcp::MonoPictureAsset(asset));
158                 }
159         }
160
161         if (_film->audio_channels ()) {
162                 _sound_asset.reset (
163                         new dcp::SoundAsset (dcp::Fraction(_film->video_frame_rate(), 1), _film->audio_frame_rate(), _film->audio_channels(), _film->audio_language(), standard)
164                         );
165
166                 _sound_asset->set_metadata (mxf_metadata());
167
168                 if (_film->encrypted ()) {
169                         _sound_asset->set_key (_film->key ());
170                 }
171
172                 DCPOMATIC_ASSERT (_film->directory());
173
174                 vector<dcp::Channel> active;
175                 BOOST_FOREACH (int i, _film->mapped_audio_channels()) {
176                         active.push_back (static_cast<dcp::Channel>(i));
177                 }
178
179                 /* Write the sound asset into the film directory so that we leave the creation
180                    of the DCP directory until the last minute.
181                 */
182                 _sound_asset_writer = _sound_asset->start_write (
183                         _film->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
184                         active,
185                         _film->contains_atmos_content()
186                         );
187         }
188 }
189
190 /** @param frame reel-relative frame */
191 void
192 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
193 {
194         shared_ptr<InfoFileHandle> handle = _film->info_file_handle(_period, false);
195         dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
196         checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
197         checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
198         checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
199 }
200
201 dcp::FrameInfo
202 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
203 {
204         dcp::FrameInfo frame_info;
205         dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
206         checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
207         checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
208
209         char hash_buffer[33];
210         checked_fread (hash_buffer, 32, info->get(), info->file());
211         hash_buffer[32] = '\0';
212         frame_info.hash = hash_buffer;
213
214         return frame_info;
215 }
216
217 long
218 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
219 {
220         switch (eyes) {
221         case EYES_BOTH:
222                 return frame * _info_size;
223         case EYES_LEFT:
224                 return frame * _info_size * 2;
225         case EYES_RIGHT:
226                 return frame * _info_size * 2 + _info_size;
227         default:
228                 DCPOMATIC_ASSERT (false);
229         }
230
231         DCPOMATIC_ASSERT (false);
232 }
233
234 Frame
235 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
236 {
237         shared_ptr<Job> job = _job.lock ();
238
239         if (job) {
240                 job->sub (_("Checking existing image data"));
241         }
242
243         /* Try to open the existing asset */
244         FILE* asset_file = fopen_boost (asset, "rb");
245         if (!asset_file) {
246                 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
247                 return 0;
248         } else {
249                 LOG_GENERAL ("Opened existing asset at %1", asset.string());
250         }
251
252         shared_ptr<InfoFileHandle> info_file;
253
254         try {
255                 info_file = _film->info_file_handle (_period, true);
256         } catch (OpenFileError &) {
257                 LOG_GENERAL_NC ("Could not open film info file");
258                 fclose (asset_file);
259                 return 0;
260         }
261
262         /* Offset of the last dcp::FrameInfo in the info file */
263         int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
264         LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
265
266         Frame first_nonexistant_frame;
267         if (_film->three_d ()) {
268                 /* Start looking at the last left frame */
269                 first_nonexistant_frame = n / 2;
270         } else {
271                 first_nonexistant_frame = n;
272         }
273
274         while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
275                 --first_nonexistant_frame;
276         }
277
278         if (!_film->three_d() && first_nonexistant_frame > 0) {
279                 /* If we are doing 3D we might have found a good L frame with no R, so only
280                    do this if we're in 2D and we've just found a good B(oth) frame.
281                 */
282                 ++first_nonexistant_frame;
283         }
284
285         LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
286
287         fclose (asset_file);
288
289         return first_nonexistant_frame;
290 }
291
292 void
293 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
294 {
295         if (!_picture_asset_writer) {
296                 /* We're not writing any data */
297                 return;
298         }
299
300         dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data(), encoded->size());
301         write_frame_info (frame, eyes, fin);
302         _last_written[eyes] = encoded;
303 }
304
305
306 void
307 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
308 {
309         if (!_atmos_asset) {
310                 _atmos_asset = metadata.create (dcp::Fraction(_film->video_frame_rate(), 1));
311                 if (_film->encrypted()) {
312                         _atmos_asset->set_key(_film->key());
313                 }
314                 _atmos_asset_writer = _atmos_asset->start_write (
315                         _film->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
316                         );
317         }
318         _atmos_asset_writer->write (atmos);
319 }
320
321
322 void
323 ReelWriter::fake_write (int size)
324 {
325         if (!_picture_asset_writer) {
326                 /* We're not writing any data */
327                 return;
328         }
329
330         _picture_asset_writer->fake_write (size);
331 }
332
333 void
334 ReelWriter::repeat_write (Frame frame, Eyes eyes)
335 {
336         if (!_picture_asset_writer) {
337                 /* We're not writing any data */
338                 return;
339         }
340
341         dcp::FrameInfo fin = _picture_asset_writer->write (
342                 _last_written[eyes]->data(),
343                 _last_written[eyes]->size()
344                 );
345         write_frame_info (frame, eyes, fin);
346 }
347
348 void
349 ReelWriter::finish ()
350 {
351         if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
352                 /* Nothing was written to the picture asset */
353                 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
354                 _picture_asset.reset ();
355         }
356
357         if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
358                 /* Nothing was written to the sound asset */
359                 _sound_asset.reset ();
360         }
361
362         /* Hard-link any video asset file into the DCP */
363         if (_picture_asset) {
364                 DCPOMATIC_ASSERT (_picture_asset->file());
365                 boost::filesystem::path video_from = _picture_asset->file().get();
366                 boost::filesystem::path video_to;
367                 video_to /= _film->dir (_film->dcp_name());
368                 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
369                 /* There may be an existing "to" file if we are recreating a DCP in the same place without
370                    changing any video.
371                 */
372                 boost::system::error_code ec;
373                 boost::filesystem::remove (video_to, ec);
374
375                 boost::filesystem::create_hard_link (video_from, video_to, ec);
376                 if (ec) {
377                         LOG_WARNING_NC ("Hard-link failed; copying instead");
378                         shared_ptr<Job> job = _job.lock ();
379                         if (job) {
380                                 job->sub (_("Copying video file into DCP"));
381                                 try {
382                                         copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
383                                 } catch (exception& e) {
384                                         LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
385                                         throw FileError (e.what(), video_from);
386                                 }
387                         } else {
388                                 boost::filesystem::copy_file (video_from, video_to, ec);
389                                 if (ec) {
390                                         LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
391                                         throw FileError (ec.message(), video_from);
392                                 }
393                         }
394                 }
395
396                 _picture_asset->set_file (video_to);
397         }
398
399         /* Move the audio asset into the DCP */
400         if (_sound_asset) {
401                 boost::filesystem::path audio_to;
402                 audio_to /= _film->dir (_film->dcp_name ());
403                 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
404                 audio_to /= aaf;
405
406                 boost::system::error_code ec;
407                 boost::filesystem::rename (_film->file (aaf), audio_to, ec);
408                 if (ec) {
409                         throw FileError (
410                                 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
411                                 );
412                 }
413
414                 _sound_asset->set_file (audio_to);
415         }
416
417         if (_atmos_asset) {
418                 _atmos_asset_writer->finalize ();
419                 boost::filesystem::path atmos_to;
420                 atmos_to /= _film->dir (_film->dcp_name());
421                 string const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
422                 atmos_to /= aaf;
423
424                 boost::system::error_code ec;
425                 boost::filesystem::rename (_film->file(aaf), atmos_to, ec);
426                 if (ec) {
427                         throw FileError (
428                                 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
429                                 );
430                 }
431
432                 _atmos_asset->set_file (atmos_to);
433         }
434 }
435
436 template <class T>
437 shared_ptr<T>
438 maybe_add_text (
439         shared_ptr<dcp::SubtitleAsset> asset,
440         int64_t picture_duration,
441         shared_ptr<dcp::Reel> reel,
442         list<ReferencedReelAsset> const & refs,
443         list<shared_ptr<Font> > const & fonts,
444         shared_ptr<const Film> film,
445         DCPTimePeriod period
446         )
447 {
448         Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
449
450         shared_ptr<T> reel_asset;
451
452         if (asset) {
453                 /* Add the font to the subtitle content */
454                 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
455                         asset->add_font (j->id(), j->file().get_value_or(default_font_file()));
456                 }
457
458                 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
459                         boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
460                         boost::filesystem::create_directories (directory);
461                         asset->write (directory / ("sub_" + asset->id() + ".xml"));
462                 } else {
463                         /* All our assets should be the same length; use the picture asset length here
464                            as a reference to set the subtitle one.  We'll use the duration rather than
465                            the intrinsic duration; we don't care if the picture asset has been trimmed, we're
466                            just interested in its presentation length.
467                         */
468                         dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
469
470                         asset->write (
471                                 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
472                                 );
473                 }
474
475                 reel_asset.reset (
476                         new T (
477                                 asset,
478                                 dcp::Fraction (film->video_frame_rate(), 1),
479                                 picture_duration,
480                                 0
481                                 )
482                         );
483         } else {
484                 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
485                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
486                         shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
487                         if (k && j.period == period) {
488                                 reel_asset = k;
489                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
490                                 if (k->hash()) {
491                                         k->asset_ref()->set_hash (k->hash().get());
492                                 }
493                         }
494                 }
495         }
496
497         if (reel_asset) {
498                 if (reel_asset->actual_duration() != period_duration) {
499                         throw ProgrammingError (
500                                 __FILE__, __LINE__,
501                                 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
502                                 );
503                 }
504                 reel->add (reel_asset);
505         }
506
507         return reel_asset;
508 }
509
510 shared_ptr<dcp::Reel>
511 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
512 {
513         LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
514
515         shared_ptr<dcp::Reel> reel (new dcp::Reel ());
516
517         shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
518
519         if (_picture_asset) {
520                 /* We have made a picture asset of our own.  Put it into the reel */
521                 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
522                 if (mono) {
523                         reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
524                 }
525
526                 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
527                 if (stereo) {
528                         reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
529                 }
530         } else {
531                 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
532                 /* We don't have a picture asset of our own; hopefully we have one to reference */
533                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
534                         shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
535                         if (k) {
536                                 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
537                         }
538                         if (k && j.period == _period) {
539                                 reel_picture_asset = k;
540                         }
541                 }
542         }
543
544         Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
545
546         DCPOMATIC_ASSERT (reel_picture_asset);
547         if (reel_picture_asset->duration() != period_duration) {
548                 throw ProgrammingError (
549                         __FILE__, __LINE__,
550                         String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
551                         );
552         }
553         reel->add (reel_picture_asset);
554
555         /* If we have a hash for this asset in the CPL, assume that it is correct */
556         if (reel_picture_asset->hash()) {
557                 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
558         }
559
560         shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
561
562         if (_sound_asset) {
563                 /* We have made a sound asset of our own.  Put it into the reel */
564                 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
565         } else {
566                 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
567                 /* We don't have a sound asset of our own; hopefully we have one to reference */
568                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
569                         shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
570                         if (k) {
571                                 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
572                         }
573                         if (k && j.period == _period) {
574                                 reel_sound_asset = k;
575                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
576                                 if (k->hash()) {
577                                         k->asset_ref()->set_hash (k->hash().get());
578                                 }
579                         }
580                 }
581         }
582
583         DCPOMATIC_ASSERT (reel_sound_asset);
584         if (reel_sound_asset->actual_duration() != period_duration) {
585                 LOG_ERROR (
586                         "Reel sound asset has length %1 but reel period is %2",
587                         reel_sound_asset->actual_duration(),
588                         period_duration
589                         );
590                 if (reel_sound_asset->actual_duration() != period_duration) {
591                         throw ProgrammingError (
592                                 __FILE__, __LINE__,
593                                 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
594                                 );
595                 }
596
597         }
598         reel->add (reel_sound_asset);
599
600         maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
601         for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
602                 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
603                         i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
604                         );
605                 if (a) {
606                         a->set_annotation_text (i->first.name);
607                         a->set_language (i->first.language);
608                 }
609         }
610
611         Film::Markers markers = _film->markers ();
612         _film->add_ffoc_lfoc (markers);
613         Film::Markers reel_markers;
614         for (Film::Markers::const_iterator i = markers.begin(); i != markers.end(); ++i) {
615                 if (_period.contains(i->second)) {
616                         reel_markers[i->first] = i->second;
617                 }
618         }
619
620         if (!reel_markers.empty ()) {
621                 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
622                 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
623                         int h, m, s, f;
624                         DCPTime relative = i->second - _period.from;
625                         relative.split (_film->video_frame_rate(), h, m, s, f);
626                         ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
627                 }
628                 reel->add (ma);
629         }
630
631         if (_atmos_asset) {
632                 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
633         }
634
635         return reel;
636 }
637
638 void
639 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
640 {
641         if (_picture_asset) {
642                 _picture_asset->hash (set_progress);
643         }
644
645         if (_sound_asset) {
646                 _sound_asset->hash (set_progress);
647         }
648
649         if (_atmos_asset) {
650                 _atmos_asset->hash (set_progress);
651         }
652 }
653
654 Frame
655 ReelWriter::start () const
656 {
657         return _period.from.frames_floor (_film->video_frame_rate());
658 }
659
660
661 void
662 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
663 {
664         if (!_sound_asset_writer) {
665                 return;
666         }
667
668         DCPOMATIC_ASSERT (audio);
669         _sound_asset_writer->write (audio->data(), audio->frames());
670 }
671
672 void
673 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
674 {
675         shared_ptr<dcp::SubtitleAsset> asset;
676
677         switch (type) {
678         case TEXT_OPEN_SUBTITLE:
679                 asset = _subtitle_asset;
680                 break;
681         case TEXT_CLOSED_CAPTION:
682                 DCPOMATIC_ASSERT (track);
683                 asset = _closed_caption_assets[*track];
684                 break;
685         default:
686                 DCPOMATIC_ASSERT (false);
687         }
688
689         if (!asset) {
690                 vector<dcp::LanguageTag> lang = _film->subtitle_languages ();
691                 if (_film->interop ()) {
692                         shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
693                         s->set_movie_title (_film->name ());
694                         if (type == TEXT_OPEN_SUBTITLE) {
695                                 s->set_language (lang.empty() ? "Unknown" : lang.front().to_string());
696                         } else {
697                                 s->set_language (track->language);
698                         }
699                         s->set_reel_number (raw_convert<string> (_reel_index + 1));
700                         asset = s;
701                 } else {
702                         shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
703                         s->set_content_title_text (_film->name ());
704                         s->set_metadata (mxf_metadata());
705                         if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
706                                 s->set_language (lang.front().to_string());
707                         } else {
708                                 s->set_language (track->language);
709                         }
710                         s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
711                         s->set_reel_number (_reel_index + 1);
712                         s->set_time_code_rate (_film->video_frame_rate ());
713                         s->set_start_time (dcp::Time ());
714                         if (_film->encrypted ()) {
715                                 s->set_key (_film->key ());
716                         }
717                         asset = s;
718                 }
719         }
720
721         switch (type) {
722         case TEXT_OPEN_SUBTITLE:
723                 _subtitle_asset = asset;
724                 break;
725         case TEXT_CLOSED_CAPTION:
726                 DCPOMATIC_ASSERT (track);
727                 _closed_caption_assets[*track] = asset;
728                 break;
729         default:
730                 DCPOMATIC_ASSERT (false);
731         }
732
733         BOOST_FOREACH (StringText i, subs.string) {
734                 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
735                 i.set_in  (i.in()  - dcp::Time (_period.from.seconds(), i.in().tcr));
736                 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
737                 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
738         }
739
740         BOOST_FOREACH (BitmapText i, subs.bitmap) {
741                 asset->add (
742                         shared_ptr<dcp::Subtitle>(
743                                 new dcp::SubtitleImage(
744                                         i.image->as_png(),
745                                         dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
746                                         dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
747                                         i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
748                                         dcp::Time(), dcp::Time()
749                                         )
750                                 )
751                         );
752         }
753 }
754
755 bool
756 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
757 {
758         LOG_GENERAL ("Checking existing picture frame %1", frame);
759
760         /* Read the data from the info file; for 3D we just check the left
761            frames until we find a good one.
762         */
763         dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
764
765         bool ok = true;
766
767         /* Read the data from the asset and hash it */
768         dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
769         ArrayData data (info.size);
770         size_t const read = fread (data.data(), 1, data.size(), asset_file);
771         LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
772         if (read != static_cast<size_t> (data.size ())) {
773                 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
774                 ok = false;
775         } else {
776                 Digester digester;
777                 digester.add (data.data(), data.size());
778                 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
779                 if (digester.get() != info.hash) {
780                         LOG_GENERAL ("Existing frame %1 failed hash check", frame);
781                         ok = false;
782                 }
783         }
784
785         return ok;
786 }