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