Move the code to find Liberation into util.cc.
[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                 /* Add the font to the subtitle content */
443                 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
444                         asset->add_font (j->id(), j->file().get_value_or(default_font_file()));
445                 }
446
447                 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
448                         boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
449                         boost::filesystem::create_directories (directory);
450                         asset->write (directory / ("sub_" + asset->id() + ".xml"));
451                 } else {
452                         /* All our assets should be the same length; use the picture asset length here
453                            as a reference to set the subtitle one.  We'll use the duration rather than
454                            the intrinsic duration; we don't care if the picture asset has been trimmed, we're
455                            just interested in its presentation length.
456                         */
457                         dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
458
459                         asset->write (
460                                 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
461                                 );
462                 }
463
464                 reel_asset.reset (
465                         new T (
466                                 asset,
467                                 dcp::Fraction (film->video_frame_rate(), 1),
468                                 picture_duration,
469                                 0
470                                 )
471                         );
472         } else {
473                 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
474                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
475                         shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
476                         if (k && j.period == period) {
477                                 reel_asset = k;
478                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
479                                 if (k->hash()) {
480                                         k->asset_ref()->set_hash (k->hash().get());
481                                 }
482                         }
483                 }
484         }
485
486         if (reel_asset) {
487                 if (reel_asset->actual_duration() != period_duration) {
488                         throw ProgrammingError (
489                                 __FILE__, __LINE__,
490                                 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
491                                 );
492                 }
493                 reel->add (reel_asset);
494         }
495
496         return reel_asset;
497 }
498
499 shared_ptr<dcp::Reel>
500 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
501 {
502         LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
503
504         shared_ptr<dcp::Reel> reel (new dcp::Reel ());
505
506         shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
507
508         if (_picture_asset) {
509                 /* We have made a picture asset of our own.  Put it into the reel */
510                 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
511                 if (mono) {
512                         reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
513                 }
514
515                 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
516                 if (stereo) {
517                         reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
518                 }
519         } else {
520                 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
521                 /* We don't have a picture asset of our own; hopefully we have one to reference */
522                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
523                         shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
524                         if (k) {
525                                 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
526                         }
527                         if (k && j.period == _period) {
528                                 reel_picture_asset = k;
529                         }
530                 }
531         }
532
533         Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
534
535         DCPOMATIC_ASSERT (reel_picture_asset);
536         if (reel_picture_asset->duration() != period_duration) {
537                 throw ProgrammingError (
538                         __FILE__, __LINE__,
539                         String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
540                         );
541         }
542         reel->add (reel_picture_asset);
543
544         /* If we have a hash for this asset in the CPL, assume that it is correct */
545         if (reel_picture_asset->hash()) {
546                 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
547         }
548
549         shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
550
551         if (_sound_asset) {
552                 /* We have made a sound asset of our own.  Put it into the reel */
553                 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
554         } else {
555                 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
556                 /* We don't have a sound asset of our own; hopefully we have one to reference */
557                 BOOST_FOREACH (ReferencedReelAsset j, refs) {
558                         shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
559                         if (k) {
560                                 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
561                         }
562                         if (k && j.period == _period) {
563                                 reel_sound_asset = k;
564                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
565                                 if (k->hash()) {
566                                         k->asset_ref()->set_hash (k->hash().get());
567                                 }
568                         }
569                 }
570         }
571
572         DCPOMATIC_ASSERT (reel_sound_asset);
573         if (reel_sound_asset->actual_duration() != period_duration) {
574                 LOG_ERROR (
575                         "Reel sound asset has length %1 but reel period is %2",
576                         reel_sound_asset->actual_duration(),
577                         period_duration
578                         );
579                 if (reel_sound_asset->actual_duration() != period_duration) {
580                         throw ProgrammingError (
581                                 __FILE__, __LINE__,
582                                 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
583                                 );
584                 }
585
586         }
587         reel->add (reel_sound_asset);
588
589         maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
590         for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
591                 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
592                         i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
593                         );
594                 a->set_annotation_text (i->first.name);
595                 a->set_language (i->first.language);
596         }
597
598         map<dcp::Marker, DCPTime> markers = _film->markers ();
599         map<dcp::Marker, DCPTime> reel_markers;
600         for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
601                 if (_period.contains(i->second)) {
602                         reel_markers[i->first] = i->second;
603                 }
604         }
605
606         if (!reel_markers.empty ()) {
607                 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
608                 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
609                         int h, m, s, f;
610                         DCPTime relative = i->second - _period.from;
611                         relative.split (_film->video_frame_rate(), h, m, s, f);
612                         ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
613                 }
614                 reel->add (ma);
615         }
616
617         if (_atmos_asset) {
618                 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
619         }
620
621         return reel;
622 }
623
624 void
625 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
626 {
627         if (_picture_asset) {
628                 _picture_asset->hash (set_progress);
629         }
630
631         if (_sound_asset) {
632                 _sound_asset->hash (set_progress);
633         }
634
635         if (_atmos_asset) {
636                 _atmos_asset->hash (set_progress);
637         }
638 }
639
640 Frame
641 ReelWriter::start () const
642 {
643         return _period.from.frames_floor (_film->video_frame_rate());
644 }
645
646
647 void
648 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
649 {
650         if (!_sound_asset_writer) {
651                 return;
652         }
653
654         DCPOMATIC_ASSERT (audio);
655         _sound_asset_writer->write (audio->data(), audio->frames());
656 }
657
658 void
659 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
660 {
661         shared_ptr<dcp::SubtitleAsset> asset;
662
663         switch (type) {
664         case TEXT_OPEN_SUBTITLE:
665                 asset = _subtitle_asset;
666                 break;
667         case TEXT_CLOSED_CAPTION:
668                 DCPOMATIC_ASSERT (track);
669                 asset = _closed_caption_assets[*track];
670                 break;
671         default:
672                 DCPOMATIC_ASSERT (false);
673         }
674
675         if (!asset) {
676                 string lang = _film->subtitle_language ();
677                 if (_film->interop ()) {
678                         shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
679                         s->set_movie_title (_film->name ());
680                         if (type == TEXT_OPEN_SUBTITLE) {
681                                 s->set_language (lang.empty() ? "Unknown" : lang);
682                         } else {
683                                 s->set_language (track->language);
684                         }
685                         s->set_reel_number (raw_convert<string> (_reel_index + 1));
686                         asset = s;
687                 } else {
688                         shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
689                         s->set_content_title_text (_film->name ());
690                         s->set_metadata (mxf_metadata());
691                         if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
692                                 s->set_language (lang);
693                         } else {
694                                 s->set_language (track->language);
695                         }
696                         s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
697                         s->set_reel_number (_reel_index + 1);
698                         s->set_time_code_rate (_film->video_frame_rate ());
699                         s->set_start_time (dcp::Time ());
700                         if (_film->encrypted ()) {
701                                 s->set_key (_film->key ());
702                         }
703                         asset = s;
704                 }
705         }
706
707         switch (type) {
708         case TEXT_OPEN_SUBTITLE:
709                 _subtitle_asset = asset;
710                 break;
711         case TEXT_CLOSED_CAPTION:
712                 DCPOMATIC_ASSERT (track);
713                 _closed_caption_assets[*track] = asset;
714                 break;
715         default:
716                 DCPOMATIC_ASSERT (false);
717         }
718
719         BOOST_FOREACH (StringText i, subs.string) {
720                 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
721                 i.set_in  (i.in()  - dcp::Time (_period.from.seconds(), i.in().tcr));
722                 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
723                 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
724         }
725
726         BOOST_FOREACH (BitmapText i, subs.bitmap) {
727                 asset->add (
728                         shared_ptr<dcp::Subtitle>(
729                                 new dcp::SubtitleImage(
730                                         i.image->as_png(),
731                                         dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
732                                         dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
733                                         i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
734                                         dcp::Time(), dcp::Time()
735                                         )
736                                 )
737                         );
738         }
739 }
740
741 bool
742 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
743 {
744         LOG_GENERAL ("Checking existing picture frame %1", frame);
745
746         /* Read the data from the info file; for 3D we just check the left
747            frames until we find a good one.
748         */
749         dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
750
751         bool ok = true;
752
753         /* Read the data from the asset and hash it */
754         dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
755         Data data (info.size);
756         size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
757         LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
758         if (read != static_cast<size_t> (data.size ())) {
759                 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
760                 ok = false;
761         } else {
762                 Digester digester;
763                 digester.add (data.data().get(), data.size());
764                 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
765                 if (digester.get() != info.hash) {
766                         LOG_GENERAL ("Existing frame %1 failed hash check", frame);
767                         ok = false;
768                 }
769         }
770
771         return ok;
772 }