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"
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>
62 using std::dynamic_pointer_cast;
65 using std::make_shared;
68 using std::shared_ptr;
72 using boost::optional;
73 #if BOOST_VERSION >= 106100
74 using namespace boost::placeholders;
78 using dcp::raw_convert;
79 using namespace dcpomatic;
82 int const ReelWriter::_info_size = 48;
85 static dcp::MXFMetadata
88 dcp::MXFMetadata meta;
89 auto config = Config::instance();
90 if (!config->dcp_company_name().empty()) {
91 meta.company_name = config->dcp_company_name ();
93 if (!config->dcp_product_name().empty()) {
94 meta.product_name = config->dcp_product_name ();
96 if (!config->dcp_product_version().empty()) {
97 meta.product_version = config->dcp_product_version ();
103 /** @param job Related job, or 0.
104 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
105 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
106 * subtitle / closed caption files.
108 ReelWriter::ReelWriter (
109 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
111 : WeakConstFilm (weak_film)
113 , _reel_index (reel_index)
114 , _reel_count (reel_count)
115 , _content_summary (film()->content_summary(period))
117 , _text_only (text_only)
118 , _font_metrics(film()->frame_size().height)
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_nonexistent_frame = check_existing_picture_asset (asset);
132 if (_first_nonexistent_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_nonexistent_frame > 0 ? dcp::PictureAsset::Behaviour::OVERWRITE_EXISTING : dcp::PictureAsset::Behaviour::MAKE_NEW);
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 /* Always make 16-channel sound assets for SMPTE; libdcp will pad unused channels for us */
182 standard == dcp::Standard::SMPTE ? MAX_DCP_AUDIO_CHANNELS : 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 /* Write the sound asset into the film directory so that we leave the creation
196 of the DCP directory until the last minute.
198 _sound_asset_writer = _sound_asset->start_write (
199 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
200 film()->audio_channels(),
201 film()->contains_atmos_content() ? dcp::SoundAsset::AtmosSync::ENABLED : dcp::SoundAsset::AtmosSync::DISABLED,
202 film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
206 _default_font = dcp::ArrayData(default_font_file());
210 /** @param frame reel-relative frame */
212 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
214 auto handle = film()->info_file_handle(_period, false);
215 handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
216 handle->get().checked_write(&info.offset, sizeof(info.offset));
217 handle->get().checked_write(&info.size, sizeof(info.size));
218 handle->get().checked_write(info.hash.c_str(), info.hash.size());
223 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
225 dcp::FrameInfo frame_info;
226 info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
227 info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
228 info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
230 char hash_buffer[33];
231 info->get().checked_read(hash_buffer, 32);
232 hash_buffer[32] = '\0';
233 frame_info.hash = hash_buffer;
240 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
244 return frame * _info_size;
246 return frame * _info_size * 2;
248 return frame * _info_size * 2 + _info_size;
250 DCPOMATIC_ASSERT (false);
253 DCPOMATIC_ASSERT (false);
258 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
260 auto job = _job.lock ();
263 job->sub (_("Checking existing image data"));
266 /* Try to open the existing asset */
267 dcp::File asset_file(asset, "rb");
269 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
272 LOG_GENERAL ("Opened existing asset at %1", asset.string());
275 shared_ptr<InfoFileHandle> info_file;
278 info_file = film()->info_file_handle (_period, true);
279 } catch (OpenFileError &) {
280 LOG_GENERAL_NC ("Could not open film info file");
284 /* Offset of the last dcp::FrameInfo in the info file */
285 int const n = (boost::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
286 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);
288 Frame first_nonexistent_frame;
289 if (film()->three_d()) {
290 /* Start looking at the last left frame */
291 first_nonexistent_frame = n / 2;
293 first_nonexistent_frame = n;
296 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
297 --first_nonexistent_frame;
300 if (!film()->three_d() && first_nonexistent_frame > 0) {
301 /* If we are doing 3D we might have found a good L frame with no R, so only
302 do this if we're in 2D and we've just found a good B(oth) frame.
304 ++first_nonexistent_frame;
307 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
309 return first_nonexistent_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[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(_last_written[eyes]->data(), _last_written[eyes]->size());
364 write_frame_info (frame, eyes, fin);
369 ReelWriter::finish (boost::filesystem::path output_dcp)
371 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
372 /* Nothing was written to the picture asset */
373 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
374 _picture_asset.reset ();
377 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
378 /* Nothing was written to the sound asset */
379 _sound_asset.reset ();
382 /* Hard-link any video asset file into the DCP */
383 if (_picture_asset) {
384 DCPOMATIC_ASSERT (_picture_asset->file());
385 boost::filesystem::path video_from = _picture_asset->file().get();
386 boost::filesystem::path video_to = output_dcp;
387 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
388 /* There may be an existing "to" file if we are recreating a DCP in the same place without
391 boost::system::error_code ec;
392 boost::filesystem::remove (video_to, ec);
394 boost::filesystem::create_hard_link (video_from, video_to, ec);
396 LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
397 auto job = _job.lock ();
399 job->sub (_("Copying video file into DCP"));
401 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
402 } catch (exception& e) {
403 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
404 throw FileError (e.what(), video_from);
407 boost::filesystem::copy_file (video_from, video_to, ec);
409 LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
410 throw FileError (ec.message(), video_from);
415 _picture_asset->set_file (video_to);
418 /* Move the audio asset into the DCP */
420 boost::filesystem::path audio_to = output_dcp;
421 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
424 boost::system::error_code ec;
425 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
428 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
432 _sound_asset->set_file (audio_to);
436 _atmos_asset_writer->finalize ();
437 boost::filesystem::path atmos_to = output_dcp;
438 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
441 boost::system::error_code ec;
442 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
445 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
449 _atmos_asset->set_file (atmos_to);
454 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
455 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
457 template <class Interop, class SMPTE, class Result>
460 shared_ptr<dcp::SubtitleAsset> asset,
461 int64_t picture_duration,
462 shared_ptr<dcp::Reel> reel,
465 optional<string> content_summary,
466 list<ReferencedReelAsset> const & refs,
467 FontIdMap const& fonts,
468 shared_ptr<dcpomatic::Font> chosen_interop_font,
469 dcp::ArrayData default_font,
470 shared_ptr<const Film> film,
471 DCPTimePeriod period,
472 boost::filesystem::path output_dcp,
476 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
478 shared_ptr<Result> reel_asset;
481 if (film->interop()) {
482 if (chosen_interop_font) {
483 /* We only add one font, as Interop will ignore subsequent ones (and some validators will
484 * complain if they are even present)
486 asset->add_font(fonts.get(chosen_interop_font), chosen_interop_font->data().get_value_or(default_font));
489 for (auto const& font: fonts.map()) {
490 asset->add_font(font.second, font.first->data().get_value_or(default_font));
494 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
495 auto directory = output_dcp / interop->id ();
496 boost::filesystem::create_directories (directory);
497 interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
498 reel_asset = make_shared<Interop> (
500 dcp::Fraction(film->video_frame_rate(), 1),
504 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
505 /* All our assets should be the same length; use the picture asset length here
506 as a reference to set the subtitle one. We'll use the duration rather than
507 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
508 just interested in its presentation length.
510 smpte->set_intrinsic_duration(picture_duration);
512 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
514 reel_asset = make_shared<SMPTE> (
516 dcp::Fraction(film->video_frame_rate(), 1),
523 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
525 auto k = dynamic_pointer_cast<Result> (j.asset);
526 if (k && j.period == period) {
528 /* If we have a hash for this asset in the CPL, assume that it is correct */
530 k->asset_ref()->set_hash (k->hash().get());
537 if (!text_only && reel_asset->actual_duration() != period_duration) {
538 throw ProgrammingError (
540 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
543 reel->add (reel_asset);
550 shared_ptr<dcp::ReelPictureAsset>
551 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
553 shared_ptr<dcp::ReelPictureAsset> reel_asset;
555 if (_picture_asset) {
556 /* We have made a picture asset of our own. Put it into the reel */
557 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
559 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
562 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
564 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
567 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
568 /* We don't have a picture asset of our own; hopefully we have one to reference */
570 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
572 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
574 if (k && j.period == _period) {
580 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
582 DCPOMATIC_ASSERT (reel_asset);
583 if (reel_asset->duration() != period_duration) {
584 throw ProgrammingError (
586 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
589 reel->add (reel_asset);
591 /* If we have a hash for this asset in the CPL, assume that it is correct */
592 if (reel_asset->hash()) {
593 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
601 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
603 shared_ptr<dcp::ReelSoundAsset> reel_asset;
606 /* We have made a sound asset of our own. Put it into the reel */
607 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
609 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
610 /* We don't have a sound asset of our own; hopefully we have one to reference */
612 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
614 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
616 if (k && j.period == _period) {
618 /* If we have a hash for this asset in the CPL, assume that it is correct */
620 k->asset_ref()->set_hash (k->hash().get());
626 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
628 DCPOMATIC_ASSERT (reel_asset);
629 if (reel_asset->actual_duration() != period_duration) {
631 "Reel sound asset has length %1 but reel period is %2",
632 reel_asset->actual_duration(),
635 if (reel_asset->actual_duration() != period_duration) {
636 throw ProgrammingError (
638 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
643 reel->add (reel_asset);
648 ReelWriter::create_reel_text (
649 shared_ptr<dcp::Reel> reel,
650 list<ReferencedReelAsset> const & refs,
651 FontIdMap const& fonts,
652 shared_ptr<dcpomatic::Font> chosen_interop_font,
654 boost::filesystem::path output_dcp,
655 bool ensure_subtitles,
656 set<DCPTextTrack> ensure_closed_captions
659 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
660 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
664 /* We have a subtitle asset that we either made or are referencing */
665 if (auto main_language = film()->subtitle_languages().first) {
666 subtitle->set_language (*main_language);
668 } else if (ensure_subtitles) {
669 /* We had no subtitle asset, but we've been asked to make sure there is one */
670 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
671 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
688 for (auto const& i: _closed_caption_assets) {
689 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
690 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
692 DCPOMATIC_ASSERT (a);
693 a->set_annotation_text (i.first.name);
694 if (i.first.language) {
695 a->set_language (i.first.language.get());
698 ensure_closed_captions.erase (i.first);
701 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
702 for (auto i: ensure_closed_captions) {
703 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
704 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
719 DCPOMATIC_ASSERT (a);
720 a->set_annotation_text (i.name);
722 a->set_language (i.language.get());
729 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
731 auto markers = film()->markers();
732 film()->add_ffoc_lfoc(markers);
733 Film::Markers reel_markers;
734 for (auto const& i: markers) {
735 if (_period.contains(i.second)) {
736 reel_markers[i.first] = i.second;
740 if (!reel_markers.empty ()) {
741 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
742 for (auto const& i: reel_markers) {
743 DCPTime relative = i.second - _period.from;
744 auto hmsf = relative.split (film()->video_frame_rate());
745 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
752 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
753 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
755 shared_ptr<dcp::Reel>
756 ReelWriter::create_reel (
757 list<ReferencedReelAsset> const & refs,
758 FontIdMap const & fonts,
759 shared_ptr<dcpomatic::Font> chosen_interop_font,
760 boost::filesystem::path output_dcp,
761 bool ensure_subtitles,
762 set<DCPTextTrack> ensure_closed_captions
765 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
767 auto reel = make_shared<dcp::Reel>();
769 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
770 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
771 * how big they are, we don't care about that.
773 int64_t duration = 0;
775 auto reel_picture_asset = create_reel_picture (reel, refs);
776 duration = reel_picture_asset->actual_duration ();
777 create_reel_sound (reel, refs);
778 create_reel_markers (reel);
781 create_reel_text (reel, refs, fonts, chosen_interop_font, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
784 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
791 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
794 if (_picture_asset) {
795 _picture_asset->hash (set_progress);
799 _sound_asset->hash (set_progress);
803 _atmos_asset->hash (set_progress);
805 } catch (boost::thread_interrupted) {
806 /* set_progress contains an interruption_point, so any of these methods
807 * may throw thread_interrupted, at which point we just give up.
813 ReelWriter::start () const
815 return _period.from.frames_floor (film()->video_frame_rate());
820 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
822 if (!_sound_asset_writer) {
826 DCPOMATIC_ASSERT (audio);
827 _sound_asset_writer->write(audio->data(), audio->channels(), audio->frames());
831 shared_ptr<dcp::SubtitleAsset>
832 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
834 shared_ptr<dcp::SubtitleAsset> asset;
836 auto lang = film()->subtitle_languages();
837 if (film()->interop()) {
838 auto s = make_shared<dcp::InteropSubtitleAsset>();
839 s->set_movie_title (film()->name());
840 if (type == TextType::OPEN_SUBTITLE) {
841 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
842 } else if (track->language) {
843 s->set_language (track->language->to_string());
845 s->set_reel_number (raw_convert<string> (_reel_index + 1));
848 auto s = make_shared<dcp::SMPTESubtitleAsset>();
849 s->set_content_title_text (film()->name());
850 s->set_metadata (mxf_metadata());
851 if (type == TextType::OPEN_SUBTITLE && lang.first) {
852 s->set_language (*lang.first);
853 } else if (track && track->language) {
854 s->set_language (dcp::LanguageTag(track->language->to_string()));
856 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
857 s->set_reel_number (_reel_index + 1);
858 s->set_time_code_rate (film()->video_frame_rate());
859 s->set_start_time (dcp::Time ());
860 if (film()->encrypted()) {
861 s->set_key (film()->key());
868 std::make_shared<dcp::SubtitleString>(
869 optional<std::string>(),
876 dcp::Time(0, 0, 0, 0, 24),
877 dcp::Time(0, 0, 1, 0, 24),
899 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const
901 if (dcp::uses_baseline(subtitle.valign_standard) == dcp::uses_baseline(to)) {
902 /* The from and to standards use the same alignment reference */
903 return subtitle.v_position();
906 auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
907 auto const height = _font_metrics.height(subtitle);
909 float correction = 0;
910 switch (subtitle.v_align()) {
911 case dcp::VAlign::TOP:
912 correction = height - baseline_to_bottom;
914 case dcp::VAlign::CENTER:
915 correction = (height / 2) - baseline_to_bottom;
917 case dcp::VAlign::BOTTOM:
918 correction = baseline_to_bottom;
922 return subtitle.v_position() + (dcp::uses_bounding_box(subtitle.valign_standard) ? correction : -correction);
927 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts)
929 shared_ptr<dcp::SubtitleAsset> asset;
932 case TextType::OPEN_SUBTITLE:
933 asset = _subtitle_asset;
935 case TextType::CLOSED_CAPTION:
936 DCPOMATIC_ASSERT (track);
937 asset = _closed_caption_assets[*track];
940 DCPOMATIC_ASSERT (false);
944 asset = empty_text_asset (type, track, false);
948 case TextType::OPEN_SUBTITLE:
949 _subtitle_asset = asset;
951 case TextType::CLOSED_CAPTION:
952 DCPOMATIC_ASSERT (track);
953 _closed_caption_assets[*track] = asset;
956 DCPOMATIC_ASSERT (false);
959 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
960 auto const tcr = 1000;
962 for (auto i: subs.string) {
963 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
964 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
965 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::SubtitleStandard::INTEROP : dcp::SubtitleStandard::SMPTE_2014));
966 auto sub = make_shared<dcp::SubtitleString>(i);
967 if (type == TextType::OPEN_SUBTITLE) {
968 sub->set_font(fonts.get(i.font));
973 for (auto i: subs.bitmap) {
975 make_shared<dcp::SubtitleImage>(
976 image_as_png(i.image),
977 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
978 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
979 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
980 dcp::Time(), dcp::Time()
988 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
990 LOG_GENERAL ("Checking existing picture frame %1", frame);
992 /* Read the data from the info file; for 3D we just check the left
993 frames until we find a good one.
995 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
999 /* Read the data from the asset and hash it */
1000 asset_file.seek(info.offset, SEEK_SET);
1001 ArrayData data (info.size);
1002 size_t const read = asset_file.read(data.data(), 1, data.size());
1003 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
1004 if (read != static_cast<size_t> (data.size ())) {
1005 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
1009 digester.add (data.data(), data.size());
1010 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
1011 if (digester.get() != info.hash) {
1012 LOG_GENERAL ("Existing frame %1 failed hash check", frame);