2 Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 DCP-o-matic is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
22 #include "audio_buffers.h"
23 #include "compose.hpp"
26 #include "dcpomatic_log.h"
29 #include "font_data.h"
31 #include "image_png.h"
34 #include "reel_writer.h"
35 #include <dcp/atmos_asset.h>
36 #include <dcp/atmos_asset_writer.h>
37 #include <dcp/certificate_chain.h>
40 #include <dcp/interop_subtitle_asset.h>
41 #include <dcp/mono_picture_asset.h>
42 #include <dcp/raw_convert.h>
44 #include <dcp/reel_atmos_asset.h>
45 #include <dcp/reel_interop_closed_caption_asset.h>
46 #include <dcp/reel_interop_subtitle_asset.h>
47 #include <dcp/reel_markers_asset.h>
48 #include <dcp/reel_mono_picture_asset.h>
49 #include <dcp/reel_smpte_closed_caption_asset.h>
50 #include <dcp/reel_smpte_subtitle_asset.h>
51 #include <dcp/reel_sound_asset.h>
52 #include <dcp/reel_stereo_picture_asset.h>
53 #include <dcp/smpte_subtitle_asset.h>
54 #include <dcp/sound_asset.h>
55 #include <dcp/sound_asset_writer.h>
56 #include <dcp/stereo_picture_asset.h>
57 #include <dcp/subtitle_image.h>
63 using std::dynamic_pointer_cast;
66 using std::make_shared;
69 using std::shared_ptr;
73 using boost::optional;
74 #if BOOST_VERSION >= 106100
75 using namespace boost::placeholders;
79 using dcp::raw_convert;
80 using namespace dcpomatic;
83 int const ReelWriter::_info_size = 48;
86 static dcp::MXFMetadata
89 dcp::MXFMetadata meta;
90 auto config = Config::instance();
91 if (!config->dcp_company_name().empty()) {
92 meta.company_name = config->dcp_company_name ();
94 if (!config->dcp_product_name().empty()) {
95 meta.product_name = config->dcp_product_name ();
97 if (!config->dcp_product_version().empty()) {
98 meta.product_version = config->dcp_product_version ();
104 /** @param job Related job, or 0.
105 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
106 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
107 * subtitle / closed caption files.
109 ReelWriter::ReelWriter (
110 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
112 : WeakConstFilm (weak_film)
114 , _reel_index (reel_index)
115 , _reel_count (reel_count)
116 , _content_summary (film()->content_summary(period))
118 , _text_only (text_only)
120 /* Create or find our picture asset in a subdirectory, named
121 according to those film's parameters which affect the video
122 output. We will hard-link it into the DCP later.
125 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
127 boost::filesystem::path const asset =
128 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
130 _first_nonexistant_frame = check_existing_picture_asset (asset);
132 if (_first_nonexistant_frame < period.duration().frames_round(film()->video_frame_rate())) {
133 /* We do not have a complete picture asset. If there is an
134 existing asset, break any hard links to it as we are about
135 to change its contents (if only by changing the IDs); see
138 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
140 job->sub (_("Copying old video file"));
141 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
143 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
145 boost::filesystem::remove (asset);
146 boost::filesystem::rename (asset.string() + ".tmp", asset);
150 if (film()->three_d()) {
151 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
153 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
156 _picture_asset->set_size (film()->frame_size());
157 _picture_asset->set_metadata (mxf_metadata());
159 if (film()->encrypted()) {
160 _picture_asset->set_key (film()->key());
161 _picture_asset->set_context_id (film()->context_id());
164 _picture_asset->set_file (asset);
165 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
166 } else if (!text_only) {
167 /* We already have a complete picture asset that we can just re-use */
168 /* XXX: what about if the encryption key changes? */
169 if (film()->three_d()) {
170 _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
172 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
176 if (film()->audio_channels()) {
177 auto lang = film()->audio_language();
178 _sound_asset = make_shared<dcp::SoundAsset> (
179 dcp::Fraction(film()->video_frame_rate(), 1),
180 film()->audio_frame_rate(),
181 film()->audio_channels(),
182 lang ? *lang : dcp::LanguageTag("en-US"),
186 _sound_asset->set_metadata (mxf_metadata());
188 if (film()->encrypted()) {
189 _sound_asset->set_key (film()->key());
192 DCPOMATIC_ASSERT (film()->directory());
194 /* Write the sound asset into the film directory so that we leave the creation
195 of the DCP directory until the last minute.
197 _sound_asset_writer = _sound_asset->start_write (
198 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
199 film()->contains_atmos_content()
203 _default_font = dcp::ArrayData(default_font_file());
207 /** @param frame reel-relative frame */
209 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
211 auto handle = film()->info_file_handle(_period, false);
212 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
213 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
214 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
215 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
220 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
222 dcp::FrameInfo frame_info;
223 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
224 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
225 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
227 char hash_buffer[33];
228 checked_fread (hash_buffer, 32, info->get(), info->file());
229 hash_buffer[32] = '\0';
230 frame_info.hash = hash_buffer;
237 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
241 return frame * _info_size;
243 return frame * _info_size * 2;
245 return frame * _info_size * 2 + _info_size;
247 DCPOMATIC_ASSERT (false);
250 DCPOMATIC_ASSERT (false);
255 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
257 auto job = _job.lock ();
260 job->sub (_("Checking existing image data"));
263 /* Try to open the existing asset */
264 auto asset_file = fopen_boost (asset, "rb");
266 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
269 LOG_GENERAL ("Opened existing asset at %1", asset.string());
272 shared_ptr<InfoFileHandle> info_file;
275 info_file = film()->info_file_handle (_period, true);
276 } catch (OpenFileError &) {
277 LOG_GENERAL_NC ("Could not open film info file");
282 /* Offset of the last dcp::FrameInfo in the info file */
283 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
284 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
286 Frame first_nonexistant_frame;
287 if (film()->three_d()) {
288 /* Start looking at the last left frame */
289 first_nonexistant_frame = n / 2;
291 first_nonexistant_frame = n;
294 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
295 --first_nonexistant_frame;
298 if (!film()->three_d() && first_nonexistant_frame > 0) {
299 /* If we are doing 3D we might have found a good L frame with no R, so only
300 do this if we're in 2D and we've just found a good B(oth) frame.
302 ++first_nonexistant_frame;
305 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
309 return first_nonexistant_frame;
314 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
316 if (!_picture_asset_writer) {
317 /* We're not writing any data */
321 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
322 write_frame_info (frame, eyes, fin);
323 _last_written[static_cast<int>(eyes)] = encoded;
328 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
331 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
332 if (film()->encrypted()) {
333 _atmos_asset->set_key(film()->key());
335 _atmos_asset_writer = _atmos_asset->start_write (
336 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
339 _atmos_asset_writer->write (atmos);
344 ReelWriter::fake_write (int size)
346 if (!_picture_asset_writer) {
347 /* We're not writing any data */
351 _picture_asset_writer->fake_write (size);
356 ReelWriter::repeat_write (Frame frame, Eyes eyes)
358 if (!_picture_asset_writer) {
359 /* We're not writing any data */
363 auto fin = _picture_asset_writer->write (
364 _last_written[static_cast<int>(eyes)]->data(),
365 _last_written[static_cast<int>(eyes)]->size()
367 write_frame_info (frame, eyes, fin);
372 ReelWriter::finish (boost::filesystem::path output_dcp)
374 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
375 /* Nothing was written to the picture asset */
376 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
377 _picture_asset.reset ();
380 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
381 /* Nothing was written to the sound asset */
382 _sound_asset.reset ();
385 /* Hard-link any video asset file into the DCP */
386 if (_picture_asset) {
387 DCPOMATIC_ASSERT (_picture_asset->file());
388 boost::filesystem::path video_from = _picture_asset->file().get();
389 boost::filesystem::path video_to = output_dcp;
390 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
391 /* There may be an existing "to" file if we are recreating a DCP in the same place without
394 boost::system::error_code ec;
395 boost::filesystem::remove (video_to, ec);
397 boost::filesystem::create_hard_link (video_from, video_to, ec);
399 LOG_WARNING_NC ("Hard-link failed; copying instead");
400 auto job = _job.lock ();
402 job->sub (_("Copying video file into DCP"));
404 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
405 } catch (exception& e) {
406 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
407 throw FileError (e.what(), video_from);
410 boost::filesystem::copy_file (video_from, video_to, ec);
412 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
413 throw FileError (ec.message(), video_from);
418 _picture_asset->set_file (video_to);
421 /* Move the audio asset into the DCP */
423 boost::filesystem::path audio_to = output_dcp;
424 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
427 boost::system::error_code ec;
428 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
431 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
435 _sound_asset->set_file (audio_to);
439 _atmos_asset_writer->finalize ();
440 boost::filesystem::path atmos_to = output_dcp;
441 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
444 boost::system::error_code ec;
445 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
448 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
452 _atmos_asset->set_file (atmos_to);
457 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
458 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
460 template <class Interop, class SMPTE, class Result>
463 shared_ptr<dcp::SubtitleAsset> asset,
464 int64_t picture_duration,
465 shared_ptr<dcp::Reel> reel,
466 list<ReferencedReelAsset> const & refs,
467 vector<FontData> const & fonts,
468 dcp::ArrayData default_font,
469 shared_ptr<const Film> film,
470 DCPTimePeriod period,
471 boost::filesystem::path output_dcp,
475 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
477 shared_ptr<Result> reel_asset;
480 /* Add the font to the subtitle content */
481 for (auto const& j: fonts) {
482 asset->add_font (j.id, j.data.get_value_or(default_font));
485 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
486 auto directory = output_dcp / interop->id ();
487 boost::filesystem::create_directories (directory);
488 interop->write (directory / ("sub_" + interop->id() + ".xml"));
489 reel_asset = make_shared<Interop> (
491 dcp::Fraction(film->video_frame_rate(), 1),
495 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
496 /* All our assets should be the same length; use the picture asset length here
497 as a reference to set the subtitle one. We'll use the duration rather than
498 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
499 just interested in its presentation length.
501 smpte->set_intrinsic_duration(picture_duration);
503 output_dcp / ("sub_" + asset->id() + ".mxf")
505 reel_asset = make_shared<SMPTE> (
507 dcp::Fraction(film->video_frame_rate(), 1),
514 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
516 auto k = dynamic_pointer_cast<Result> (j.asset);
517 if (k && j.period == period) {
519 /* If we have a hash for this asset in the CPL, assume that it is correct */
521 k->asset_ref()->set_hash (k->hash().get());
528 if (!text_only && reel_asset->actual_duration() != period_duration) {
529 throw ProgrammingError (
531 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
534 reel->add (reel_asset);
541 shared_ptr<dcp::ReelPictureAsset>
542 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
544 shared_ptr<dcp::ReelPictureAsset> reel_asset;
546 if (_picture_asset) {
547 /* We have made a picture asset of our own. Put it into the reel */
548 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
550 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
553 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
555 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
558 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
559 /* We don't have a picture asset of our own; hopefully we have one to reference */
561 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
563 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
565 if (k && j.period == _period) {
571 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
573 DCPOMATIC_ASSERT (reel_asset);
574 if (reel_asset->duration() != period_duration) {
575 throw ProgrammingError (
577 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
580 reel->add (reel_asset);
582 /* If we have a hash for this asset in the CPL, assume that it is correct */
583 if (reel_asset->hash()) {
584 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
592 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
594 shared_ptr<dcp::ReelSoundAsset> reel_asset;
597 /* We have made a sound asset of our own. Put it into the reel */
598 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
600 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
601 /* We don't have a sound asset of our own; hopefully we have one to reference */
603 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
605 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
607 if (k && j.period == _period) {
609 /* If we have a hash for this asset in the CPL, assume that it is correct */
611 k->asset_ref()->set_hash (k->hash().get());
617 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
619 DCPOMATIC_ASSERT (reel_asset);
620 if (reel_asset->actual_duration() != period_duration) {
622 "Reel sound asset has length %1 but reel period is %2",
623 reel_asset->actual_duration(),
626 if (reel_asset->actual_duration() != period_duration) {
627 throw ProgrammingError (
629 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
634 reel->add (reel_asset);
639 ReelWriter::create_reel_text (
640 shared_ptr<dcp::Reel> reel,
641 list<ReferencedReelAsset> const & refs,
642 vector<FontData> const& fonts,
644 boost::filesystem::path output_dcp,
645 bool ensure_subtitles,
646 set<DCPTextTrack> ensure_closed_captions
649 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
650 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
654 /* We have a subtitle asset that we either made or are referencing */
655 if (auto main_language = film()->subtitle_languages().first) {
656 subtitle->set_language (*main_language);
658 } else if (ensure_subtitles) {
659 /* We had no subtitle asset, but we've been asked to make sure there is one */
660 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
661 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
674 for (auto const& i: _closed_caption_assets) {
675 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
676 i.second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
678 DCPOMATIC_ASSERT (a);
679 a->set_annotation_text (i.first.name);
680 if (i.first.language) {
681 a->set_language (i.first.language.get());
684 ensure_closed_captions.erase (i.first);
687 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
688 for (auto i: ensure_closed_captions) {
689 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
690 empty_text_asset(TextType::CLOSED_CAPTION, i, true), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
692 DCPOMATIC_ASSERT (a);
693 a->set_annotation_text (i.name);
695 a->set_language (i.language.get());
702 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
704 auto markers = film()->markers();
705 film()->add_ffoc_lfoc(markers);
706 Film::Markers reel_markers;
707 for (auto const& i: markers) {
708 if (_period.contains(i.second)) {
709 reel_markers[i.first] = i.second;
713 if (!reel_markers.empty ()) {
714 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration(), 0);
715 for (auto const& i: reel_markers) {
716 DCPTime relative = i.second - _period.from;
717 auto hmsf = relative.split (film()->video_frame_rate());
718 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
725 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
726 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
728 shared_ptr<dcp::Reel>
729 ReelWriter::create_reel (
730 list<ReferencedReelAsset> const & refs,
731 vector<FontData> const & fonts,
732 boost::filesystem::path output_dcp,
733 bool ensure_subtitles,
734 set<DCPTextTrack> ensure_closed_captions
737 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
739 auto reel = make_shared<dcp::Reel>();
741 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
742 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
743 * how big they are, we don't care about that.
745 int64_t duration = 0;
747 auto reel_picture_asset = create_reel_picture (reel, refs);
748 duration = reel_picture_asset->actual_duration ();
749 create_reel_sound (reel, refs);
750 create_reel_markers (reel);
753 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
756 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
764 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
767 if (_picture_asset) {
768 _picture_asset->hash (set_progress);
772 _sound_asset->hash (set_progress);
776 _atmos_asset->hash (set_progress);
778 } catch (boost::thread_interrupted) {
779 /* set_progress contains an interruption_point, so any of these methods
780 * may throw thread_interrupted, at which point we just give up.
786 ReelWriter::start () const
788 return _period.from.frames_floor (film()->video_frame_rate());
793 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
795 if (!_sound_asset_writer) {
799 DCPOMATIC_ASSERT (audio);
800 _sound_asset_writer->write (audio->data(), audio->frames());
804 shared_ptr<dcp::SubtitleAsset>
805 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
807 shared_ptr<dcp::SubtitleAsset> asset;
809 auto lang = film()->subtitle_languages();
810 if (film()->interop()) {
811 auto s = make_shared<dcp::InteropSubtitleAsset>();
812 s->set_movie_title (film()->name());
813 if (type == TextType::OPEN_SUBTITLE) {
814 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
815 } else if (track->language) {
816 s->set_language (track->language->to_string());
818 s->set_reel_number (raw_convert<string> (_reel_index + 1));
821 auto s = make_shared<dcp::SMPTESubtitleAsset>();
822 s->set_content_title_text (film()->name());
823 s->set_metadata (mxf_metadata());
824 if (type == TextType::OPEN_SUBTITLE && lang.first) {
825 s->set_language (*lang.first);
826 } else if (track && track->language) {
827 s->set_language (dcp::LanguageTag(track->language->to_string()));
829 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
830 s->set_reel_number (_reel_index + 1);
831 s->set_time_code_rate (film()->video_frame_rate());
832 s->set_start_time (dcp::Time ());
833 if (film()->encrypted()) {
834 s->set_key (film()->key());
838 std::make_shared<dcp::SubtitleString>(
839 optional<std::string>(),
846 dcp::Time(0, 0, 0, 0, 24),
847 dcp::Time(0, 0, 1, 0, 24),
870 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
872 shared_ptr<dcp::SubtitleAsset> asset;
875 case TextType::OPEN_SUBTITLE:
876 asset = _subtitle_asset;
878 case TextType::CLOSED_CAPTION:
879 DCPOMATIC_ASSERT (track);
880 asset = _closed_caption_assets[*track];
883 DCPOMATIC_ASSERT (false);
887 asset = empty_text_asset (type, track, false);
891 case TextType::OPEN_SUBTITLE:
892 _subtitle_asset = asset;
894 case TextType::CLOSED_CAPTION:
895 DCPOMATIC_ASSERT (track);
896 _closed_caption_assets[*track] = asset;
899 DCPOMATIC_ASSERT (false);
902 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
903 auto const tcr = 1000;
905 for (auto i: subs.string) {
906 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
907 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
908 asset->add (make_shared<dcp::SubtitleString>(i));
911 for (auto i: subs.bitmap) {
913 make_shared<dcp::SubtitleImage>(
914 image_as_png(i.image),
915 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
916 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
917 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
918 dcp::Time(), dcp::Time()
926 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
928 LOG_GENERAL ("Checking existing picture frame %1", frame);
930 /* Read the data from the info file; for 3D we just check the left
931 frames until we find a good one.
933 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
937 /* Read the data from the asset and hash it */
938 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
939 ArrayData data (info.size);
940 size_t const read = fread (data.data(), 1, data.size(), asset_file);
941 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
942 if (read != static_cast<size_t> (data.size ())) {
943 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
947 digester.add (data.data(), data.size());
948 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
949 if (digester.get() != info.hash) {
950 LOG_GENERAL ("Existing frame %1 failed hash check", frame);