Make Atmos content work more like other content. Now its MXFs
[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                         );
175         }
176 }
177
178 /** @param frame reel-relative frame */
179 void
180 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
181 {
182         shared_ptr<InfoFileHandle> handle = _film->info_file_handle(_period, false);
183         dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
184         checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
185         checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
186         checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
187 }
188
189 dcp::FrameInfo
190 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
191 {
192         dcp::FrameInfo frame_info;
193         dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
194         checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
195         checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
196
197         char hash_buffer[33];
198         checked_fread (hash_buffer, 32, info->get(), info->file());
199         hash_buffer[32] = '\0';
200         frame_info.hash = hash_buffer;
201
202         return frame_info;
203 }
204
205 long
206 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
207 {
208         switch (eyes) {
209         case EYES_BOTH:
210                 return frame * _info_size;
211         case EYES_LEFT:
212                 return frame * _info_size * 2;
213         case EYES_RIGHT:
214                 return frame * _info_size * 2 + _info_size;
215         default:
216                 DCPOMATIC_ASSERT (false);
217         }
218
219         DCPOMATIC_ASSERT (false);
220 }
221
222 Frame
223 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
224 {
225         shared_ptr<Job> job = _job.lock ();
226
227         if (job) {
228                 job->sub (_("Checking existing image data"));
229         }
230
231         /* Try to open the existing asset */
232         FILE* asset_file = fopen_boost (asset, "rb");
233         if (!asset_file) {
234                 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
235                 return 0;
236         } else {
237                 LOG_GENERAL ("Opened existing asset at %1", asset.string());
238         }
239
240         shared_ptr<InfoFileHandle> info_file;
241
242         try {
243                 info_file = _film->info_file_handle (_period, true);
244         } catch (OpenFileError) {
245                 LOG_GENERAL_NC ("Could not open film info file");
246                 fclose (asset_file);
247                 return 0;
248         }
249
250         /* Offset of the last dcp::FrameInfo in the info file */
251         int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
252         LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
253
254         Frame first_nonexistant_frame;
255         if (_film->three_d ()) {
256                 /* Start looking at the last left frame */
257                 first_nonexistant_frame = n / 2;
258         } else {
259                 first_nonexistant_frame = n;
260         }
261
262         while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
263                 --first_nonexistant_frame;
264         }
265
266         if (!_film->three_d() && first_nonexistant_frame > 0) {
267                 /* If we are doing 3D we might have found a good L frame with no R, so only
268                    do this if we're in 2D and we've just found a good B(oth) frame.
269                 */
270                 ++first_nonexistant_frame;
271         }
272
273         LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
274
275         fclose (asset_file);
276
277         return first_nonexistant_frame;
278 }
279
280 void
281 ReelWriter::write (optional<Data> encoded, Frame frame, Eyes eyes)
282 {
283         if (!_picture_asset_writer) {
284                 /* We're not writing any data */
285                 return;
286         }
287
288         dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data().get (), encoded->size());
289         write_frame_info (frame, eyes, fin);
290         _last_written[eyes] = encoded;
291 }
292
293
294 void
295 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
296 {
297         if (!_atmos_asset) {
298                 _atmos_asset = metadata.create (dcp::Fraction(_film->video_frame_rate(), 1));
299                 if (_film->encrypted()) {
300                         _atmos_asset->set_key(_film->key());
301                 }
302                 _atmos_asset_writer = _atmos_asset->start_write (
303                         _film->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
304                         );
305         }
306         _atmos_asset_writer->write (atmos);
307 }
308
309
310 void
311 ReelWriter::fake_write (int size)
312 {
313         if (!_picture_asset_writer) {
314                 /* We're not writing any data */
315                 return;
316         }
317
318         _picture_asset_writer->fake_write (size);
319 }
320
321 void
322 ReelWriter::repeat_write (Frame frame, Eyes eyes)
323 {
324         if (!_picture_asset_writer) {
325                 /* We're not writing any data */
326                 return;
327         }
328
329         dcp::FrameInfo fin = _picture_asset_writer->write (
330                 _last_written[eyes]->data().get(),
331                 _last_written[eyes]->size()
332                 );
333         write_frame_info (frame, eyes, fin);
334 }
335
336 void
337 ReelWriter::finish ()
338 {
339         if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
340                 /* Nothing was written to the picture asset */
341                 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
342                 _picture_asset.reset ();
343         }
344
345         if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
346                 /* Nothing was written to the sound asset */
347                 _sound_asset.reset ();
348         }
349
350         /* Hard-link any video asset file into the DCP */
351         if (_picture_asset) {
352                 DCPOMATIC_ASSERT (_picture_asset->file());
353                 boost::filesystem::path video_from = _picture_asset->file().get();
354                 boost::filesystem::path video_to;
355                 video_to /= _film->dir (_film->dcp_name());
356                 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
357                 /* There may be an existing "to" file if we are recreating a DCP in the same place without
358                    changing any video.
359                 */
360                 boost::system::error_code ec;
361                 boost::filesystem::remove (video_to, ec);
362
363                 boost::filesystem::create_hard_link (video_from, video_to, ec);
364                 if (ec) {
365                         LOG_WARNING_NC ("Hard-link failed; copying instead");
366                         shared_ptr<Job> job = _job.lock ();
367                         if (job) {
368                                 job->sub (_("Copying video file into DCP"));
369                                 try {
370                                         copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
371                                 } catch (exception& e) {
372                                         LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
373                                         throw FileError (e.what(), video_from);
374                                 }
375                         } else {
376                                 boost::filesystem::copy_file (video_from, video_to, ec);
377                                 if (ec) {
378                                         LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
379                                         throw FileError (ec.message(), video_from);
380                                 }
381                         }
382                 }
383
384                 _picture_asset->set_file (video_to);
385         }
386
387         /* Move the audio asset into the DCP */
388         if (_sound_asset) {
389                 boost::filesystem::path audio_to;
390                 audio_to /= _film->dir (_film->dcp_name ());
391                 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
392                 audio_to /= aaf;
393
394                 boost::system::error_code ec;
395                 boost::filesystem::rename (_film->file (aaf), audio_to, ec);
396                 if (ec) {
397                         throw FileError (
398                                 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
399                                 );
400                 }
401
402                 _sound_asset->set_file (audio_to);
403         }
404
405         if (_atmos_asset) {
406                 _atmos_asset_writer->finalize ();
407                 boost::filesystem::path atmos_to;
408                 atmos_to /= _film->dir (_film->dcp_name());
409                 string const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
410                 atmos_to /= aaf;
411
412                 boost::system::error_code ec;
413                 boost::filesystem::rename (_film->file(aaf), atmos_to, ec);
414                 if (ec) {
415                         throw FileError (
416                                 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
417                                 );
418                 }
419
420                 _atmos_asset->set_file (atmos_to);
421         }
422 }
423
424 template <class T>
425 shared_ptr<T>
426 maybe_add_text (
427         shared_ptr<dcp::SubtitleAsset> asset,
428         int64_t picture_duration,
429         shared_ptr<dcp::Reel> reel,
430         list<ReferencedReelAsset> const & refs,
431         list<shared_ptr<Font> > const & fonts,
432         shared_ptr<const Film> film,
433         DCPTimePeriod period
434         )
435 {
436         Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
437
438         shared_ptr<T> reel_asset;
439
440         if (asset) {
441                 boost::filesystem::path liberation_normal;
442                 try {
443                         liberation_normal = shared_path() / "LiberationSans-Regular.ttf";
444                         if (!boost::filesystem::exists (liberation_normal)) {
445                                 /* Hack for unit tests */
446                                 liberation_normal = shared_path() / "fonts" / "LiberationSans-Regular.ttf";
447                         }
448                 } catch (boost::filesystem::filesystem_error& e) {
449
450                 }
451
452                 if (!boost::filesystem::exists(liberation_normal)) {
453                         liberation_normal = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf";
454                 }
455
456                 /* Add the font to the subtitle content */
457                 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
458                         asset->add_font (j->id(), j->file().get_value_or(liberation_normal));
459                 }
460
461                 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
462                         boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
463                         boost::filesystem::create_directories (directory);
464                         asset->write (directory / ("sub_" + asset->id() + ".xml"));
465                 } else {
466                         /* All our assets should be the same length; use the picture asset length here
467                            as a reference to set the subtitle one.  We'll use the duration rather than
468                            the intrinsic duration; we don't care if the picture asset has been trimmed, we're
469                            just interested in its presentation length.
470                         */
471                         dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
472
473                         asset->write (
474                                 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
475                                 );
476                 }
477
478                 reel_asset.reset (
479                         new T (
480                                 asset,
481                                 dcp::Fraction (film->video_frame_rate(), 1),
482                                 picture_duration,
483                                 0
484                                 )
485                         );
486         } else {
487                 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
488                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
489                         shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
490                         if (k && j.period == period) {
491                                 reel_asset = k;
492                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
493                                 if (k->hash()) {
494                                         k->asset_ref()->set_hash (k->hash().get());
495                                 }
496                         }
497                 }
498         }
499
500         if (reel_asset) {
501                 if (reel_asset->actual_duration() != period_duration) {
502                         throw ProgrammingError (
503                                 __FILE__, __LINE__,
504                                 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
505                                 );
506                 }
507                 reel->add (reel_asset);
508         }
509
510         return reel_asset;
511 }
512
513 shared_ptr<dcp::Reel>
514 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
515 {
516         LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
517
518         shared_ptr<dcp::Reel> reel (new dcp::Reel ());
519
520         shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
521
522         if (_picture_asset) {
523                 /* We have made a picture asset of our own.  Put it into the reel */
524                 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
525                 if (mono) {
526                         reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
527                 }
528
529                 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
530                 if (stereo) {
531                         reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
532                 }
533         } else {
534                 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
535                 /* We don't have a picture asset of our own; hopefully we have one to reference */
536                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
537                         shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
538                         if (k) {
539                                 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
540                         }
541                         if (k && j.period == _period) {
542                                 reel_picture_asset = k;
543                         }
544                 }
545         }
546
547         Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
548
549         DCPOMATIC_ASSERT (reel_picture_asset);
550         if (reel_picture_asset->duration() != period_duration) {
551                 throw ProgrammingError (
552                         __FILE__, __LINE__,
553                         String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
554                         );
555         }
556         reel->add (reel_picture_asset);
557
558         /* If we have a hash for this asset in the CPL, assume that it is correct */
559         if (reel_picture_asset->hash()) {
560                 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
561         }
562
563         shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
564
565         if (_sound_asset) {
566                 /* We have made a sound asset of our own.  Put it into the reel */
567                 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
568         } else {
569                 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
570                 /* We don't have a sound asset of our own; hopefully we have one to reference */
571                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
572                         shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
573                         if (k) {
574                                 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
575                         }
576                         if (k && j.period == _period) {
577                                 reel_sound_asset = k;
578                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
579                                 if (k->hash()) {
580                                         k->asset_ref()->set_hash (k->hash().get());
581                                 }
582                         }
583                 }
584         }
585
586         DCPOMATIC_ASSERT (reel_sound_asset);
587         if (reel_sound_asset->actual_duration() != period_duration) {
588                 LOG_ERROR (
589                         "Reel sound asset has length %1 but reel period is %2",
590                         reel_sound_asset->actual_duration(),
591                         period_duration
592                         );
593                 if (reel_sound_asset->actual_duration() != period_duration) {
594                         throw ProgrammingError (
595                                 __FILE__, __LINE__,
596                                 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
597                                 );
598                 }
599
600         }
601         reel->add (reel_sound_asset);
602
603         maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
604         for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
605                 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
606                         i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
607                         );
608                 a->set_annotation_text (i->first.name);
609                 a->set_language (i->first.language);
610         }
611
612         map<dcp::Marker, DCPTime> markers = _film->markers ();
613         map<dcp::Marker, DCPTime> reel_markers;
614         for (map<dcp::Marker, DCPTime>::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                 string lang = _film->subtitle_language ();
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);
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);
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         Data data (info.size);
770         size_t const read = fread (data.data().get(), 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().get(), 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 }