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 <dcp/atmos_asset.h>
38 #include <dcp/atmos_asset_writer.h>
39 #include <dcp/certificate_chain.h>
42 #include <dcp/filesystem.h>
43 #include <dcp/interop_subtitle_asset.h>
44 #include <dcp/mono_j2k_picture_asset.h>
45 #include <dcp/raw_convert.h>
47 #include <dcp/reel_atmos_asset.h>
48 #include <dcp/reel_interop_closed_caption_asset.h>
49 #include <dcp/reel_interop_subtitle_asset.h>
50 #include <dcp/reel_markers_asset.h>
51 #include <dcp/reel_mono_picture_asset.h>
52 #include <dcp/reel_smpte_closed_caption_asset.h>
53 #include <dcp/reel_smpte_subtitle_asset.h>
54 #include <dcp/reel_sound_asset.h>
55 #include <dcp/reel_stereo_picture_asset.h>
56 #include <dcp/smpte_subtitle_asset.h>
57 #include <dcp/sound_asset.h>
58 #include <dcp/sound_asset_writer.h>
59 #include <dcp/stereo_j2k_picture_asset.h>
60 #include <dcp/subtitle_image.h>
65 using std::dynamic_pointer_cast;
68 using std::make_shared;
71 using std::shared_ptr;
75 using boost::optional;
76 #if BOOST_VERSION >= 106100
77 using namespace boost::placeholders;
81 using dcp::raw_convert;
82 using namespace dcpomatic;
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 (dcp::filesystem::exists(asset) && dcp::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 dcp::filesystem::copy_file(asset, asset.string() + ".tmp");
145 dcp::filesystem::remove(asset);
146 dcp::filesystem::rename(asset.string() + ".tmp", asset);
149 auto const rate = dcp::Fraction(film()->video_frame_rate(), 1);
151 auto setup = [this](shared_ptr<dcp::PictureAsset> asset) {
152 asset->set_size(film()->frame_size());
153 asset->set_metadata(mxf_metadata());
155 if (film()->encrypted()) {
156 asset->set_key(film()->key());
157 asset->set_context_id(film()->context_id());
161 if (film()->video_encoding() == VideoEncoding::JPEG2000) {
162 if (film()->three_d()) {
163 _j2k_picture_asset = std::make_shared<dcp::StereoJ2KPictureAsset>(rate, standard);
165 _j2k_picture_asset = std::make_shared<dcp::MonoJ2KPictureAsset>(rate, standard);
167 setup(_j2k_picture_asset);
168 _j2k_picture_asset->set_file(asset);
169 _j2k_picture_asset_writer = _j2k_picture_asset->start_write(asset, _first_nonexistent_frame > 0 ? dcp::Behaviour::OVERWRITE_EXISTING : dcp::Behaviour::MAKE_NEW);
171 _mpeg2_picture_asset = std::make_shared<dcp::MonoMPEG2PictureAsset>(rate);
172 setup(_mpeg2_picture_asset);
173 _mpeg2_picture_asset->set_file(asset);
174 _mpeg2_picture_asset_writer = _mpeg2_picture_asset->start_write(asset, _first_nonexistent_frame > 0 ? dcp::Behaviour::OVERWRITE_EXISTING : dcp::Behaviour::MAKE_NEW);
177 } else if (!text_only) {
178 /* We already have a complete picture asset that we can just re-use */
179 /* XXX: what about if the encryption key changes? */
180 if (film()->video_encoding() == VideoEncoding::JPEG2000) {
181 if (film()->three_d()) {
182 _j2k_picture_asset = make_shared<dcp::StereoJ2KPictureAsset>(asset);
184 _j2k_picture_asset = make_shared<dcp::MonoJ2KPictureAsset>(asset);
187 _mpeg2_picture_asset = make_shared<dcp::MonoMPEG2PictureAsset>(asset);
191 if (film()->audio_channels()) {
192 auto lang = film()->audio_language();
193 _sound_asset = make_shared<dcp::SoundAsset> (
194 dcp::Fraction(film()->video_frame_rate(), 1),
195 film()->audio_frame_rate(),
196 film()->audio_channels(),
197 lang ? *lang : dcp::LanguageTag("en-US"),
201 _sound_asset->set_metadata (mxf_metadata());
203 if (film()->encrypted()) {
204 _sound_asset->set_key (film()->key());
207 DCPOMATIC_ASSERT (film()->directory());
209 std::vector<dcp::Channel> extra_active_channels;
210 for (auto channel: std::vector<dcp::Channel>{dcp::Channel::HI, dcp::Channel::VI, dcp::Channel::BSL, dcp::Channel::BSR}) {
211 if (channel_is_mapped(film(), channel)) {
212 extra_active_channels.push_back(channel);
216 /* Write the sound asset into the film directory so that we leave the creation
217 of the DCP directory until the last minute.
219 _sound_asset_writer = _sound_asset->start_write (
220 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
221 extra_active_channels,
222 film()->contains_atmos_content() ? dcp::SoundAsset::AtmosSync::ENABLED : dcp::SoundAsset::AtmosSync::DISABLED,
223 film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
227 _default_font = dcp::ArrayData(default_font_file());
232 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
234 auto job = _job.lock ();
237 job->sub (_("Checking existing image data"));
240 /* Try to open the existing asset */
241 dcp::File asset_file(asset, "rb");
243 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
246 LOG_GENERAL ("Opened existing asset at %1", asset.string());
249 shared_ptr<InfoFileHandle> info_file;
252 info_file = film()->info_file_handle (_period, true);
253 } catch (OpenFileError &) {
254 LOG_GENERAL_NC ("Could not open film info file");
258 /* Offset of the last dcp::FrameInfo in the info file */
259 int const n = (dcp::filesystem::file_size(info_file->get().path()) / J2KFrameInfo::size_on_disk()) - 1;
260 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())
262 Frame first_nonexistent_frame;
263 if (film()->three_d()) {
264 /* Start looking at the last left frame */
265 first_nonexistent_frame = n / 2;
267 first_nonexistent_frame = n;
270 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
271 --first_nonexistent_frame;
274 if (!film()->three_d() && first_nonexistent_frame > 0) {
275 /* If we are doing 3D we might have found a good L frame with no R, so only
276 do this if we're in 2D and we've just found a good B(oth) frame.
278 ++first_nonexistent_frame;
281 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
283 return first_nonexistent_frame;
288 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
290 if (!_j2k_picture_asset_writer) {
291 /* We're not writing any data */
295 auto fin = J2KFrameInfo(_j2k_picture_asset_writer->write(encoded->data(), encoded->size()));
296 fin.write(film()->info_file_handle(_period, false), frame, eyes);
297 _last_written[eyes] = encoded;
302 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
305 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
306 if (film()->encrypted()) {
307 _atmos_asset->set_key(film()->key());
309 _atmos_asset_writer = _atmos_asset->start_write (
310 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
313 _atmos_asset_writer->write (atmos);
318 ReelWriter::write(shared_ptr<dcp::MonoMPEG2PictureFrame> image)
320 _mpeg2_picture_asset_writer->write(image->data(), image->size());
325 ReelWriter::fake_write(dcp::J2KFrameInfo const& info)
327 if (!_j2k_picture_asset_writer) {
328 /* We're not writing any data */
332 _j2k_picture_asset_writer->fake_write(info);
337 ReelWriter::repeat_write (Frame frame, Eyes eyes)
339 if (!_j2k_picture_asset_writer) {
340 /* We're not writing any data */
344 auto fin = J2KFrameInfo(_j2k_picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size()));
345 fin.write(film()->info_file_handle(_period, false), frame, eyes);
350 ReelWriter::finish (boost::filesystem::path output_dcp)
352 if (_j2k_picture_asset_writer && !_j2k_picture_asset_writer->finalize()) {
353 /* Nothing was written to the J2K picture asset */
354 LOG_GENERAL("Nothing was written to J2K asset for reel %1 of %2", _reel_index, _reel_count);
355 _j2k_picture_asset.reset();
358 if (_mpeg2_picture_asset_writer && !_mpeg2_picture_asset_writer->finalize()) {
359 /* Nothing was written to the MPEG2 picture asset */
360 LOG_GENERAL("Nothing was written to MPEG2 asset for reel %1 of %2", _reel_index, _reel_count);
361 _mpeg2_picture_asset.reset();
364 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
365 /* Nothing was written to the sound asset */
366 _sound_asset.reset ();
369 shared_ptr<dcp::PictureAsset> picture_asset;
370 if (_j2k_picture_asset) {
371 picture_asset = _j2k_picture_asset;
372 } else if (_mpeg2_picture_asset) {
373 picture_asset = _mpeg2_picture_asset;
376 /* Hard-link any video asset file into the DCP */
378 auto const file = picture_asset->file();
379 DCPOMATIC_ASSERT(file);
381 auto video_from = *file;
382 auto video_to = output_dcp;
383 video_to /= video_asset_filename(picture_asset, _reel_index, _reel_count, _content_summary);
384 /* There may be an existing "to" file if we are recreating a DCP in the same place without
387 boost::system::error_code ec;
388 dcp::filesystem::remove(video_to, ec);
390 dcp::filesystem::create_hard_link(video_from, video_to, ec);
392 LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
393 auto job = _job.lock ();
395 job->sub (_("Copying video file into DCP"));
397 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
398 } catch (exception& e) {
399 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
400 throw FileError (e.what(), video_from);
403 dcp::filesystem::copy_file(video_from, video_to, ec);
405 LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
406 throw FileError (ec.message(), video_from);
411 picture_asset->set_file(video_to);
414 /* Move the audio asset into the DCP */
416 boost::filesystem::path audio_to = output_dcp;
417 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
420 boost::system::error_code ec;
421 dcp::filesystem::rename(film()->file(aaf), audio_to, ec);
424 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
428 _sound_asset->set_file (audio_to);
432 _atmos_asset_writer->finalize ();
433 boost::filesystem::path atmos_to = output_dcp;
434 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
437 boost::system::error_code ec;
438 dcp::filesystem::rename(film()->file(aaf), atmos_to, ec);
441 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
445 _atmos_asset->set_file (atmos_to);
450 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
451 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
453 template <class Interop, class SMPTE, class Result>
456 shared_ptr<dcp::SubtitleAsset> asset,
457 int64_t picture_duration,
458 shared_ptr<dcp::Reel> reel,
461 optional<string> content_summary,
462 list<ReferencedReelAsset> const & refs,
463 shared_ptr<const Film> film,
464 DCPTimePeriod period,
465 boost::filesystem::path output_dcp,
469 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
471 shared_ptr<Result> reel_asset;
474 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
475 auto directory = output_dcp / interop->id ();
476 dcp::filesystem::create_directories(directory);
477 interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
478 reel_asset = make_shared<Interop> (
480 dcp::Fraction(film->video_frame_rate(), 1),
484 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
485 /* All our assets should be the same length; use the picture asset length here
486 as a reference to set the subtitle one. We'll use the duration rather than
487 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
488 just interested in its presentation length.
490 smpte->set_intrinsic_duration(picture_duration);
492 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
494 reel_asset = make_shared<SMPTE> (
496 dcp::Fraction(film->video_frame_rate(), 1),
503 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
505 auto k = dynamic_pointer_cast<Result> (j.asset);
506 if (k && j.period == period) {
508 /* If we have a hash for this asset in the CPL, assume that it is correct */
510 k->asset_ref()->set_hash (k->hash().get());
517 if (!text_only && reel_asset->actual_duration() != period_duration) {
518 throw ProgrammingError (
520 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
523 reel->add (reel_asset);
530 shared_ptr<dcp::ReelPictureAsset>
531 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
533 shared_ptr<dcp::ReelPictureAsset> reel_asset;
535 if (_j2k_picture_asset) {
536 /* We have made a picture asset of our own. Put it into the reel */
537 if (auto mono = dynamic_pointer_cast<dcp::MonoJ2KPictureAsset>(_j2k_picture_asset)) {
538 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
541 if (auto stereo = dynamic_pointer_cast<dcp::StereoJ2KPictureAsset>(_j2k_picture_asset)) {
542 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
544 } else if (_mpeg2_picture_asset) {
545 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(_mpeg2_picture_asset, 0);
547 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
548 /* We don't have a picture asset of our own; hopefully we have one to reference */
550 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
552 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
554 if (k && j.period == _period) {
560 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
562 DCPOMATIC_ASSERT (reel_asset);
563 if (reel_asset->duration() != period_duration) {
564 throw ProgrammingError (
566 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
569 reel->add (reel_asset);
571 /* If we have a hash for this asset in the CPL, assume that it is correct */
572 if (reel_asset->hash()) {
573 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
581 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
583 shared_ptr<dcp::ReelSoundAsset> reel_asset;
586 /* We have made a sound asset of our own. Put it into the reel */
587 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
589 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
590 /* We don't have a sound asset of our own; hopefully we have one to reference */
592 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
594 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
596 if (k && j.period == _period) {
598 /* If we have a hash for this asset in the CPL, assume that it is correct */
600 k->asset_ref()->set_hash (k->hash().get());
606 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
608 DCPOMATIC_ASSERT (reel_asset);
609 if (reel_asset->actual_duration() != period_duration) {
611 "Reel sound asset has length %1 but reel period is %2",
612 reel_asset->actual_duration(),
615 if (reel_asset->actual_duration() != period_duration) {
616 throw ProgrammingError (
618 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
623 reel->add (reel_asset);
628 ReelWriter::create_reel_text (
629 shared_ptr<dcp::Reel> reel,
630 list<ReferencedReelAsset> const & refs,
632 boost::filesystem::path output_dcp,
633 bool ensure_subtitles,
634 set<DCPTextTrack> ensure_closed_captions
637 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
638 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
641 if (!subtitle && ensure_subtitles) {
642 /* We had no subtitle asset, but we've been asked to make sure there is one */
643 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
644 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
659 /* We have a subtitle asset that we either made or are referencing */
660 if (auto main_language = film()->subtitle_languages().first) {
661 subtitle->set_language (*main_language);
665 for (auto const& i: _closed_caption_assets) {
666 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
667 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
669 DCPOMATIC_ASSERT (a);
670 a->set_annotation_text (i.first.name);
671 if (i.first.language) {
672 a->set_language (i.first.language.get());
675 ensure_closed_captions.erase (i.first);
678 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
679 for (auto i: ensure_closed_captions) {
680 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
681 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
693 DCPOMATIC_ASSERT (a);
694 a->set_annotation_text (i.name);
696 a->set_language (i.language.get());
703 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
705 auto markers = film()->markers();
706 film()->add_ffoc_lfoc(markers);
707 Film::Markers reel_markers;
708 for (auto const& i: markers) {
709 if (_period.contains(i.second)) {
710 reel_markers[i.first] = i.second;
714 if (!reel_markers.empty ()) {
715 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
716 for (auto const& i: reel_markers) {
717 DCPTime relative = i.second - _period.from;
718 auto hmsf = relative.split (film()->video_frame_rate());
719 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
726 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
727 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
729 shared_ptr<dcp::Reel>
730 ReelWriter::create_reel (
731 list<ReferencedReelAsset> const & refs,
732 boost::filesystem::path output_dcp,
733 bool ensure_subtitles,
734 set<DCPTextTrack> ensure_closed_captions
737 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
739 auto reel = make_shared<dcp::Reel>();
741 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
742 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
743 * how big they are, we don't care about that.
745 int64_t duration = 0;
747 auto reel_picture_asset = create_reel_picture (reel, refs);
748 duration = reel_picture_asset->actual_duration ();
749 create_reel_sound (reel, refs);
750 if (!film()->interop()) {
751 create_reel_markers(reel);
755 create_reel_text(reel, refs, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
758 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
765 /** @param set_progress Method to call with progress; first parameter is the number of bytes
766 * done, second parameter is the number of bytes in total.
769 ReelWriter::calculate_digests(std::function<void (int64_t, int64_t)> set_progress)
772 vector<shared_ptr<const dcp::Asset>> assets;
774 if (_j2k_picture_asset) {
775 assets.push_back(_j2k_picture_asset);
777 if (_mpeg2_picture_asset) {
778 assets.push_back(_mpeg2_picture_asset);
781 assets.push_back(_sound_asset);
784 assets.push_back(_atmos_asset);
787 int64_t total_size = 0;
788 for (auto asset: assets) {
789 total_size += asset->file() ? boost::filesystem::file_size(*asset->file()) : 0;
792 int64_t total_done = 0;
793 for (auto asset: assets) {
794 asset->hash([&total_done, total_size, set_progress](int64_t done, int64_t) {
795 set_progress(total_done + done, total_size);
797 total_done += asset->file() ? boost::filesystem::file_size(*asset->file()) : 0;
800 } catch (boost::thread_interrupted) {
801 /* set_progress contains an interruption_point, so any of these methods
802 * may throw thread_interrupted, at which point we just give up.
808 ReelWriter::start () const
810 return _period.from.frames_floor (film()->video_frame_rate());
815 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
817 if (!_sound_asset_writer) {
821 DCPOMATIC_ASSERT (audio);
822 _sound_asset_writer->write(audio->data(), audio->channels(), audio->frames());
826 shared_ptr<dcp::SubtitleAsset>
827 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
829 shared_ptr<dcp::SubtitleAsset> asset;
830 optional<string> font;
832 auto lang = film()->subtitle_languages();
833 if (film()->interop()) {
834 auto s = make_shared<dcp::InteropSubtitleAsset>();
835 s->set_movie_title (film()->name());
836 if (type == TextType::OPEN_SUBTITLE) {
837 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
838 } else if (track->language) {
839 s->set_language (track->language->to_string());
841 s->set_reel_number (raw_convert<string> (_reel_index + 1));
844 auto s = make_shared<dcp::SMPTESubtitleAsset>();
845 s->set_content_title_text (film()->name());
846 s->set_metadata (mxf_metadata());
847 if (type == TextType::OPEN_SUBTITLE && lang.first) {
848 s->set_language (*lang.first);
849 } else if (track && track->language) {
850 s->set_language (dcp::LanguageTag(track->language->to_string()));
852 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
853 s->set_reel_number (_reel_index + 1);
854 s->set_time_code_rate (film()->video_frame_rate());
855 s->set_start_time (dcp::Time ());
856 if (film()->encrypted()) {
857 s->set_key (film()->key());
864 std::make_shared<dcp::SubtitleString>(
872 dcp::Time(0, 0, 0, 0, 24),
873 dcp::Time(0, 0, 1, 0, 24),
886 std::vector<dcp::Ruby>()
890 if (!film()->interop()) {
891 /* We must have a LoadFont since we have a Text */
893 asset->ensure_font(*font, _default_font);
902 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const
904 if (dcp::uses_baseline(subtitle.valign_standard) == dcp::uses_baseline(to)) {
905 /* The from and to standards use the same alignment reference */
906 return subtitle.v_position();
909 auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
910 auto const height = _font_metrics.height(subtitle);
912 float correction = 0;
913 switch (subtitle.v_align()) {
914 case dcp::VAlign::TOP:
915 correction = height - baseline_to_bottom;
917 case dcp::VAlign::CENTER:
918 correction = (height / 2) - baseline_to_bottom;
920 case dcp::VAlign::BOTTOM:
921 correction = baseline_to_bottom;
925 return subtitle.v_position() + (dcp::uses_bounding_box(subtitle.valign_standard) ? correction : -correction);
930 ReelWriter::write(PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts, shared_ptr<dcpomatic::Font> chosen_interop_font)
932 shared_ptr<dcp::SubtitleAsset> asset;
935 case TextType::OPEN_SUBTITLE:
936 asset = _subtitle_asset;
938 case TextType::CLOSED_CAPTION:
939 DCPOMATIC_ASSERT (track);
940 asset = _closed_caption_assets[*track];
943 DCPOMATIC_ASSERT (false);
947 asset = empty_text_asset (type, track, false);
951 case TextType::OPEN_SUBTITLE:
952 _subtitle_asset = asset;
954 case TextType::CLOSED_CAPTION:
955 DCPOMATIC_ASSERT (track);
956 _closed_caption_assets[*track] = asset;
959 DCPOMATIC_ASSERT (false);
962 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
963 auto const tcr = 1000;
965 for (auto i: subs.string) {
966 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
967 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
968 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::SubtitleStandard::INTEROP : dcp::SubtitleStandard::SMPTE_2014));
969 auto sub = make_shared<dcp::SubtitleString>(i);
970 /* i.font is a shared_ptr<Font> which uniquely identifies the font we want,
971 * though if we are Interop we can only have one font, so we'll use the chosen
974 auto font = film()->interop() ? chosen_interop_font : i.font;
975 /* We can get the corresponding ID from fonts */
976 auto const font_id_to_use = fonts.get(font);
977 /* Give this subtitle the correct font ID */
978 sub->set_font(font_id_to_use);
980 /* Make sure the asset LoadFonts the font we just asked for */
981 asset->ensure_font(font_id_to_use, font->data().get_value_or(_default_font));
984 for (auto i: subs.bitmap) {
986 make_shared<dcp::SubtitleImage>(
987 image_as_png(i.image),
988 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
989 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
990 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
991 dcp::Time(), dcp::Time()
999 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
1001 LOG_GENERAL ("Checking existing picture frame %1", frame);
1003 /* Read the data from the info file; for 3D we just check the left
1004 frames until we find a good one.
1006 auto const info = J2KFrameInfo(info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
1010 /* Read the data from the asset and hash it */
1011 asset_file.seek(info.offset, SEEK_SET);
1012 ArrayData data (info.size);
1013 size_t const read = asset_file.read(data.data(), 1, data.size());
1014 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
1015 if (read != static_cast<size_t> (data.size ())) {
1016 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
1020 digester.add (data.data(), data.size());
1021 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
1022 if (digester.get() != info.hash) {
1023 LOG_GENERAL ("Existing frame %1 failed hash check", frame);