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 /* Write the sound asset into the film directory so that we leave the creation
182 of the DCP directory until the last minute.
184 _sound_asset_writer = _sound_asset->start_write (
185 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
186 film()->contains_atmos_content()
190 _default_font = dcp::ArrayData(default_font_file());
193 /** @param frame reel-relative frame */
195 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
197 auto handle = film()->info_file_handle(_period, false);
198 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
199 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
200 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
201 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
205 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
207 dcp::FrameInfo frame_info;
208 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
209 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
210 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
212 char hash_buffer[33];
213 checked_fread (hash_buffer, 32, info->get(), info->file());
214 hash_buffer[32] = '\0';
215 frame_info.hash = hash_buffer;
221 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
225 return frame * _info_size;
227 return frame * _info_size * 2;
229 return frame * _info_size * 2 + _info_size;
231 DCPOMATIC_ASSERT (false);
234 DCPOMATIC_ASSERT (false);
238 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
240 auto job = _job.lock ();
243 job->sub (_("Checking existing image data"));
246 /* Try to open the existing asset */
247 auto asset_file = fopen_boost (asset, "rb");
249 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
252 LOG_GENERAL ("Opened existing asset at %1", asset.string());
255 shared_ptr<InfoFileHandle> info_file;
258 info_file = film()->info_file_handle (_period, true);
259 } catch (OpenFileError &) {
260 LOG_GENERAL_NC ("Could not open film info file");
265 /* Offset of the last dcp::FrameInfo in the info file */
266 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
267 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
269 Frame first_nonexistant_frame;
270 if (film()->three_d()) {
271 /* Start looking at the last left frame */
272 first_nonexistant_frame = n / 2;
274 first_nonexistant_frame = n;
277 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
278 --first_nonexistant_frame;
281 if (!film()->three_d() && first_nonexistant_frame > 0) {
282 /* If we are doing 3D we might have found a good L frame with no R, so only
283 do this if we're in 2D and we've just found a good B(oth) frame.
285 ++first_nonexistant_frame;
288 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
292 return first_nonexistant_frame;
296 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
298 if (!_picture_asset_writer) {
299 /* We're not writing any data */
303 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
304 write_frame_info (frame, eyes, fin);
305 _last_written[static_cast<int>(eyes)] = encoded;
310 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
313 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
314 if (film()->encrypted()) {
315 _atmos_asset->set_key(film()->key());
317 _atmos_asset_writer = _atmos_asset->start_write (
318 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
321 _atmos_asset_writer->write (atmos);
326 ReelWriter::fake_write (int size)
328 if (!_picture_asset_writer) {
329 /* We're not writing any data */
333 _picture_asset_writer->fake_write (size);
337 ReelWriter::repeat_write (Frame frame, Eyes eyes)
339 if (!_picture_asset_writer) {
340 /* We're not writing any data */
344 auto fin = _picture_asset_writer->write (
345 _last_written[static_cast<int>(eyes)]->data(),
346 _last_written[static_cast<int>(eyes)]->size()
348 write_frame_info (frame, eyes, fin);
352 ReelWriter::finish (boost::filesystem::path output_dcp)
354 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
355 /* Nothing was written to the picture asset */
356 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
357 _picture_asset.reset ();
360 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
361 /* Nothing was written to the sound asset */
362 _sound_asset.reset ();
365 /* Hard-link any video asset file into the DCP */
366 if (_picture_asset) {
367 DCPOMATIC_ASSERT (_picture_asset->file());
368 boost::filesystem::path video_from = _picture_asset->file().get();
369 boost::filesystem::path video_to = output_dcp;
370 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
371 /* There may be an existing "to" file if we are recreating a DCP in the same place without
374 boost::system::error_code ec;
375 boost::filesystem::remove (video_to, ec);
377 boost::filesystem::create_hard_link (video_from, video_to, ec);
379 LOG_WARNING_NC ("Hard-link failed; copying instead");
380 auto job = _job.lock ();
382 job->sub (_("Copying video file into DCP"));
384 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
385 } catch (exception& e) {
386 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
387 throw FileError (e.what(), video_from);
390 boost::filesystem::copy_file (video_from, video_to, ec);
392 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
393 throw FileError (ec.message(), video_from);
398 _picture_asset->set_file (video_to);
401 /* Move the audio asset into the DCP */
403 boost::filesystem::path audio_to = output_dcp;
404 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
407 boost::system::error_code ec;
408 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
411 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
415 _sound_asset->set_file (audio_to);
419 _atmos_asset_writer->finalize ();
420 boost::filesystem::path atmos_to = output_dcp;
421 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
424 boost::system::error_code ec;
425 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
428 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
432 _atmos_asset->set_file (atmos_to);
439 shared_ptr<dcp::SubtitleAsset> asset,
440 int64_t picture_duration,
441 shared_ptr<dcp::Reel> reel,
442 list<ReferencedReelAsset> const & refs,
443 vector<FontData> const & fonts,
444 dcp::ArrayData default_font,
445 shared_ptr<const Film> film,
446 DCPTimePeriod period,
447 boost::filesystem::path output_dcp,
451 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
453 shared_ptr<T> reel_asset;
456 /* Add the font to the subtitle content */
457 for (auto const& j: fonts) {
458 asset->add_font (j.id, j.data.get_value_or(default_font));
461 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
462 auto directory = output_dcp / asset->id ();
463 boost::filesystem::create_directories (directory);
464 asset->write (directory / ("sub_" + asset->id() + ".xml"));
466 /* All our assets should be the same length; use the picture asset length here
467 as a reference to set the subtitle one. We'll use the duration rather than
468 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
469 just interested in its presentation length.
471 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
474 output_dcp / ("sub_" + asset->id() + ".mxf")
478 reel_asset = make_shared<T> (
480 dcp::Fraction(film->video_frame_rate(), 1),
485 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
487 auto k = dynamic_pointer_cast<T> (j.asset);
488 if (k && j.period == period) {
490 /* If we have a hash for this asset in the CPL, assume that it is correct */
492 k->asset_ref()->set_hash (k->hash().get());
499 if (!text_only && reel_asset->actual_duration() != period_duration) {
500 throw ProgrammingError (
502 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
505 reel->add (reel_asset);
512 shared_ptr<dcp::ReelPictureAsset>
513 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
515 shared_ptr<dcp::ReelPictureAsset> reel_asset;
517 if (_picture_asset) {
518 /* We have made a picture asset of our own. Put it into the reel */
519 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
521 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
524 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
526 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
529 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
530 /* We don't have a picture asset of our own; hopefully we have one to reference */
532 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
534 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
536 if (k && j.period == _period) {
542 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
544 DCPOMATIC_ASSERT (reel_asset);
545 if (reel_asset->duration() != period_duration) {
546 throw ProgrammingError (
548 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
551 reel->add (reel_asset);
553 /* If we have a hash for this asset in the CPL, assume that it is correct */
554 if (reel_asset->hash()) {
555 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
563 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
565 shared_ptr<dcp::ReelSoundAsset> reel_asset;
568 /* We have made a sound asset of our own. Put it into the reel */
569 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
571 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
572 /* We don't have a sound asset of our own; hopefully we have one to reference */
574 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
576 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
578 if (k && j.period == _period) {
580 /* If we have a hash for this asset in the CPL, assume that it is correct */
582 k->asset_ref()->set_hash (k->hash().get());
588 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
590 DCPOMATIC_ASSERT (reel_asset);
591 if (reel_asset->actual_duration() != period_duration) {
593 "Reel sound asset has length %1 but reel period is %2",
594 reel_asset->actual_duration(),
597 if (reel_asset->actual_duration() != period_duration) {
598 throw ProgrammingError (
600 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
605 reel->add (reel_asset);
610 ReelWriter::create_reel_text (
611 shared_ptr<dcp::Reel> reel,
612 list<ReferencedReelAsset> const & refs,
613 vector<FontData> const& fonts,
615 boost::filesystem::path output_dcp,
616 bool ensure_subtitles,
617 set<DCPTextTrack> ensure_closed_captions
620 auto subtitle = maybe_add_text<dcp::ReelSubtitleAsset> (
621 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
625 /* We have a subtitle asset that we either made or are referencing */
626 if (auto main_language = film()->subtitle_languages().first) {
627 subtitle->set_language (*main_language);
629 } else if (ensure_subtitles) {
630 /* We had no subtitle asset, but we've been asked to make sure there is one */
631 subtitle = maybe_add_text<dcp::ReelSubtitleAsset>(
632 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>()),
645 for (auto const& i: _closed_caption_assets) {
646 auto a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
647 i.second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
649 DCPOMATIC_ASSERT (a);
650 a->set_annotation_text (i.first.name);
651 if (!i.first.language.empty()) {
652 a->set_language (dcp::LanguageTag(i.first.language));
655 ensure_closed_captions.erase (i.first);
658 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
659 for (auto i: ensure_closed_captions) {
660 auto a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
661 empty_text_asset(TextType::CLOSED_CAPTION, i), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
663 DCPOMATIC_ASSERT (a);
664 a->set_annotation_text (i.name);
665 if (!i.language.empty()) {
666 a->set_language (dcp::LanguageTag(i.language));
674 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
676 auto markers = film()->markers();
677 film()->add_ffoc_lfoc(markers);
678 Film::Markers reel_markers;
679 for (auto const& i: markers) {
680 if (_period.contains(i.second)) {
681 reel_markers[i.first] = i.second;
685 if (!reel_markers.empty ()) {
686 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration(), 0);
687 for (auto const& i: reel_markers) {
688 DCPTime relative = i.second - _period.from;
689 auto hmsf = relative.split (film()->video_frame_rate());
690 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
697 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
698 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
700 shared_ptr<dcp::Reel>
701 ReelWriter::create_reel (
702 list<ReferencedReelAsset> const & refs,
703 vector<FontData> const & fonts,
704 boost::filesystem::path output_dcp,
705 bool ensure_subtitles,
706 set<DCPTextTrack> ensure_closed_captions
709 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
711 auto reel = make_shared<dcp::Reel>();
713 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
714 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
715 * how big they are, we don't care about that.
717 int64_t duration = 0;
719 auto reel_picture_asset = create_reel_picture (reel, refs);
720 duration = reel_picture_asset->actual_duration ();
721 create_reel_sound (reel, refs);
722 create_reel_markers (reel);
725 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
728 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
735 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
737 if (_picture_asset) {
738 _picture_asset->hash (set_progress);
742 _sound_asset->hash (set_progress);
746 _atmos_asset->hash (set_progress);
751 ReelWriter::start () const
753 return _period.from.frames_floor (film()->video_frame_rate());
758 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
760 if (!_sound_asset_writer) {
764 DCPOMATIC_ASSERT (audio);
765 _sound_asset_writer->write (audio->data(), audio->frames());
769 shared_ptr<dcp::SubtitleAsset>
770 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track) const
772 shared_ptr<dcp::SubtitleAsset> asset;
774 auto lang = film()->subtitle_languages();
775 if (film()->interop()) {
776 auto s = make_shared<dcp::InteropSubtitleAsset>();
777 s->set_movie_title (film()->name());
778 if (type == TextType::OPEN_SUBTITLE) {
779 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
780 } else if (!track->language.empty()) {
781 s->set_language (track->language);
783 s->set_reel_number (raw_convert<string> (_reel_index + 1));
786 auto s = make_shared<dcp::SMPTESubtitleAsset>();
787 s->set_content_title_text (film()->name());
788 s->set_metadata (mxf_metadata());
789 if (type == TextType::OPEN_SUBTITLE && lang.first) {
790 s->set_language (*lang.first);
791 } else if (track && !track->language.empty()) {
792 s->set_language (dcp::LanguageTag(track->language));
794 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
795 s->set_reel_number (_reel_index + 1);
796 s->set_time_code_rate (film()->video_frame_rate());
797 s->set_start_time (dcp::Time ());
798 if (film()->encrypted()) {
799 s->set_key (film()->key());
802 std::make_shared<dcp::SubtitleString>(
803 optional<std::string>(),
810 dcp::Time(0, 0, 0, 0, 24),
811 dcp::Time(0, 0, 1, 0, 24),
832 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
834 shared_ptr<dcp::SubtitleAsset> asset;
837 case TextType::OPEN_SUBTITLE:
838 asset = _subtitle_asset;
840 case TextType::CLOSED_CAPTION:
841 DCPOMATIC_ASSERT (track);
842 asset = _closed_caption_assets[*track];
845 DCPOMATIC_ASSERT (false);
849 asset = empty_text_asset (type, track);
853 case TextType::OPEN_SUBTITLE:
854 _subtitle_asset = asset;
856 case TextType::CLOSED_CAPTION:
857 DCPOMATIC_ASSERT (track);
858 _closed_caption_assets[*track] = asset;
861 DCPOMATIC_ASSERT (false);
864 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
865 auto const tcr = 1000;
867 for (auto i: subs.string) {
868 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
869 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
870 asset->add (make_shared<dcp::SubtitleString>(i));
873 for (auto i: subs.bitmap) {
875 make_shared<dcp::SubtitleImage>(
877 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
878 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
879 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
880 dcp::Time(), dcp::Time()
887 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
889 LOG_GENERAL ("Checking existing picture frame %1", frame);
891 /* Read the data from the info file; for 3D we just check the left
892 frames until we find a good one.
894 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
898 /* Read the data from the asset and hash it */
899 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
900 ArrayData data (info.size);
901 size_t const read = fread (data.data(), 1, data.size(), asset_file);
902 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
903 if (read != static_cast<size_t> (data.size ())) {
904 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
908 digester.add (data.data(), data.size());
909 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
910 if (digester.get() != info.hash) {
911 LOG_GENERAL ("Existing frame %1 failed hash check", frame);