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