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/filesystem.h>
42 #include <dcp/interop_subtitle_asset.h>
43 #include <dcp/mono_picture_asset.h>
44 #include <dcp/raw_convert.h>
46 #include <dcp/reel_atmos_asset.h>
47 #include <dcp/reel_interop_closed_caption_asset.h>
48 #include <dcp/reel_interop_subtitle_asset.h>
49 #include <dcp/reel_markers_asset.h>
50 #include <dcp/reel_mono_picture_asset.h>
51 #include <dcp/reel_smpte_closed_caption_asset.h>
52 #include <dcp/reel_smpte_subtitle_asset.h>
53 #include <dcp/reel_sound_asset.h>
54 #include <dcp/reel_stereo_picture_asset.h>
55 #include <dcp/smpte_subtitle_asset.h>
56 #include <dcp/sound_asset.h>
57 #include <dcp/sound_asset_writer.h>
58 #include <dcp/stereo_picture_asset.h>
59 #include <dcp/subtitle_image.h>
64 using std::dynamic_pointer_cast;
67 using std::make_shared;
70 using std::shared_ptr;
74 using boost::optional;
75 #if BOOST_VERSION >= 106100
76 using namespace boost::placeholders;
80 using dcp::raw_convert;
81 using namespace dcpomatic;
84 int const ReelWriter::_info_size = 48;
87 static dcp::MXFMetadata
90 dcp::MXFMetadata meta;
91 auto config = Config::instance();
92 if (!config->dcp_company_name().empty()) {
93 meta.company_name = config->dcp_company_name ();
95 if (!config->dcp_product_name().empty()) {
96 meta.product_name = config->dcp_product_name ();
98 if (!config->dcp_product_version().empty()) {
99 meta.product_version = config->dcp_product_version ();
105 /** @param job Related job, or 0.
106 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
107 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
108 * subtitle / closed caption files.
110 ReelWriter::ReelWriter (
111 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
113 : WeakConstFilm (weak_film)
115 , _reel_index (reel_index)
116 , _reel_count (reel_count)
117 , _content_summary (film()->content_summary(period))
119 , _text_only (text_only)
120 , _font_metrics(film()->frame_size().height)
122 /* Create or find our picture asset in a subdirectory, named
123 according to those film's parameters which affect the video
124 output. We will hard-link it into the DCP later.
127 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
129 boost::filesystem::path const asset =
130 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
132 _first_nonexistent_frame = check_existing_picture_asset (asset);
134 if (_first_nonexistent_frame < period.duration().frames_round(film()->video_frame_rate())) {
135 /* We do not have a complete picture asset. If there is an
136 existing asset, break any hard links to it as we are about
137 to change its contents (if only by changing the IDs); see
140 if (dcp::filesystem::exists(asset) && dcp::filesystem::hard_link_count(asset) > 1) {
142 job->sub (_("Copying old video file"));
143 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
145 dcp::filesystem::copy_file(asset, asset.string() + ".tmp");
147 dcp::filesystem::remove(asset);
148 dcp::filesystem::rename(asset.string() + ".tmp", asset);
152 if (film()->three_d()) {
153 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
155 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
158 _picture_asset->set_size (film()->frame_size());
159 _picture_asset->set_metadata (mxf_metadata());
161 if (film()->encrypted()) {
162 _picture_asset->set_key (film()->key());
163 _picture_asset->set_context_id (film()->context_id());
166 _picture_asset->set_file (asset);
167 _picture_asset_writer = _picture_asset->start_write(asset, _first_nonexistent_frame > 0 ? dcp::PictureAsset::Behaviour::OVERWRITE_EXISTING : dcp::PictureAsset::Behaviour::MAKE_NEW);
168 } else if (!text_only) {
169 /* We already have a complete picture asset that we can just re-use */
170 /* XXX: what about if the encryption key changes? */
171 if (film()->three_d()) {
172 _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
174 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
178 if (film()->audio_channels()) {
179 auto lang = film()->audio_language();
180 _sound_asset = make_shared<dcp::SoundAsset> (
181 dcp::Fraction(film()->video_frame_rate(), 1),
182 film()->audio_frame_rate(),
183 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 for (auto channel: std::vector<dcp::Channel>{dcp::Channel::HI, dcp::Channel::VI, dcp::Channel::BSL, dcp::Channel::BSR}) {
198 if (channel_is_mapped(film(), channel)) {
199 extra_active_channels.push_back(channel);
203 /* Write the sound asset into the film directory so that we leave the creation
204 of the DCP directory until the last minute.
206 _sound_asset_writer = _sound_asset->start_write (
207 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
208 extra_active_channels,
209 film()->contains_atmos_content() ? dcp::SoundAsset::AtmosSync::ENABLED : dcp::SoundAsset::AtmosSync::DISABLED,
210 film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
214 _default_font = dcp::ArrayData(default_font_file());
218 /** @param frame reel-relative frame */
220 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
222 auto handle = film()->info_file_handle(_period, false);
223 handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
224 handle->get().checked_write(&info.offset, sizeof(info.offset));
225 handle->get().checked_write(&info.size, sizeof(info.size));
226 handle->get().checked_write(info.hash.c_str(), info.hash.size());
231 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
233 dcp::FrameInfo frame_info;
234 info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
235 info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
236 info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
238 char hash_buffer[33];
239 info->get().checked_read(hash_buffer, 32);
240 hash_buffer[32] = '\0';
241 frame_info.hash = hash_buffer;
248 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
252 return frame * _info_size;
254 return frame * _info_size * 2;
256 return frame * _info_size * 2 + _info_size;
258 DCPOMATIC_ASSERT (false);
261 DCPOMATIC_ASSERT (false);
266 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
268 auto job = _job.lock ();
271 job->sub (_("Checking existing image data"));
274 /* Try to open the existing asset */
275 dcp::File asset_file(asset, "rb");
277 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
280 LOG_GENERAL ("Opened existing asset at %1", asset.string());
283 shared_ptr<InfoFileHandle> info_file;
286 info_file = film()->info_file_handle (_period, true);
287 } catch (OpenFileError &) {
288 LOG_GENERAL_NC ("Could not open film info file");
292 /* Offset of the last dcp::FrameInfo in the info file */
293 int const n = (dcp::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
294 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, dcp::filesystem::file_size(info_file->get().path()), _info_size);
296 Frame first_nonexistent_frame;
297 if (film()->three_d()) {
298 /* Start looking at the last left frame */
299 first_nonexistent_frame = n / 2;
301 first_nonexistent_frame = n;
304 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
305 --first_nonexistent_frame;
308 if (!film()->three_d() && first_nonexistent_frame > 0) {
309 /* If we are doing 3D we might have found a good L frame with no R, so only
310 do this if we're in 2D and we've just found a good B(oth) frame.
312 ++first_nonexistent_frame;
315 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
317 return first_nonexistent_frame;
322 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
324 if (!_picture_asset_writer) {
325 /* We're not writing any data */
329 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
330 write_frame_info (frame, eyes, fin);
331 _last_written[eyes] = encoded;
336 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
339 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
340 if (film()->encrypted()) {
341 _atmos_asset->set_key(film()->key());
343 _atmos_asset_writer = _atmos_asset->start_write (
344 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
347 _atmos_asset_writer->write (atmos);
352 ReelWriter::fake_write (int size)
354 if (!_picture_asset_writer) {
355 /* We're not writing any data */
359 _picture_asset_writer->fake_write (size);
364 ReelWriter::repeat_write (Frame frame, Eyes eyes)
366 if (!_picture_asset_writer) {
367 /* We're not writing any data */
371 auto fin = _picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size());
372 write_frame_info (frame, eyes, fin);
377 ReelWriter::finish (boost::filesystem::path output_dcp)
379 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
380 /* Nothing was written to the picture asset */
381 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
382 _picture_asset.reset ();
385 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
386 /* Nothing was written to the sound asset */
387 _sound_asset.reset ();
390 /* Hard-link any video asset file into the DCP */
391 if (_picture_asset) {
392 DCPOMATIC_ASSERT (_picture_asset->file());
393 boost::filesystem::path video_from = _picture_asset->file().get();
394 boost::filesystem::path video_to = output_dcp;
395 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
396 /* There may be an existing "to" file if we are recreating a DCP in the same place without
399 boost::system::error_code ec;
400 dcp::filesystem::remove(video_to, ec);
402 dcp::filesystem::create_hard_link(video_from, video_to, ec);
404 LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
405 auto job = _job.lock ();
407 job->sub (_("Copying video file into DCP"));
409 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
410 } catch (exception& e) {
411 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
412 throw FileError (e.what(), video_from);
415 dcp::filesystem::copy_file(video_from, video_to, ec);
417 LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
418 throw FileError (ec.message(), video_from);
423 _picture_asset->set_file (video_to);
426 /* Move the audio asset into the DCP */
428 boost::filesystem::path audio_to = output_dcp;
429 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
432 boost::system::error_code ec;
433 dcp::filesystem::rename(film()->file(aaf), audio_to, ec);
436 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
440 _sound_asset->set_file (audio_to);
444 _atmos_asset_writer->finalize ();
445 boost::filesystem::path atmos_to = output_dcp;
446 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
449 boost::system::error_code ec;
450 dcp::filesystem::rename(film()->file(aaf), atmos_to, ec);
453 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
457 _atmos_asset->set_file (atmos_to);
462 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
463 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
465 template <class Interop, class SMPTE, class Result>
468 shared_ptr<dcp::SubtitleAsset> asset,
469 int64_t picture_duration,
470 shared_ptr<dcp::Reel> reel,
473 optional<string> content_summary,
474 list<ReferencedReelAsset> const & refs,
475 shared_ptr<const Film> film,
476 DCPTimePeriod period,
477 boost::filesystem::path output_dcp,
481 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
483 shared_ptr<Result> reel_asset;
486 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
487 auto directory = output_dcp / interop->id ();
488 dcp::filesystem::create_directories(directory);
489 interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
490 reel_asset = make_shared<Interop> (
492 dcp::Fraction(film->video_frame_rate(), 1),
496 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
497 /* All our assets should be the same length; use the picture asset length here
498 as a reference to set the subtitle one. We'll use the duration rather than
499 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
500 just interested in its presentation length.
502 smpte->set_intrinsic_duration(picture_duration);
504 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
506 reel_asset = make_shared<SMPTE> (
508 dcp::Fraction(film->video_frame_rate(), 1),
515 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
517 auto k = dynamic_pointer_cast<Result> (j.asset);
518 if (k && j.period == period) {
520 /* If we have a hash for this asset in the CPL, assume that it is correct */
522 k->asset_ref()->set_hash (k->hash().get());
529 if (!text_only && reel_asset->actual_duration() != period_duration) {
530 throw ProgrammingError (
532 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
535 reel->add (reel_asset);
542 shared_ptr<dcp::ReelPictureAsset>
543 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
545 shared_ptr<dcp::ReelPictureAsset> reel_asset;
547 if (_picture_asset) {
548 /* We have made a picture asset of our own. Put it into the reel */
549 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
551 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
554 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
556 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
559 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
560 /* We don't have a picture asset of our own; hopefully we have one to reference */
562 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
564 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
566 if (k && j.period == _period) {
572 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
574 DCPOMATIC_ASSERT (reel_asset);
575 if (reel_asset->duration() != period_duration) {
576 throw ProgrammingError (
578 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
581 reel->add (reel_asset);
583 /* If we have a hash for this asset in the CPL, assume that it is correct */
584 if (reel_asset->hash()) {
585 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
593 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
595 shared_ptr<dcp::ReelSoundAsset> reel_asset;
598 /* We have made a sound asset of our own. Put it into the reel */
599 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
601 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
602 /* We don't have a sound asset of our own; hopefully we have one to reference */
604 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
606 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
608 if (k && j.period == _period) {
610 /* If we have a hash for this asset in the CPL, assume that it is correct */
612 k->asset_ref()->set_hash (k->hash().get());
618 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
620 DCPOMATIC_ASSERT (reel_asset);
621 if (reel_asset->actual_duration() != period_duration) {
623 "Reel sound asset has length %1 but reel period is %2",
624 reel_asset->actual_duration(),
627 if (reel_asset->actual_duration() != period_duration) {
628 throw ProgrammingError (
630 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
635 reel->add (reel_asset);
640 ReelWriter::create_reel_text (
641 shared_ptr<dcp::Reel> reel,
642 list<ReferencedReelAsset> const & refs,
644 boost::filesystem::path output_dcp,
645 bool ensure_subtitles,
646 set<DCPTextTrack> ensure_closed_captions
649 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
650 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
653 if (!subtitle && ensure_subtitles) {
654 /* We had no subtitle asset, but we've been asked to make sure there is one */
655 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
656 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
671 /* We have a subtitle asset that we either made or are referencing */
672 if (auto main_language = film()->subtitle_languages().first) {
673 subtitle->set_language (*main_language);
677 for (auto const& i: _closed_caption_assets) {
678 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
679 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
681 DCPOMATIC_ASSERT (a);
682 a->set_annotation_text (i.first.name);
683 if (i.first.language) {
684 a->set_language (i.first.language.get());
687 ensure_closed_captions.erase (i.first);
690 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
691 for (auto i: ensure_closed_captions) {
692 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
693 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
705 DCPOMATIC_ASSERT (a);
706 a->set_annotation_text (i.name);
708 a->set_language (i.language.get());
715 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
717 auto markers = film()->markers();
718 film()->add_ffoc_lfoc(markers);
719 Film::Markers reel_markers;
720 for (auto const& i: markers) {
721 if (_period.contains(i.second)) {
722 reel_markers[i.first] = i.second;
726 if (!reel_markers.empty ()) {
727 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
728 for (auto const& i: reel_markers) {
729 DCPTime relative = i.second - _period.from;
730 auto hmsf = relative.split (film()->video_frame_rate());
731 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
738 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
739 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
741 shared_ptr<dcp::Reel>
742 ReelWriter::create_reel (
743 list<ReferencedReelAsset> const & refs,
744 boost::filesystem::path output_dcp,
745 bool ensure_subtitles,
746 set<DCPTextTrack> ensure_closed_captions
749 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
751 auto reel = make_shared<dcp::Reel>();
753 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
754 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
755 * how big they are, we don't care about that.
757 int64_t duration = 0;
759 auto reel_picture_asset = create_reel_picture (reel, refs);
760 duration = reel_picture_asset->actual_duration ();
761 create_reel_sound (reel, refs);
762 if (!film()->interop()) {
763 create_reel_markers(reel);
767 create_reel_text(reel, refs, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
770 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
777 /** @param set_progress Method to call with progress; first parameter is the number of bytes
778 * done, second parameter is the number of bytes in total.
781 ReelWriter::calculate_digests(std::function<void (int64_t, int64_t)> set_progress)
784 vector<shared_ptr<const dcp::Asset>> assets;
786 if (_picture_asset) {
787 assets.push_back(_picture_asset);
790 assets.push_back(_sound_asset);
793 assets.push_back(_atmos_asset);
796 int64_t total_size = 0;
797 for (auto asset: assets) {
798 total_size += asset->file() ? boost::filesystem::file_size(*asset->file()) : 0;
801 int64_t total_done = 0;
802 for (auto asset: assets) {
803 asset->hash([&total_done, total_size, set_progress](int64_t done, int64_t) {
804 set_progress(total_done + done, total_size);
806 total_done += asset->file() ? boost::filesystem::file_size(*asset->file()) : 0;
809 } catch (boost::thread_interrupted) {
810 /* set_progress contains an interruption_point, so any of these methods
811 * may throw thread_interrupted, at which point we just give up.
817 ReelWriter::start () const
819 return _period.from.frames_floor (film()->video_frame_rate());
824 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
826 if (!_sound_asset_writer) {
830 DCPOMATIC_ASSERT (audio);
831 _sound_asset_writer->write(audio->data(), audio->channels(), audio->frames());
835 shared_ptr<dcp::SubtitleAsset>
836 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
838 shared_ptr<dcp::SubtitleAsset> asset;
839 optional<string> font;
841 auto lang = film()->subtitle_languages();
842 if (film()->interop()) {
843 auto s = make_shared<dcp::InteropSubtitleAsset>();
844 s->set_movie_title (film()->name());
845 if (type == TextType::OPEN_SUBTITLE) {
846 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
847 } else if (track->language) {
848 s->set_language (track->language->to_string());
850 s->set_reel_number (raw_convert<string> (_reel_index + 1));
853 auto s = make_shared<dcp::SMPTESubtitleAsset>();
854 s->set_content_title_text (film()->name());
855 s->set_metadata (mxf_metadata());
856 if (type == TextType::OPEN_SUBTITLE && lang.first) {
857 s->set_language (*lang.first);
858 } else if (track && track->language) {
859 s->set_language (dcp::LanguageTag(track->language->to_string()));
861 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
862 s->set_reel_number (_reel_index + 1);
863 s->set_time_code_rate (film()->video_frame_rate());
864 s->set_start_time (dcp::Time ());
865 if (film()->encrypted()) {
866 s->set_key (film()->key());
873 std::make_shared<dcp::SubtitleString>(
881 dcp::Time(0, 0, 0, 0, 24),
882 dcp::Time(0, 0, 1, 0, 24),
895 std::vector<dcp::Ruby>()
899 if (!film()->interop()) {
900 /* We must have a LoadFont since we have a Text */
902 asset->ensure_font(*font, _default_font);
911 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const
913 if (dcp::uses_baseline(subtitle.valign_standard) == dcp::uses_baseline(to)) {
914 /* The from and to standards use the same alignment reference */
915 return subtitle.v_position();
918 auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
919 auto const height = _font_metrics.height(subtitle);
921 float correction = 0;
922 switch (subtitle.v_align()) {
923 case dcp::VAlign::TOP:
924 correction = height - baseline_to_bottom;
926 case dcp::VAlign::CENTER:
927 correction = (height / 2) - baseline_to_bottom;
929 case dcp::VAlign::BOTTOM:
930 correction = baseline_to_bottom;
934 return subtitle.v_position() + (dcp::uses_bounding_box(subtitle.valign_standard) ? correction : -correction);
939 ReelWriter::write(PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts, shared_ptr<dcpomatic::Font> chosen_interop_font)
941 shared_ptr<dcp::SubtitleAsset> asset;
944 case TextType::OPEN_SUBTITLE:
945 asset = _subtitle_asset;
947 case TextType::CLOSED_CAPTION:
948 DCPOMATIC_ASSERT (track);
949 asset = _closed_caption_assets[*track];
952 DCPOMATIC_ASSERT (false);
956 asset = empty_text_asset (type, track, false);
960 case TextType::OPEN_SUBTITLE:
961 _subtitle_asset = asset;
963 case TextType::CLOSED_CAPTION:
964 DCPOMATIC_ASSERT (track);
965 _closed_caption_assets[*track] = asset;
968 DCPOMATIC_ASSERT (false);
971 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
972 auto const tcr = 1000;
974 for (auto i: subs.string) {
975 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
976 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
977 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::SubtitleStandard::INTEROP : dcp::SubtitleStandard::SMPTE_2014));
978 auto sub = make_shared<dcp::SubtitleString>(i);
979 /* i.font is a shared_ptr<Font> which uniquely identifies the font we want,
980 * though if we are Interop we can only have one font, so we'll use the chosen
983 auto font = film()->interop() ? chosen_interop_font : i.font;
984 /* We can get the corresponding ID from fonts */
985 auto const font_id_to_use = fonts.get(font);
986 /* Give this subtitle the correct font ID */
987 sub->set_font(font_id_to_use);
989 /* Make sure the asset LoadFonts the font we just asked for */
990 asset->ensure_font(font_id_to_use, font->data().get_value_or(_default_font));
993 for (auto i: subs.bitmap) {
995 make_shared<dcp::SubtitleImage>(
996 image_as_png(i.image),
997 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
998 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
999 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
1000 dcp::Time(), dcp::Time()
1008 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
1010 LOG_GENERAL ("Checking existing picture frame %1", frame);
1012 /* Read the data from the info file; for 3D we just check the left
1013 frames until we find a good one.
1015 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
1019 /* Read the data from the asset and hash it */
1020 asset_file.seek(info.offset, SEEK_SET);
1021 ArrayData data (info.size);
1022 size_t const read = asset_file.read(data.data(), 1, data.size());
1023 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
1024 if (read != static_cast<size_t> (data.size ())) {
1025 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
1029 digester.add (data.data(), data.size());
1030 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
1031 if (digester.get() != info.hash) {
1032 LOG_GENERAL ("Existing frame %1 failed hash check", frame);