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/mono_picture_asset.h>
36 #include <dcp/stereo_picture_asset.h>
37 #include <dcp/sound_asset.h>
38 #include <dcp/sound_asset_writer.h>
40 #include <dcp/reel_atmos_asset.h>
41 #include <dcp/reel_mono_picture_asset.h>
42 #include <dcp/reel_stereo_picture_asset.h>
43 #include <dcp/reel_sound_asset.h>
44 #include <dcp/reel_subtitle_asset.h>
45 #include <dcp/reel_closed_caption_asset.h>
46 #include <dcp/reel_markers_asset.h>
49 #include <dcp/certificate_chain.h>
50 #include <dcp/interop_subtitle_asset.h>
51 #include <dcp/smpte_subtitle_asset.h>
52 #include <dcp/raw_convert.h>
53 #include <dcp/subtitle_image.h>
64 using std::shared_ptr;
65 using std::make_shared;
66 using boost::optional;
67 using std::dynamic_pointer_cast;
68 #if BOOST_VERSION >= 106100
69 using namespace boost::placeholders;
74 using dcp::raw_convert;
75 using namespace dcpomatic;
77 int const ReelWriter::_info_size = 48;
79 static dcp::MXFMetadata
82 dcp::MXFMetadata meta;
83 auto config = Config::instance();
84 if (!config->dcp_company_name().empty()) {
85 meta.company_name = config->dcp_company_name ();
87 if (!config->dcp_product_name().empty()) {
88 meta.product_name = config->dcp_product_name ();
90 if (!config->dcp_product_version().empty()) {
91 meta.product_version = config->dcp_product_version ();
96 /** @param job Related job, or 0.
97 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
98 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
99 * subtitle / closed caption files.
101 ReelWriter::ReelWriter (
102 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
104 : WeakConstFilm (weak_film)
106 , _reel_index (reel_index)
107 , _reel_count (reel_count)
108 , _content_summary (film()->content_summary(period))
110 , _text_only (text_only)
112 /* Create or find our picture asset in a subdirectory, named
113 according to those film's parameters which affect the video
114 output. We will hard-link it into the DCP later.
117 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
119 boost::filesystem::path const asset =
120 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
122 _first_nonexistant_frame = check_existing_picture_asset (asset);
124 if (_first_nonexistant_frame < period.duration().frames_round(film()->video_frame_rate())) {
125 /* We do not have a complete picture asset. If there is an
126 existing asset, break any hard links to it as we are about
127 to change its contents (if only by changing the IDs); see
130 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
132 job->sub (_("Copying old video file"));
133 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
135 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
137 boost::filesystem::remove (asset);
138 boost::filesystem::rename (asset.string() + ".tmp", asset);
142 if (film()->three_d()) {
143 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
145 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
148 _picture_asset->set_size (film()->frame_size());
149 _picture_asset->set_metadata (mxf_metadata());
151 if (film()->encrypted()) {
152 _picture_asset->set_key (film()->key());
153 _picture_asset->set_context_id (film()->context_id());
156 _picture_asset->set_file (asset);
157 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
158 } else if (!text_only) {
159 /* We already have a complete picture asset that we can just re-use */
160 /* XXX: what about if the encryption key changes? */
161 if (film()->three_d()) {
162 _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
164 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
168 if (film()->audio_channels()) {
169 _sound_asset = make_shared<dcp::SoundAsset> (
170 dcp::Fraction(film()->video_frame_rate(), 1), film()->audio_frame_rate(), film()->audio_channels(), film()->audio_language(), standard
173 _sound_asset->set_metadata (mxf_metadata());
175 if (film()->encrypted()) {
176 _sound_asset->set_key (film()->key());
179 DCPOMATIC_ASSERT (film()->directory());
181 vector<dcp::Channel> active;
182 for (auto i: film()->mapped_audio_channels()) {
183 active.push_back (static_cast<dcp::Channel>(i));
186 /* Write the sound asset into the film directory so that we leave the creation
187 of the DCP directory until the last minute.
189 _sound_asset_writer = _sound_asset->start_write (
190 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
192 film()->contains_atmos_content()
196 _default_font = dcp::ArrayData(default_font_file());
199 /** @param frame reel-relative frame */
201 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
203 auto handle = film()->info_file_handle(_period, false);
204 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
205 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
206 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
207 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
211 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
213 dcp::FrameInfo frame_info;
214 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
215 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
216 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
218 char hash_buffer[33];
219 checked_fread (hash_buffer, 32, info->get(), info->file());
220 hash_buffer[32] = '\0';
221 frame_info.hash = hash_buffer;
227 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
231 return frame * _info_size;
233 return frame * _info_size * 2;
235 return frame * _info_size * 2 + _info_size;
237 DCPOMATIC_ASSERT (false);
240 DCPOMATIC_ASSERT (false);
244 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
246 auto job = _job.lock ();
249 job->sub (_("Checking existing image data"));
252 /* Try to open the existing asset */
253 auto asset_file = fopen_boost (asset, "rb");
255 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
258 LOG_GENERAL ("Opened existing asset at %1", asset.string());
261 shared_ptr<InfoFileHandle> info_file;
264 info_file = film()->info_file_handle (_period, true);
265 } catch (OpenFileError &) {
266 LOG_GENERAL_NC ("Could not open film info file");
271 /* Offset of the last dcp::FrameInfo in the info file */
272 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
273 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 Frame first_nonexistant_frame;
276 if (film()->three_d()) {
277 /* Start looking at the last left frame */
278 first_nonexistant_frame = n / 2;
280 first_nonexistant_frame = n;
283 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
284 --first_nonexistant_frame;
287 if (!film()->three_d() && first_nonexistant_frame > 0) {
288 /* If we are doing 3D we might have found a good L frame with no R, so only
289 do this if we're in 2D and we've just found a good B(oth) frame.
291 ++first_nonexistant_frame;
294 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
298 return first_nonexistant_frame;
302 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
304 if (!_picture_asset_writer) {
305 /* We're not writing any data */
309 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
310 write_frame_info (frame, eyes, fin);
311 _last_written[static_cast<int>(eyes)] = encoded;
316 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
319 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
320 if (film()->encrypted()) {
321 _atmos_asset->set_key(film()->key());
323 _atmos_asset_writer = _atmos_asset->start_write (
324 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
327 _atmos_asset_writer->write (atmos);
332 ReelWriter::fake_write (int size)
334 if (!_picture_asset_writer) {
335 /* We're not writing any data */
339 _picture_asset_writer->fake_write (size);
343 ReelWriter::repeat_write (Frame frame, Eyes eyes)
345 if (!_picture_asset_writer) {
346 /* We're not writing any data */
350 auto fin = _picture_asset_writer->write (
351 _last_written[static_cast<int>(eyes)]->data(),
352 _last_written[static_cast<int>(eyes)]->size()
354 write_frame_info (frame, eyes, fin);
358 ReelWriter::finish (boost::filesystem::path output_dcp)
360 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
361 /* Nothing was written to the picture asset */
362 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
363 _picture_asset.reset ();
366 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
367 /* Nothing was written to the sound asset */
368 _sound_asset.reset ();
371 /* Hard-link any video asset file into the DCP */
372 if (_picture_asset) {
373 DCPOMATIC_ASSERT (_picture_asset->file());
374 boost::filesystem::path video_from = _picture_asset->file().get();
375 boost::filesystem::path video_to = output_dcp;
376 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
377 /* There may be an existing "to" file if we are recreating a DCP in the same place without
380 boost::system::error_code ec;
381 boost::filesystem::remove (video_to, ec);
383 boost::filesystem::create_hard_link (video_from, video_to, ec);
385 LOG_WARNING_NC ("Hard-link failed; copying instead");
386 auto job = _job.lock ();
388 job->sub (_("Copying video file into DCP"));
390 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
391 } catch (exception& e) {
392 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
393 throw FileError (e.what(), video_from);
396 boost::filesystem::copy_file (video_from, video_to, ec);
398 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
399 throw FileError (ec.message(), video_from);
404 _picture_asset->set_file (video_to);
407 /* Move the audio asset into the DCP */
409 boost::filesystem::path audio_to = output_dcp;
410 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
413 boost::system::error_code ec;
414 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
417 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
421 _sound_asset->set_file (audio_to);
425 _atmos_asset_writer->finalize ();
426 boost::filesystem::path atmos_to = output_dcp;
427 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
430 boost::system::error_code ec;
431 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
434 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
438 _atmos_asset->set_file (atmos_to);
445 shared_ptr<dcp::SubtitleAsset> asset,
446 int64_t picture_duration,
447 shared_ptr<dcp::Reel> reel,
448 list<ReferencedReelAsset> const & refs,
449 vector<FontData> const & fonts,
450 dcp::ArrayData default_font,
451 shared_ptr<const Film> film,
452 DCPTimePeriod period,
453 boost::filesystem::path output_dcp,
457 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
459 shared_ptr<T> reel_asset;
462 /* Add the font to the subtitle content */
463 for (auto const& j: fonts) {
464 asset->add_font (j.id, j.data.get_value_or(default_font));
467 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
468 auto directory = output_dcp / asset->id ();
469 boost::filesystem::create_directories (directory);
470 asset->write (directory / ("sub_" + asset->id() + ".xml"));
472 /* All our assets should be the same length; use the picture asset length here
473 as a reference to set the subtitle one. We'll use the duration rather than
474 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
475 just interested in its presentation length.
477 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
480 output_dcp / ("sub_" + asset->id() + ".mxf")
484 reel_asset = make_shared<T> (
486 dcp::Fraction(film->video_frame_rate(), 1),
491 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
493 auto k = dynamic_pointer_cast<T> (j.asset);
494 if (k && j.period == period) {
496 /* If we have a hash for this asset in the CPL, assume that it is correct */
498 k->asset_ref()->set_hash (k->hash().get());
505 if (!text_only && reel_asset->actual_duration() != period_duration) {
506 throw ProgrammingError (
508 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
511 reel->add (reel_asset);
518 shared_ptr<dcp::ReelPictureAsset>
519 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
521 shared_ptr<dcp::ReelPictureAsset> reel_asset;
523 if (_picture_asset) {
524 /* We have made a picture asset of our own. Put it into the reel */
525 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
527 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
530 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
532 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
535 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
536 /* We don't have a picture asset of our own; hopefully we have one to reference */
538 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
540 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
542 if (k && j.period == _period) {
548 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
550 DCPOMATIC_ASSERT (reel_asset);
551 if (reel_asset->duration() != period_duration) {
552 throw ProgrammingError (
554 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
557 reel->add (reel_asset);
559 /* If we have a hash for this asset in the CPL, assume that it is correct */
560 if (reel_asset->hash()) {
561 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
569 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
571 shared_ptr<dcp::ReelSoundAsset> reel_asset;
574 /* We have made a sound asset of our own. Put it into the reel */
575 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
577 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
578 /* We don't have a sound asset of our own; hopefully we have one to reference */
580 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
582 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
584 if (k && j.period == _period) {
586 /* If we have a hash for this asset in the CPL, assume that it is correct */
588 k->asset_ref()->set_hash (k->hash().get());
594 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
596 DCPOMATIC_ASSERT (reel_asset);
597 if (reel_asset->actual_duration() != period_duration) {
599 "Reel sound asset has length %1 but reel period is %2",
600 reel_asset->actual_duration(),
603 if (reel_asset->actual_duration() != period_duration) {
604 throw ProgrammingError (
606 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
611 reel->add (reel_asset);
616 ReelWriter::create_reel_text (
617 shared_ptr<dcp::Reel> reel,
618 list<ReferencedReelAsset> const & refs,
619 vector<FontData> const& fonts,
621 boost::filesystem::path output_dcp,
622 bool ensure_subtitles,
623 set<DCPTextTrack> ensure_closed_captions
626 auto subtitle = maybe_add_text<dcp::ReelSubtitleAsset> (
627 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
631 /* We have a subtitle asset that we either made or are referencing */
632 if (!film()->subtitle_languages().empty()) {
633 subtitle->set_language (film()->subtitle_languages().front());
635 } else if (ensure_subtitles) {
636 /* We had no subtitle asset, but we've been asked to make sure there is one */
637 subtitle = maybe_add_text<dcp::ReelSubtitleAsset>(
638 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>()),
651 for (auto const& i: _closed_caption_assets) {
652 auto a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
653 i.second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
655 DCPOMATIC_ASSERT (a);
656 a->set_annotation_text (i.first.name);
657 if (!i.first.language.empty()) {
658 a->set_language (dcp::LanguageTag(i.first.language));
661 ensure_closed_captions.erase (i.first);
664 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
665 for (auto i: ensure_closed_captions) {
666 auto a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
667 empty_text_asset(TextType::CLOSED_CAPTION, i), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
669 DCPOMATIC_ASSERT (a);
670 a->set_annotation_text (i.name);
671 if (!i.language.empty()) {
672 a->set_language (dcp::LanguageTag(i.language));
680 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
682 auto markers = film()->markers();
683 film()->add_ffoc_lfoc(markers);
684 Film::Markers reel_markers;
685 for (auto const& i: markers) {
686 if (_period.contains(i.second)) {
687 reel_markers[i.first] = i.second;
691 if (!reel_markers.empty ()) {
692 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration(), 0);
693 for (auto const& i: reel_markers) {
695 DCPTime relative = i.second - _period.from;
696 relative.split (film()->video_frame_rate(), h, m, s, f);
697 ma->set (i.first, dcp::Time(h, m, s, f, film()->video_frame_rate()));
704 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
705 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
707 shared_ptr<dcp::Reel>
708 ReelWriter::create_reel (
709 list<ReferencedReelAsset> const & refs,
710 vector<FontData> const & fonts,
711 boost::filesystem::path output_dcp,
712 bool ensure_subtitles,
713 set<DCPTextTrack> ensure_closed_captions
716 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
718 auto reel = make_shared<dcp::Reel>();
720 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
721 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
722 * how big they are, we don't care about that.
724 int64_t duration = 0;
726 auto reel_picture_asset = create_reel_picture (reel, refs);
727 duration = reel_picture_asset->actual_duration ();
728 create_reel_sound (reel, refs);
729 create_reel_markers (reel);
732 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
735 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
742 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
744 if (_picture_asset) {
745 _picture_asset->hash (set_progress);
749 _sound_asset->hash (set_progress);
753 _atmos_asset->hash (set_progress);
758 ReelWriter::start () const
760 return _period.from.frames_floor (film()->video_frame_rate());
765 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
767 if (!_sound_asset_writer) {
771 DCPOMATIC_ASSERT (audio);
772 _sound_asset_writer->write (audio->data(), audio->frames());
776 shared_ptr<dcp::SubtitleAsset>
777 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track) const
779 shared_ptr<dcp::SubtitleAsset> asset;
781 auto lang = film()->subtitle_languages();
782 if (film()->interop()) {
783 auto s = make_shared<dcp::InteropSubtitleAsset>();
784 s->set_movie_title (film()->name());
785 if (type == TextType::OPEN_SUBTITLE) {
786 s->set_language (lang.empty() ? "Unknown" : lang.front().to_string());
787 } else if (!track->language.empty()) {
788 s->set_language (track->language);
790 s->set_reel_number (raw_convert<string> (_reel_index + 1));
793 auto s = make_shared<dcp::SMPTESubtitleAsset>();
794 s->set_content_title_text (film()->name());
795 s->set_metadata (mxf_metadata());
796 if (type == TextType::OPEN_SUBTITLE && !lang.empty()) {
797 s->set_language (lang.front());
798 } else if (track && !track->language.empty()) {
799 s->set_language (dcp::LanguageTag(track->language));
801 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
802 s->set_reel_number (_reel_index + 1);
803 s->set_time_code_rate (film()->video_frame_rate());
804 s->set_start_time (dcp::Time ());
805 if (film()->encrypted()) {
806 s->set_key (film()->key());
809 std::make_shared<dcp::SubtitleString>(
810 optional<std::string>(),
817 dcp::Time(0, 0, 0, 0, 24),
818 dcp::Time(0, 0, 1, 0, 24),
839 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
841 shared_ptr<dcp::SubtitleAsset> asset;
844 case TextType::OPEN_SUBTITLE:
845 asset = _subtitle_asset;
847 case TextType::CLOSED_CAPTION:
848 DCPOMATIC_ASSERT (track);
849 asset = _closed_caption_assets[*track];
852 DCPOMATIC_ASSERT (false);
856 asset = empty_text_asset (type, track);
860 case TextType::OPEN_SUBTITLE:
861 _subtitle_asset = asset;
863 case TextType::CLOSED_CAPTION:
864 DCPOMATIC_ASSERT (track);
865 _closed_caption_assets[*track] = asset;
868 DCPOMATIC_ASSERT (false);
871 auto const vfr = film()->video_frame_rate();
873 for (auto i: subs.string) {
874 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), vfr));
875 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), vfr));
876 asset->add (make_shared<dcp::SubtitleString>(i));
879 for (auto i: subs.bitmap) {
881 make_shared<dcp::SubtitleImage>(
883 dcp::Time(period.from.seconds() - _period.from.seconds(), vfr),
884 dcp::Time(period.to.seconds() - _period.from.seconds(), vfr),
885 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
886 dcp::Time(), dcp::Time()
893 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
895 LOG_GENERAL ("Checking existing picture frame %1", frame);
897 /* Read the data from the info file; for 3D we just check the left
898 frames until we find a good one.
900 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
904 /* Read the data from the asset and hash it */
905 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
906 ArrayData data (info.size);
907 size_t const read = fread (data.data(), 1, data.size(), asset_file);
908 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
909 if (read != static_cast<size_t> (data.size ())) {
910 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
914 digester.add (data.data(), data.size());
915 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
916 if (digester.get() != info.hash) {
917 LOG_GENERAL ("Existing frame %1 failed hash check", frame);