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