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