fa84180f000179b5605804f944d96532c86d43f4
[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         shared_ptr<dcp::ReelSubtitleAsset> subtitle = maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
601         if (subtitle && !_film->subtitle_languages().empty()) {
602                 subtitle->set_language (_film->subtitle_languages().front());
603         }
604
605         for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
606                 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
607                         i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
608                         );
609                 if (a) {
610                         a->set_annotation_text (i->first.name);
611                         a->set_language (dcp::LanguageTag(i->first.language));
612                 }
613         }
614
615         Film::Markers markers = _film->markers ();
616         _film->add_ffoc_lfoc (markers);
617         Film::Markers reel_markers;
618         for (Film::Markers::const_iterator i = markers.begin(); i != markers.end(); ++i) {
619                 if (_period.contains(i->second)) {
620                         reel_markers[i->first] = i->second;
621                 }
622         }
623
624         if (!reel_markers.empty ()) {
625                 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
626                 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
627                         int h, m, s, f;
628                         DCPTime relative = i->second - _period.from;
629                         relative.split (_film->video_frame_rate(), h, m, s, f);
630                         ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
631                 }
632                 reel->add (ma);
633         }
634
635         if (_atmos_asset) {
636                 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
637         }
638
639         return reel;
640 }
641
642 void
643 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
644 {
645         if (_picture_asset) {
646                 _picture_asset->hash (set_progress);
647         }
648
649         if (_sound_asset) {
650                 _sound_asset->hash (set_progress);
651         }
652
653         if (_atmos_asset) {
654                 _atmos_asset->hash (set_progress);
655         }
656 }
657
658 Frame
659 ReelWriter::start () const
660 {
661         return _period.from.frames_floor (_film->video_frame_rate());
662 }
663
664
665 void
666 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
667 {
668         if (!_sound_asset_writer) {
669                 return;
670         }
671
672         DCPOMATIC_ASSERT (audio);
673         _sound_asset_writer->write (audio->data(), audio->frames());
674 }
675
676 void
677 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
678 {
679         shared_ptr<dcp::SubtitleAsset> asset;
680
681         switch (type) {
682         case TEXT_OPEN_SUBTITLE:
683                 asset = _subtitle_asset;
684                 break;
685         case TEXT_CLOSED_CAPTION:
686                 DCPOMATIC_ASSERT (track);
687                 asset = _closed_caption_assets[*track];
688                 break;
689         default:
690                 DCPOMATIC_ASSERT (false);
691         }
692
693         if (!asset) {
694                 vector<dcp::LanguageTag> lang = _film->subtitle_languages ();
695                 if (_film->interop ()) {
696                         shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
697                         s->set_movie_title (_film->name ());
698                         if (type == TEXT_OPEN_SUBTITLE) {
699                                 s->set_language (lang.empty() ? "Unknown" : lang.front().to_string());
700                         } else {
701                                 s->set_language (track->language);
702                         }
703                         s->set_reel_number (raw_convert<string> (_reel_index + 1));
704                         asset = s;
705                 } else {
706                         shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
707                         s->set_content_title_text (_film->name ());
708                         s->set_metadata (mxf_metadata());
709                         if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
710                                 s->set_language (lang.front());
711                         } else {
712                                 s->set_language (dcp::LanguageTag(track->language));
713                         }
714                         s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
715                         s->set_reel_number (_reel_index + 1);
716                         s->set_time_code_rate (_film->video_frame_rate ());
717                         s->set_start_time (dcp::Time ());
718                         if (_film->encrypted ()) {
719                                 s->set_key (_film->key ());
720                         }
721                         asset = s;
722                 }
723         }
724
725         switch (type) {
726         case TEXT_OPEN_SUBTITLE:
727                 _subtitle_asset = asset;
728                 break;
729         case TEXT_CLOSED_CAPTION:
730                 DCPOMATIC_ASSERT (track);
731                 _closed_caption_assets[*track] = asset;
732                 break;
733         default:
734                 DCPOMATIC_ASSERT (false);
735         }
736
737         BOOST_FOREACH (StringText i, subs.string) {
738                 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
739                 i.set_in  (i.in()  - dcp::Time (_period.from.seconds(), i.in().tcr));
740                 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
741                 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
742         }
743
744         BOOST_FOREACH (BitmapText i, subs.bitmap) {
745                 asset->add (
746                         shared_ptr<dcp::Subtitle>(
747                                 new dcp::SubtitleImage(
748                                         i.image->as_png(),
749                                         dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
750                                         dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
751                                         i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
752                                         dcp::Time(), dcp::Time()
753                                         )
754                                 )
755                         );
756         }
757 }
758
759 bool
760 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
761 {
762         LOG_GENERAL ("Checking existing picture frame %1", frame);
763
764         /* Read the data from the info file; for 3D we just check the left
765            frames until we find a good one.
766         */
767         dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
768
769         bool ok = true;
770
771         /* Read the data from the asset and hash it */
772         dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
773         ArrayData data (info.size);
774         size_t const read = fread (data.data(), 1, data.size(), asset_file);
775         LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
776         if (read != static_cast<size_t> (data.size ())) {
777                 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
778                 ok = false;
779         } else {
780                 Digester digester;
781                 digester.add (data.data(), data.size());
782                 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
783                 if (digester.get() != info.hash) {
784                         LOG_GENERAL ("Existing frame %1 failed hash check", frame);
785                         ok = false;
786                 }
787         }
788
789         return ok;
790 }