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"
29 #include "font_data.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>
63 using std::dynamic_pointer_cast;
66 using std::make_shared;
69 using std::shared_ptr;
73 using boost::optional;
74 #if BOOST_VERSION >= 106100
75 using namespace boost::placeholders;
79 using dcp::raw_convert;
80 using namespace dcpomatic;
83 int const ReelWriter::_info_size = 48;
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 ();
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)
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_nonexistant_frame = check_existing_picture_asset (asset);
131 if (_first_nonexistant_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_nonexistant_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());
205 /** @param frame reel-relative frame */
207 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
209 auto handle = film()->info_file_handle(_period, false);
210 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
211 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
212 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
213 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
217 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
219 dcp::FrameInfo frame_info;
220 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
221 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
222 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
224 char hash_buffer[33];
225 checked_fread (hash_buffer, 32, info->get(), info->file());
226 hash_buffer[32] = '\0';
227 frame_info.hash = hash_buffer;
233 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
237 return frame * _info_size;
239 return frame * _info_size * 2;
241 return frame * _info_size * 2 + _info_size;
243 DCPOMATIC_ASSERT (false);
246 DCPOMATIC_ASSERT (false);
250 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
252 auto job = _job.lock ();
255 job->sub (_("Checking existing image data"));
258 /* Try to open the existing asset */
259 auto asset_file = fopen_boost (asset, "rb");
261 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
264 LOG_GENERAL ("Opened existing asset at %1", asset.string());
267 shared_ptr<InfoFileHandle> info_file;
270 info_file = film()->info_file_handle (_period, true);
271 } catch (OpenFileError &) {
272 LOG_GENERAL_NC ("Could not open film info file");
277 /* Offset of the last dcp::FrameInfo in the info file */
278 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
279 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
281 Frame first_nonexistant_frame;
282 if (film()->three_d()) {
283 /* Start looking at the last left frame */
284 first_nonexistant_frame = n / 2;
286 first_nonexistant_frame = n;
289 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
290 --first_nonexistant_frame;
293 if (!film()->three_d() && first_nonexistant_frame > 0) {
294 /* If we are doing 3D we might have found a good L frame with no R, so only
295 do this if we're in 2D and we've just found a good B(oth) frame.
297 ++first_nonexistant_frame;
300 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
304 return first_nonexistant_frame;
308 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
310 if (!_picture_asset_writer) {
311 /* We're not writing any data */
315 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
316 write_frame_info (frame, eyes, fin);
317 _last_written[static_cast<int>(eyes)] = encoded;
322 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
325 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
326 if (film()->encrypted()) {
327 _atmos_asset->set_key(film()->key());
329 _atmos_asset_writer = _atmos_asset->start_write (
330 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
333 _atmos_asset_writer->write (atmos);
338 ReelWriter::fake_write (int size)
340 if (!_picture_asset_writer) {
341 /* We're not writing any data */
345 _picture_asset_writer->fake_write (size);
349 ReelWriter::repeat_write (Frame frame, Eyes eyes)
351 if (!_picture_asset_writer) {
352 /* We're not writing any data */
356 auto fin = _picture_asset_writer->write (
357 _last_written[static_cast<int>(eyes)]->data(),
358 _last_written[static_cast<int>(eyes)]->size()
360 write_frame_info (frame, eyes, fin);
364 ReelWriter::finish (boost::filesystem::path output_dcp)
366 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
367 /* Nothing was written to the picture asset */
368 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
369 _picture_asset.reset ();
372 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
373 /* Nothing was written to the sound asset */
374 _sound_asset.reset ();
377 /* Hard-link any video asset file into the DCP */
378 if (_picture_asset) {
379 DCPOMATIC_ASSERT (_picture_asset->file());
380 boost::filesystem::path video_from = _picture_asset->file().get();
381 boost::filesystem::path video_to = output_dcp;
382 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
383 /* There may be an existing "to" file if we are recreating a DCP in the same place without
386 boost::system::error_code ec;
387 boost::filesystem::remove (video_to, ec);
389 boost::filesystem::create_hard_link (video_from, video_to, ec);
391 LOG_WARNING_NC ("Hard-link failed; copying instead");
392 auto job = _job.lock ();
394 job->sub (_("Copying video file into DCP"));
396 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
397 } catch (exception& e) {
398 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
399 throw FileError (e.what(), video_from);
402 boost::filesystem::copy_file (video_from, video_to, ec);
404 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
405 throw FileError (ec.message(), video_from);
410 _picture_asset->set_file (video_to);
413 /* Move the audio asset into the DCP */
415 boost::filesystem::path audio_to = output_dcp;
416 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
419 boost::system::error_code ec;
420 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
423 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
427 _sound_asset->set_file (audio_to);
431 _atmos_asset_writer->finalize ();
432 boost::filesystem::path atmos_to = output_dcp;
433 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
436 boost::system::error_code ec;
437 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
440 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
444 _atmos_asset->set_file (atmos_to);
449 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
450 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
452 template <class Interop, class SMPTE, class Result>
455 shared_ptr<dcp::SubtitleAsset> asset,
456 int64_t picture_duration,
457 shared_ptr<dcp::Reel> reel,
458 list<ReferencedReelAsset> const & refs,
459 vector<FontData> const & fonts,
460 dcp::ArrayData default_font,
461 shared_ptr<const Film> film,
462 DCPTimePeriod period,
463 boost::filesystem::path output_dcp,
467 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
469 shared_ptr<Result> reel_asset;
472 /* Add the font to the subtitle content */
473 for (auto const& j: fonts) {
474 asset->add_font (j.id, j.data.get_value_or(default_font));
477 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
478 auto directory = output_dcp / interop->id ();
479 boost::filesystem::create_directories (directory);
480 interop->write (directory / ("sub_" + interop->id() + ".xml"));
481 reel_asset = make_shared<Interop> (
483 dcp::Fraction(film->video_frame_rate(), 1),
487 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
488 /* All our assets should be the same length; use the picture asset length here
489 as a reference to set the subtitle one. We'll use the duration rather than
490 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
491 just interested in its presentation length.
493 smpte->set_intrinsic_duration(picture_duration);
495 output_dcp / ("sub_" + asset->id() + ".mxf")
497 reel_asset = make_shared<SMPTE> (
499 dcp::Fraction(film->video_frame_rate(), 1),
506 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
508 auto k = dynamic_pointer_cast<Result> (j.asset);
509 if (k && j.period == period) {
511 /* If we have a hash for this asset in the CPL, assume that it is correct */
513 k->asset_ref()->set_hash (k->hash().get());
520 if (!text_only && reel_asset->actual_duration() != period_duration) {
521 throw ProgrammingError (
523 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
526 reel->add (reel_asset);
533 shared_ptr<dcp::ReelPictureAsset>
534 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
536 shared_ptr<dcp::ReelPictureAsset> reel_asset;
538 if (_picture_asset) {
539 /* We have made a picture asset of our own. Put it into the reel */
540 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
542 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
545 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
547 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
550 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
551 /* We don't have a picture asset of our own; hopefully we have one to reference */
553 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
555 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
557 if (k && j.period == _period) {
563 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
565 DCPOMATIC_ASSERT (reel_asset);
566 if (reel_asset->duration() != period_duration) {
567 throw ProgrammingError (
569 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
572 reel->add (reel_asset);
574 /* If we have a hash for this asset in the CPL, assume that it is correct */
575 if (reel_asset->hash()) {
576 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
584 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
586 shared_ptr<dcp::ReelSoundAsset> reel_asset;
589 /* We have made a sound asset of our own. Put it into the reel */
590 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
592 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
593 /* We don't have a sound asset of our own; hopefully we have one to reference */
595 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
597 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
599 if (k && j.period == _period) {
601 /* If we have a hash for this asset in the CPL, assume that it is correct */
603 k->asset_ref()->set_hash (k->hash().get());
609 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
611 DCPOMATIC_ASSERT (reel_asset);
612 if (reel_asset->actual_duration() != period_duration) {
614 "Reel sound asset has length %1 but reel period is %2",
615 reel_asset->actual_duration(),
618 if (reel_asset->actual_duration() != period_duration) {
619 throw ProgrammingError (
621 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
626 reel->add (reel_asset);
631 ReelWriter::create_reel_text (
632 shared_ptr<dcp::Reel> reel,
633 list<ReferencedReelAsset> const & refs,
634 vector<FontData> const& fonts,
636 boost::filesystem::path output_dcp,
637 bool ensure_subtitles,
638 set<DCPTextTrack> ensure_closed_captions
641 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
642 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
646 /* We have a subtitle asset that we either made or are referencing */
647 if (auto main_language = film()->subtitle_languages().first) {
648 subtitle->set_language (*main_language);
650 } else if (ensure_subtitles) {
651 /* We had no subtitle asset, but we've been asked to make sure there is one */
652 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
653 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
666 for (auto const& i: _closed_caption_assets) {
667 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
668 i.second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
670 DCPOMATIC_ASSERT (a);
671 a->set_annotation_text (i.first.name);
672 if (i.first.language) {
673 a->set_language (i.first.language.get());
676 ensure_closed_captions.erase (i.first);
679 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
680 for (auto i: ensure_closed_captions) {
681 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
682 empty_text_asset(TextType::CLOSED_CAPTION, i, true), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
684 DCPOMATIC_ASSERT (a);
685 a->set_annotation_text (i.name);
687 a->set_language (i.language.get());
694 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
696 auto markers = film()->markers();
697 film()->add_ffoc_lfoc(markers);
698 Film::Markers reel_markers;
699 for (auto const& i: markers) {
700 if (_period.contains(i.second)) {
701 reel_markers[i.first] = i.second;
705 if (!reel_markers.empty ()) {
706 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration(), 0);
707 for (auto const& i: reel_markers) {
708 DCPTime relative = i.second - _period.from;
709 auto hmsf = relative.split (film()->video_frame_rate());
710 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
717 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
718 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
720 shared_ptr<dcp::Reel>
721 ReelWriter::create_reel (
722 list<ReferencedReelAsset> const & refs,
723 vector<FontData> const & fonts,
724 boost::filesystem::path output_dcp,
725 bool ensure_subtitles,
726 set<DCPTextTrack> ensure_closed_captions
729 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
731 auto reel = make_shared<dcp::Reel>();
733 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
734 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
735 * how big they are, we don't care about that.
737 int64_t duration = 0;
739 auto reel_picture_asset = create_reel_picture (reel, refs);
740 duration = reel_picture_asset->actual_duration ();
741 create_reel_sound (reel, refs);
742 create_reel_markers (reel);
745 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
748 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
755 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
758 if (_picture_asset) {
759 _picture_asset->hash (set_progress);
763 _sound_asset->hash (set_progress);
767 _atmos_asset->hash (set_progress);
769 } catch (boost::thread_interrupted) {
770 /* set_progress contains an interruption_point, so any of these methods
771 * may throw thread_interrupted, at which point we just give up.
776 ReelWriter::start () const
778 return _period.from.frames_floor (film()->video_frame_rate());
783 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
785 if (!_sound_asset_writer) {
789 DCPOMATIC_ASSERT (audio);
790 _sound_asset_writer->write (audio->data(), audio->frames());
794 shared_ptr<dcp::SubtitleAsset>
795 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
797 shared_ptr<dcp::SubtitleAsset> asset;
799 auto lang = film()->subtitle_languages();
800 if (film()->interop()) {
801 auto s = make_shared<dcp::InteropSubtitleAsset>();
802 s->set_movie_title (film()->name());
803 if (type == TextType::OPEN_SUBTITLE) {
804 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
805 } else if (track->language) {
806 s->set_language (track->language->to_string());
808 s->set_reel_number (raw_convert<string> (_reel_index + 1));
811 auto s = make_shared<dcp::SMPTESubtitleAsset>();
812 s->set_content_title_text (film()->name());
813 s->set_metadata (mxf_metadata());
814 if (type == TextType::OPEN_SUBTITLE && lang.first) {
815 s->set_language (*lang.first);
816 } else if (track && track->language) {
817 s->set_language (dcp::LanguageTag(track->language->to_string()));
819 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
820 s->set_reel_number (_reel_index + 1);
821 s->set_time_code_rate (film()->video_frame_rate());
822 s->set_start_time (dcp::Time ());
823 if (film()->encrypted()) {
824 s->set_key (film()->key());
828 std::make_shared<dcp::SubtitleString>(
829 optional<std::string>(),
836 dcp::Time(0, 0, 0, 0, 24),
837 dcp::Time(0, 0, 1, 0, 24),
860 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
862 shared_ptr<dcp::SubtitleAsset> asset;
865 case TextType::OPEN_SUBTITLE:
866 asset = _subtitle_asset;
868 case TextType::CLOSED_CAPTION:
869 DCPOMATIC_ASSERT (track);
870 asset = _closed_caption_assets[*track];
873 DCPOMATIC_ASSERT (false);
877 asset = empty_text_asset (type, track, false);
881 case TextType::OPEN_SUBTITLE:
882 _subtitle_asset = asset;
884 case TextType::CLOSED_CAPTION:
885 DCPOMATIC_ASSERT (track);
886 _closed_caption_assets[*track] = asset;
889 DCPOMATIC_ASSERT (false);
892 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
893 auto const tcr = 1000;
895 for (auto i: subs.string) {
896 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
897 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
898 asset->add (make_shared<dcp::SubtitleString>(i));
901 for (auto i: subs.bitmap) {
903 make_shared<dcp::SubtitleImage>(
904 image_as_png(i.image),
905 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
906 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
907 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
908 dcp::Time(), dcp::Time()
915 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
917 LOG_GENERAL ("Checking existing picture frame %1", frame);
919 /* Read the data from the info file; for 3D we just check the left
920 frames until we find a good one.
922 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
926 /* Read the data from the asset and hash it */
927 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
928 ArrayData data (info.size);
929 size_t const read = fread (data.data(), 1, data.size(), asset_file);
930 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
931 if (read != static_cast<size_t> (data.size ())) {
932 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
936 digester.add (data.data(), data.size());
937 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
938 if (digester.get() != info.hash) {
939 LOG_GENERAL ("Existing frame %1 failed hash check", frame);