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 boost::optional;
66 using std::dynamic_pointer_cast;
67 #if BOOST_VERSION >= 106100
68 using namespace boost::placeholders;
73 using dcp::raw_convert;
74 using namespace dcpomatic;
76 int const ReelWriter::_info_size = 48;
78 static dcp::MXFMetadata
81 dcp::MXFMetadata meta;
82 Config* config = Config::instance();
83 if (!config->dcp_company_name().empty()) {
84 meta.company_name = config->dcp_company_name ();
86 if (!config->dcp_product_name().empty()) {
87 meta.product_name = config->dcp_product_name ();
89 if (!config->dcp_product_version().empty()) {
90 meta.product_version = config->dcp_product_version ();
95 /** @param job Related job, or 0.
96 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
97 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
98 * subtitle / closed caption files.
100 ReelWriter::ReelWriter (
101 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
103 : WeakConstFilm (weak_film)
105 , _reel_index (reel_index)
106 , _reel_count (reel_count)
107 , _content_summary (film()->content_summary(period))
109 , _text_only (text_only)
111 /* Create or find our picture asset in a subdirectory, named
112 according to those film's parameters which affect the video
113 output. We will hard-link it into the DCP later.
116 dcp::Standard const standard = film()->interop() ? dcp::INTEROP : dcp::SMPTE;
118 boost::filesystem::path const asset =
119 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
121 _first_nonexistant_frame = check_existing_picture_asset (asset);
123 if (_first_nonexistant_frame < period.duration().frames_round(film()->video_frame_rate())) {
124 /* We do not have a complete picture asset. If there is an
125 existing asset, break any hard links to it as we are about
126 to change its contents (if only by changing the IDs); see
129 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
131 job->sub (_("Copying old video file"));
132 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
134 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
136 boost::filesystem::remove (asset);
137 boost::filesystem::rename (asset.string() + ".tmp", asset);
141 if (film()->three_d()) {
142 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
144 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
147 _picture_asset->set_size (film()->frame_size());
148 _picture_asset->set_metadata (mxf_metadata());
150 if (film()->encrypted()) {
151 _picture_asset->set_key (film()->key());
152 _picture_asset->set_context_id (film()->context_id());
155 _picture_asset->set_file (asset);
156 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
157 } else if (!text_only) {
158 /* We already have a complete picture asset that we can just re-use */
159 /* XXX: what about if the encryption key changes? */
160 if (film()->three_d()) {
161 _picture_asset.reset (new dcp::StereoPictureAsset(asset));
163 _picture_asset.reset (new dcp::MonoPictureAsset(asset));
167 if (film()->audio_channels()) {
169 new dcp::SoundAsset (dcp::Fraction(film()->video_frame_rate(), 1), film()->audio_frame_rate(), film()->audio_channels(), film()->audio_language(), standard)
172 _sound_asset->set_metadata (mxf_metadata());
174 if (film()->encrypted()) {
175 _sound_asset->set_key (film()->key());
178 DCPOMATIC_ASSERT (film()->directory());
180 vector<dcp::Channel> active;
181 for (auto i: film()->mapped_audio_channels()) {
182 active.push_back (static_cast<dcp::Channel>(i));
185 /* Write the sound asset into the film directory so that we leave the creation
186 of the DCP directory until the last minute.
188 _sound_asset_writer = _sound_asset->start_write (
189 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
191 film()->contains_atmos_content()
195 _default_font = dcp::ArrayData(default_font_file());
198 /** @param frame reel-relative frame */
200 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
202 shared_ptr<InfoFileHandle> handle = film()->info_file_handle(_period, false);
203 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
204 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
205 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
206 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
210 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
212 dcp::FrameInfo frame_info;
213 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
214 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
215 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
217 char hash_buffer[33];
218 checked_fread (hash_buffer, 32, info->get(), info->file());
219 hash_buffer[32] = '\0';
220 frame_info.hash = hash_buffer;
226 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
230 return frame * _info_size;
232 return frame * _info_size * 2;
234 return frame * _info_size * 2 + _info_size;
236 DCPOMATIC_ASSERT (false);
239 DCPOMATIC_ASSERT (false);
243 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
245 shared_ptr<Job> job = _job.lock ();
248 job->sub (_("Checking existing image data"));
251 /* Try to open the existing asset */
252 FILE* asset_file = fopen_boost (asset, "rb");
254 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
257 LOG_GENERAL ("Opened existing asset at %1", asset.string());
260 shared_ptr<InfoFileHandle> info_file;
263 info_file = film()->info_file_handle (_period, true);
264 } catch (OpenFileError &) {
265 LOG_GENERAL_NC ("Could not open film info file");
270 /* Offset of the last dcp::FrameInfo in the info file */
271 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
272 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
274 Frame first_nonexistant_frame;
275 if (film()->three_d()) {
276 /* Start looking at the last left frame */
277 first_nonexistant_frame = n / 2;
279 first_nonexistant_frame = n;
282 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
283 --first_nonexistant_frame;
286 if (!film()->three_d() && first_nonexistant_frame > 0) {
287 /* If we are doing 3D we might have found a good L frame with no R, so only
288 do this if we're in 2D and we've just found a good B(oth) frame.
290 ++first_nonexistant_frame;
293 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
297 return first_nonexistant_frame;
301 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
303 if (!_picture_asset_writer) {
304 /* We're not writing any data */
308 dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data(), encoded->size());
309 write_frame_info (frame, eyes, fin);
310 _last_written[eyes] = encoded;
315 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
318 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
319 if (film()->encrypted()) {
320 _atmos_asset->set_key(film()->key());
322 _atmos_asset_writer = _atmos_asset->start_write (
323 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
326 _atmos_asset_writer->write (atmos);
331 ReelWriter::fake_write (int size)
333 if (!_picture_asset_writer) {
334 /* We're not writing any data */
338 _picture_asset_writer->fake_write (size);
342 ReelWriter::repeat_write (Frame frame, Eyes eyes)
344 if (!_picture_asset_writer) {
345 /* We're not writing any data */
349 dcp::FrameInfo fin = _picture_asset_writer->write (
350 _last_written[eyes]->data(),
351 _last_written[eyes]->size()
353 write_frame_info (frame, eyes, fin);
357 ReelWriter::finish (boost::filesystem::path output_dcp)
359 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
360 /* Nothing was written to the picture asset */
361 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
362 _picture_asset.reset ();
365 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
366 /* Nothing was written to the sound asset */
367 _sound_asset.reset ();
370 /* Hard-link any video asset file into the DCP */
371 if (_picture_asset) {
372 DCPOMATIC_ASSERT (_picture_asset->file());
373 boost::filesystem::path video_from = _picture_asset->file().get();
374 boost::filesystem::path video_to = output_dcp;
375 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
376 /* There may be an existing "to" file if we are recreating a DCP in the same place without
379 boost::system::error_code ec;
380 boost::filesystem::remove (video_to, ec);
382 boost::filesystem::create_hard_link (video_from, video_to, ec);
384 LOG_WARNING_NC ("Hard-link failed; copying instead");
385 shared_ptr<Job> job = _job.lock ();
387 job->sub (_("Copying video file into DCP"));
389 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
390 } catch (exception& e) {
391 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
392 throw FileError (e.what(), video_from);
395 boost::filesystem::copy_file (video_from, video_to, ec);
397 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
398 throw FileError (ec.message(), video_from);
403 _picture_asset->set_file (video_to);
406 /* Move the audio asset into the DCP */
408 boost::filesystem::path audio_to = output_dcp;
409 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
412 boost::system::error_code ec;
413 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
416 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
420 _sound_asset->set_file (audio_to);
424 _atmos_asset_writer->finalize ();
425 boost::filesystem::path atmos_to = output_dcp;
426 string const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
429 boost::system::error_code ec;
430 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
433 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
437 _atmos_asset->set_file (atmos_to);
444 shared_ptr<dcp::SubtitleAsset> asset,
445 int64_t picture_duration,
446 shared_ptr<dcp::Reel> reel,
447 list<ReferencedReelAsset> const & refs,
448 vector<FontData> const & fonts,
449 dcp::ArrayData default_font,
450 shared_ptr<const Film> film,
451 DCPTimePeriod period,
452 boost::filesystem::path output_dcp,
456 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
458 shared_ptr<T> reel_asset;
461 /* Add the font to the subtitle content */
462 for (auto const& j: fonts) {
463 asset->add_font (j.id, j.data.get_value_or(default_font));
466 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
467 boost::filesystem::path directory = output_dcp / asset->id ();
468 boost::filesystem::create_directories (directory);
469 asset->write (directory / ("sub_" + asset->id() + ".xml"));
471 /* All our assets should be the same length; use the picture asset length here
472 as a reference to set the subtitle one. We'll use the duration rather than
473 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
474 just interested in its presentation length.
476 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
479 output_dcp / ("sub_" + asset->id() + ".mxf")
486 dcp::Fraction (film->video_frame_rate(), 1),
492 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
494 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
495 if (k && j.period == period) {
497 /* If we have a hash for this asset in the CPL, assume that it is correct */
499 k->asset_ref()->set_hash (k->hash().get());
506 if (!text_only && reel_asset->actual_duration() != period_duration) {
507 throw ProgrammingError (
509 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
512 reel->add (reel_asset);
519 shared_ptr<dcp::ReelPictureAsset>
520 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
522 shared_ptr<dcp::ReelPictureAsset> reel_asset;
524 if (_picture_asset) {
525 /* We have made a picture asset of our own. Put it into the reel */
526 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
528 reel_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
531 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
533 reel_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
536 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
537 /* We don't have a picture asset of our own; hopefully we have one to reference */
539 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
541 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
543 if (k && j.period == _period) {
549 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
551 DCPOMATIC_ASSERT (reel_asset);
552 if (reel_asset->duration() != period_duration) {
553 throw ProgrammingError (
555 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
558 reel->add (reel_asset);
560 /* If we have a hash for this asset in the CPL, assume that it is correct */
561 if (reel_asset->hash()) {
562 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
570 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
572 shared_ptr<dcp::ReelSoundAsset> reel_asset;
575 /* We have made a sound asset of our own. Put it into the reel */
576 reel_asset.reset (new dcp::ReelSoundAsset(_sound_asset, 0));
578 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
579 /* We don't have a sound asset of our own; hopefully we have one to reference */
581 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
583 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
585 if (k && j.period == _period) {
587 /* If we have a hash for this asset in the CPL, assume that it is correct */
589 k->asset_ref()->set_hash (k->hash().get());
595 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
597 DCPOMATIC_ASSERT (reel_asset);
598 if (reel_asset->actual_duration() != period_duration) {
600 "Reel sound asset has length %1 but reel period is %2",
601 reel_asset->actual_duration(),
604 if (reel_asset->actual_duration() != period_duration) {
605 throw ProgrammingError (
607 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
612 reel->add (reel_asset);
617 ReelWriter::create_reel_text (
618 shared_ptr<dcp::Reel> reel,
619 list<ReferencedReelAsset> const & refs,
620 vector<FontData> const& fonts,
622 boost::filesystem::path output_dcp,
623 bool ensure_subtitles,
624 set<DCPTextTrack> ensure_closed_captions
627 shared_ptr<dcp::ReelSubtitleAsset> subtitle = maybe_add_text<dcp::ReelSubtitleAsset> (
628 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
632 /* We have a subtitle asset that we either made or are referencing */
633 if (!film()->subtitle_languages().empty()) {
634 subtitle->set_language (film()->subtitle_languages().front());
636 } else if (ensure_subtitles) {
637 /* We had no subtitle asset, but we've been asked to make sure there is one */
638 subtitle = maybe_add_text<dcp::ReelSubtitleAsset>(
639 empty_text_asset(TEXT_OPEN_SUBTITLE, optional<DCPTextTrack>()),
652 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
653 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
654 i->second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
656 DCPOMATIC_ASSERT (a);
657 a->set_annotation_text (i->first.name);
658 if (!i->first.language.empty()) {
659 a->set_language (dcp::LanguageTag(i->first.language));
662 ensure_closed_captions.erase (i->first);
665 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
666 for (auto i: ensure_closed_captions) {
667 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
668 empty_text_asset(TEXT_CLOSED_CAPTION, i), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
670 DCPOMATIC_ASSERT (a);
671 a->set_annotation_text (i.name);
672 if (!i.language.empty()) {
673 a->set_language (dcp::LanguageTag(i.language));
681 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
683 Film::Markers markers = film()->markers();
684 film()->add_ffoc_lfoc(markers);
685 Film::Markers reel_markers;
686 for (Film::Markers::const_iterator i = markers.begin(); i != markers.end(); ++i) {
687 if (_period.contains(i->second)) {
688 reel_markers[i->first] = i->second;
692 if (!reel_markers.empty ()) {
693 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(film()->video_frame_rate(), 1), 0));
694 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
696 DCPTime relative = i->second - _period.from;
697 relative.split (film()->video_frame_rate(), h, m, s, f);
698 ma->set (i->first, dcp::Time(h, m, s, f, film()->video_frame_rate()));
705 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
706 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
708 shared_ptr<dcp::Reel>
709 ReelWriter::create_reel (
710 list<ReferencedReelAsset> const & refs,
711 vector<FontData> const & fonts,
712 boost::filesystem::path output_dcp,
713 bool ensure_subtitles,
714 set<DCPTextTrack> ensure_closed_captions
717 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
719 shared_ptr<dcp::Reel> reel (new dcp::Reel());
721 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
722 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
723 * how big they are, we don't care about that.
725 int64_t duration = 0;
727 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset = create_reel_picture (reel, refs);
728 duration = reel_picture_asset->actual_duration ();
729 create_reel_sound (reel, refs);
730 create_reel_markers (reel);
733 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
736 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
743 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
745 if (_picture_asset) {
746 _picture_asset->hash (set_progress);
750 _sound_asset->hash (set_progress);
754 _atmos_asset->hash (set_progress);
759 ReelWriter::start () const
761 return _period.from.frames_floor (film()->video_frame_rate());
766 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
768 if (!_sound_asset_writer) {
772 DCPOMATIC_ASSERT (audio);
773 _sound_asset_writer->write (audio->data(), audio->frames());
777 shared_ptr<dcp::SubtitleAsset>
778 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track) const
780 shared_ptr<dcp::SubtitleAsset> asset;
782 vector<dcp::LanguageTag> lang = film()->subtitle_languages();
783 if (film()->interop()) {
784 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
785 s->set_movie_title (film()->name());
786 if (type == TEXT_OPEN_SUBTITLE) {
787 s->set_language (lang.empty() ? "Unknown" : lang.front().to_string());
788 } else if (!track->language.empty()) {
789 s->set_language (track->language);
791 s->set_reel_number (raw_convert<string> (_reel_index + 1));
794 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
795 s->set_content_title_text (film()->name());
796 s->set_metadata (mxf_metadata());
797 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
798 s->set_language (lang.front());
799 } else if (track && !track->language.empty()) {
800 s->set_language (dcp::LanguageTag(track->language));
802 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
803 s->set_reel_number (_reel_index + 1);
804 s->set_time_code_rate (film()->video_frame_rate());
805 s->set_start_time (dcp::Time ());
806 if (film()->encrypted()) {
807 s->set_key (film()->key());
817 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
819 shared_ptr<dcp::SubtitleAsset> asset;
822 case TEXT_OPEN_SUBTITLE:
823 asset = _subtitle_asset;
825 case TEXT_CLOSED_CAPTION:
826 DCPOMATIC_ASSERT (track);
827 asset = _closed_caption_assets[*track];
830 DCPOMATIC_ASSERT (false);
834 asset = empty_text_asset (type, track);
838 case TEXT_OPEN_SUBTITLE:
839 _subtitle_asset = asset;
841 case TEXT_CLOSED_CAPTION:
842 DCPOMATIC_ASSERT (track);
843 _closed_caption_assets[*track] = asset;
846 DCPOMATIC_ASSERT (false);
849 for (auto i: subs.string) {
850 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
851 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
852 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
853 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
856 for (auto i: subs.bitmap) {
858 shared_ptr<dcp::Subtitle>(
859 new dcp::SubtitleImage(
861 dcp::Time(period.from.seconds() - _period.from.seconds(), film()->video_frame_rate()),
862 dcp::Time(period.to.seconds() - _period.from.seconds(), film()->video_frame_rate()),
863 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
864 dcp::Time(), dcp::Time()
872 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
874 LOG_GENERAL ("Checking existing picture frame %1", frame);
876 /* Read the data from the info file; for 3D we just check the left
877 frames until we find a good one.
879 dcp::FrameInfo const info = read_frame_info (info_file, frame, film()->three_d() ? EYES_LEFT : EYES_BOTH);
883 /* Read the data from the asset and hash it */
884 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
885 ArrayData data (info.size);
886 size_t const read = fread (data.data(), 1, data.size(), asset_file);
887 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
888 if (read != static_cast<size_t> (data.size ())) {
889 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
893 digester.add (data.data(), data.size());
894 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
895 if (digester.get() != info.hash) {
896 LOG_GENERAL ("Existing frame %1 failed hash check", frame);