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