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