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"
30 #include "image_png.h"
33 #include "reel_writer.h"
34 #include <dcp/atmos_asset.h>
35 #include <dcp/atmos_asset_writer.h>
36 #include <dcp/certificate_chain.h>
39 #include <dcp/interop_subtitle_asset.h>
40 #include <dcp/mono_picture_asset.h>
41 #include <dcp/raw_convert.h>
43 #include <dcp/reel_atmos_asset.h>
44 #include <dcp/reel_interop_closed_caption_asset.h>
45 #include <dcp/reel_interop_subtitle_asset.h>
46 #include <dcp/reel_markers_asset.h>
47 #include <dcp/reel_mono_picture_asset.h>
48 #include <dcp/reel_smpte_closed_caption_asset.h>
49 #include <dcp/reel_smpte_subtitle_asset.h>
50 #include <dcp/reel_sound_asset.h>
51 #include <dcp/reel_stereo_picture_asset.h>
52 #include <dcp/smpte_subtitle_asset.h>
53 #include <dcp/sound_asset.h>
54 #include <dcp/sound_asset_writer.h>
55 #include <dcp/stereo_picture_asset.h>
56 #include <dcp/subtitle_image.h>
61 using std::dynamic_pointer_cast;
64 using std::make_shared;
67 using std::shared_ptr;
71 using boost::optional;
72 #if BOOST_VERSION >= 106100
73 using namespace boost::placeholders;
77 using dcp::raw_convert;
78 using namespace dcpomatic;
81 int const ReelWriter::_info_size = 48;
84 static dcp::MXFMetadata
87 dcp::MXFMetadata meta;
88 auto config = Config::instance();
89 if (!config->dcp_company_name().empty()) {
90 meta.company_name = config->dcp_company_name ();
92 if (!config->dcp_product_name().empty()) {
93 meta.product_name = config->dcp_product_name ();
95 if (!config->dcp_product_version().empty()) {
96 meta.product_version = config->dcp_product_version ();
102 /** @param job Related job, or 0.
103 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
104 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
105 * subtitle / closed caption files.
107 ReelWriter::ReelWriter (
108 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
110 : WeakConstFilm (weak_film)
112 , _reel_index (reel_index)
113 , _reel_count (reel_count)
114 , _content_summary (film()->content_summary(period))
116 , _text_only (text_only)
117 , _font_metrics(film()->frame_size().height)
119 /* Create or find our picture asset in a subdirectory, named
120 according to those film's parameters which affect the video
121 output. We will hard-link it into the DCP later.
124 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
126 boost::filesystem::path const asset =
127 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
129 _first_nonexistent_frame = check_existing_picture_asset (asset);
131 if (_first_nonexistent_frame < period.duration().frames_round(film()->video_frame_rate())) {
132 /* We do not have a complete picture asset. If there is an
133 existing asset, break any hard links to it as we are about
134 to change its contents (if only by changing the IDs); see
137 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
139 job->sub (_("Copying old video file"));
140 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
142 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
144 boost::filesystem::remove (asset);
145 boost::filesystem::rename (asset.string() + ".tmp", asset);
149 if (film()->three_d()) {
150 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
152 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
155 _picture_asset->set_size (film()->frame_size());
156 _picture_asset->set_metadata (mxf_metadata());
158 if (film()->encrypted()) {
159 _picture_asset->set_key (film()->key());
160 _picture_asset->set_context_id (film()->context_id());
163 _picture_asset->set_file (asset);
164 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistent_frame > 0);
165 } else if (!text_only) {
166 /* We already have a complete picture asset that we can just re-use */
167 /* XXX: what about if the encryption key changes? */
168 if (film()->three_d()) {
169 _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
171 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
175 if (film()->audio_channels()) {
176 auto lang = film()->audio_language();
177 _sound_asset = make_shared<dcp::SoundAsset> (
178 dcp::Fraction(film()->video_frame_rate(), 1),
179 film()->audio_frame_rate(),
180 film()->audio_channels(),
181 lang ? *lang : dcp::LanguageTag("en-US"),
185 _sound_asset->set_metadata (mxf_metadata());
187 if (film()->encrypted()) {
188 _sound_asset->set_key (film()->key());
191 DCPOMATIC_ASSERT (film()->directory());
193 /* Write the sound asset into the film directory so that we leave the creation
194 of the DCP directory until the last minute.
196 _sound_asset_writer = _sound_asset->start_write (
197 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
198 film()->contains_atmos_content(),
199 !film()->limit_to_smpte_bv20()
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 handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
213 handle->get().checked_write(&info.offset, sizeof(info.offset));
214 handle->get().checked_write(&info.size, sizeof(info.size));
215 handle->get().checked_write(info.hash.c_str(), info.hash.size());
220 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
222 dcp::FrameInfo frame_info;
223 info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
224 info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
225 info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
227 char hash_buffer[33];
228 info->get().checked_read(hash_buffer, 32);
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 dcp::File asset_file(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");
281 /* Offset of the last dcp::FrameInfo in the info file */
282 int const n = (boost::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
283 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);
285 Frame first_nonexistent_frame;
286 if (film()->three_d()) {
287 /* Start looking at the last left frame */
288 first_nonexistent_frame = n / 2;
290 first_nonexistent_frame = n;
293 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
294 --first_nonexistent_frame;
297 if (!film()->three_d() && first_nonexistent_frame > 0) {
298 /* If we are doing 3D we might have found a good L frame with no R, so only
299 do this if we're in 2D and we've just found a good B(oth) frame.
301 ++first_nonexistent_frame;
304 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
306 return first_nonexistent_frame;
311 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
313 if (!_picture_asset_writer) {
314 /* We're not writing any data */
318 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
319 write_frame_info (frame, eyes, fin);
320 _last_written[eyes] = encoded;
325 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
328 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
329 if (film()->encrypted()) {
330 _atmos_asset->set_key(film()->key());
332 _atmos_asset_writer = _atmos_asset->start_write (
333 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
336 _atmos_asset_writer->write (atmos);
341 ReelWriter::fake_write (int size)
343 if (!_picture_asset_writer) {
344 /* We're not writing any data */
348 _picture_asset_writer->fake_write (size);
353 ReelWriter::repeat_write (Frame frame, Eyes eyes)
355 if (!_picture_asset_writer) {
356 /* We're not writing any data */
360 auto fin = _picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size());
361 write_frame_info (frame, eyes, fin);
366 ReelWriter::finish (boost::filesystem::path output_dcp)
368 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
369 /* Nothing was written to the picture asset */
370 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
371 _picture_asset.reset ();
374 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
375 /* Nothing was written to the sound asset */
376 _sound_asset.reset ();
379 /* Hard-link any video asset file into the DCP */
380 if (_picture_asset) {
381 DCPOMATIC_ASSERT (_picture_asset->file());
382 boost::filesystem::path video_from = _picture_asset->file().get();
383 boost::filesystem::path video_to = output_dcp;
384 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
385 /* There may be an existing "to" file if we are recreating a DCP in the same place without
388 boost::system::error_code ec;
389 boost::filesystem::remove (video_to, ec);
391 boost::filesystem::create_hard_link (video_from, video_to, ec);
393 LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
394 auto job = _job.lock ();
396 job->sub (_("Copying video file into DCP"));
398 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
399 } catch (exception& e) {
400 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
401 throw FileError (e.what(), video_from);
404 boost::filesystem::copy_file (video_from, video_to, ec);
406 LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
407 throw FileError (ec.message(), video_from);
412 _picture_asset->set_file (video_to);
415 /* Move the audio asset into the DCP */
417 boost::filesystem::path audio_to = output_dcp;
418 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
421 boost::system::error_code ec;
422 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
425 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
429 _sound_asset->set_file (audio_to);
433 _atmos_asset_writer->finalize ();
434 boost::filesystem::path atmos_to = output_dcp;
435 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
438 boost::system::error_code ec;
439 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
442 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
446 _atmos_asset->set_file (atmos_to);
451 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
452 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
454 template <class Interop, class SMPTE, class Result>
457 shared_ptr<dcp::SubtitleAsset> asset,
458 int64_t picture_duration,
459 shared_ptr<dcp::Reel> reel,
462 optional<string> content_summary,
463 list<ReferencedReelAsset> const & refs,
464 FontIdMap const& fonts,
465 shared_ptr<dcpomatic::Font> chosen_interop_font,
466 dcp::ArrayData default_font,
467 shared_ptr<const Film> film,
468 DCPTimePeriod period,
469 boost::filesystem::path output_dcp,
473 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
475 shared_ptr<Result> reel_asset;
478 if (film->interop()) {
479 if (chosen_interop_font) {
480 /* We only add one font, as Interop will ignore subsequent ones (and some validators will
481 * complain if they are even present)
483 asset->add_font(fonts.get(chosen_interop_font), chosen_interop_font->data().get_value_or(default_font));
486 for (auto const& font: fonts.map()) {
487 asset->add_font(font.second, font.first->data().get_value_or(default_font));
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,
648 FontIdMap const& fonts,
649 shared_ptr<dcpomatic::Font> chosen_interop_font,
651 boost::filesystem::path output_dcp,
652 bool ensure_subtitles,
653 set<DCPTextTrack> ensure_closed_captions
656 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
657 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
661 /* We have a subtitle asset that we either made or are referencing */
662 if (auto main_language = film()->subtitle_languages().first) {
663 subtitle->set_language (*main_language);
665 } else if (ensure_subtitles) {
666 /* We had no subtitle asset, but we've been asked to make sure there is one */
667 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
668 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
685 for (auto const& i: _closed_caption_assets) {
686 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
687 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
689 DCPOMATIC_ASSERT (a);
690 a->set_annotation_text (i.first.name);
691 if (i.first.language) {
692 a->set_language (i.first.language.get());
695 ensure_closed_captions.erase (i.first);
698 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
699 for (auto i: ensure_closed_captions) {
700 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
701 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
716 DCPOMATIC_ASSERT (a);
717 a->set_annotation_text (i.name);
719 a->set_language (i.language.get());
726 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
728 auto markers = film()->markers();
729 film()->add_ffoc_lfoc(markers);
730 Film::Markers reel_markers;
731 for (auto const& i: markers) {
732 if (_period.contains(i.second)) {
733 reel_markers[i.first] = i.second;
737 if (!reel_markers.empty ()) {
738 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
739 for (auto const& i: reel_markers) {
740 DCPTime relative = i.second - _period.from;
741 auto hmsf = relative.split (film()->video_frame_rate());
742 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
749 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
750 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
752 shared_ptr<dcp::Reel>
753 ReelWriter::create_reel (
754 list<ReferencedReelAsset> const & refs,
755 FontIdMap const & fonts,
756 shared_ptr<dcpomatic::Font> chosen_interop_font,
757 boost::filesystem::path output_dcp,
758 bool ensure_subtitles,
759 set<DCPTextTrack> ensure_closed_captions
762 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
764 auto reel = make_shared<dcp::Reel>();
766 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
767 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
768 * how big they are, we don't care about that.
770 int64_t duration = 0;
772 auto reel_picture_asset = create_reel_picture (reel, refs);
773 duration = reel_picture_asset->actual_duration ();
774 create_reel_sound (reel, refs);
775 create_reel_markers (reel);
778 create_reel_text (reel, refs, fonts, chosen_interop_font, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
781 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
788 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
791 if (_picture_asset) {
792 _picture_asset->hash (set_progress);
796 _sound_asset->hash (set_progress);
800 _atmos_asset->hash (set_progress);
802 } catch (boost::thread_interrupted) {
803 /* set_progress contains an interruption_point, so any of these methods
804 * may throw thread_interrupted, at which point we just give up.
810 ReelWriter::start () const
812 return _period.from.frames_floor (film()->video_frame_rate());
817 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
819 if (!_sound_asset_writer) {
823 DCPOMATIC_ASSERT (audio);
824 _sound_asset_writer->write (audio->data(), audio->frames());
828 shared_ptr<dcp::SubtitleAsset>
829 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
831 shared_ptr<dcp::SubtitleAsset> asset;
833 auto lang = film()->subtitle_languages();
834 if (film()->interop()) {
835 auto s = make_shared<dcp::InteropSubtitleAsset>();
836 s->set_movie_title (film()->name());
837 if (type == TextType::OPEN_SUBTITLE) {
838 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
839 } else if (track->language) {
840 s->set_language (track->language->to_string());
842 s->set_reel_number (raw_convert<string> (_reel_index + 1));
845 auto s = make_shared<dcp::SMPTESubtitleAsset>();
846 s->set_content_title_text (film()->name());
847 s->set_metadata (mxf_metadata());
848 if (type == TextType::OPEN_SUBTITLE && lang.first) {
849 s->set_language (*lang.first);
850 } else if (track && track->language) {
851 s->set_language (dcp::LanguageTag(track->language->to_string()));
853 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
854 s->set_reel_number (_reel_index + 1);
855 s->set_time_code_rate (film()->video_frame_rate());
856 s->set_start_time (dcp::Time ());
857 if (film()->encrypted()) {
858 s->set_key (film()->key());
865 std::make_shared<dcp::SubtitleString>(
866 optional<std::string>(),
873 dcp::Time(0, 0, 0, 0, 24),
874 dcp::Time(0, 0, 1, 0, 24),
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)
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 if (type == TextType::OPEN_SUBTITLE) {
965 sub->set_font(fonts.get(i.font));
970 for (auto i: subs.bitmap) {
972 make_shared<dcp::SubtitleImage>(
973 image_as_png(i.image),
974 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
975 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
976 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
977 dcp::Time(), dcp::Time()
985 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
987 LOG_GENERAL ("Checking existing picture frame %1", frame);
989 /* Read the data from the info file; for 3D we just check the left
990 frames until we find a good one.
992 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
996 /* Read the data from the asset and hash it */
997 asset_file.seek(info.offset, SEEK_SET);
998 ArrayData data (info.size);
999 size_t const read = asset_file.read(data.data(), 1, data.size());
1000 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
1001 if (read != static_cast<size_t> (data.size ())) {
1002 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
1006 digester.add (data.data(), data.size());
1007 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
1008 if (digester.get() != info.hash) {
1009 LOG_GENERAL ("Existing frame %1 failed hash check", frame);