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