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"
26 #include "dcpomatic_log.h"
30 #include "image_png.h"
33 #include "reel_writer.h"
34 #include <dcp/atmos_asset.h>
35 #include <dcp/atmos_asset_writer.h>
36 #include <dcp/certificate_chain.h>
39 #include <dcp/interop_subtitle_asset.h>
40 #include <dcp/mono_picture_asset.h>
41 #include <dcp/raw_convert.h>
43 #include <dcp/reel_atmos_asset.h>
44 #include <dcp/reel_interop_closed_caption_asset.h>
45 #include <dcp/reel_interop_subtitle_asset.h>
46 #include <dcp/reel_markers_asset.h>
47 #include <dcp/reel_mono_picture_asset.h>
48 #include <dcp/reel_smpte_closed_caption_asset.h>
49 #include <dcp/reel_smpte_subtitle_asset.h>
50 #include <dcp/reel_sound_asset.h>
51 #include <dcp/reel_stereo_picture_asset.h>
52 #include <dcp/smpte_subtitle_asset.h>
53 #include <dcp/sound_asset.h>
54 #include <dcp/sound_asset_writer.h>
55 #include <dcp/stereo_picture_asset.h>
56 #include <dcp/subtitle_image.h>
61 using std::dynamic_pointer_cast;
64 using std::make_shared;
67 using std::shared_ptr;
71 using boost::optional;
72 #if BOOST_VERSION >= 106100
73 using namespace boost::placeholders;
77 using dcp::raw_convert;
78 using namespace dcpomatic;
81 int const ReelWriter::_info_size = 48;
84 static dcp::MXFMetadata
87 dcp::MXFMetadata meta;
88 auto config = Config::instance();
89 if (!config->dcp_company_name().empty()) {
90 meta.company_name = config->dcp_company_name ();
92 if (!config->dcp_product_name().empty()) {
93 meta.product_name = config->dcp_product_name ();
95 if (!config->dcp_product_version().empty()) {
96 meta.product_version = config->dcp_product_version ();
102 /** @param job Related job, or 0.
103 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
104 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
105 * subtitle / closed caption files.
107 ReelWriter::ReelWriter (
108 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
110 : WeakConstFilm (weak_film)
112 , _reel_index (reel_index)
113 , _reel_count (reel_count)
114 , _content_summary (film()->content_summary(period))
116 , _text_only (text_only)
117 , _font_metrics(film()->frame_size().height)
119 /* Create or find our picture asset in a subdirectory, named
120 according to those film's parameters which affect the video
121 output. We will hard-link it into the DCP later.
124 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
126 boost::filesystem::path const asset =
127 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
129 _first_nonexistent_frame = check_existing_picture_asset (asset);
131 if (_first_nonexistent_frame < period.duration().frames_round(film()->video_frame_rate())) {
132 /* We do not have a complete picture asset. If there is an
133 existing asset, break any hard links to it as we are about
134 to change its contents (if only by changing the IDs); see
137 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
139 job->sub (_("Copying old video file"));
140 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
142 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
144 boost::filesystem::remove (asset);
145 boost::filesystem::rename (asset.string() + ".tmp", asset);
149 if (film()->three_d()) {
150 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
152 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
155 _picture_asset->set_size (film()->frame_size());
156 _picture_asset->set_metadata (mxf_metadata());
158 if (film()->encrypted()) {
159 _picture_asset->set_key (film()->key());
160 _picture_asset->set_context_id (film()->context_id());
163 _picture_asset->set_file (asset);
164 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistent_frame > 0);
165 } else if (!text_only) {
166 /* We already have a complete picture asset that we can just re-use */
167 /* XXX: what about if the encryption key changes? */
168 if (film()->three_d()) {
169 _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
171 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
175 if (film()->audio_channels()) {
176 auto lang = film()->audio_language();
177 _sound_asset = make_shared<dcp::SoundAsset> (
178 dcp::Fraction(film()->video_frame_rate(), 1),
179 film()->audio_frame_rate(),
180 film()->audio_channels(),
181 lang ? *lang : dcp::LanguageTag("en-US"),
185 _sound_asset->set_metadata (mxf_metadata());
187 if (film()->encrypted()) {
188 _sound_asset->set_key (film()->key());
191 DCPOMATIC_ASSERT (film()->directory());
193 /* Write the sound asset into the film directory so that we leave the creation
194 of the DCP directory until the last minute.
196 _sound_asset_writer = _sound_asset->start_write (
197 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
198 film()->contains_atmos_content()
202 _default_font = dcp::ArrayData(default_font_file());
206 /** @param frame reel-relative frame */
208 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
210 auto handle = film()->info_file_handle(_period, false);
211 handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
212 handle->get().checked_write(&info.offset, sizeof(info.offset));
213 handle->get().checked_write(&info.size, sizeof(info.size));
214 handle->get().checked_write(info.hash.c_str(), info.hash.size());
219 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
221 dcp::FrameInfo frame_info;
222 info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
223 info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
224 info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
226 char hash_buffer[33];
227 info->get().checked_read(hash_buffer, 32);
228 hash_buffer[32] = '\0';
229 frame_info.hash = hash_buffer;
236 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
240 return frame * _info_size;
242 return frame * _info_size * 2;
244 return frame * _info_size * 2 + _info_size;
246 DCPOMATIC_ASSERT (false);
249 DCPOMATIC_ASSERT (false);
254 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
256 auto job = _job.lock ();
259 job->sub (_("Checking existing image data"));
262 /* Try to open the existing asset */
263 dcp::File asset_file(asset, "rb");
265 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
268 LOG_GENERAL ("Opened existing asset at %1", asset.string());
271 shared_ptr<InfoFileHandle> info_file;
274 info_file = film()->info_file_handle (_period, true);
275 } catch (OpenFileError &) {
276 LOG_GENERAL_NC ("Could not open film info file");
280 /* Offset of the last dcp::FrameInfo in the info file */
281 int const n = (boost::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
282 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);
284 Frame first_nonexistent_frame;
285 if (film()->three_d()) {
286 /* Start looking at the last left frame */
287 first_nonexistent_frame = n / 2;
289 first_nonexistent_frame = n;
292 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
293 --first_nonexistent_frame;
296 if (!film()->three_d() && first_nonexistent_frame > 0) {
297 /* If we are doing 3D we might have found a good L frame with no R, so only
298 do this if we're in 2D and we've just found a good B(oth) frame.
300 ++first_nonexistent_frame;
303 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
305 return first_nonexistent_frame;
310 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
312 if (!_picture_asset_writer) {
313 /* We're not writing any data */
317 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
318 write_frame_info (frame, eyes, fin);
319 _last_written[eyes] = encoded;
324 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
327 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
328 if (film()->encrypted()) {
329 _atmos_asset->set_key(film()->key());
331 _atmos_asset_writer = _atmos_asset->start_write (
332 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
335 _atmos_asset_writer->write (atmos);
340 ReelWriter::fake_write (int size)
342 if (!_picture_asset_writer) {
343 /* We're not writing any data */
347 _picture_asset_writer->fake_write (size);
352 ReelWriter::repeat_write (Frame frame, Eyes eyes)
354 if (!_picture_asset_writer) {
355 /* We're not writing any data */
359 auto fin = _picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size());
360 write_frame_info (frame, eyes, fin);
365 ReelWriter::finish (boost::filesystem::path output_dcp)
367 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
368 /* Nothing was written to the picture asset */
369 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
370 _picture_asset.reset ();
373 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
374 /* Nothing was written to the sound asset */
375 _sound_asset.reset ();
378 /* Hard-link any video asset file into the DCP */
379 if (_picture_asset) {
380 DCPOMATIC_ASSERT (_picture_asset->file());
381 boost::filesystem::path video_from = _picture_asset->file().get();
382 boost::filesystem::path 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 boost::filesystem::remove (video_to, ec);
390 boost::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 boost::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 boost::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 boost::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 FontIdMap const& fonts,
464 shared_ptr<dcpomatic::Font> chosen_interop_font,
465 dcp::ArrayData default_font,
466 shared_ptr<const Film> film,
467 DCPTimePeriod period,
468 boost::filesystem::path output_dcp,
472 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
474 shared_ptr<Result> reel_asset;
477 if (film->interop()) {
478 if (chosen_interop_font) {
479 /* We only add one font, as Interop will ignore subsequent ones (and some validators will
480 * complain if they are even present)
482 asset->add_font(fonts.get(chosen_interop_font), chosen_interop_font->data().get_value_or(default_font));
485 for (auto const& font: fonts.map()) {
486 asset->add_font(font.second, font.first->data().get_value_or(default_font));
490 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
491 auto directory = output_dcp / interop->id ();
492 boost::filesystem::create_directories (directory);
493 interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
494 reel_asset = make_shared<Interop> (
496 dcp::Fraction(film->video_frame_rate(), 1),
500 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
501 /* All our assets should be the same length; use the picture asset length here
502 as a reference to set the subtitle one. We'll use the duration rather than
503 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
504 just interested in its presentation length.
506 smpte->set_intrinsic_duration(picture_duration);
508 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
510 reel_asset = make_shared<SMPTE> (
512 dcp::Fraction(film->video_frame_rate(), 1),
519 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
521 auto k = dynamic_pointer_cast<Result> (j.asset);
522 if (k && j.period == period) {
524 /* If we have a hash for this asset in the CPL, assume that it is correct */
526 k->asset_ref()->set_hash (k->hash().get());
533 if (!text_only && reel_asset->actual_duration() != period_duration) {
534 throw ProgrammingError (
536 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
539 reel->add (reel_asset);
546 shared_ptr<dcp::ReelPictureAsset>
547 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
549 shared_ptr<dcp::ReelPictureAsset> reel_asset;
551 if (_picture_asset) {
552 /* We have made a picture asset of our own. Put it into the reel */
553 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
555 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
558 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
560 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
563 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
564 /* We don't have a picture asset of our own; hopefully we have one to reference */
566 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
568 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
570 if (k && j.period == _period) {
576 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
578 DCPOMATIC_ASSERT (reel_asset);
579 if (reel_asset->duration() != period_duration) {
580 throw ProgrammingError (
582 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
585 reel->add (reel_asset);
587 /* If we have a hash for this asset in the CPL, assume that it is correct */
588 if (reel_asset->hash()) {
589 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
597 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
599 shared_ptr<dcp::ReelSoundAsset> reel_asset;
602 /* We have made a sound asset of our own. Put it into the reel */
603 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
605 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
606 /* We don't have a sound asset of our own; hopefully we have one to reference */
608 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
610 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
612 if (k && j.period == _period) {
614 /* If we have a hash for this asset in the CPL, assume that it is correct */
616 k->asset_ref()->set_hash (k->hash().get());
622 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
624 DCPOMATIC_ASSERT (reel_asset);
625 if (reel_asset->actual_duration() != period_duration) {
627 "Reel sound asset has length %1 but reel period is %2",
628 reel_asset->actual_duration(),
631 if (reel_asset->actual_duration() != period_duration) {
632 throw ProgrammingError (
634 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
639 reel->add (reel_asset);
644 ReelWriter::create_reel_text (
645 shared_ptr<dcp::Reel> reel,
646 list<ReferencedReelAsset> const & refs,
647 FontIdMap const& fonts,
648 shared_ptr<dcpomatic::Font> chosen_interop_font,
650 boost::filesystem::path output_dcp,
651 bool ensure_subtitles,
652 set<DCPTextTrack> ensure_closed_captions
655 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
656 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
660 /* We have a subtitle asset that we either made or are referencing */
661 if (auto main_language = film()->subtitle_languages().first) {
662 subtitle->set_language (*main_language);
664 } else if (ensure_subtitles) {
665 /* We had no subtitle asset, but we've been asked to make sure there is one */
666 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
667 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
684 for (auto const& i: _closed_caption_assets) {
685 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
686 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
688 DCPOMATIC_ASSERT (a);
689 a->set_annotation_text (i.first.name);
690 if (i.first.language) {
691 a->set_language (i.first.language.get());
694 ensure_closed_captions.erase (i.first);
697 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
698 for (auto i: ensure_closed_captions) {
699 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
700 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
715 DCPOMATIC_ASSERT (a);
716 a->set_annotation_text (i.name);
718 a->set_language (i.language.get());
725 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
727 auto markers = film()->markers();
728 film()->add_ffoc_lfoc(markers);
729 Film::Markers reel_markers;
730 for (auto const& i: markers) {
731 if (_period.contains(i.second)) {
732 reel_markers[i.first] = i.second;
736 if (!reel_markers.empty ()) {
737 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
738 for (auto const& i: reel_markers) {
739 DCPTime relative = i.second - _period.from;
740 auto hmsf = relative.split (film()->video_frame_rate());
741 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
748 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
749 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
751 shared_ptr<dcp::Reel>
752 ReelWriter::create_reel (
753 list<ReferencedReelAsset> const & refs,
754 FontIdMap const & fonts,
755 shared_ptr<dcpomatic::Font> chosen_interop_font,
756 boost::filesystem::path output_dcp,
757 bool ensure_subtitles,
758 set<DCPTextTrack> ensure_closed_captions
761 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
763 auto reel = make_shared<dcp::Reel>();
765 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
766 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
767 * how big they are, we don't care about that.
769 int64_t duration = 0;
771 auto reel_picture_asset = create_reel_picture (reel, refs);
772 duration = reel_picture_asset->actual_duration ();
773 create_reel_sound (reel, refs);
774 create_reel_markers (reel);
777 create_reel_text (reel, refs, fonts, chosen_interop_font, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
780 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
787 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
790 if (_picture_asset) {
791 _picture_asset->hash (set_progress);
795 _sound_asset->hash (set_progress);
799 _atmos_asset->hash (set_progress);
801 } catch (boost::thread_interrupted) {
802 /* set_progress contains an interruption_point, so any of these methods
803 * may throw thread_interrupted, at which point we just give up.
809 ReelWriter::start () const
811 return _period.from.frames_floor (film()->video_frame_rate());
816 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
818 if (!_sound_asset_writer) {
822 DCPOMATIC_ASSERT (audio);
823 _sound_asset_writer->write (audio->data(), audio->frames());
827 shared_ptr<dcp::SubtitleAsset>
828 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
830 shared_ptr<dcp::SubtitleAsset> asset;
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>(
865 optional<std::string>(),
872 dcp::Time(0, 0, 0, 0, 24),
873 dcp::Time(0, 0, 1, 0, 24),
895 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::Standard to) const
897 if (subtitle.valign_standard == to) {
898 return subtitle.v_position();
901 auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
902 auto const height = _font_metrics.height(subtitle);
904 float correction = 0;
905 switch (subtitle.v_align()) {
906 case dcp::VAlign::TOP:
907 correction = height - baseline_to_bottom;
909 case dcp::VAlign::CENTER:
910 correction = (height / 2) - baseline_to_bottom;
912 case dcp::VAlign::BOTTOM:
913 correction = baseline_to_bottom;
917 return subtitle.v_position() + ((subtitle.valign_standard == dcp::Standard::SMPTE) ? correction : -correction);
922 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts)
924 shared_ptr<dcp::SubtitleAsset> asset;
927 case TextType::OPEN_SUBTITLE:
928 asset = _subtitle_asset;
930 case TextType::CLOSED_CAPTION:
931 DCPOMATIC_ASSERT (track);
932 asset = _closed_caption_assets[*track];
935 DCPOMATIC_ASSERT (false);
939 asset = empty_text_asset (type, track, false);
943 case TextType::OPEN_SUBTITLE:
944 _subtitle_asset = asset;
946 case TextType::CLOSED_CAPTION:
947 DCPOMATIC_ASSERT (track);
948 _closed_caption_assets[*track] = asset;
951 DCPOMATIC_ASSERT (false);
954 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
955 auto const tcr = 1000;
957 for (auto i: subs.string) {
958 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
959 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
960 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE));
961 auto sub = make_shared<dcp::SubtitleString>(i);
962 if (type == TextType::OPEN_SUBTITLE) {
963 sub->set_font(fonts.get(i.font));
968 for (auto i: subs.bitmap) {
970 make_shared<dcp::SubtitleImage>(
971 image_as_png(i.image),
972 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
973 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
974 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
975 dcp::Time(), dcp::Time()
983 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
985 LOG_GENERAL ("Checking existing picture frame %1", frame);
987 /* Read the data from the info file; for 3D we just check the left
988 frames until we find a good one.
990 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
994 /* Read the data from the asset and hash it */
995 asset_file.seek(info.offset, SEEK_SET);
996 ArrayData data (info.size);
997 size_t const read = asset_file.read(data.data(), 1, data.size());
998 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
999 if (read != static_cast<size_t> (data.size ())) {
1000 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
1004 digester.add (data.data(), data.size());
1005 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
1006 if (digester.get() != info.hash) {
1007 LOG_GENERAL ("Existing frame %1 failed hash check", frame);