2 Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
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.
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.
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/>.
21 #include "reel_writer.h"
26 #include "dcpomatic_log.h"
28 #include "font_data.h"
29 #include "compose.hpp"
31 #include "audio_buffers.h"
33 #include <dcp/atmos_asset.h>
34 #include <dcp/atmos_asset_writer.h>
35 #include <dcp/certificate_chain.h>
38 #include <dcp/interop_subtitle_asset.h>
39 #include <dcp/mono_picture_asset.h>
40 #include <dcp/raw_convert.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>
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;
76 using dcp::raw_convert;
77 using namespace dcpomatic;
79 int const ReelWriter::_info_size = 48;
81 static dcp::MXFMetadata
84 dcp::MXFMetadata meta;
85 auto config = Config::instance();
86 if (!config->dcp_company_name().empty()) {
87 meta.company_name = config->dcp_company_name ();
89 if (!config->dcp_product_name().empty()) {
90 meta.product_name = config->dcp_product_name ();
92 if (!config->dcp_product_version().empty()) {
93 meta.product_version = config->dcp_product_version ();
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.
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
106 : WeakConstFilm (weak_film)
108 , _reel_index (reel_index)
109 , _reel_count (reel_count)
110 , _content_summary (film()->content_summary(period))
112 , _text_only (text_only)
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.
119 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
121 boost::filesystem::path const asset =
122 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
124 _first_nonexistant_frame = check_existing_picture_asset (asset);
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
132 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
134 job->sub (_("Copying old video file"));
135 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
137 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
139 boost::filesystem::remove (asset);
140 boost::filesystem::rename (asset.string() + ".tmp", asset);
144 if (film()->three_d()) {
145 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
147 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
150 _picture_asset->set_size (film()->frame_size());
151 _picture_asset->set_metadata (mxf_metadata());
153 if (film()->encrypted()) {
154 _picture_asset->set_key (film()->key());
155 _picture_asset->set_context_id (film()->context_id());
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);
166 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
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"),
180 _sound_asset->set_metadata (mxf_metadata());
182 if (film()->encrypted()) {
183 _sound_asset->set_key (film()->key());
186 DCPOMATIC_ASSERT (film()->directory());
188 /* Write the sound asset into the film directory so that we leave the creation
189 of the DCP directory until the last minute.
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()
197 _default_font = dcp::ArrayData(default_font_file());
200 /** @param frame reel-relative frame */
202 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
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());
212 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
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());
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;
228 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
232 return frame * _info_size;
234 return frame * _info_size * 2;
236 return frame * _info_size * 2 + _info_size;
238 DCPOMATIC_ASSERT (false);
241 DCPOMATIC_ASSERT (false);
245 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
247 auto job = _job.lock ();
250 job->sub (_("Checking existing image data"));
253 /* Try to open the existing asset */
254 auto asset_file = fopen_boost (asset, "rb");
256 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
259 LOG_GENERAL ("Opened existing asset at %1", asset.string());
262 shared_ptr<InfoFileHandle> info_file;
265 info_file = film()->info_file_handle (_period, true);
266 } catch (OpenFileError &) {
267 LOG_GENERAL_NC ("Could not open film info file");
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);
276 Frame first_nonexistant_frame;
277 if (film()->three_d()) {
278 /* Start looking at the last left frame */
279 first_nonexistant_frame = n / 2;
281 first_nonexistant_frame = n;
284 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
285 --first_nonexistant_frame;
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.
292 ++first_nonexistant_frame;
295 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
299 return first_nonexistant_frame;
303 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
305 if (!_picture_asset_writer) {
306 /* We're not writing any data */
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;
317 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
320 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
321 if (film()->encrypted()) {
322 _atmos_asset->set_key(film()->key());
324 _atmos_asset_writer = _atmos_asset->start_write (
325 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
328 _atmos_asset_writer->write (atmos);
333 ReelWriter::fake_write (int size)
335 if (!_picture_asset_writer) {
336 /* We're not writing any data */
340 _picture_asset_writer->fake_write (size);
344 ReelWriter::repeat_write (Frame frame, Eyes eyes)
346 if (!_picture_asset_writer) {
347 /* We're not writing any data */
351 auto fin = _picture_asset_writer->write (
352 _last_written[static_cast<int>(eyes)]->data(),
353 _last_written[static_cast<int>(eyes)]->size()
355 write_frame_info (frame, eyes, fin);
359 ReelWriter::finish (boost::filesystem::path output_dcp)
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 ();
367 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
368 /* Nothing was written to the sound asset */
369 _sound_asset.reset ();
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
381 boost::system::error_code ec;
382 boost::filesystem::remove (video_to, ec);
384 boost::filesystem::create_hard_link (video_from, video_to, ec);
386 LOG_WARNING_NC ("Hard-link failed; copying instead");
387 auto job = _job.lock ();
389 job->sub (_("Copying video file into DCP"));
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);
397 boost::filesystem::copy_file (video_from, video_to, 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);
405 _picture_asset->set_file (video_to);
408 /* Move the audio asset into the DCP */
410 boost::filesystem::path audio_to = output_dcp;
411 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
414 boost::system::error_code ec;
415 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
418 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
422 _sound_asset->set_file (audio_to);
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);
431 boost::system::error_code ec;
432 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
435 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
439 _atmos_asset->set_file (atmos_to);
443 template <class Interop, class SMPTE, class Result>
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,
458 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
460 shared_ptr<Result> reel_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));
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> (
474 dcp::Fraction(film->video_frame_rate(), 1),
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.
484 smpte->set_intrinsic_duration(picture_duration);
486 output_dcp / ("sub_" + asset->id() + ".mxf")
488 reel_asset = make_shared<SMPTE> (
490 dcp::Fraction(film->video_frame_rate(), 1),
497 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
499 auto k = dynamic_pointer_cast<Result> (j.asset);
500 if (k && j.period == period) {
502 /* If we have a hash for this asset in the CPL, assume that it is correct */
504 k->asset_ref()->set_hash (k->hash().get());
511 if (!text_only && reel_asset->actual_duration() != period_duration) {
512 throw ProgrammingError (
514 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
517 reel->add (reel_asset);
524 shared_ptr<dcp::ReelPictureAsset>
525 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
527 shared_ptr<dcp::ReelPictureAsset> reel_asset;
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);
533 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
536 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
538 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
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 */
544 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
546 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
548 if (k && j.period == _period) {
554 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
556 DCPOMATIC_ASSERT (reel_asset);
557 if (reel_asset->duration() != period_duration) {
558 throw ProgrammingError (
560 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
563 reel->add (reel_asset);
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());
575 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
577 shared_ptr<dcp::ReelSoundAsset> reel_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);
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 */
586 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
588 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
590 if (k && j.period == _period) {
592 /* If we have a hash for this asset in the CPL, assume that it is correct */
594 k->asset_ref()->set_hash (k->hash().get());
600 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
602 DCPOMATIC_ASSERT (reel_asset);
603 if (reel_asset->actual_duration() != period_duration) {
605 "Reel sound asset has length %1 but reel period is %2",
606 reel_asset->actual_duration(),
609 if (reel_asset->actual_duration() != period_duration) {
610 throw ProgrammingError (
612 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
617 reel->add (reel_asset);
622 ReelWriter::create_reel_text (
623 shared_ptr<dcp::Reel> reel,
624 list<ReferencedReelAsset> const & refs,
625 vector<FontData> const& fonts,
627 boost::filesystem::path output_dcp,
628 bool ensure_subtitles,
629 set<DCPTextTrack> ensure_closed_captions
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
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);
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>()),
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
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());
667 ensure_closed_captions.erase (i.first);
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), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
675 DCPOMATIC_ASSERT (a);
676 a->set_annotation_text (i.name);
678 a->set_language (i.language.get());
686 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
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;
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()));
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.
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
721 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
723 auto reel = make_shared<dcp::Reel>();
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.
729 int64_t duration = 0;
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);
737 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
740 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
747 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
750 if (_picture_asset) {
751 _picture_asset->hash (set_progress);
755 _sound_asset->hash (set_progress);
759 _atmos_asset->hash (set_progress);
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.
768 ReelWriter::start () const
770 return _period.from.frames_floor (film()->video_frame_rate());
775 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
777 if (!_sound_asset_writer) {
781 DCPOMATIC_ASSERT (audio);
782 _sound_asset_writer->write (audio->data(), audio->frames());
786 shared_ptr<dcp::SubtitleAsset>
787 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track) const
789 shared_ptr<dcp::SubtitleAsset> asset;
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());
800 s->set_reel_number (raw_convert<string> (_reel_index + 1));
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()));
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());
819 std::make_shared<dcp::SubtitleString>(
820 optional<std::string>(),
827 dcp::Time(0, 0, 0, 0, 24),
828 dcp::Time(0, 0, 1, 0, 24),
849 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
851 shared_ptr<dcp::SubtitleAsset> asset;
854 case TextType::OPEN_SUBTITLE:
855 asset = _subtitle_asset;
857 case TextType::CLOSED_CAPTION:
858 DCPOMATIC_ASSERT (track);
859 asset = _closed_caption_assets[*track];
862 DCPOMATIC_ASSERT (false);
866 asset = empty_text_asset (type, track);
870 case TextType::OPEN_SUBTITLE:
871 _subtitle_asset = asset;
873 case TextType::CLOSED_CAPTION:
874 DCPOMATIC_ASSERT (track);
875 _closed_caption_assets[*track] = asset;
878 DCPOMATIC_ASSERT (false);
881 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
882 auto const tcr = 1000;
884 for (auto i: subs.string) {
885 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
886 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
887 asset->add (make_shared<dcp::SubtitleString>(i));
890 for (auto i: subs.bitmap) {
892 make_shared<dcp::SubtitleImage>(
894 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
895 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
896 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
897 dcp::Time(), dcp::Time()
904 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
906 LOG_GENERAL ("Checking existing picture frame %1", frame);
908 /* Read the data from the info file; for 3D we just check the left
909 frames until we find a good one.
911 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
915 /* Read the data from the asset and hash it */
916 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
917 ArrayData data (info.size);
918 size_t const read = fread (data.data(), 1, data.size(), asset_file);
919 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
920 if (read != static_cast<size_t> (data.size ())) {
921 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
925 digester.add (data.data(), data.size());
926 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
927 if (digester.get() != info.hash) {
928 LOG_GENERAL ("Existing frame %1 failed hash check", frame);