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