Move Image::as_png() out to image_as_png().
[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         dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
212         checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
213         checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
214         checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
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         dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
223         checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
224         checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
225
226         char hash_buffer[33];
227         checked_fread (hash_buffer, 32, info->get(), info->file());
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         auto asset_file = fopen_boost (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                 fclose (asset_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->file()) / _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->file()), _info_size);
284
285         Frame first_nonexistant_frame;
286         if (film()->three_d()) {
287                 /* Start looking at the last left frame */
288                 first_nonexistant_frame = n / 2;
289         } else {
290                 first_nonexistant_frame = n;
291         }
292
293         while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
294                 --first_nonexistant_frame;
295         }
296
297         if (!film()->three_d() && first_nonexistant_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_nonexistant_frame;
302         }
303
304         LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
305
306         fclose (asset_file);
307
308         return first_nonexistant_frame;
309 }
310
311
312 void
313 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
314 {
315         if (!_picture_asset_writer) {
316                 /* We're not writing any data */
317                 return;
318         }
319
320         auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
321         write_frame_info (frame, eyes, fin);
322         _last_written[static_cast<int>(eyes)] = encoded;
323 }
324
325
326 void
327 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
328 {
329         if (!_atmos_asset) {
330                 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
331                 if (film()->encrypted()) {
332                         _atmos_asset->set_key(film()->key());
333                 }
334                 _atmos_asset_writer = _atmos_asset->start_write (
335                         film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
336                         );
337         }
338         _atmos_asset_writer->write (atmos);
339 }
340
341
342 void
343 ReelWriter::fake_write (int size)
344 {
345         if (!_picture_asset_writer) {
346                 /* We're not writing any data */
347                 return;
348         }
349
350         _picture_asset_writer->fake_write (size);
351 }
352
353
354 void
355 ReelWriter::repeat_write (Frame frame, Eyes eyes)
356 {
357         if (!_picture_asset_writer) {
358                 /* We're not writing any data */
359                 return;
360         }
361
362         auto fin = _picture_asset_writer->write (
363                 _last_written[static_cast<int>(eyes)]->data(),
364                 _last_written[static_cast<int>(eyes)]->size()
365                 );
366         write_frame_info (frame, eyes, fin);
367 }
368
369
370 void
371 ReelWriter::finish (boost::filesystem::path output_dcp)
372 {
373         if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
374                 /* Nothing was written to the picture asset */
375                 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
376                 _picture_asset.reset ();
377         }
378
379         if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
380                 /* Nothing was written to the sound asset */
381                 _sound_asset.reset ();
382         }
383
384         /* Hard-link any video asset file into the DCP */
385         if (_picture_asset) {
386                 DCPOMATIC_ASSERT (_picture_asset->file());
387                 boost::filesystem::path video_from = _picture_asset->file().get();
388                 boost::filesystem::path video_to = output_dcp;
389                 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
390                 /* There may be an existing "to" file if we are recreating a DCP in the same place without
391                    changing any video.
392                 */
393                 boost::system::error_code ec;
394                 boost::filesystem::remove (video_to, ec);
395
396                 boost::filesystem::create_hard_link (video_from, video_to, ec);
397                 if (ec) {
398                         LOG_WARNING_NC ("Hard-link failed; copying instead");
399                         auto job = _job.lock ();
400                         if (job) {
401                                 job->sub (_("Copying video file into DCP"));
402                                 try {
403                                         copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
404                                 } catch (exception& e) {
405                                         LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
406                                         throw FileError (e.what(), video_from);
407                                 }
408                         } else {
409                                 boost::filesystem::copy_file (video_from, video_to, ec);
410                                 if (ec) {
411                                         LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
412                                         throw FileError (ec.message(), video_from);
413                                 }
414                         }
415                 }
416
417                 _picture_asset->set_file (video_to);
418         }
419
420         /* Move the audio asset into the DCP */
421         if (_sound_asset) {
422                 boost::filesystem::path audio_to = output_dcp;
423                 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
424                 audio_to /= aaf;
425
426                 boost::system::error_code ec;
427                 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
428                 if (ec) {
429                         throw FileError (
430                                 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
431                                 );
432                 }
433
434                 _sound_asset->set_file (audio_to);
435         }
436
437         if (_atmos_asset) {
438                 _atmos_asset_writer->finalize ();
439                 boost::filesystem::path atmos_to = output_dcp;
440                 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
441                 atmos_to /= aaf;
442
443                 boost::system::error_code ec;
444                 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
445                 if (ec) {
446                         throw FileError (
447                                 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
448                                 );
449                 }
450
451                 _atmos_asset->set_file (atmos_to);
452         }
453 }
454
455
456 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
457  *  A SubtitleAsset can be provided, or we will use one from @ref refs if not.
458  */
459 template <class Interop, class SMPTE, class Result>
460 shared_ptr<Result>
461 maybe_add_text (
462         shared_ptr<dcp::SubtitleAsset> asset,
463         int64_t picture_duration,
464         shared_ptr<dcp::Reel> reel,
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 / ("sub_" + interop->id() + ".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 / ("sub_" + asset->id() + ".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, 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                         refs,
664                         fonts,
665                         _default_font,
666                         film(),
667                         _period,
668                         output_dcp,
669                         _text_only
670                         );
671         }
672
673         for (auto const& i: _closed_caption_assets) {
674                 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
675                         i.second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
676                         );
677                 DCPOMATIC_ASSERT (a);
678                 a->set_annotation_text (i.first.name);
679                 if (i.first.language) {
680                         a->set_language (i.first.language.get());
681                 }
682
683                 ensure_closed_captions.erase (i.first);
684         }
685
686         /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
687         for (auto i: ensure_closed_captions) {
688                 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
689                         empty_text_asset(TextType::CLOSED_CAPTION, i, true), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
690                         );
691                 DCPOMATIC_ASSERT (a);
692                 a->set_annotation_text (i.name);
693                 if (i.language) {
694                         a->set_language (i.language.get());
695                 }
696         }
697 }
698
699
700 void
701 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
702 {
703         auto markers = film()->markers();
704         film()->add_ffoc_lfoc(markers);
705         Film::Markers reel_markers;
706         for (auto const& i: markers) {
707                 if (_period.contains(i.second)) {
708                         reel_markers[i.first] = i.second;
709                 }
710         }
711
712         if (!reel_markers.empty ()) {
713                 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
714                 for (auto const& i: reel_markers) {
715                         DCPTime relative = i.second - _period.from;
716                         auto hmsf = relative.split (film()->video_frame_rate());
717                         ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
718                 }
719                 reel->add (ma);
720         }
721 }
722
723
724 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
725  *  @param ensure_closed_captions make sure the reel has these closed caption tracks.
726  */
727 shared_ptr<dcp::Reel>
728 ReelWriter::create_reel (
729         list<ReferencedReelAsset> const & refs,
730         vector<FontData> const & fonts,
731         boost::filesystem::path output_dcp,
732         bool ensure_subtitles,
733         set<DCPTextTrack> ensure_closed_captions
734         )
735 {
736         LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
737
738         auto reel = make_shared<dcp::Reel>();
739
740         /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
741          * how long the subtitle / CCAP assets should be.  However, since we're only writing them to see
742          * how big they are, we don't care about that.
743          */
744         int64_t duration = 0;
745         if (!_text_only) {
746                 auto reel_picture_asset = create_reel_picture (reel, refs);
747                 duration = reel_picture_asset->actual_duration ();
748                 create_reel_sound (reel, refs);
749                 create_reel_markers (reel);
750         }
751
752         create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
753
754         if (_atmos_asset) {
755                 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
756         }
757
758         return reel;
759 }
760
761 void
762 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
763 try
764 {
765         if (_picture_asset) {
766                 _picture_asset->hash (set_progress);
767         }
768
769         if (_sound_asset) {
770                 _sound_asset->hash (set_progress);
771         }
772
773         if (_atmos_asset) {
774                 _atmos_asset->hash (set_progress);
775         }
776 } catch (boost::thread_interrupted) {
777         /* set_progress contains an interruption_point, so any of these methods
778          * may throw thread_interrupted, at which point we just give up.
779          */
780 }
781
782
783 Frame
784 ReelWriter::start () const
785 {
786         return _period.from.frames_floor (film()->video_frame_rate());
787 }
788
789
790 void
791 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
792 {
793         if (!_sound_asset_writer) {
794                 return;
795         }
796
797         DCPOMATIC_ASSERT (audio);
798         _sound_asset_writer->write (audio->data(), audio->frames());
799 }
800
801
802 shared_ptr<dcp::SubtitleAsset>
803 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
804 {
805         shared_ptr<dcp::SubtitleAsset> asset;
806
807         auto lang = film()->subtitle_languages();
808         if (film()->interop()) {
809                 auto s = make_shared<dcp::InteropSubtitleAsset>();
810                 s->set_movie_title (film()->name());
811                 if (type == TextType::OPEN_SUBTITLE) {
812                         s->set_language (lang.first ? lang.first->to_string() : "Unknown");
813                 } else if (track->language) {
814                         s->set_language (track->language->to_string());
815                 }
816                 s->set_reel_number (raw_convert<string> (_reel_index + 1));
817                 asset = s;
818         } else {
819                 auto s = make_shared<dcp::SMPTESubtitleAsset>();
820                 s->set_content_title_text (film()->name());
821                 s->set_metadata (mxf_metadata());
822                 if (type == TextType::OPEN_SUBTITLE && lang.first) {
823                         s->set_language (*lang.first);
824                 } else if (track && track->language) {
825                         s->set_language (dcp::LanguageTag(track->language->to_string()));
826                 }
827                 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
828                 s->set_reel_number (_reel_index + 1);
829                 s->set_time_code_rate (film()->video_frame_rate());
830                 s->set_start_time (dcp::Time ());
831                 if (film()->encrypted()) {
832                         s->set_key (film()->key());
833                 }
834                 if (with_dummy) {
835                         s->add (
836                                 std::make_shared<dcp::SubtitleString>(
837                                         optional<std::string>(),
838                                         false,
839                                         false,
840                                         false,
841                                         dcp::Colour(),
842                                         42,
843                                         1.0,
844                                         dcp::Time(0, 0, 0, 0, 24),
845                                         dcp::Time(0, 0, 1, 0, 24),
846                                         0.5,
847                                         dcp::HAlign::CENTER,
848                                         0.5,
849                                         dcp::VAlign::CENTER,
850                                         dcp::Direction::LTR,
851                                         " ",
852                                         dcp::Effect::NONE,
853                                         dcp::Colour(),
854                                         dcp::Time(),
855                                         dcp::Time(),
856                                         0
857                                         )
858                                );
859                 }
860                 asset = s;
861         }
862
863         return asset;
864 }
865
866
867 void
868 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
869 {
870         shared_ptr<dcp::SubtitleAsset> asset;
871
872         switch (type) {
873         case TextType::OPEN_SUBTITLE:
874                 asset = _subtitle_asset;
875                 break;
876         case TextType::CLOSED_CAPTION:
877                 DCPOMATIC_ASSERT (track);
878                 asset = _closed_caption_assets[*track];
879                 break;
880         default:
881                 DCPOMATIC_ASSERT (false);
882         }
883
884         if (!asset) {
885                 asset = empty_text_asset (type, track, false);
886         }
887
888         switch (type) {
889         case TextType::OPEN_SUBTITLE:
890                 _subtitle_asset = asset;
891                 break;
892         case TextType::CLOSED_CAPTION:
893                 DCPOMATIC_ASSERT (track);
894                 _closed_caption_assets[*track] = asset;
895                 break;
896         default:
897                 DCPOMATIC_ASSERT (false);
898         }
899
900         /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
901         auto const tcr = 1000;
902
903         for (auto i: subs.string) {
904                 i.set_in  (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
905                 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
906                 asset->add (make_shared<dcp::SubtitleString>(i));
907         }
908
909         for (auto i: subs.bitmap) {
910                 asset->add (
911                         make_shared<dcp::SubtitleImage>(
912                                 image_as_png(i.image),
913                                 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
914                                 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
915                                 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
916                                 dcp::Time(), dcp::Time()
917                                 )
918                         );
919         }
920 }
921
922
923 bool
924 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
925 {
926         LOG_GENERAL ("Checking existing picture frame %1", frame);
927
928         /* Read the data from the info file; for 3D we just check the left
929            frames until we find a good one.
930         */
931         auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
932
933         bool ok = true;
934
935         /* Read the data from the asset and hash it */
936         dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
937         ArrayData data (info.size);
938         size_t const read = fread (data.data(), 1, data.size(), asset_file);
939         LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
940         if (read != static_cast<size_t> (data.size ())) {
941                 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
942                 ok = false;
943         } else {
944                 Digester digester;
945                 digester.add (data.data(), data.size());
946                 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
947                 if (digester.get() != info.hash) {
948                         LOG_GENERAL ("Existing frame %1 failed hash check", frame);
949                         ok = false;
950                 }
951         }
952
953         return ok;
954 }