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/>.
22 #include "audio_buffers.h"
23 #include "compose.hpp"
26 #include "dcpomatic_log.h"
29 #include "font_data.h"
31 #include "image_png.h"
34 #include "reel_writer.h"
35 #include <dcp/atmos_asset.h>
36 #include <dcp/atmos_asset_writer.h>
37 #include <dcp/certificate_chain.h>
40 #include <dcp/interop_subtitle_asset.h>
41 #include <dcp/mono_picture_asset.h>
42 #include <dcp/raw_convert.h>
44 #include <dcp/reel_atmos_asset.h>
45 #include <dcp/reel_interop_closed_caption_asset.h>
46 #include <dcp/reel_interop_subtitle_asset.h>
47 #include <dcp/reel_markers_asset.h>
48 #include <dcp/reel_mono_picture_asset.h>
49 #include <dcp/reel_smpte_closed_caption_asset.h>
50 #include <dcp/reel_smpte_subtitle_asset.h>
51 #include <dcp/reel_sound_asset.h>
52 #include <dcp/reel_stereo_picture_asset.h>
53 #include <dcp/smpte_subtitle_asset.h>
54 #include <dcp/sound_asset.h>
55 #include <dcp/sound_asset_writer.h>
56 #include <dcp/stereo_picture_asset.h>
57 #include <dcp/subtitle_image.h>
62 using std::dynamic_pointer_cast;
65 using std::make_shared;
68 using std::shared_ptr;
72 using boost::optional;
73 #if BOOST_VERSION >= 106100
74 using namespace boost::placeholders;
78 using dcp::raw_convert;
79 using namespace dcpomatic;
82 int const ReelWriter::_info_size = 48;
85 static dcp::MXFMetadata
88 dcp::MXFMetadata meta;
89 auto config = Config::instance();
90 if (!config->dcp_company_name().empty()) {
91 meta.company_name = config->dcp_company_name ();
93 if (!config->dcp_product_name().empty()) {
94 meta.product_name = config->dcp_product_name ();
96 if (!config->dcp_product_version().empty()) {
97 meta.product_version = config->dcp_product_version ();
103 /** @param job Related job, or 0.
104 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
105 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
106 * subtitle / closed caption files.
108 ReelWriter::ReelWriter (
109 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
111 : WeakConstFilm (weak_film)
113 , _reel_index (reel_index)
114 , _reel_count (reel_count)
115 , _content_summary (film()->content_summary(period))
117 , _text_only (text_only)
119 /* Create or find our picture asset in a subdirectory, named
120 according to those film's parameters which affect the video
121 output. We will hard-link it into the DCP later.
124 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
126 boost::filesystem::path const asset =
127 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
129 _first_nonexistant_frame = check_existing_picture_asset (asset);
131 if (_first_nonexistant_frame < period.duration().frames_round(film()->video_frame_rate())) {
132 /* We do not have a complete picture asset. If there is an
133 existing asset, break any hard links to it as we are about
134 to change its contents (if only by changing the IDs); see
137 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
139 job->sub (_("Copying old video file"));
140 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
142 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
144 boost::filesystem::remove (asset);
145 boost::filesystem::rename (asset.string() + ".tmp", asset);
149 if (film()->three_d()) {
150 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
152 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
155 _picture_asset->set_size (film()->frame_size());
156 _picture_asset->set_metadata (mxf_metadata());
158 if (film()->encrypted()) {
159 _picture_asset->set_key (film()->key());
160 _picture_asset->set_context_id (film()->context_id());
163 _picture_asset->set_file (asset);
164 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
165 } else if (!text_only) {
166 /* We already have a complete picture asset that we can just re-use */
167 /* XXX: what about if the encryption key changes? */
168 if (film()->three_d()) {
169 _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
171 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
175 if (film()->audio_channels()) {
176 auto lang = film()->audio_language();
177 _sound_asset = make_shared<dcp::SoundAsset> (
178 dcp::Fraction(film()->video_frame_rate(), 1),
179 film()->audio_frame_rate(),
180 film()->audio_channels(),
181 lang ? *lang : dcp::LanguageTag("en-US"),
185 _sound_asset->set_metadata (mxf_metadata());
187 if (film()->encrypted()) {
188 _sound_asset->set_key (film()->key());
191 DCPOMATIC_ASSERT (film()->directory());
193 /* Write the sound asset into the film directory so that we leave the creation
194 of the DCP directory until the last minute.
196 _sound_asset_writer = _sound_asset->start_write (
197 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
198 film()->contains_atmos_content()
202 _default_font = dcp::ArrayData(default_font_file());
206 /** @param frame reel-relative frame */
208 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
210 auto handle = film()->info_file_handle(_period, false);
211 handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
212 handle->get().checked_write(&info.offset, sizeof(info.offset));
213 handle->get().checked_write(&info.size, sizeof(info.size));
214 handle->get().checked_write(info.hash.c_str(), info.hash.size());
219 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
221 dcp::FrameInfo frame_info;
222 info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
223 info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
224 info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
226 char hash_buffer[33];
227 info->get().checked_read(hash_buffer, 32);
228 hash_buffer[32] = '\0';
229 frame_info.hash = hash_buffer;
236 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
240 return frame * _info_size;
242 return frame * _info_size * 2;
244 return frame * _info_size * 2 + _info_size;
246 DCPOMATIC_ASSERT (false);
249 DCPOMATIC_ASSERT (false);
254 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
256 auto job = _job.lock ();
259 job->sub (_("Checking existing image data"));
262 /* Try to open the existing asset */
263 dcp::File asset_file(asset, "rb");
265 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
268 LOG_GENERAL ("Opened existing asset at %1", asset.string());
271 shared_ptr<InfoFileHandle> info_file;
274 info_file = film()->info_file_handle (_period, true);
275 } catch (OpenFileError &) {
276 LOG_GENERAL_NC ("Could not open film info file");
280 /* Offset of the last dcp::FrameInfo in the info file */
281 int const n = (boost::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
282 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->get().path()), _info_size);
284 Frame first_nonexistant_frame;
285 if (film()->three_d()) {
286 /* Start looking at the last left frame */
287 first_nonexistant_frame = n / 2;
289 first_nonexistant_frame = n;
292 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
293 --first_nonexistant_frame;
296 if (!film()->three_d() && first_nonexistant_frame > 0) {
297 /* If we are doing 3D we might have found a good L frame with no R, so only
298 do this if we're in 2D and we've just found a good B(oth) frame.
300 ++first_nonexistant_frame;
303 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
305 return first_nonexistant_frame;
310 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
312 if (!_picture_asset_writer) {
313 /* We're not writing any data */
317 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
318 write_frame_info (frame, eyes, fin);
319 _last_written[static_cast<int>(eyes)] = encoded;
324 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
327 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
328 if (film()->encrypted()) {
329 _atmos_asset->set_key(film()->key());
331 _atmos_asset_writer = _atmos_asset->start_write (
332 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
335 _atmos_asset_writer->write (atmos);
340 ReelWriter::fake_write (int size)
342 if (!_picture_asset_writer) {
343 /* We're not writing any data */
347 _picture_asset_writer->fake_write (size);
352 ReelWriter::repeat_write (Frame frame, Eyes eyes)
354 if (!_picture_asset_writer) {
355 /* We're not writing any data */
359 auto fin = _picture_asset_writer->write (
360 _last_written[static_cast<int>(eyes)]->data(),
361 _last_written[static_cast<int>(eyes)]->size()
363 write_frame_info (frame, eyes, fin);
368 ReelWriter::finish (boost::filesystem::path output_dcp)
370 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
371 /* Nothing was written to the picture asset */
372 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
373 _picture_asset.reset ();
376 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
377 /* Nothing was written to the sound asset */
378 _sound_asset.reset ();
381 /* Hard-link any video asset file into the DCP */
382 if (_picture_asset) {
383 DCPOMATIC_ASSERT (_picture_asset->file());
384 boost::filesystem::path video_from = _picture_asset->file().get();
385 boost::filesystem::path video_to = output_dcp;
386 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
387 /* There may be an existing "to" file if we are recreating a DCP in the same place without
390 boost::system::error_code ec;
391 boost::filesystem::remove (video_to, ec);
393 boost::filesystem::create_hard_link (video_from, video_to, ec);
395 LOG_WARNING_NC ("Hard-link failed; copying instead");
396 auto job = _job.lock ();
398 job->sub (_("Copying video file into DCP"));
400 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
401 } catch (exception& e) {
402 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
403 throw FileError (e.what(), video_from);
406 boost::filesystem::copy_file (video_from, video_to, ec);
408 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
409 throw FileError (ec.message(), video_from);
414 _picture_asset->set_file (video_to);
417 /* Move the audio asset into the DCP */
419 boost::filesystem::path audio_to = output_dcp;
420 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
423 boost::system::error_code ec;
424 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
427 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
431 _sound_asset->set_file (audio_to);
435 _atmos_asset_writer->finalize ();
436 boost::filesystem::path atmos_to = output_dcp;
437 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
440 boost::system::error_code ec;
441 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
444 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
448 _atmos_asset->set_file (atmos_to);
453 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
454 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
456 template <class Interop, class SMPTE, class Result>
459 shared_ptr<dcp::SubtitleAsset> asset,
460 int64_t picture_duration,
461 shared_ptr<dcp::Reel> reel,
462 list<ReferencedReelAsset> const & refs,
463 vector<FontData> const & fonts,
464 dcp::ArrayData default_font,
465 shared_ptr<const Film> film,
466 DCPTimePeriod period,
467 boost::filesystem::path output_dcp,
471 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
473 shared_ptr<Result> reel_asset;
476 /* Add the font to the subtitle content */
477 for (auto const& j: fonts) {
478 asset->add_font (j.id, j.data.get_value_or(default_font));
481 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
482 auto directory = output_dcp / interop->id ();
483 boost::filesystem::create_directories (directory);
484 interop->write (directory / ("sub_" + interop->id() + ".xml"));
485 reel_asset = make_shared<Interop> (
487 dcp::Fraction(film->video_frame_rate(), 1),
491 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
492 /* All our assets should be the same length; use the picture asset length here
493 as a reference to set the subtitle one. We'll use the duration rather than
494 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
495 just interested in its presentation length.
497 smpte->set_intrinsic_duration(picture_duration);
499 output_dcp / ("sub_" + asset->id() + ".mxf")
501 reel_asset = make_shared<SMPTE> (
503 dcp::Fraction(film->video_frame_rate(), 1),
510 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
512 auto k = dynamic_pointer_cast<Result> (j.asset);
513 if (k && j.period == period) {
515 /* If we have a hash for this asset in the CPL, assume that it is correct */
517 k->asset_ref()->set_hash (k->hash().get());
524 if (!text_only && reel_asset->actual_duration() != period_duration) {
525 throw ProgrammingError (
527 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
530 reel->add (reel_asset);
537 shared_ptr<dcp::ReelPictureAsset>
538 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
540 shared_ptr<dcp::ReelPictureAsset> reel_asset;
542 if (_picture_asset) {
543 /* We have made a picture asset of our own. Put it into the reel */
544 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
546 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
549 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
551 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
554 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
555 /* We don't have a picture asset of our own; hopefully we have one to reference */
557 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
559 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
561 if (k && j.period == _period) {
567 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
569 DCPOMATIC_ASSERT (reel_asset);
570 if (reel_asset->duration() != period_duration) {
571 throw ProgrammingError (
573 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
576 reel->add (reel_asset);
578 /* If we have a hash for this asset in the CPL, assume that it is correct */
579 if (reel_asset->hash()) {
580 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
588 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
590 shared_ptr<dcp::ReelSoundAsset> reel_asset;
593 /* We have made a sound asset of our own. Put it into the reel */
594 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
596 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
597 /* We don't have a sound asset of our own; hopefully we have one to reference */
599 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
601 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
603 if (k && j.period == _period) {
605 /* If we have a hash for this asset in the CPL, assume that it is correct */
607 k->asset_ref()->set_hash (k->hash().get());
613 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
615 DCPOMATIC_ASSERT (reel_asset);
616 if (reel_asset->actual_duration() != period_duration) {
618 "Reel sound asset has length %1 but reel period is %2",
619 reel_asset->actual_duration(),
622 if (reel_asset->actual_duration() != period_duration) {
623 throw ProgrammingError (
625 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
630 reel->add (reel_asset);
635 ReelWriter::create_reel_text (
636 shared_ptr<dcp::Reel> reel,
637 list<ReferencedReelAsset> const & refs,
638 vector<FontData> const& fonts,
640 boost::filesystem::path output_dcp,
641 bool ensure_subtitles,
642 set<DCPTextTrack> ensure_closed_captions
645 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
646 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
650 /* We have a subtitle asset that we either made or are referencing */
651 if (auto main_language = film()->subtitle_languages().first) {
652 subtitle->set_language (*main_language);
654 } else if (ensure_subtitles) {
655 /* We had no subtitle asset, but we've been asked to make sure there is one */
656 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
657 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
670 for (auto const& i: _closed_caption_assets) {
671 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
672 i.second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
674 DCPOMATIC_ASSERT (a);
675 a->set_annotation_text (i.first.name);
676 if (i.first.language) {
677 a->set_language (i.first.language.get());
680 ensure_closed_captions.erase (i.first);
683 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
684 for (auto i: ensure_closed_captions) {
685 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
686 empty_text_asset(TextType::CLOSED_CAPTION, i, true), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
688 DCPOMATIC_ASSERT (a);
689 a->set_annotation_text (i.name);
691 a->set_language (i.language.get());
698 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
700 auto markers = film()->markers();
701 film()->add_ffoc_lfoc(markers);
702 Film::Markers reel_markers;
703 for (auto const& i: markers) {
704 if (_period.contains(i.second)) {
705 reel_markers[i.first] = i.second;
709 if (!reel_markers.empty ()) {
710 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
711 for (auto const& i: reel_markers) {
712 DCPTime relative = i.second - _period.from;
713 auto hmsf = relative.split (film()->video_frame_rate());
714 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
721 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
722 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
724 shared_ptr<dcp::Reel>
725 ReelWriter::create_reel (
726 list<ReferencedReelAsset> const & refs,
727 vector<FontData> const & fonts,
728 boost::filesystem::path output_dcp,
729 bool ensure_subtitles,
730 set<DCPTextTrack> ensure_closed_captions
733 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
735 auto reel = make_shared<dcp::Reel>();
737 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
738 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
739 * how big they are, we don't care about that.
741 int64_t duration = 0;
743 auto reel_picture_asset = create_reel_picture (reel, refs);
744 duration = reel_picture_asset->actual_duration ();
745 create_reel_sound (reel, refs);
746 create_reel_markers (reel);
749 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
752 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
759 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
762 if (_picture_asset) {
763 _picture_asset->hash (set_progress);
767 _sound_asset->hash (set_progress);
771 _atmos_asset->hash (set_progress);
773 } catch (boost::thread_interrupted) {
774 /* set_progress contains an interruption_point, so any of these methods
775 * may throw thread_interrupted, at which point we just give up.
781 ReelWriter::start () const
783 return _period.from.frames_floor (film()->video_frame_rate());
788 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
790 if (!_sound_asset_writer) {
794 DCPOMATIC_ASSERT (audio);
795 _sound_asset_writer->write (audio->data(), audio->frames());
799 shared_ptr<dcp::SubtitleAsset>
800 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
802 shared_ptr<dcp::SubtitleAsset> asset;
804 auto lang = film()->subtitle_languages();
805 if (film()->interop()) {
806 auto s = make_shared<dcp::InteropSubtitleAsset>();
807 s->set_movie_title (film()->name());
808 if (type == TextType::OPEN_SUBTITLE) {
809 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
810 } else if (track->language) {
811 s->set_language (track->language->to_string());
813 s->set_reel_number (raw_convert<string> (_reel_index + 1));
816 auto s = make_shared<dcp::SMPTESubtitleAsset>();
817 s->set_content_title_text (film()->name());
818 s->set_metadata (mxf_metadata());
819 if (type == TextType::OPEN_SUBTITLE && lang.first) {
820 s->set_language (*lang.first);
821 } else if (track && track->language) {
822 s->set_language (dcp::LanguageTag(track->language->to_string()));
824 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
825 s->set_reel_number (_reel_index + 1);
826 s->set_time_code_rate (film()->video_frame_rate());
827 s->set_start_time (dcp::Time ());
828 if (film()->encrypted()) {
829 s->set_key (film()->key());
833 std::make_shared<dcp::SubtitleString>(
834 optional<std::string>(),
841 dcp::Time(0, 0, 0, 0, 24),
842 dcp::Time(0, 0, 1, 0, 24),
865 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
867 shared_ptr<dcp::SubtitleAsset> asset;
870 case TextType::OPEN_SUBTITLE:
871 asset = _subtitle_asset;
873 case TextType::CLOSED_CAPTION:
874 DCPOMATIC_ASSERT (track);
875 asset = _closed_caption_assets[*track];
878 DCPOMATIC_ASSERT (false);
882 asset = empty_text_asset (type, track, false);
886 case TextType::OPEN_SUBTITLE:
887 _subtitle_asset = asset;
889 case TextType::CLOSED_CAPTION:
890 DCPOMATIC_ASSERT (track);
891 _closed_caption_assets[*track] = asset;
894 DCPOMATIC_ASSERT (false);
897 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
898 auto const tcr = 1000;
900 for (auto i: subs.string) {
901 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
902 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
903 asset->add (make_shared<dcp::SubtitleString>(i));
906 for (auto i: subs.bitmap) {
908 make_shared<dcp::SubtitleImage>(
909 image_as_png(i.image),
910 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
911 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
912 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
913 dcp::Time(), dcp::Time()
921 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
923 LOG_GENERAL ("Checking existing picture frame %1", frame);
925 /* Read the data from the info file; for 3D we just check the left
926 frames until we find a good one.
928 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
932 /* Read the data from the asset and hash it */
933 asset_file.seek(info.offset, SEEK_SET);
934 ArrayData data (info.size);
935 size_t const read = asset_file.read(data.data(), 1, data.size());
936 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
937 if (read != static_cast<size_t> (data.size ())) {
938 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
942 digester.add (data.data(), data.size());
943 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
944 if (digester.get() != info.hash) {
945 LOG_GENERAL ("Existing frame %1 failed hash check", frame);