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 /* Always make 16-channel sound assets for SMPTE; libdcp will pad unused channels for us */
183 standard == dcp::Standard::SMPTE ? MAX_DCP_AUDIO_CHANNELS : film()->audio_channels(),
184 lang ? *lang : dcp::LanguageTag("en-US"),
188 _sound_asset->set_metadata (mxf_metadata());
190 if (film()->encrypted()) {
191 _sound_asset->set_key (film()->key());
194 DCPOMATIC_ASSERT (film()->directory());
196 std::vector<dcp::Channel> extra_active_channels;
197 auto add_if_mapped = [this, &extra_active_channels](dcp::Channel channel) {
198 if (channel_is_mapped(film(), channel)) {
199 extra_active_channels.push_back(channel);
203 add_if_mapped(dcp::Channel::HI);
204 add_if_mapped(dcp::Channel::VI);
205 add_if_mapped(dcp::Channel::BSL);
206 add_if_mapped(dcp::Channel::BSR);
208 /* Write the sound asset into the film directory so that we leave the creation
209 of the DCP directory until the last minute.
211 _sound_asset_writer = _sound_asset->start_write (
212 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
213 extra_active_channels,
214 film()->contains_atmos_content() ? dcp::SoundAsset::AtmosSync::ENABLED : dcp::SoundAsset::AtmosSync::DISABLED,
215 film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
219 _default_font = dcp::ArrayData(default_font_file());
223 /** @param frame reel-relative frame */
225 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
227 auto handle = film()->info_file_handle(_period, false);
228 handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
229 handle->get().checked_write(&info.offset, sizeof(info.offset));
230 handle->get().checked_write(&info.size, sizeof(info.size));
231 handle->get().checked_write(info.hash.c_str(), info.hash.size());
236 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
238 dcp::FrameInfo frame_info;
239 info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
240 info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
241 info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
243 char hash_buffer[33];
244 info->get().checked_read(hash_buffer, 32);
245 hash_buffer[32] = '\0';
246 frame_info.hash = hash_buffer;
253 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
257 return frame * _info_size;
259 return frame * _info_size * 2;
261 return frame * _info_size * 2 + _info_size;
263 DCPOMATIC_ASSERT (false);
266 DCPOMATIC_ASSERT (false);
271 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
273 auto job = _job.lock ();
276 job->sub (_("Checking existing image data"));
279 /* Try to open the existing asset */
280 dcp::File asset_file(asset, "rb");
282 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
285 LOG_GENERAL ("Opened existing asset at %1", asset.string());
288 shared_ptr<InfoFileHandle> info_file;
291 info_file = film()->info_file_handle (_period, true);
292 } catch (OpenFileError &) {
293 LOG_GENERAL_NC ("Could not open film info file");
297 /* Offset of the last dcp::FrameInfo in the info file */
298 int const n = (boost::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
299 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);
301 Frame first_nonexistent_frame;
302 if (film()->three_d()) {
303 /* Start looking at the last left frame */
304 first_nonexistent_frame = n / 2;
306 first_nonexistent_frame = n;
309 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
310 --first_nonexistent_frame;
313 if (!film()->three_d() && first_nonexistent_frame > 0) {
314 /* If we are doing 3D we might have found a good L frame with no R, so only
315 do this if we're in 2D and we've just found a good B(oth) frame.
317 ++first_nonexistent_frame;
320 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
322 return first_nonexistent_frame;
327 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
329 if (!_picture_asset_writer) {
330 /* We're not writing any data */
334 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
335 write_frame_info (frame, eyes, fin);
336 _last_written[eyes] = encoded;
341 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
344 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
345 if (film()->encrypted()) {
346 _atmos_asset->set_key(film()->key());
348 _atmos_asset_writer = _atmos_asset->start_write (
349 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
352 _atmos_asset_writer->write (atmos);
357 ReelWriter::fake_write (int size)
359 if (!_picture_asset_writer) {
360 /* We're not writing any data */
364 _picture_asset_writer->fake_write (size);
369 ReelWriter::repeat_write (Frame frame, Eyes eyes)
371 if (!_picture_asset_writer) {
372 /* We're not writing any data */
376 auto fin = _picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size());
377 write_frame_info (frame, eyes, fin);
382 ReelWriter::finish (boost::filesystem::path output_dcp)
384 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
385 /* Nothing was written to the picture asset */
386 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
387 _picture_asset.reset ();
390 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
391 /* Nothing was written to the sound asset */
392 _sound_asset.reset ();
395 /* Hard-link any video asset file into the DCP */
396 if (_picture_asset) {
397 DCPOMATIC_ASSERT (_picture_asset->file());
398 boost::filesystem::path video_from = _picture_asset->file().get();
399 boost::filesystem::path video_to = output_dcp;
400 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
401 /* There may be an existing "to" file if we are recreating a DCP in the same place without
404 boost::system::error_code ec;
405 boost::filesystem::remove (video_to, ec);
407 boost::filesystem::create_hard_link (video_from, video_to, ec);
409 LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
410 auto job = _job.lock ();
412 job->sub (_("Copying video file into DCP"));
414 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
415 } catch (exception& e) {
416 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
417 throw FileError (e.what(), video_from);
420 boost::filesystem::copy_file (video_from, video_to, ec);
422 LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
423 throw FileError (ec.message(), video_from);
428 _picture_asset->set_file (video_to);
431 /* Move the audio asset into the DCP */
433 boost::filesystem::path audio_to = output_dcp;
434 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
437 boost::system::error_code ec;
438 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
441 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
445 _sound_asset->set_file (audio_to);
449 _atmos_asset_writer->finalize ();
450 boost::filesystem::path atmos_to = output_dcp;
451 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
454 boost::system::error_code ec;
455 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
458 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
462 _atmos_asset->set_file (atmos_to);
467 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
468 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
470 template <class Interop, class SMPTE, class Result>
473 shared_ptr<dcp::SubtitleAsset> asset,
474 int64_t picture_duration,
475 shared_ptr<dcp::Reel> reel,
478 optional<string> content_summary,
479 list<ReferencedReelAsset> const & refs,
480 shared_ptr<const Film> film,
481 DCPTimePeriod period,
482 boost::filesystem::path output_dcp,
486 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
488 shared_ptr<Result> reel_asset;
491 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
492 auto directory = output_dcp / interop->id ();
493 boost::filesystem::create_directories (directory);
494 interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
495 reel_asset = make_shared<Interop> (
497 dcp::Fraction(film->video_frame_rate(), 1),
501 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
502 /* All our assets should be the same length; use the picture asset length here
503 as a reference to set the subtitle one. We'll use the duration rather than
504 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
505 just interested in its presentation length.
507 smpte->set_intrinsic_duration(picture_duration);
509 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
511 reel_asset = make_shared<SMPTE> (
513 dcp::Fraction(film->video_frame_rate(), 1),
520 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
522 auto k = dynamic_pointer_cast<Result> (j.asset);
523 if (k && j.period == period) {
525 /* If we have a hash for this asset in the CPL, assume that it is correct */
527 k->asset_ref()->set_hash (k->hash().get());
534 if (!text_only && reel_asset->actual_duration() != period_duration) {
535 throw ProgrammingError (
537 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
540 reel->add (reel_asset);
547 shared_ptr<dcp::ReelPictureAsset>
548 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
550 shared_ptr<dcp::ReelPictureAsset> reel_asset;
552 if (_picture_asset) {
553 /* We have made a picture asset of our own. Put it into the reel */
554 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
556 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
559 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
561 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
564 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
565 /* We don't have a picture asset of our own; hopefully we have one to reference */
567 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
569 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
571 if (k && j.period == _period) {
577 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
579 DCPOMATIC_ASSERT (reel_asset);
580 if (reel_asset->duration() != period_duration) {
581 throw ProgrammingError (
583 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
586 reel->add (reel_asset);
588 /* If we have a hash for this asset in the CPL, assume that it is correct */
589 if (reel_asset->hash()) {
590 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
598 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
600 shared_ptr<dcp::ReelSoundAsset> reel_asset;
603 /* We have made a sound asset of our own. Put it into the reel */
604 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
606 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
607 /* We don't have a sound asset of our own; hopefully we have one to reference */
609 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
611 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
613 if (k && j.period == _period) {
615 /* If we have a hash for this asset in the CPL, assume that it is correct */
617 k->asset_ref()->set_hash (k->hash().get());
623 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
625 DCPOMATIC_ASSERT (reel_asset);
626 if (reel_asset->actual_duration() != period_duration) {
628 "Reel sound asset has length %1 but reel period is %2",
629 reel_asset->actual_duration(),
632 if (reel_asset->actual_duration() != period_duration) {
633 throw ProgrammingError (
635 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
640 reel->add (reel_asset);
645 ReelWriter::create_reel_text (
646 shared_ptr<dcp::Reel> reel,
647 list<ReferencedReelAsset> const & refs,
649 boost::filesystem::path output_dcp,
650 bool ensure_subtitles,
651 set<DCPTextTrack> ensure_closed_captions
654 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
655 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
658 if (!subtitle && 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),
676 /* We have a subtitle asset that we either made or are referencing */
677 if (auto main_language = film()->subtitle_languages().first) {
678 subtitle->set_language (*main_language);
682 for (auto const& i: _closed_caption_assets) {
683 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
684 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
686 DCPOMATIC_ASSERT (a);
687 a->set_annotation_text (i.first.name);
688 if (i.first.language) {
689 a->set_language (i.first.language.get());
692 ensure_closed_captions.erase (i.first);
695 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
696 for (auto i: ensure_closed_captions) {
697 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
698 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
710 DCPOMATIC_ASSERT (a);
711 a->set_annotation_text (i.name);
713 a->set_language (i.language.get());
720 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
722 auto markers = film()->markers();
723 film()->add_ffoc_lfoc(markers);
724 Film::Markers reel_markers;
725 for (auto const& i: markers) {
726 if (_period.contains(i.second)) {
727 reel_markers[i.first] = i.second;
731 if (!reel_markers.empty ()) {
732 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
733 for (auto const& i: reel_markers) {
734 DCPTime relative = i.second - _period.from;
735 auto hmsf = relative.split (film()->video_frame_rate());
736 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
743 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
744 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
746 shared_ptr<dcp::Reel>
747 ReelWriter::create_reel (
748 list<ReferencedReelAsset> const & refs,
749 boost::filesystem::path output_dcp,
750 bool ensure_subtitles,
751 set<DCPTextTrack> ensure_closed_captions
754 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
756 auto reel = make_shared<dcp::Reel>();
758 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
759 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
760 * how big they are, we don't care about that.
762 int64_t duration = 0;
764 auto reel_picture_asset = create_reel_picture (reel, refs);
765 duration = reel_picture_asset->actual_duration ();
766 create_reel_sound (reel, refs);
767 if (!film()->interop()) {
768 create_reel_markers(reel);
772 create_reel_text(reel, refs, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
775 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
782 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
785 if (_picture_asset) {
786 _picture_asset->hash (set_progress);
790 _sound_asset->hash (set_progress);
794 _atmos_asset->hash (set_progress);
796 } catch (boost::thread_interrupted) {
797 /* set_progress contains an interruption_point, so any of these methods
798 * may throw thread_interrupted, at which point we just give up.
804 ReelWriter::start () const
806 return _period.from.frames_floor (film()->video_frame_rate());
811 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
813 if (!_sound_asset_writer) {
817 DCPOMATIC_ASSERT (audio);
818 _sound_asset_writer->write(audio->data(), audio->channels(), audio->frames());
822 shared_ptr<dcp::SubtitleAsset>
823 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
825 shared_ptr<dcp::SubtitleAsset> asset;
826 optional<string> font;
828 auto lang = film()->subtitle_languages();
829 if (film()->interop()) {
830 auto s = make_shared<dcp::InteropSubtitleAsset>();
831 s->set_movie_title (film()->name());
832 if (type == TextType::OPEN_SUBTITLE) {
833 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
834 } else if (track->language) {
835 s->set_language (track->language->to_string());
837 s->set_reel_number (raw_convert<string> (_reel_index + 1));
840 auto s = make_shared<dcp::SMPTESubtitleAsset>();
841 s->set_content_title_text (film()->name());
842 s->set_metadata (mxf_metadata());
843 if (type == TextType::OPEN_SUBTITLE && lang.first) {
844 s->set_language (*lang.first);
845 } else if (track && track->language) {
846 s->set_language (dcp::LanguageTag(track->language->to_string()));
848 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
849 s->set_reel_number (_reel_index + 1);
850 s->set_time_code_rate (film()->video_frame_rate());
851 s->set_start_time (dcp::Time ());
852 if (film()->encrypted()) {
853 s->set_key (film()->key());
860 std::make_shared<dcp::SubtitleString>(
868 dcp::Time(0, 0, 0, 0, 24),
869 dcp::Time(0, 0, 1, 0, 24),
885 if (!film()->interop()) {
886 /* We must have a LoadFont since we have a Text */
888 asset->ensure_font(*font, _default_font);
897 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const
899 if (dcp::uses_baseline(subtitle.valign_standard) == dcp::uses_baseline(to)) {
900 /* The from and to standards use the same alignment reference */
901 return subtitle.v_position();
904 auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
905 auto const height = _font_metrics.height(subtitle);
907 float correction = 0;
908 switch (subtitle.v_align()) {
909 case dcp::VAlign::TOP:
910 correction = height - baseline_to_bottom;
912 case dcp::VAlign::CENTER:
913 correction = (height / 2) - baseline_to_bottom;
915 case dcp::VAlign::BOTTOM:
916 correction = baseline_to_bottom;
920 return subtitle.v_position() + (dcp::uses_bounding_box(subtitle.valign_standard) ? correction : -correction);
925 ReelWriter::write(PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts, shared_ptr<dcpomatic::Font> chosen_interop_font)
927 shared_ptr<dcp::SubtitleAsset> asset;
930 case TextType::OPEN_SUBTITLE:
931 asset = _subtitle_asset;
933 case TextType::CLOSED_CAPTION:
934 DCPOMATIC_ASSERT (track);
935 asset = _closed_caption_assets[*track];
938 DCPOMATIC_ASSERT (false);
942 asset = empty_text_asset (type, track, false);
946 case TextType::OPEN_SUBTITLE:
947 _subtitle_asset = asset;
949 case TextType::CLOSED_CAPTION:
950 DCPOMATIC_ASSERT (track);
951 _closed_caption_assets[*track] = asset;
954 DCPOMATIC_ASSERT (false);
957 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
958 auto const tcr = 1000;
960 for (auto i: subs.string) {
961 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
962 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
963 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::SubtitleStandard::INTEROP : dcp::SubtitleStandard::SMPTE_2014));
964 auto sub = make_shared<dcp::SubtitleString>(i);
965 /* i.font is a shared_ptr<Font> which uniquely identifies the font we want,
966 * though if we are Interop we can only have one font, so we'll use the chosen
969 auto font = film()->interop() ? chosen_interop_font : i.font;
970 /* We can get the corresponding ID from fonts */
971 auto const font_id_to_use = fonts.get(font);
972 /* Give this subtitle the correct font ID */
973 sub->set_font(font_id_to_use);
975 /* Make sure the asset LoadFonts the font we just asked for */
976 asset->ensure_font(font_id_to_use, font->data().get_value_or(_default_font));
979 for (auto i: subs.bitmap) {
981 make_shared<dcp::SubtitleImage>(
982 image_as_png(i.image),
983 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
984 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
985 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
986 dcp::Time(), dcp::Time()
994 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
996 LOG_GENERAL ("Checking existing picture frame %1", frame);
998 /* Read the data from the info file; for 3D we just check the left
999 frames until we find a good one.
1001 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
1005 /* Read the data from the asset and hash it */
1006 asset_file.seek(info.offset, SEEK_SET);
1007 ArrayData data (info.size);
1008 size_t const read = asset_file.read(data.data(), 1, data.size());
1009 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
1010 if (read != static_cast<size_t> (data.size ())) {
1011 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
1015 digester.add (data.data(), data.size());
1016 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
1017 if (digester.get() != info.hash) {
1018 LOG_GENERAL ("Existing frame %1 failed hash check", frame);