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"
25 #include "constants.h"
27 #include "dcpomatic_log.h"
30 #include "film_util.h"
32 #include "image_png.h"
35 #include "reel_writer.h"
36 #include <dcp/atmos_asset.h>
37 #include <dcp/atmos_asset_writer.h>
38 #include <dcp/certificate_chain.h>
41 #include <dcp/interop_subtitle_asset.h>
42 #include <dcp/mono_picture_asset.h>
43 #include <dcp/raw_convert.h>
45 #include <dcp/reel_atmos_asset.h>
46 #include <dcp/reel_interop_closed_caption_asset.h>
47 #include <dcp/reel_interop_subtitle_asset.h>
48 #include <dcp/reel_markers_asset.h>
49 #include <dcp/reel_mono_picture_asset.h>
50 #include <dcp/reel_smpte_closed_caption_asset.h>
51 #include <dcp/reel_smpte_subtitle_asset.h>
52 #include <dcp/reel_sound_asset.h>
53 #include <dcp/reel_stereo_picture_asset.h>
54 #include <dcp/smpte_subtitle_asset.h>
55 #include <dcp/sound_asset.h>
56 #include <dcp/sound_asset_writer.h>
57 #include <dcp/stereo_picture_asset.h>
58 #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)
119 , _font_metrics(film()->frame_size().height)
121 /* Create or find our picture asset in a subdirectory, named
122 according to those film's parameters which affect the video
123 output. We will hard-link it into the DCP later.
126 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
128 boost::filesystem::path const asset =
129 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
131 _first_nonexistent_frame = check_existing_picture_asset (asset);
133 if (_first_nonexistent_frame < period.duration().frames_round(film()->video_frame_rate())) {
134 /* We do not have a complete picture asset. If there is an
135 existing asset, break any hard links to it as we are about
136 to change its contents (if only by changing the IDs); see
139 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
141 job->sub (_("Copying old video file"));
142 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
144 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
146 boost::filesystem::remove (asset);
147 boost::filesystem::rename (asset.string() + ".tmp", asset);
151 if (film()->three_d()) {
152 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
154 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
157 _picture_asset->set_size (film()->frame_size());
158 _picture_asset->set_metadata (mxf_metadata());
160 if (film()->encrypted()) {
161 _picture_asset->set_key (film()->key());
162 _picture_asset->set_context_id (film()->context_id());
165 _picture_asset->set_file (asset);
166 _picture_asset_writer = _picture_asset->start_write(asset, _first_nonexistent_frame > 0 ? dcp::PictureAsset::Behaviour::OVERWRITE_EXISTING : dcp::PictureAsset::Behaviour::MAKE_NEW);
167 } else if (!text_only) {
168 /* We already have a complete picture asset that we can just re-use */
169 /* XXX: what about if the encryption key changes? */
170 if (film()->three_d()) {
171 _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
173 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
177 if (film()->audio_channels()) {
178 auto lang = film()->audio_language();
179 _sound_asset = make_shared<dcp::SoundAsset> (
180 dcp::Fraction(film()->video_frame_rate(), 1),
181 film()->audio_frame_rate(),
182 film()->audio_channels(),
183 lang ? *lang : dcp::LanguageTag("en-US"),
187 _sound_asset->set_metadata (mxf_metadata());
189 if (film()->encrypted()) {
190 _sound_asset->set_key (film()->key());
193 DCPOMATIC_ASSERT (film()->directory());
195 std::vector<dcp::Channel> extra_active_channels;
196 auto add_if_mapped = [this, &extra_active_channels](dcp::Channel channel) {
197 if (channel_is_mapped(film(), channel)) {
198 extra_active_channels.push_back(channel);
202 add_if_mapped(dcp::Channel::HI);
203 add_if_mapped(dcp::Channel::VI);
204 add_if_mapped(dcp::Channel::BSL);
205 add_if_mapped(dcp::Channel::BSR);
207 /* Write the sound asset into the film directory so that we leave the creation
208 of the DCP directory until the last minute.
210 _sound_asset_writer = _sound_asset->start_write (
211 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
212 extra_active_channels,
213 film()->contains_atmos_content() ? dcp::SoundAsset::AtmosSync::ENABLED : dcp::SoundAsset::AtmosSync::DISABLED,
214 film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
218 _default_font = dcp::ArrayData(default_font_file());
222 /** @param frame reel-relative frame */
224 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
226 auto handle = film()->info_file_handle(_period, false);
227 handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
228 handle->get().checked_write(&info.offset, sizeof(info.offset));
229 handle->get().checked_write(&info.size, sizeof(info.size));
230 handle->get().checked_write(info.hash.c_str(), info.hash.size());
235 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
237 dcp::FrameInfo frame_info;
238 info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
239 info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
240 info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
242 char hash_buffer[33];
243 info->get().checked_read(hash_buffer, 32);
244 hash_buffer[32] = '\0';
245 frame_info.hash = hash_buffer;
252 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
256 return frame * _info_size;
258 return frame * _info_size * 2;
260 return frame * _info_size * 2 + _info_size;
262 DCPOMATIC_ASSERT (false);
265 DCPOMATIC_ASSERT (false);
270 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
272 auto job = _job.lock ();
275 job->sub (_("Checking existing image data"));
278 /* Try to open the existing asset */
279 dcp::File asset_file(asset, "rb");
281 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
284 LOG_GENERAL ("Opened existing asset at %1", asset.string());
287 shared_ptr<InfoFileHandle> info_file;
290 info_file = film()->info_file_handle (_period, true);
291 } catch (OpenFileError &) {
292 LOG_GENERAL_NC ("Could not open film info file");
296 /* Offset of the last dcp::FrameInfo in the info file */
297 int const n = (boost::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
298 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->get().path()), _info_size);
300 Frame first_nonexistent_frame;
301 if (film()->three_d()) {
302 /* Start looking at the last left frame */
303 first_nonexistent_frame = n / 2;
305 first_nonexistent_frame = n;
308 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
309 --first_nonexistent_frame;
312 if (!film()->three_d() && first_nonexistent_frame > 0) {
313 /* If we are doing 3D we might have found a good L frame with no R, so only
314 do this if we're in 2D and we've just found a good B(oth) frame.
316 ++first_nonexistent_frame;
319 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
321 return first_nonexistent_frame;
326 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
328 if (!_picture_asset_writer) {
329 /* We're not writing any data */
333 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
334 write_frame_info (frame, eyes, fin);
335 _last_written[eyes] = encoded;
340 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
343 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
344 if (film()->encrypted()) {
345 _atmos_asset->set_key(film()->key());
347 _atmos_asset_writer = _atmos_asset->start_write (
348 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
351 _atmos_asset_writer->write (atmos);
356 ReelWriter::fake_write (int size)
358 if (!_picture_asset_writer) {
359 /* We're not writing any data */
363 _picture_asset_writer->fake_write (size);
368 ReelWriter::repeat_write (Frame frame, Eyes eyes)
370 if (!_picture_asset_writer) {
371 /* We're not writing any data */
375 auto fin = _picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size());
376 write_frame_info (frame, eyes, fin);
381 ReelWriter::finish (boost::filesystem::path output_dcp)
383 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
384 /* Nothing was written to the picture asset */
385 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
386 _picture_asset.reset ();
389 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
390 /* Nothing was written to the sound asset */
391 _sound_asset.reset ();
394 /* Hard-link any video asset file into the DCP */
395 if (_picture_asset) {
396 DCPOMATIC_ASSERT (_picture_asset->file());
397 boost::filesystem::path video_from = _picture_asset->file().get();
398 boost::filesystem::path video_to = output_dcp;
399 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
400 /* There may be an existing "to" file if we are recreating a DCP in the same place without
403 boost::system::error_code ec;
404 boost::filesystem::remove (video_to, ec);
406 boost::filesystem::create_hard_link (video_from, video_to, ec);
408 LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
409 auto job = _job.lock ();
411 job->sub (_("Copying video file into DCP"));
413 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
414 } catch (exception& e) {
415 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
416 throw FileError (e.what(), video_from);
419 boost::filesystem::copy_file (video_from, video_to, ec);
421 LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
422 throw FileError (ec.message(), video_from);
427 _picture_asset->set_file (video_to);
430 /* Move the audio asset into the DCP */
432 boost::filesystem::path audio_to = output_dcp;
433 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
436 boost::system::error_code ec;
437 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
440 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
444 _sound_asset->set_file (audio_to);
448 _atmos_asset_writer->finalize ();
449 boost::filesystem::path atmos_to = output_dcp;
450 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
453 boost::system::error_code ec;
454 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
457 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
461 _atmos_asset->set_file (atmos_to);
466 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
467 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
469 template <class Interop, class SMPTE, class Result>
472 shared_ptr<dcp::SubtitleAsset> asset,
473 int64_t picture_duration,
474 shared_ptr<dcp::Reel> reel,
477 optional<string> content_summary,
478 list<ReferencedReelAsset> const & refs,
479 shared_ptr<const Film> film,
480 DCPTimePeriod period,
481 boost::filesystem::path output_dcp,
485 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
487 shared_ptr<Result> reel_asset;
490 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
491 auto directory = output_dcp / interop->id ();
492 boost::filesystem::create_directories (directory);
493 interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
494 reel_asset = make_shared<Interop> (
496 dcp::Fraction(film->video_frame_rate(), 1),
500 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
501 /* All our assets should be the same length; use the picture asset length here
502 as a reference to set the subtitle one. We'll use the duration rather than
503 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
504 just interested in its presentation length.
506 smpte->set_intrinsic_duration(picture_duration);
508 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
510 reel_asset = make_shared<SMPTE> (
512 dcp::Fraction(film->video_frame_rate(), 1),
519 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
521 auto k = dynamic_pointer_cast<Result> (j.asset);
522 if (k && j.period == period) {
524 /* If we have a hash for this asset in the CPL, assume that it is correct */
526 k->asset_ref()->set_hash (k->hash().get());
533 if (!text_only && reel_asset->actual_duration() != period_duration) {
534 throw ProgrammingError (
536 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
539 reel->add (reel_asset);
546 shared_ptr<dcp::ReelPictureAsset>
547 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
549 shared_ptr<dcp::ReelPictureAsset> reel_asset;
551 if (_picture_asset) {
552 /* We have made a picture asset of our own. Put it into the reel */
553 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
555 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
558 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
560 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
563 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
564 /* We don't have a picture asset of our own; hopefully we have one to reference */
566 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
568 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
570 if (k && j.period == _period) {
576 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
578 DCPOMATIC_ASSERT (reel_asset);
579 if (reel_asset->duration() != period_duration) {
580 throw ProgrammingError (
582 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
585 reel->add (reel_asset);
587 /* If we have a hash for this asset in the CPL, assume that it is correct */
588 if (reel_asset->hash()) {
589 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
597 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
599 shared_ptr<dcp::ReelSoundAsset> reel_asset;
602 /* We have made a sound asset of our own. Put it into the reel */
603 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
605 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
606 /* We don't have a sound asset of our own; hopefully we have one to reference */
608 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
610 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
612 if (k && j.period == _period) {
614 /* If we have a hash for this asset in the CPL, assume that it is correct */
616 k->asset_ref()->set_hash (k->hash().get());
622 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
624 DCPOMATIC_ASSERT (reel_asset);
625 if (reel_asset->actual_duration() != period_duration) {
627 "Reel sound asset has length %1 but reel period is %2",
628 reel_asset->actual_duration(),
631 if (reel_asset->actual_duration() != period_duration) {
632 throw ProgrammingError (
634 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
639 reel->add (reel_asset);
644 ReelWriter::create_reel_text (
645 shared_ptr<dcp::Reel> reel,
646 list<ReferencedReelAsset> const & refs,
648 boost::filesystem::path output_dcp,
649 bool ensure_subtitles,
650 set<DCPTextTrack> ensure_closed_captions
653 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
654 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
657 if (!subtitle && ensure_subtitles) {
658 /* We had no subtitle asset, but we've been asked to make sure there is one */
659 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
660 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
675 /* We have a subtitle asset that we either made or are referencing */
676 if (auto main_language = film()->subtitle_languages().first) {
677 subtitle->set_language (*main_language);
681 for (auto const& i: _closed_caption_assets) {
682 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
683 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
685 DCPOMATIC_ASSERT (a);
686 a->set_annotation_text (i.first.name);
687 if (i.first.language) {
688 a->set_language (i.first.language.get());
691 ensure_closed_captions.erase (i.first);
694 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
695 for (auto i: ensure_closed_captions) {
696 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
697 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
709 DCPOMATIC_ASSERT (a);
710 a->set_annotation_text (i.name);
712 a->set_language (i.language.get());
719 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
721 auto markers = film()->markers();
722 film()->add_ffoc_lfoc(markers);
723 Film::Markers reel_markers;
724 for (auto const& i: markers) {
725 if (_period.contains(i.second)) {
726 reel_markers[i.first] = i.second;
730 if (!reel_markers.empty ()) {
731 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
732 for (auto const& i: reel_markers) {
733 DCPTime relative = i.second - _period.from;
734 auto hmsf = relative.split (film()->video_frame_rate());
735 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
742 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
743 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
745 shared_ptr<dcp::Reel>
746 ReelWriter::create_reel (
747 list<ReferencedReelAsset> const & refs,
748 boost::filesystem::path output_dcp,
749 bool ensure_subtitles,
750 set<DCPTextTrack> ensure_closed_captions
753 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
755 auto reel = make_shared<dcp::Reel>();
757 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
758 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
759 * how big they are, we don't care about that.
761 int64_t duration = 0;
763 auto reel_picture_asset = create_reel_picture (reel, refs);
764 duration = reel_picture_asset->actual_duration ();
765 create_reel_sound (reel, refs);
766 if (!film()->interop()) {
767 create_reel_markers(reel);
771 create_reel_text(reel, refs, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
774 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
781 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
784 if (_picture_asset) {
785 _picture_asset->hash (set_progress);
789 _sound_asset->hash (set_progress);
793 _atmos_asset->hash (set_progress);
795 } catch (boost::thread_interrupted) {
796 /* set_progress contains an interruption_point, so any of these methods
797 * may throw thread_interrupted, at which point we just give up.
803 ReelWriter::start () const
805 return _period.from.frames_floor (film()->video_frame_rate());
810 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
812 if (!_sound_asset_writer) {
816 DCPOMATIC_ASSERT (audio);
817 _sound_asset_writer->write(audio->data(), audio->channels(), audio->frames());
821 shared_ptr<dcp::SubtitleAsset>
822 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
824 shared_ptr<dcp::SubtitleAsset> asset;
825 optional<string> font;
827 auto lang = film()->subtitle_languages();
828 if (film()->interop()) {
829 auto s = make_shared<dcp::InteropSubtitleAsset>();
830 s->set_movie_title (film()->name());
831 if (type == TextType::OPEN_SUBTITLE) {
832 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
833 } else if (track->language) {
834 s->set_language (track->language->to_string());
836 s->set_reel_number (raw_convert<string> (_reel_index + 1));
839 auto s = make_shared<dcp::SMPTESubtitleAsset>();
840 s->set_content_title_text (film()->name());
841 s->set_metadata (mxf_metadata());
842 if (type == TextType::OPEN_SUBTITLE && lang.first) {
843 s->set_language (*lang.first);
844 } else if (track && track->language) {
845 s->set_language (dcp::LanguageTag(track->language->to_string()));
847 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
848 s->set_reel_number (_reel_index + 1);
849 s->set_time_code_rate (film()->video_frame_rate());
850 s->set_start_time (dcp::Time ());
851 if (film()->encrypted()) {
852 s->set_key (film()->key());
859 std::make_shared<dcp::SubtitleString>(
867 dcp::Time(0, 0, 0, 0, 24),
868 dcp::Time(0, 0, 1, 0, 24),
884 if (!film()->interop()) {
885 /* We must have a LoadFont since we have a Text */
887 asset->ensure_font(*font, _default_font);
896 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const
898 if (dcp::uses_baseline(subtitle.valign_standard) == dcp::uses_baseline(to)) {
899 /* The from and to standards use the same alignment reference */
900 return subtitle.v_position();
903 auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
904 auto const height = _font_metrics.height(subtitle);
906 float correction = 0;
907 switch (subtitle.v_align()) {
908 case dcp::VAlign::TOP:
909 correction = height - baseline_to_bottom;
911 case dcp::VAlign::CENTER:
912 correction = (height / 2) - baseline_to_bottom;
914 case dcp::VAlign::BOTTOM:
915 correction = baseline_to_bottom;
919 return subtitle.v_position() + (dcp::uses_bounding_box(subtitle.valign_standard) ? correction : -correction);
924 ReelWriter::write(PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts, shared_ptr<dcpomatic::Font> chosen_interop_font)
926 shared_ptr<dcp::SubtitleAsset> asset;
929 case TextType::OPEN_SUBTITLE:
930 asset = _subtitle_asset;
932 case TextType::CLOSED_CAPTION:
933 DCPOMATIC_ASSERT (track);
934 asset = _closed_caption_assets[*track];
937 DCPOMATIC_ASSERT (false);
941 asset = empty_text_asset (type, track, false);
945 case TextType::OPEN_SUBTITLE:
946 _subtitle_asset = asset;
948 case TextType::CLOSED_CAPTION:
949 DCPOMATIC_ASSERT (track);
950 _closed_caption_assets[*track] = asset;
953 DCPOMATIC_ASSERT (false);
956 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
957 auto const tcr = 1000;
959 for (auto i: subs.string) {
960 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
961 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
962 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::SubtitleStandard::INTEROP : dcp::SubtitleStandard::SMPTE_2014));
963 auto sub = make_shared<dcp::SubtitleString>(i);
964 /* i.font is a shared_ptr<Font> which uniquely identifies the font we want,
965 * though if we are Interop we can only have one font, so we'll use the chosen
968 auto font = film()->interop() ? chosen_interop_font : i.font;
969 /* We can get the corresponding ID from fonts */
970 auto const font_id_to_use = fonts.get(font);
971 /* Give this subtitle the correct font ID */
972 sub->set_font(font_id_to_use);
974 /* Make sure the asset LoadFonts the font we just asked for */
975 asset->ensure_font(font_id_to_use, font->data().get_value_or(_default_font));
978 for (auto i: subs.bitmap) {
980 make_shared<dcp::SubtitleImage>(
981 image_as_png(i.image),
982 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
983 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
984 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
985 dcp::Time(), dcp::Time()
993 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
995 LOG_GENERAL ("Checking existing picture frame %1", frame);
997 /* Read the data from the info file; for 3D we just check the left
998 frames until we find a good one.
1000 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
1004 /* Read the data from the asset and hash it */
1005 asset_file.seek(info.offset, SEEK_SET);
1006 ArrayData data (info.size);
1007 size_t const read = asset_file.read(data.data(), 1, data.size());
1008 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
1009 if (read != static_cast<size_t> (data.size ())) {
1010 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
1014 digester.add (data.data(), data.size());
1015 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
1016 if (digester.get() != info.hash) {
1017 LOG_GENERAL ("Existing frame %1 failed hash check", frame);