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"
31 #include "frame_info.h"
33 #include "image_png.h"
36 #include "reel_writer.h"
37 #include "remembered_asset.h"
38 #include <dcp/atmos_asset.h>
39 #include <dcp/atmos_asset_writer.h>
40 #include <dcp/certificate_chain.h>
43 #include <dcp/filesystem.h>
44 #include <dcp/interop_subtitle_asset.h>
45 #include <dcp/mono_j2k_picture_asset.h>
46 #include <dcp/raw_convert.h>
48 #include <dcp/reel_atmos_asset.h>
49 #include <dcp/reel_interop_closed_caption_asset.h>
50 #include <dcp/reel_interop_subtitle_asset.h>
51 #include <dcp/reel_markers_asset.h>
52 #include <dcp/reel_mono_picture_asset.h>
53 #include <dcp/reel_smpte_closed_caption_asset.h>
54 #include <dcp/reel_smpte_subtitle_asset.h>
55 #include <dcp/reel_sound_asset.h>
56 #include <dcp/reel_stereo_picture_asset.h>
57 #include <dcp/smpte_subtitle_asset.h>
58 #include <dcp/sound_asset.h>
59 #include <dcp/sound_asset_writer.h>
60 #include <dcp/stereo_j2k_picture_asset.h>
61 #include <dcp/subtitle_image.h>
66 using std::dynamic_pointer_cast;
69 using std::make_shared;
72 using std::shared_ptr;
76 using boost::optional;
77 #if BOOST_VERSION >= 106100
78 using namespace boost::placeholders;
82 using dcp::raw_convert;
83 using namespace dcpomatic;
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, boost::filesystem::path output_dir
112 : WeakConstFilm(weak_film)
113 , _output_dir(std::move(output_dir))
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 _default_font = dcp::ArrayData(default_font_file());
128 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
130 auto remembered_assets = film()->read_remembered_assets();
131 DCPOMATIC_ASSERT(film()->directory());
133 auto existing_asset_filename = find_asset(remembered_assets, *film()->directory(), period, film()->video_identifier());
134 if (existing_asset_filename) {
135 _first_nonexistent_frame = check_existing_picture_asset(*existing_asset_filename);
138 if (_first_nonexistent_frame < period.duration().frames_round(film()->video_frame_rate())) {
139 /* No existing asset, or an incomplete one */
141 auto const rate = dcp::Fraction(film()->video_frame_rate(), 1);
143 auto setup = [this](shared_ptr<dcp::PictureAsset> asset) {
144 asset->set_size(film()->frame_size());
145 asset->set_metadata(mxf_metadata());
147 if (film()->encrypted()) {
148 asset->set_key(film()->key());
149 asset->set_context_id(film()->context_id());
153 shared_ptr<dcp::PictureAsset> picture_asset;
155 if (film()->video_encoding() == VideoEncoding::JPEG2000) {
156 if (film()->three_d()) {
157 _j2k_picture_asset = std::make_shared<dcp::StereoJ2KPictureAsset>(rate, standard);
159 _j2k_picture_asset = std::make_shared<dcp::MonoJ2KPictureAsset>(rate, standard);
161 setup(_j2k_picture_asset);
162 picture_asset = _j2k_picture_asset;
164 _mpeg2_picture_asset = std::make_shared<dcp::MonoMPEG2PictureAsset>(rate);
165 setup(_mpeg2_picture_asset);
166 picture_asset = _mpeg2_picture_asset;
169 auto new_asset_filename = _output_dir / video_asset_filename(picture_asset, _reel_index, _reel_count, _content_summary);
170 if (_first_nonexistent_frame > 0) {
171 LOG_GENERAL("Re-using partial asset %1: has frames up to %2", *existing_asset_filename, _first_nonexistent_frame);
172 dcp::filesystem::rename(*existing_asset_filename, new_asset_filename);
174 remembered_assets.push_back(RememberedAsset(new_asset_filename.filename(), period, film()->video_identifier()));
175 film()->write_remembered_assets(remembered_assets);
176 picture_asset->set_file(new_asset_filename);
178 dcp::Behaviour const behaviour = _first_nonexistent_frame > 0 ? dcp::Behaviour::OVERWRITE_EXISTING : dcp::Behaviour::MAKE_NEW;
179 if (_j2k_picture_asset) {
180 _j2k_picture_asset_writer = _j2k_picture_asset->start_write(new_asset_filename, behaviour);
182 _mpeg2_picture_asset_writer = _mpeg2_picture_asset->start_write(new_asset_filename, behaviour);
185 LOG_GENERAL("Re-using complete asset %1", *existing_asset_filename);
186 /* We already have a complete picture asset that we can just re-use */
187 /* XXX: what about if the encryption key changes? */
188 auto new_asset_filename = _output_dir / existing_asset_filename->filename();
189 dcp::filesystem::copy(*existing_asset_filename, new_asset_filename);
190 remembered_assets.push_back(RememberedAsset(new_asset_filename, period, film()->video_identifier()));
191 film()->write_remembered_assets(remembered_assets);
193 if (film()->video_encoding() == VideoEncoding::JPEG2000) {
194 if (film()->three_d()) {
195 _j2k_picture_asset = make_shared<dcp::StereoJ2KPictureAsset>(new_asset_filename);
197 _j2k_picture_asset = make_shared<dcp::MonoJ2KPictureAsset>(new_asset_filename);
200 _mpeg2_picture_asset = make_shared<dcp::MonoMPEG2PictureAsset>(new_asset_filename);
204 if (film()->audio_channels()) {
205 auto lang = film()->audio_language();
206 _sound_asset = make_shared<dcp::SoundAsset> (
207 dcp::Fraction(film()->video_frame_rate(), 1),
208 film()->audio_frame_rate(),
209 film()->audio_channels(),
210 lang ? *lang : dcp::LanguageTag("en-US"),
214 _sound_asset->set_metadata (mxf_metadata());
216 if (film()->encrypted()) {
217 _sound_asset->set_key (film()->key());
220 DCPOMATIC_ASSERT (film()->directory());
222 std::vector<dcp::Channel> extra_active_channels;
223 for (auto channel: std::vector<dcp::Channel>{dcp::Channel::HI, dcp::Channel::VI, dcp::Channel::BSL, dcp::Channel::BSR}) {
224 if (channel_is_mapped(film(), channel)) {
225 extra_active_channels.push_back(channel);
229 /* Write the sound asset into the film directory so that we leave the creation
230 of the DCP directory until the last minute.
232 _sound_asset_writer = _sound_asset->start_write (
233 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
234 extra_active_channels,
235 film()->contains_atmos_content() ? dcp::SoundAsset::AtmosSync::ENABLED : dcp::SoundAsset::AtmosSync::DISABLED,
236 film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
243 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
245 auto job = _job.lock ();
248 job->sub (_("Checking existing image data"));
251 /* Try to open the existing asset */
252 dcp::File asset_file(asset, "rb");
254 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
257 LOG_GENERAL ("Opened existing asset at %1", asset.string());
260 shared_ptr<InfoFileHandle> info_file;
263 info_file = film()->info_file_handle (_period, true);
264 } catch (OpenFileError &) {
265 LOG_GENERAL_NC ("Could not open film info file");
269 /* Offset of the last dcp::FrameInfo in the info file */
270 int const n = (dcp::filesystem::file_size(info_file->get().path()) / J2KFrameInfo::size_on_disk()) - 1;
271 LOG_GENERAL("The last FI is %1; info file is %2, info size %3", n, dcp::filesystem::file_size(info_file->get().path()), J2KFrameInfo::size_on_disk())
273 Frame first_nonexistent_frame;
274 if (film()->three_d()) {
275 /* Start looking at the last left frame */
276 first_nonexistent_frame = n / 2;
278 first_nonexistent_frame = n;
281 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
282 --first_nonexistent_frame;
285 if (!film()->three_d() && first_nonexistent_frame > 0) {
286 /* If we are doing 3D we might have found a good L frame with no R, so only
287 do this if we're in 2D and we've just found a good B(oth) frame.
289 ++first_nonexistent_frame;
292 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
294 return first_nonexistent_frame;
299 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
301 if (!_j2k_picture_asset_writer) {
302 /* We're not writing any data */
306 auto fin = J2KFrameInfo(_j2k_picture_asset_writer->write(encoded->data(), encoded->size()));
307 fin.write(film()->info_file_handle(_period, false), frame, eyes);
308 _last_written[eyes] = encoded;
313 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
316 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
317 if (film()->encrypted()) {
318 _atmos_asset->set_key(film()->key());
320 _atmos_asset_writer = _atmos_asset->start_write (
321 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
324 _atmos_asset_writer->write (atmos);
329 ReelWriter::write(shared_ptr<dcp::MonoMPEG2PictureFrame> image)
331 _mpeg2_picture_asset_writer->write(image->data(), image->size());
336 ReelWriter::fake_write(dcp::J2KFrameInfo const& info)
338 if (!_j2k_picture_asset_writer) {
339 /* We're not writing any data */
343 _j2k_picture_asset_writer->fake_write(info);
348 ReelWriter::repeat_write (Frame frame, Eyes eyes)
350 if (!_j2k_picture_asset_writer) {
351 /* We're not writing any data */
355 auto fin = J2KFrameInfo(_j2k_picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size()));
356 fin.write(film()->info_file_handle(_period, false), frame, eyes);
361 ReelWriter::finish (boost::filesystem::path output_dcp)
363 if (_j2k_picture_asset_writer && !_j2k_picture_asset_writer->finalize()) {
364 /* Nothing was written to the J2K picture asset */
365 LOG_GENERAL("Nothing was written to J2K asset for reel %1 of %2", _reel_index, _reel_count);
366 _j2k_picture_asset.reset();
369 if (_mpeg2_picture_asset_writer && !_mpeg2_picture_asset_writer->finalize()) {
370 /* Nothing was written to the MPEG2 picture asset */
371 LOG_GENERAL("Nothing was written to MPEG2 asset for reel %1 of %2", _reel_index, _reel_count);
372 _mpeg2_picture_asset.reset();
375 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
376 /* Nothing was written to the sound asset */
377 _sound_asset.reset ();
380 /* Move the audio asset into the DCP */
382 boost::filesystem::path audio_to = output_dcp;
383 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
386 boost::system::error_code ec;
387 dcp::filesystem::rename(film()->file(aaf), audio_to, ec);
390 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
394 _sound_asset->set_file (audio_to);
398 _atmos_asset_writer->finalize ();
399 boost::filesystem::path atmos_to = output_dcp;
400 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
403 boost::system::error_code ec;
404 dcp::filesystem::rename(film()->file(aaf), atmos_to, ec);
407 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
411 _atmos_asset->set_file (atmos_to);
416 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
417 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
419 template <class Interop, class SMPTE, class Result>
422 shared_ptr<dcp::SubtitleAsset> asset,
423 int64_t picture_duration,
424 shared_ptr<dcp::Reel> reel,
427 optional<string> content_summary,
428 list<ReferencedReelAsset> const & refs,
429 shared_ptr<const Film> film,
430 DCPTimePeriod period,
431 boost::filesystem::path output_dcp,
435 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
437 shared_ptr<Result> reel_asset;
440 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
441 auto directory = output_dcp / interop->id ();
442 dcp::filesystem::create_directories(directory);
443 interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
444 reel_asset = make_shared<Interop> (
446 dcp::Fraction(film->video_frame_rate(), 1),
450 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
451 /* All our assets should be the same length; use the picture asset length here
452 as a reference to set the subtitle one. We'll use the duration rather than
453 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
454 just interested in its presentation length.
456 smpte->set_intrinsic_duration(picture_duration);
458 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
460 reel_asset = make_shared<SMPTE> (
462 dcp::Fraction(film->video_frame_rate(), 1),
469 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
471 auto k = dynamic_pointer_cast<Result> (j.asset);
472 if (k && j.period == period) {
474 /* If we have a hash for this asset in the CPL, assume that it is correct */
476 k->asset_ref()->set_hash (k->hash().get());
483 if (!text_only && reel_asset->actual_duration() != period_duration) {
484 throw ProgrammingError (
486 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
489 reel->add (reel_asset);
496 shared_ptr<dcp::ReelPictureAsset>
497 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
499 shared_ptr<dcp::ReelPictureAsset> reel_asset;
501 if (_j2k_picture_asset) {
502 /* We have made a picture asset of our own. Put it into the reel */
503 if (auto mono = dynamic_pointer_cast<dcp::MonoJ2KPictureAsset>(_j2k_picture_asset)) {
504 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
507 if (auto stereo = dynamic_pointer_cast<dcp::StereoJ2KPictureAsset>(_j2k_picture_asset)) {
508 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
510 } else if (_mpeg2_picture_asset) {
511 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(_mpeg2_picture_asset, 0);
513 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
514 /* We don't have a picture asset of our own; hopefully we have one to reference */
516 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
518 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
520 if (k && j.period == _period) {
526 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
528 DCPOMATIC_ASSERT (reel_asset);
529 if (reel_asset->duration() != period_duration) {
530 throw ProgrammingError (
532 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
535 reel->add (reel_asset);
537 /* If we have a hash for this asset in the CPL, assume that it is correct */
538 if (reel_asset->hash()) {
539 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
547 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
549 shared_ptr<dcp::ReelSoundAsset> reel_asset;
552 /* We have made a sound asset of our own. Put it into the reel */
553 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
555 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
556 /* We don't have a sound asset of our own; hopefully we have one to reference */
558 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
560 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
562 if (k && j.period == _period) {
564 /* If we have a hash for this asset in the CPL, assume that it is correct */
566 k->asset_ref()->set_hash (k->hash().get());
572 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
574 DCPOMATIC_ASSERT (reel_asset);
575 if (reel_asset->actual_duration() != period_duration) {
577 "Reel sound asset has length %1 but reel period is %2",
578 reel_asset->actual_duration(),
581 if (reel_asset->actual_duration() != period_duration) {
582 throw ProgrammingError (
584 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
589 reel->add (reel_asset);
594 ReelWriter::create_reel_text (
595 shared_ptr<dcp::Reel> reel,
596 list<ReferencedReelAsset> const & refs,
598 boost::filesystem::path output_dcp,
599 bool ensure_subtitles,
600 set<DCPTextTrack> ensure_closed_captions
603 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
604 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
607 if (!subtitle && ensure_subtitles) {
608 /* We had no subtitle asset, but we've been asked to make sure there is one */
609 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
610 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
625 /* We have a subtitle asset that we either made or are referencing */
626 if (auto main_language = film()->subtitle_languages().first) {
627 subtitle->set_language (*main_language);
631 for (auto const& i: _closed_caption_assets) {
632 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
633 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
635 DCPOMATIC_ASSERT (a);
636 a->set_annotation_text (i.first.name);
637 if (i.first.language) {
638 a->set_language (i.first.language.get());
641 ensure_closed_captions.erase (i.first);
644 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
645 for (auto i: ensure_closed_captions) {
646 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
647 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
659 DCPOMATIC_ASSERT (a);
660 a->set_annotation_text (i.name);
662 a->set_language (i.language.get());
669 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
671 auto markers = film()->markers();
672 film()->add_ffoc_lfoc(markers);
673 Film::Markers reel_markers;
674 for (auto const& i: markers) {
675 if (_period.contains(i.second)) {
676 reel_markers[i.first] = i.second;
680 if (!reel_markers.empty ()) {
681 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
682 for (auto const& i: reel_markers) {
683 DCPTime relative = i.second - _period.from;
684 auto hmsf = relative.split (film()->video_frame_rate());
685 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
692 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
693 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
695 shared_ptr<dcp::Reel>
696 ReelWriter::create_reel (
697 list<ReferencedReelAsset> const & refs,
698 boost::filesystem::path output_dcp,
699 bool ensure_subtitles,
700 set<DCPTextTrack> ensure_closed_captions
703 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
705 auto reel = make_shared<dcp::Reel>();
707 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
708 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
709 * how big they are, we don't care about that.
711 int64_t duration = 0;
713 auto reel_picture_asset = create_reel_picture (reel, refs);
714 duration = reel_picture_asset->actual_duration ();
715 create_reel_sound (reel, refs);
716 if (!film()->interop()) {
717 create_reel_markers(reel);
721 create_reel_text(reel, refs, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
724 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
731 /** @param set_progress Method to call with progress; first parameter is the number of bytes
732 * done, second parameter is the number of bytes in total.
735 ReelWriter::calculate_digests(std::function<void (int64_t, int64_t)> set_progress)
738 vector<shared_ptr<const dcp::Asset>> assets;
740 if (_j2k_picture_asset) {
741 assets.push_back(_j2k_picture_asset);
743 if (_mpeg2_picture_asset) {
744 assets.push_back(_mpeg2_picture_asset);
747 assets.push_back(_sound_asset);
750 assets.push_back(_atmos_asset);
753 int64_t total_size = 0;
754 for (auto asset: assets) {
755 total_size += asset->file() ? boost::filesystem::file_size(*asset->file()) : 0;
758 int64_t total_done = 0;
759 for (auto asset: assets) {
760 asset->hash([&total_done, total_size, set_progress](int64_t done, int64_t) {
761 set_progress(total_done + done, total_size);
763 total_done += asset->file() ? boost::filesystem::file_size(*asset->file()) : 0;
766 } catch (boost::thread_interrupted) {
767 /* set_progress contains an interruption_point, so any of these methods
768 * may throw thread_interrupted, at which point we just give up.
774 ReelWriter::start () const
776 return _period.from.frames_floor (film()->video_frame_rate());
781 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
783 if (!_sound_asset_writer) {
787 DCPOMATIC_ASSERT (audio);
788 _sound_asset_writer->write(audio->data(), audio->channels(), audio->frames());
792 shared_ptr<dcp::SubtitleAsset>
793 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
795 shared_ptr<dcp::SubtitleAsset> asset;
796 optional<string> font;
798 auto lang = film()->subtitle_languages();
799 if (film()->interop()) {
800 auto s = make_shared<dcp::InteropSubtitleAsset>();
801 s->set_movie_title (film()->name());
802 if (type == TextType::OPEN_SUBTITLE) {
803 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
804 } else if (track->language) {
805 s->set_language (track->language->to_string());
807 s->set_reel_number (raw_convert<string> (_reel_index + 1));
810 auto s = make_shared<dcp::SMPTESubtitleAsset>();
811 s->set_content_title_text (film()->name());
812 s->set_metadata (mxf_metadata());
813 if (type == TextType::OPEN_SUBTITLE && lang.first) {
814 s->set_language (*lang.first);
815 } else if (track && track->language) {
816 s->set_language (dcp::LanguageTag(track->language->to_string()));
818 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
819 s->set_reel_number (_reel_index + 1);
820 s->set_time_code_rate (film()->video_frame_rate());
821 s->set_start_time (dcp::Time ());
822 if (film()->encrypted()) {
823 s->set_key (film()->key());
830 std::make_shared<dcp::SubtitleString>(
838 dcp::Time(0, 0, 0, 0, 24),
839 dcp::Time(0, 0, 1, 0, 24),
852 std::vector<dcp::Ruby>()
856 if (!film()->interop()) {
857 /* We must have a LoadFont since we have a Text */
859 asset->ensure_font(*font, _default_font);
868 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const
870 if (dcp::uses_baseline(subtitle.valign_standard) == dcp::uses_baseline(to)) {
871 /* The from and to standards use the same alignment reference */
872 return subtitle.v_position();
875 auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
876 auto const height = _font_metrics.height(subtitle);
878 float correction = 0;
879 switch (subtitle.v_align()) {
880 case dcp::VAlign::TOP:
881 correction = height - baseline_to_bottom;
883 case dcp::VAlign::CENTER:
884 correction = (height / 2) - baseline_to_bottom;
886 case dcp::VAlign::BOTTOM:
887 correction = baseline_to_bottom;
891 return subtitle.v_position() + (dcp::uses_bounding_box(subtitle.valign_standard) ? correction : -correction);
896 ReelWriter::write(PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts, shared_ptr<dcpomatic::Font> chosen_interop_font)
898 shared_ptr<dcp::SubtitleAsset> asset;
901 case TextType::OPEN_SUBTITLE:
902 asset = _subtitle_asset;
904 case TextType::CLOSED_CAPTION:
905 DCPOMATIC_ASSERT (track);
906 asset = _closed_caption_assets[*track];
909 DCPOMATIC_ASSERT (false);
913 asset = empty_text_asset (type, track, false);
917 case TextType::OPEN_SUBTITLE:
918 _subtitle_asset = asset;
920 case TextType::CLOSED_CAPTION:
921 DCPOMATIC_ASSERT (track);
922 _closed_caption_assets[*track] = asset;
925 DCPOMATIC_ASSERT (false);
928 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
929 auto const tcr = 1000;
931 for (auto i: subs.string) {
932 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
933 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
934 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::SubtitleStandard::INTEROP : dcp::SubtitleStandard::SMPTE_2014));
935 auto sub = make_shared<dcp::SubtitleString>(i);
936 /* i.font is a shared_ptr<Font> which uniquely identifies the font we want,
937 * though if we are Interop we can only have one font, so we'll use the chosen
940 auto font = film()->interop() ? chosen_interop_font : i.font;
941 /* We can get the corresponding ID from fonts */
942 auto const font_id_to_use = fonts.get(font);
943 /* Give this subtitle the correct font ID */
944 sub->set_font(font_id_to_use);
946 /* Make sure the asset LoadFonts the font we just asked for */
947 asset->ensure_font(font_id_to_use, font->data().get_value_or(_default_font));
950 for (auto i: subs.bitmap) {
952 make_shared<dcp::SubtitleImage>(
953 image_as_png(i.image),
954 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
955 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
956 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
957 dcp::Time(), dcp::Time()
965 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
967 LOG_GENERAL ("Checking existing picture frame %1", frame);
969 /* Read the data from the info file; for 3D we just check the left
970 frames until we find a good one.
972 auto const info = J2KFrameInfo(info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
976 /* Read the data from the asset and hash it */
977 asset_file.seek(info.offset, SEEK_SET);
978 ArrayData data (info.size);
979 size_t const read = asset_file.read(data.data(), 1, data.size());
980 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
981 if (read != static_cast<size_t> (data.size ())) {
982 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
986 digester.add (data.data(), data.size());
987 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
988 if (digester.get() != info.hash) {
989 LOG_GENERAL ("Existing frame %1 failed hash check", frame);