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