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 auto mapped = film()->mapped_audio_channels();
196 std::vector<dcp::Channel> extra_active_channels;
197 auto add_if_mapped = [mapped, &extra_active_channels](dcp::Channel channel) {
198 if (std::find(mapped.begin(), mapped.end(), static_cast<int>(channel)) != mapped.end()) {
199 extra_active_channels.push_back(channel);
203 add_if_mapped(dcp::Channel::HI);
204 add_if_mapped(dcp::Channel::VI);
205 add_if_mapped(dcp::Channel::BSL);
206 add_if_mapped(dcp::Channel::BSR);
208 /* Write the sound asset into the film directory so that we leave the creation
209 of the DCP directory until the last minute.
211 _sound_asset_writer = _sound_asset->start_write (
212 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
213 extra_active_channels,
214 film()->contains_atmos_content() ? dcp::SoundAsset::AtmosSync::ENABLED : dcp::SoundAsset::AtmosSync::DISABLED,
215 film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
219 _default_font = dcp::ArrayData(default_font_file());
223 /** @param frame reel-relative frame */
225 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
227 auto handle = film()->info_file_handle(_period, false);
228 handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
229 handle->get().checked_write(&info.offset, sizeof(info.offset));
230 handle->get().checked_write(&info.size, sizeof(info.size));
231 handle->get().checked_write(info.hash.c_str(), info.hash.size());
236 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
238 dcp::FrameInfo frame_info;
239 info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
240 info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
241 info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
243 char hash_buffer[33];
244 info->get().checked_read(hash_buffer, 32);
245 hash_buffer[32] = '\0';
246 frame_info.hash = hash_buffer;
253 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
257 return frame * _info_size;
259 return frame * _info_size * 2;
261 return frame * _info_size * 2 + _info_size;
263 DCPOMATIC_ASSERT (false);
266 DCPOMATIC_ASSERT (false);
271 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
273 auto job = _job.lock ();
276 job->sub (_("Checking existing image data"));
279 /* Try to open the existing asset */
280 dcp::File asset_file(asset, "rb");
282 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
285 LOG_GENERAL ("Opened existing asset at %1", asset.string());
288 shared_ptr<InfoFileHandle> info_file;
291 info_file = film()->info_file_handle (_period, true);
292 } catch (OpenFileError &) {
293 LOG_GENERAL_NC ("Could not open film info file");
297 /* Offset of the last dcp::FrameInfo in the info file */
298 int const n = (boost::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
299 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);
301 Frame first_nonexistent_frame;
302 if (film()->three_d()) {
303 /* Start looking at the last left frame */
304 first_nonexistent_frame = n / 2;
306 first_nonexistent_frame = n;
309 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
310 --first_nonexistent_frame;
313 if (!film()->three_d() && first_nonexistent_frame > 0) {
314 /* If we are doing 3D we might have found a good L frame with no R, so only
315 do this if we're in 2D and we've just found a good B(oth) frame.
317 ++first_nonexistent_frame;
320 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
322 return first_nonexistent_frame;
327 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
329 if (!_picture_asset_writer) {
330 /* We're not writing any data */
334 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
335 write_frame_info (frame, eyes, fin);
336 _last_written[eyes] = encoded;
341 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
344 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
345 if (film()->encrypted()) {
346 _atmos_asset->set_key(film()->key());
348 _atmos_asset_writer = _atmos_asset->start_write (
349 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
352 _atmos_asset_writer->write (atmos);
357 ReelWriter::fake_write (int size)
359 if (!_picture_asset_writer) {
360 /* We're not writing any data */
364 _picture_asset_writer->fake_write (size);
369 ReelWriter::repeat_write (Frame frame, Eyes eyes)
371 if (!_picture_asset_writer) {
372 /* We're not writing any data */
376 auto fin = _picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size());
377 write_frame_info (frame, eyes, fin);
382 ReelWriter::finish (boost::filesystem::path output_dcp)
384 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
385 /* Nothing was written to the picture asset */
386 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
387 _picture_asset.reset ();
390 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
391 /* Nothing was written to the sound asset */
392 _sound_asset.reset ();
395 /* Hard-link any video asset file into the DCP */
396 if (_picture_asset) {
397 DCPOMATIC_ASSERT (_picture_asset->file());
398 boost::filesystem::path video_from = _picture_asset->file().get();
399 boost::filesystem::path video_to = output_dcp;
400 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
401 /* There may be an existing "to" file if we are recreating a DCP in the same place without
404 boost::system::error_code ec;
405 boost::filesystem::remove (video_to, ec);
407 boost::filesystem::create_hard_link (video_from, video_to, ec);
409 LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
410 auto job = _job.lock ();
412 job->sub (_("Copying video file into DCP"));
414 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
415 } catch (exception& e) {
416 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
417 throw FileError (e.what(), video_from);
420 boost::filesystem::copy_file (video_from, video_to, ec);
422 LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
423 throw FileError (ec.message(), video_from);
428 _picture_asset->set_file (video_to);
431 /* Move the audio asset into the DCP */
433 boost::filesystem::path audio_to = output_dcp;
434 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
437 boost::system::error_code ec;
438 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
441 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
445 _sound_asset->set_file (audio_to);
449 _atmos_asset_writer->finalize ();
450 boost::filesystem::path atmos_to = output_dcp;
451 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
454 boost::system::error_code ec;
455 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
458 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
462 _atmos_asset->set_file (atmos_to);
467 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
468 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
470 template <class Interop, class SMPTE, class Result>
473 shared_ptr<dcp::SubtitleAsset> asset,
474 int64_t picture_duration,
475 shared_ptr<dcp::Reel> reel,
478 optional<string> content_summary,
479 list<ReferencedReelAsset> const & refs,
480 FontIdMap const& fonts,
481 shared_ptr<dcpomatic::Font> chosen_interop_font,
482 dcp::ArrayData default_font,
483 shared_ptr<const Film> film,
484 DCPTimePeriod period,
485 boost::filesystem::path output_dcp,
489 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
491 shared_ptr<Result> reel_asset;
494 if (!std::is_same<Result, dcp::ReelClosedCaptionAsset>::value) {
495 if (film->interop()) {
496 if (chosen_interop_font) {
497 /* We only add one font, as Interop will ignore subsequent ones (and some validators will
498 * complain if they are even present)
500 asset->add_font(fonts.get(chosen_interop_font), chosen_interop_font->data().get_value_or(default_font));
503 for (auto const& font: fonts.map()) {
504 asset->add_font(font.second, font.first->data().get_value_or(default_font));
509 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
510 auto directory = output_dcp / interop->id ();
511 boost::filesystem::create_directories (directory);
512 interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
513 reel_asset = make_shared<Interop> (
515 dcp::Fraction(film->video_frame_rate(), 1),
519 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
520 /* All our assets should be the same length; use the picture asset length here
521 as a reference to set the subtitle one. We'll use the duration rather than
522 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
523 just interested in its presentation length.
525 smpte->set_intrinsic_duration(picture_duration);
527 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
529 reel_asset = make_shared<SMPTE> (
531 dcp::Fraction(film->video_frame_rate(), 1),
538 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
540 auto k = dynamic_pointer_cast<Result> (j.asset);
541 if (k && j.period == period) {
543 /* If we have a hash for this asset in the CPL, assume that it is correct */
545 k->asset_ref()->set_hash (k->hash().get());
552 if (!text_only && reel_asset->actual_duration() != period_duration) {
553 throw ProgrammingError (
555 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
558 reel->add (reel_asset);
565 shared_ptr<dcp::ReelPictureAsset>
566 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
568 shared_ptr<dcp::ReelPictureAsset> reel_asset;
570 if (_picture_asset) {
571 /* We have made a picture asset of our own. Put it into the reel */
572 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
574 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
577 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
579 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
582 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
583 /* We don't have a picture asset of our own; hopefully we have one to reference */
585 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
587 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
589 if (k && j.period == _period) {
595 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
597 DCPOMATIC_ASSERT (reel_asset);
598 if (reel_asset->duration() != period_duration) {
599 throw ProgrammingError (
601 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
604 reel->add (reel_asset);
606 /* If we have a hash for this asset in the CPL, assume that it is correct */
607 if (reel_asset->hash()) {
608 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
616 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
618 shared_ptr<dcp::ReelSoundAsset> reel_asset;
621 /* We have made a sound asset of our own. Put it into the reel */
622 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
624 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
625 /* We don't have a sound asset of our own; hopefully we have one to reference */
627 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
629 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
631 if (k && j.period == _period) {
633 /* If we have a hash for this asset in the CPL, assume that it is correct */
635 k->asset_ref()->set_hash (k->hash().get());
641 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
643 DCPOMATIC_ASSERT (reel_asset);
644 if (reel_asset->actual_duration() != period_duration) {
646 "Reel sound asset has length %1 but reel period is %2",
647 reel_asset->actual_duration(),
650 if (reel_asset->actual_duration() != period_duration) {
651 throw ProgrammingError (
653 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
658 reel->add (reel_asset);
663 ReelWriter::create_reel_text (
664 shared_ptr<dcp::Reel> reel,
665 list<ReferencedReelAsset> const & refs,
666 FontIdMap const& fonts,
667 shared_ptr<dcpomatic::Font> chosen_interop_font,
669 boost::filesystem::path output_dcp,
670 bool ensure_subtitles,
671 set<DCPTextTrack> ensure_closed_captions
674 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
675 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
679 /* We have a subtitle asset that we either made or are referencing */
680 if (auto main_language = film()->subtitle_languages().first) {
681 subtitle->set_language (*main_language);
683 } else if (ensure_subtitles) {
684 /* We had no subtitle asset, but we've been asked to make sure there is one */
685 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
686 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
703 for (auto const& i: _closed_caption_assets) {
704 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
705 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
707 DCPOMATIC_ASSERT (a);
708 a->set_annotation_text (i.first.name);
709 if (i.first.language) {
710 a->set_language (i.first.language.get());
713 ensure_closed_captions.erase (i.first);
716 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
717 for (auto i: ensure_closed_captions) {
718 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
719 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
734 DCPOMATIC_ASSERT (a);
735 a->set_annotation_text (i.name);
737 a->set_language (i.language.get());
744 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
746 auto markers = film()->markers();
747 film()->add_ffoc_lfoc(markers);
748 Film::Markers reel_markers;
749 for (auto const& i: markers) {
750 if (_period.contains(i.second)) {
751 reel_markers[i.first] = i.second;
755 if (!reel_markers.empty ()) {
756 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
757 for (auto const& i: reel_markers) {
758 DCPTime relative = i.second - _period.from;
759 auto hmsf = relative.split (film()->video_frame_rate());
760 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
767 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
768 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
770 shared_ptr<dcp::Reel>
771 ReelWriter::create_reel (
772 list<ReferencedReelAsset> const & refs,
773 FontIdMap const & fonts,
774 shared_ptr<dcpomatic::Font> chosen_interop_font,
775 boost::filesystem::path output_dcp,
776 bool ensure_subtitles,
777 set<DCPTextTrack> ensure_closed_captions
780 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
782 auto reel = make_shared<dcp::Reel>();
784 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
785 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
786 * how big they are, we don't care about that.
788 int64_t duration = 0;
790 auto reel_picture_asset = create_reel_picture (reel, refs);
791 duration = reel_picture_asset->actual_duration ();
792 create_reel_sound (reel, refs);
793 create_reel_markers (reel);
796 create_reel_text (reel, refs, fonts, chosen_interop_font, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
799 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
806 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
809 if (_picture_asset) {
810 _picture_asset->hash (set_progress);
814 _sound_asset->hash (set_progress);
818 _atmos_asset->hash (set_progress);
820 } catch (boost::thread_interrupted) {
821 /* set_progress contains an interruption_point, so any of these methods
822 * may throw thread_interrupted, at which point we just give up.
828 ReelWriter::start () const
830 return _period.from.frames_floor (film()->video_frame_rate());
835 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
837 if (!_sound_asset_writer) {
841 DCPOMATIC_ASSERT (audio);
842 _sound_asset_writer->write(audio->data(), audio->channels(), audio->frames());
846 shared_ptr<dcp::SubtitleAsset>
847 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
849 shared_ptr<dcp::SubtitleAsset> asset;
851 auto lang = film()->subtitle_languages();
852 if (film()->interop()) {
853 auto s = make_shared<dcp::InteropSubtitleAsset>();
854 s->set_movie_title (film()->name());
855 if (type == TextType::OPEN_SUBTITLE) {
856 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
857 } else if (track->language) {
858 s->set_language (track->language->to_string());
860 s->set_reel_number (raw_convert<string> (_reel_index + 1));
863 auto s = make_shared<dcp::SMPTESubtitleAsset>();
864 s->set_content_title_text (film()->name());
865 s->set_metadata (mxf_metadata());
866 if (type == TextType::OPEN_SUBTITLE && lang.first) {
867 s->set_language (*lang.first);
868 } else if (track && track->language) {
869 s->set_language (dcp::LanguageTag(track->language->to_string()));
871 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
872 s->set_reel_number (_reel_index + 1);
873 s->set_time_code_rate (film()->video_frame_rate());
874 s->set_start_time (dcp::Time ());
875 if (film()->encrypted()) {
876 s->set_key (film()->key());
883 std::make_shared<dcp::SubtitleString>(
884 optional<std::string>(),
891 dcp::Time(0, 0, 0, 0, 24),
892 dcp::Time(0, 0, 1, 0, 24),
914 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const
916 if (dcp::uses_baseline(subtitle.valign_standard) == dcp::uses_baseline(to)) {
917 /* The from and to standards use the same alignment reference */
918 return subtitle.v_position();
921 auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
922 auto const height = _font_metrics.height(subtitle);
924 float correction = 0;
925 switch (subtitle.v_align()) {
926 case dcp::VAlign::TOP:
927 correction = height - baseline_to_bottom;
929 case dcp::VAlign::CENTER:
930 correction = (height / 2) - baseline_to_bottom;
932 case dcp::VAlign::BOTTOM:
933 correction = baseline_to_bottom;
937 return subtitle.v_position() + (dcp::uses_bounding_box(subtitle.valign_standard) ? correction : -correction);
942 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts)
944 shared_ptr<dcp::SubtitleAsset> asset;
947 case TextType::OPEN_SUBTITLE:
948 asset = _subtitle_asset;
950 case TextType::CLOSED_CAPTION:
951 DCPOMATIC_ASSERT (track);
952 asset = _closed_caption_assets[*track];
955 DCPOMATIC_ASSERT (false);
959 asset = empty_text_asset (type, track, false);
963 case TextType::OPEN_SUBTITLE:
964 _subtitle_asset = asset;
966 case TextType::CLOSED_CAPTION:
967 DCPOMATIC_ASSERT (track);
968 _closed_caption_assets[*track] = asset;
971 DCPOMATIC_ASSERT (false);
974 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
975 auto const tcr = 1000;
977 for (auto i: subs.string) {
978 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
979 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
980 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::SubtitleStandard::INTEROP : dcp::SubtitleStandard::SMPTE_2014));
981 auto sub = make_shared<dcp::SubtitleString>(i);
982 if (type == TextType::OPEN_SUBTITLE) {
983 sub->set_font(fonts.get(i.font));
988 for (auto i: subs.bitmap) {
990 make_shared<dcp::SubtitleImage>(
991 image_as_png(i.image),
992 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
993 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
994 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
995 dcp::Time(), dcp::Time()
1003 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
1005 LOG_GENERAL ("Checking existing picture frame %1", frame);
1007 /* Read the data from the info file; for 3D we just check the left
1008 frames until we find a good one.
1010 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
1014 /* Read the data from the asset and hash it */
1015 asset_file.seek(info.offset, SEEK_SET);
1016 ArrayData data (info.size);
1017 size_t const read = asset_file.read(data.data(), 1, data.size());
1018 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
1019 if (read != static_cast<size_t> (data.size ())) {
1020 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
1024 digester.add (data.data(), data.size());
1025 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
1026 if (digester.get() != info.hash) {
1027 LOG_GENERAL ("Existing frame %1 failed hash check", frame);