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"
33 #include "reel_writer.h"
34 #include <dcp/atmos_asset.h>
35 #include <dcp/atmos_asset_writer.h>
36 #include <dcp/certificate_chain.h>
39 #include <dcp/interop_subtitle_asset.h>
40 #include <dcp/mono_picture_asset.h>
41 #include <dcp/raw_convert.h>
43 #include <dcp/reel_atmos_asset.h>
44 #include <dcp/reel_interop_closed_caption_asset.h>
45 #include <dcp/reel_interop_subtitle_asset.h>
46 #include <dcp/reel_markers_asset.h>
47 #include <dcp/reel_mono_picture_asset.h>
48 #include <dcp/reel_smpte_closed_caption_asset.h>
49 #include <dcp/reel_smpte_subtitle_asset.h>
50 #include <dcp/reel_sound_asset.h>
51 #include <dcp/reel_stereo_picture_asset.h>
52 #include <dcp/smpte_subtitle_asset.h>
53 #include <dcp/sound_asset.h>
54 #include <dcp/sound_asset_writer.h>
55 #include <dcp/stereo_picture_asset.h>
56 #include <dcp/subtitle_image.h>
61 using std::dynamic_pointer_cast;
64 using std::make_shared;
67 using std::shared_ptr;
71 using boost::optional;
72 #if BOOST_VERSION >= 106100
73 using namespace boost::placeholders;
77 using dcp::raw_convert;
78 using namespace dcpomatic;
81 int const ReelWriter::_info_size = 48;
84 static dcp::MXFMetadata
87 dcp::MXFMetadata meta;
88 auto config = Config::instance();
89 if (!config->dcp_company_name().empty()) {
90 meta.company_name = config->dcp_company_name ();
92 if (!config->dcp_product_name().empty()) {
93 meta.product_name = config->dcp_product_name ();
95 if (!config->dcp_product_version().empty()) {
96 meta.product_version = config->dcp_product_version ();
102 /** @param job Related job, or 0.
103 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
104 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
105 * subtitle / closed caption files.
107 ReelWriter::ReelWriter (
108 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
110 : WeakConstFilm (weak_film)
112 , _reel_index (reel_index)
113 , _reel_count (reel_count)
114 , _content_summary (film()->content_summary(period))
116 , _text_only (text_only)
118 /* Create or find our picture asset in a subdirectory, named
119 according to those film's parameters which affect the video
120 output. We will hard-link it into the DCP later.
123 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
125 boost::filesystem::path const asset =
126 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
128 _first_nonexistant_frame = check_existing_picture_asset (asset);
130 if (_first_nonexistant_frame < period.duration().frames_round(film()->video_frame_rate())) {
131 /* We do not have a complete picture asset. If there is an
132 existing asset, break any hard links to it as we are about
133 to change its contents (if only by changing the IDs); see
136 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
138 job->sub (_("Copying old video file"));
139 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
141 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
143 boost::filesystem::remove (asset);
144 boost::filesystem::rename (asset.string() + ".tmp", asset);
148 if (film()->three_d()) {
149 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
151 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
154 _picture_asset->set_size (film()->frame_size());
155 _picture_asset->set_metadata (mxf_metadata());
157 if (film()->encrypted()) {
158 _picture_asset->set_key (film()->key());
159 _picture_asset->set_context_id (film()->context_id());
162 _picture_asset->set_file (asset);
163 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
164 } else if (!text_only) {
165 /* We already have a complete picture asset that we can just re-use */
166 /* XXX: what about if the encryption key changes? */
167 if (film()->three_d()) {
168 _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
170 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
174 if (film()->audio_channels()) {
175 auto lang = film()->audio_language();
176 _sound_asset = make_shared<dcp::SoundAsset> (
177 dcp::Fraction(film()->video_frame_rate(), 1),
178 film()->audio_frame_rate(),
179 film()->audio_channels(),
180 lang ? *lang : dcp::LanguageTag("en-US"),
184 _sound_asset->set_metadata (mxf_metadata());
186 if (film()->encrypted()) {
187 _sound_asset->set_key (film()->key());
190 DCPOMATIC_ASSERT (film()->directory());
192 /* Write the sound asset into the film directory so that we leave the creation
193 of the DCP directory until the last minute.
195 _sound_asset_writer = _sound_asset->start_write (
196 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
197 film()->contains_atmos_content()
201 _default_font = dcp::ArrayData(default_font_file());
205 /** @param frame reel-relative frame */
207 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
209 auto handle = film()->info_file_handle(_period, false);
210 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
211 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
212 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
213 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
218 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
220 dcp::FrameInfo frame_info;
221 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
222 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
223 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
225 char hash_buffer[33];
226 checked_fread (hash_buffer, 32, info->get(), info->file());
227 hash_buffer[32] = '\0';
228 frame_info.hash = hash_buffer;
235 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
239 return frame * _info_size;
241 return frame * _info_size * 2;
243 return frame * _info_size * 2 + _info_size;
245 DCPOMATIC_ASSERT (false);
248 DCPOMATIC_ASSERT (false);
253 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
255 auto job = _job.lock ();
258 job->sub (_("Checking existing image data"));
261 /* Try to open the existing asset */
262 auto asset_file = fopen_boost (asset, "rb");
264 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
267 LOG_GENERAL ("Opened existing asset at %1", asset.string());
270 shared_ptr<InfoFileHandle> info_file;
273 info_file = film()->info_file_handle (_period, true);
274 } catch (OpenFileError &) {
275 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->file()) / _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->file()), _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);
307 return first_nonexistant_frame;
312 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
314 if (!_picture_asset_writer) {
315 /* We're not writing any data */
319 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
320 write_frame_info (frame, eyes, fin);
321 _last_written[static_cast<int>(eyes)] = encoded;
326 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
329 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
330 if (film()->encrypted()) {
331 _atmos_asset->set_key(film()->key());
333 _atmos_asset_writer = _atmos_asset->start_write (
334 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
337 _atmos_asset_writer->write (atmos);
342 ReelWriter::fake_write (int size)
344 if (!_picture_asset_writer) {
345 /* We're not writing any data */
349 _picture_asset_writer->fake_write (size);
354 ReelWriter::repeat_write (Frame frame, Eyes eyes)
356 if (!_picture_asset_writer) {
357 /* We're not writing any data */
361 auto fin = _picture_asset_writer->write (
362 _last_written[static_cast<int>(eyes)]->data(),
363 _last_written[static_cast<int>(eyes)]->size()
365 write_frame_info (frame, eyes, fin);
370 ReelWriter::finish (boost::filesystem::path output_dcp)
372 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
373 /* Nothing was written to the picture asset */
374 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
375 _picture_asset.reset ();
378 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
379 /* Nothing was written to the sound asset */
380 _sound_asset.reset ();
383 /* Hard-link any video asset file into the DCP */
384 if (_picture_asset) {
385 DCPOMATIC_ASSERT (_picture_asset->file());
386 boost::filesystem::path video_from = _picture_asset->file().get();
387 boost::filesystem::path video_to = output_dcp;
388 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
389 /* There may be an existing "to" file if we are recreating a DCP in the same place without
392 boost::system::error_code ec;
393 boost::filesystem::remove (video_to, ec);
395 boost::filesystem::create_hard_link (video_from, video_to, ec);
397 LOG_WARNING_NC ("Hard-link failed; copying instead");
398 auto job = _job.lock ();
400 job->sub (_("Copying video file into DCP"));
402 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
403 } catch (exception& e) {
404 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
405 throw FileError (e.what(), video_from);
408 boost::filesystem::copy_file (video_from, video_to, ec);
410 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
411 throw FileError (ec.message(), video_from);
416 _picture_asset->set_file (video_to);
419 /* Move the audio asset into the DCP */
421 boost::filesystem::path audio_to = output_dcp;
422 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
425 boost::system::error_code ec;
426 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
429 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
433 _sound_asset->set_file (audio_to);
437 _atmos_asset_writer->finalize ();
438 boost::filesystem::path atmos_to = output_dcp;
439 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
442 boost::system::error_code ec;
443 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
446 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
450 _atmos_asset->set_file (atmos_to);
455 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
456 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
458 template <class Interop, class SMPTE, class Result>
461 shared_ptr<dcp::SubtitleAsset> asset,
462 int64_t picture_duration,
463 shared_ptr<dcp::Reel> reel,
464 list<ReferencedReelAsset> const & refs,
465 vector<FontData> const & fonts,
466 dcp::ArrayData default_font,
467 shared_ptr<const Film> film,
468 DCPTimePeriod period,
469 boost::filesystem::path output_dcp,
473 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
475 shared_ptr<Result> reel_asset;
478 /* Add the font to the subtitle content */
479 for (auto const& j: fonts) {
480 asset->add_font (j.id, j.data.get_value_or(default_font));
483 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
484 auto directory = output_dcp / interop->id ();
485 boost::filesystem::create_directories (directory);
486 interop->write (directory / ("sub_" + interop->id() + ".xml"));
487 reel_asset = make_shared<Interop> (
489 dcp::Fraction(film->video_frame_rate(), 1),
493 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
494 /* All our assets should be the same length; use the picture asset length here
495 as a reference to set the subtitle one. We'll use the duration rather than
496 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
497 just interested in its presentation length.
499 smpte->set_intrinsic_duration(picture_duration);
501 output_dcp / ("sub_" + asset->id() + ".mxf")
503 reel_asset = make_shared<SMPTE> (
505 dcp::Fraction(film->video_frame_rate(), 1),
512 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
514 auto k = dynamic_pointer_cast<Result> (j.asset);
515 if (k && j.period == period) {
517 /* If we have a hash for this asset in the CPL, assume that it is correct */
519 k->asset_ref()->set_hash (k->hash().get());
526 if (!text_only && reel_asset->actual_duration() != period_duration) {
527 throw ProgrammingError (
529 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
532 reel->add (reel_asset);
539 shared_ptr<dcp::ReelPictureAsset>
540 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
542 shared_ptr<dcp::ReelPictureAsset> reel_asset;
544 if (_picture_asset) {
545 /* We have made a picture asset of our own. Put it into the reel */
546 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
548 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
551 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
553 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
556 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
557 /* We don't have a picture asset of our own; hopefully we have one to reference */
559 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
561 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
563 if (k && j.period == _period) {
569 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
571 DCPOMATIC_ASSERT (reel_asset);
572 if (reel_asset->duration() != period_duration) {
573 throw ProgrammingError (
575 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
578 reel->add (reel_asset);
580 /* If we have a hash for this asset in the CPL, assume that it is correct */
581 if (reel_asset->hash()) {
582 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
590 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
592 shared_ptr<dcp::ReelSoundAsset> reel_asset;
595 /* We have made a sound asset of our own. Put it into the reel */
596 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
598 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
599 /* We don't have a sound asset of our own; hopefully we have one to reference */
601 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
603 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
605 if (k && j.period == _period) {
607 /* If we have a hash for this asset in the CPL, assume that it is correct */
609 k->asset_ref()->set_hash (k->hash().get());
615 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
617 DCPOMATIC_ASSERT (reel_asset);
618 if (reel_asset->actual_duration() != period_duration) {
620 "Reel sound asset has length %1 but reel period is %2",
621 reel_asset->actual_duration(),
624 if (reel_asset->actual_duration() != period_duration) {
625 throw ProgrammingError (
627 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
632 reel->add (reel_asset);
637 ReelWriter::create_reel_text (
638 shared_ptr<dcp::Reel> reel,
639 list<ReferencedReelAsset> const & refs,
640 vector<FontData> const& fonts,
642 boost::filesystem::path output_dcp,
643 bool ensure_subtitles,
644 set<DCPTextTrack> ensure_closed_captions
647 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
648 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
652 /* We have a subtitle asset that we either made or are referencing */
653 if (auto main_language = film()->subtitle_languages().first) {
654 subtitle->set_language (*main_language);
656 } else if (ensure_subtitles) {
657 /* We had no subtitle asset, but we've been asked to make sure there is one */
658 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
659 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
672 for (auto const& i: _closed_caption_assets) {
673 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
674 i.second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
676 DCPOMATIC_ASSERT (a);
677 a->set_annotation_text (i.first.name);
678 if (i.first.language) {
679 a->set_language (i.first.language.get());
682 ensure_closed_captions.erase (i.first);
685 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
686 for (auto i: ensure_closed_captions) {
687 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
688 empty_text_asset(TextType::CLOSED_CAPTION, i, true), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
690 DCPOMATIC_ASSERT (a);
691 a->set_annotation_text (i.name);
693 a->set_language (i.language.get());
700 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
702 auto markers = film()->markers();
703 film()->add_ffoc_lfoc(markers);
704 Film::Markers reel_markers;
705 for (auto const& i: markers) {
706 if (_period.contains(i.second)) {
707 reel_markers[i.first] = i.second;
711 if (!reel_markers.empty ()) {
712 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
713 for (auto const& i: reel_markers) {
714 DCPTime relative = i.second - _period.from;
715 auto hmsf = relative.split (film()->video_frame_rate());
716 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
723 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
724 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
726 shared_ptr<dcp::Reel>
727 ReelWriter::create_reel (
728 list<ReferencedReelAsset> const & refs,
729 vector<FontData> const & fonts,
730 boost::filesystem::path output_dcp,
731 bool ensure_subtitles,
732 set<DCPTextTrack> ensure_closed_captions
735 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
737 auto reel = make_shared<dcp::Reel>();
739 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
740 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
741 * how big they are, we don't care about that.
743 int64_t duration = 0;
745 auto reel_picture_asset = create_reel_picture (reel, refs);
746 duration = reel_picture_asset->actual_duration ();
747 create_reel_sound (reel, refs);
748 create_reel_markers (reel);
751 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
754 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
761 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
764 if (_picture_asset) {
765 _picture_asset->hash (set_progress);
769 _sound_asset->hash (set_progress);
773 _atmos_asset->hash (set_progress);
775 } catch (boost::thread_interrupted) {
776 /* set_progress contains an interruption_point, so any of these methods
777 * may throw thread_interrupted, at which point we just give up.
783 ReelWriter::start () const
785 return _period.from.frames_floor (film()->video_frame_rate());
790 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
792 if (!_sound_asset_writer) {
796 DCPOMATIC_ASSERT (audio);
797 _sound_asset_writer->write (audio->data(), audio->frames());
801 shared_ptr<dcp::SubtitleAsset>
802 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
804 shared_ptr<dcp::SubtitleAsset> asset;
806 auto lang = film()->subtitle_languages();
807 if (film()->interop()) {
808 auto s = make_shared<dcp::InteropSubtitleAsset>();
809 s->set_movie_title (film()->name());
810 if (type == TextType::OPEN_SUBTITLE) {
811 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
812 } else if (track->language) {
813 s->set_language (track->language->to_string());
815 s->set_reel_number (raw_convert<string> (_reel_index + 1));
818 auto s = make_shared<dcp::SMPTESubtitleAsset>();
819 s->set_content_title_text (film()->name());
820 s->set_metadata (mxf_metadata());
821 if (type == TextType::OPEN_SUBTITLE && lang.first) {
822 s->set_language (*lang.first);
823 } else if (track && track->language) {
824 s->set_language (dcp::LanguageTag(track->language->to_string()));
826 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
827 s->set_reel_number (_reel_index + 1);
828 s->set_time_code_rate (film()->video_frame_rate());
829 s->set_start_time (dcp::Time ());
830 if (film()->encrypted()) {
831 s->set_key (film()->key());
835 std::make_shared<dcp::SubtitleString>(
836 optional<std::string>(),
843 dcp::Time(0, 0, 0, 0, 24),
844 dcp::Time(0, 0, 1, 0, 24),
867 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
869 shared_ptr<dcp::SubtitleAsset> asset;
872 case TextType::OPEN_SUBTITLE:
873 asset = _subtitle_asset;
875 case TextType::CLOSED_CAPTION:
876 DCPOMATIC_ASSERT (track);
877 asset = _closed_caption_assets[*track];
880 DCPOMATIC_ASSERT (false);
884 asset = empty_text_asset (type, track, false);
888 case TextType::OPEN_SUBTITLE:
889 _subtitle_asset = asset;
891 case TextType::CLOSED_CAPTION:
892 DCPOMATIC_ASSERT (track);
893 _closed_caption_assets[*track] = asset;
896 DCPOMATIC_ASSERT (false);
899 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
900 auto const tcr = 1000;
902 for (auto i: subs.string) {
903 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
904 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
905 asset->add (make_shared<dcp::SubtitleString>(i));
908 for (auto i: subs.bitmap) {
910 make_shared<dcp::SubtitleImage>(
912 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
913 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
914 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
915 dcp::Time(), dcp::Time()
923 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
925 LOG_GENERAL ("Checking existing picture frame %1", frame);
927 /* Read the data from the info file; for 3D we just check the left
928 frames until we find a good one.
930 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
934 /* Read the data from the asset and hash it */
935 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
936 ArrayData data (info.size);
937 size_t const read = fread (data.data(), 1, data.size(), asset_file);
938 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
939 if (read != static_cast<size_t> (data.size ())) {
940 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
944 digester.add (data.data(), data.size());
945 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
946 if (digester.get() != info.hash) {
947 LOG_GENERAL ("Existing frame %1 failed hash check", frame);