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