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/>.
21 #include "reel_writer.h"
26 #include "dcpomatic_log.h"
28 #include "font_data.h"
29 #include "compose.hpp"
31 #include "audio_buffers.h"
33 #include <dcp/atmos_asset.h>
34 #include <dcp/atmos_asset_writer.h>
35 #include <dcp/certificate_chain.h>
38 #include <dcp/interop_subtitle_asset.h>
39 #include <dcp/mono_picture_asset.h>
40 #include <dcp/raw_convert.h>
42 #include <dcp/reel_atmos_asset.h>
43 #include <dcp/reel_interop_closed_caption_asset.h>
44 #include <dcp/reel_interop_subtitle_asset.h>
45 #include <dcp/reel_markers_asset.h>
46 #include <dcp/reel_mono_picture_asset.h>
47 #include <dcp/reel_smpte_closed_caption_asset.h>
48 #include <dcp/reel_smpte_subtitle_asset.h>
49 #include <dcp/reel_sound_asset.h>
50 #include <dcp/reel_stereo_picture_asset.h>
51 #include <dcp/smpte_subtitle_asset.h>
52 #include <dcp/sound_asset.h>
53 #include <dcp/sound_asset_writer.h>
54 #include <dcp/stereo_picture_asset.h>
55 #include <dcp/subtitle_image.h>
66 using std::shared_ptr;
67 using std::make_shared;
68 using boost::optional;
69 using std::dynamic_pointer_cast;
70 #if BOOST_VERSION >= 106100
71 using namespace boost::placeholders;
76 using dcp::raw_convert;
77 using namespace dcpomatic;
79 int const ReelWriter::_info_size = 48;
81 static dcp::MXFMetadata
84 dcp::MXFMetadata meta;
85 auto config = Config::instance();
86 if (!config->dcp_company_name().empty()) {
87 meta.company_name = config->dcp_company_name ();
89 if (!config->dcp_product_name().empty()) {
90 meta.product_name = config->dcp_product_name ();
92 if (!config->dcp_product_version().empty()) {
93 meta.product_version = config->dcp_product_version ();
98 /** @param job Related job, or 0.
99 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
100 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
101 * subtitle / closed caption files.
103 ReelWriter::ReelWriter (
104 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
106 : WeakConstFilm (weak_film)
108 , _reel_index (reel_index)
109 , _reel_count (reel_count)
110 , _content_summary (film()->content_summary(period))
112 , _text_only (text_only)
114 /* Create or find our picture asset in a subdirectory, named
115 according to those film's parameters which affect the video
116 output. We will hard-link it into the DCP later.
119 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
121 boost::filesystem::path const asset =
122 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
124 _first_nonexistant_frame = check_existing_picture_asset (asset);
126 if (_first_nonexistant_frame < period.duration().frames_round(film()->video_frame_rate())) {
127 /* We do not have a complete picture asset. If there is an
128 existing asset, break any hard links to it as we are about
129 to change its contents (if only by changing the IDs); see
132 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
134 job->sub (_("Copying old video file"));
135 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
137 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
139 boost::filesystem::remove (asset);
140 boost::filesystem::rename (asset.string() + ".tmp", asset);
144 if (film()->three_d()) {
145 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
147 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
150 _picture_asset->set_size (film()->frame_size());
151 _picture_asset->set_metadata (mxf_metadata());
153 if (film()->encrypted()) {
154 _picture_asset->set_key (film()->key());
155 _picture_asset->set_context_id (film()->context_id());
158 _picture_asset->set_file (asset);
159 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
160 } else if (!text_only) {
161 /* We already have a complete picture asset that we can just re-use */
162 /* XXX: what about if the encryption key changes? */
163 if (film()->three_d()) {
164 _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
166 _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
170 if (film()->audio_channels()) {
171 auto lang = film()->audio_language();
172 _sound_asset = make_shared<dcp::SoundAsset> (
173 dcp::Fraction(film()->video_frame_rate(), 1),
174 film()->audio_frame_rate(),
175 film()->audio_channels(),
176 lang ? *lang : dcp::LanguageTag("en-US"),
180 _sound_asset->set_metadata (mxf_metadata());
182 if (film()->encrypted()) {
183 _sound_asset->set_key (film()->key());
186 DCPOMATIC_ASSERT (film()->directory());
188 /* Write the sound asset into the film directory so that we leave the creation
189 of the DCP directory until the last minute.
191 _sound_asset_writer = _sound_asset->start_write (
192 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
193 film()->contains_atmos_content()
197 _default_font = dcp::ArrayData(default_font_file());
200 /** @param frame reel-relative frame */
202 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
204 auto handle = film()->info_file_handle(_period, false);
205 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
206 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
207 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
208 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
212 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
214 dcp::FrameInfo frame_info;
215 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
216 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
217 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
219 char hash_buffer[33];
220 checked_fread (hash_buffer, 32, info->get(), info->file());
221 hash_buffer[32] = '\0';
222 frame_info.hash = hash_buffer;
228 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
232 return frame * _info_size;
234 return frame * _info_size * 2;
236 return frame * _info_size * 2 + _info_size;
238 DCPOMATIC_ASSERT (false);
241 DCPOMATIC_ASSERT (false);
245 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
247 auto job = _job.lock ();
250 job->sub (_("Checking existing image data"));
253 /* Try to open the existing asset */
254 auto asset_file = fopen_boost (asset, "rb");
256 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
259 LOG_GENERAL ("Opened existing asset at %1", asset.string());
262 shared_ptr<InfoFileHandle> info_file;
265 info_file = film()->info_file_handle (_period, true);
266 } catch (OpenFileError &) {
267 LOG_GENERAL_NC ("Could not open film info file");
272 /* Offset of the last dcp::FrameInfo in the info file */
273 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
274 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
276 Frame first_nonexistant_frame;
277 if (film()->three_d()) {
278 /* Start looking at the last left frame */
279 first_nonexistant_frame = n / 2;
281 first_nonexistant_frame = n;
284 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
285 --first_nonexistant_frame;
288 if (!film()->three_d() && first_nonexistant_frame > 0) {
289 /* If we are doing 3D we might have found a good L frame with no R, so only
290 do this if we're in 2D and we've just found a good B(oth) frame.
292 ++first_nonexistant_frame;
295 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
299 return first_nonexistant_frame;
303 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
305 if (!_picture_asset_writer) {
306 /* We're not writing any data */
310 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
311 write_frame_info (frame, eyes, fin);
312 _last_written[static_cast<int>(eyes)] = encoded;
317 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
320 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
321 if (film()->encrypted()) {
322 _atmos_asset->set_key(film()->key());
324 _atmos_asset_writer = _atmos_asset->start_write (
325 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
328 _atmos_asset_writer->write (atmos);
333 ReelWriter::fake_write (int size)
335 if (!_picture_asset_writer) {
336 /* We're not writing any data */
340 _picture_asset_writer->fake_write (size);
344 ReelWriter::repeat_write (Frame frame, Eyes eyes)
346 if (!_picture_asset_writer) {
347 /* We're not writing any data */
351 auto fin = _picture_asset_writer->write (
352 _last_written[static_cast<int>(eyes)]->data(),
353 _last_written[static_cast<int>(eyes)]->size()
355 write_frame_info (frame, eyes, fin);
359 ReelWriter::finish (boost::filesystem::path output_dcp)
361 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
362 /* Nothing was written to the picture asset */
363 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
364 _picture_asset.reset ();
367 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
368 /* Nothing was written to the sound asset */
369 _sound_asset.reset ();
372 /* Hard-link any video asset file into the DCP */
373 if (_picture_asset) {
374 DCPOMATIC_ASSERT (_picture_asset->file());
375 boost::filesystem::path video_from = _picture_asset->file().get();
376 boost::filesystem::path video_to = output_dcp;
377 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
378 /* There may be an existing "to" file if we are recreating a DCP in the same place without
381 boost::system::error_code ec;
382 boost::filesystem::remove (video_to, ec);
384 boost::filesystem::create_hard_link (video_from, video_to, ec);
386 LOG_WARNING_NC ("Hard-link failed; copying instead");
387 auto job = _job.lock ();
389 job->sub (_("Copying video file into DCP"));
391 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
392 } catch (exception& e) {
393 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
394 throw FileError (e.what(), video_from);
397 boost::filesystem::copy_file (video_from, video_to, ec);
399 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
400 throw FileError (ec.message(), video_from);
405 _picture_asset->set_file (video_to);
408 /* Move the audio asset into the DCP */
410 boost::filesystem::path audio_to = output_dcp;
411 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
414 boost::system::error_code ec;
415 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
418 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
422 _sound_asset->set_file (audio_to);
426 _atmos_asset_writer->finalize ();
427 boost::filesystem::path atmos_to = output_dcp;
428 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
431 boost::system::error_code ec;
432 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
435 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
439 _atmos_asset->set_file (atmos_to);
444 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
445 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
447 template <class Interop, class SMPTE, class Result>
450 shared_ptr<dcp::SubtitleAsset> asset,
451 int64_t picture_duration,
452 shared_ptr<dcp::Reel> reel,
453 list<ReferencedReelAsset> const & refs,
454 vector<FontData> const & fonts,
455 dcp::ArrayData default_font,
456 shared_ptr<const Film> film,
457 DCPTimePeriod period,
458 boost::filesystem::path output_dcp,
462 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
464 shared_ptr<Result> reel_asset;
467 /* Add the font to the subtitle content */
468 for (auto const& j: fonts) {
469 asset->add_font (j.id, j.data.get_value_or(default_font));
472 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
473 auto directory = output_dcp / interop->id ();
474 boost::filesystem::create_directories (directory);
475 interop->write (directory / ("sub_" + interop->id() + ".xml"));
476 reel_asset = make_shared<Interop> (
478 dcp::Fraction(film->video_frame_rate(), 1),
482 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
483 /* All our assets should be the same length; use the picture asset length here
484 as a reference to set the subtitle one. We'll use the duration rather than
485 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
486 just interested in its presentation length.
488 smpte->set_intrinsic_duration(picture_duration);
490 output_dcp / ("sub_" + asset->id() + ".mxf")
492 reel_asset = make_shared<SMPTE> (
494 dcp::Fraction(film->video_frame_rate(), 1),
501 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
503 auto k = dynamic_pointer_cast<Result> (j.asset);
504 if (k && j.period == period) {
506 /* If we have a hash for this asset in the CPL, assume that it is correct */
508 k->asset_ref()->set_hash (k->hash().get());
515 if (!text_only && reel_asset->actual_duration() != period_duration) {
516 throw ProgrammingError (
518 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
521 reel->add (reel_asset);
528 shared_ptr<dcp::ReelPictureAsset>
529 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
531 shared_ptr<dcp::ReelPictureAsset> reel_asset;
533 if (_picture_asset) {
534 /* We have made a picture asset of our own. Put it into the reel */
535 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
537 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
540 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
542 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
545 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
546 /* We don't have a picture asset of our own; hopefully we have one to reference */
548 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
550 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
552 if (k && j.period == _period) {
558 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
560 DCPOMATIC_ASSERT (reel_asset);
561 if (reel_asset->duration() != period_duration) {
562 throw ProgrammingError (
564 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
567 reel->add (reel_asset);
569 /* If we have a hash for this asset in the CPL, assume that it is correct */
570 if (reel_asset->hash()) {
571 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
579 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
581 shared_ptr<dcp::ReelSoundAsset> reel_asset;
584 /* We have made a sound asset of our own. Put it into the reel */
585 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
587 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
588 /* We don't have a sound asset of our own; hopefully we have one to reference */
590 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
592 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
594 if (k && j.period == _period) {
596 /* If we have a hash for this asset in the CPL, assume that it is correct */
598 k->asset_ref()->set_hash (k->hash().get());
604 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
606 DCPOMATIC_ASSERT (reel_asset);
607 if (reel_asset->actual_duration() != period_duration) {
609 "Reel sound asset has length %1 but reel period is %2",
610 reel_asset->actual_duration(),
613 if (reel_asset->actual_duration() != period_duration) {
614 throw ProgrammingError (
616 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
621 reel->add (reel_asset);
626 ReelWriter::create_reel_text (
627 shared_ptr<dcp::Reel> reel,
628 list<ReferencedReelAsset> const & refs,
629 vector<FontData> const& fonts,
631 boost::filesystem::path output_dcp,
632 bool ensure_subtitles,
633 set<DCPTextTrack> ensure_closed_captions
636 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
637 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
641 /* We have a subtitle asset that we either made or are referencing */
642 if (auto main_language = film()->subtitle_languages().first) {
643 subtitle->set_language (*main_language);
645 } else if (ensure_subtitles) {
646 /* We had no subtitle asset, but we've been asked to make sure there is one */
647 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
648 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
661 for (auto const& i: _closed_caption_assets) {
662 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
663 i.second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
665 DCPOMATIC_ASSERT (a);
666 a->set_annotation_text (i.first.name);
667 if (i.first.language) {
668 a->set_language (i.first.language.get());
671 ensure_closed_captions.erase (i.first);
674 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
675 for (auto i: ensure_closed_captions) {
676 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
677 empty_text_asset(TextType::CLOSED_CAPTION, i, true), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
679 DCPOMATIC_ASSERT (a);
680 a->set_annotation_text (i.name);
682 a->set_language (i.language.get());
689 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
691 auto markers = film()->markers();
692 film()->add_ffoc_lfoc(markers);
693 Film::Markers reel_markers;
694 for (auto const& i: markers) {
695 if (_period.contains(i.second)) {
696 reel_markers[i.first] = i.second;
700 if (!reel_markers.empty ()) {
701 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration(), 0);
702 for (auto const& i: reel_markers) {
703 DCPTime relative = i.second - _period.from;
704 auto hmsf = relative.split (film()->video_frame_rate());
705 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
712 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
713 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
715 shared_ptr<dcp::Reel>
716 ReelWriter::create_reel (
717 list<ReferencedReelAsset> const & refs,
718 vector<FontData> const & fonts,
719 boost::filesystem::path output_dcp,
720 bool ensure_subtitles,
721 set<DCPTextTrack> ensure_closed_captions
724 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
726 auto reel = make_shared<dcp::Reel>();
728 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
729 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
730 * how big they are, we don't care about that.
732 int64_t duration = 0;
734 auto reel_picture_asset = create_reel_picture (reel, refs);
735 duration = reel_picture_asset->actual_duration ();
736 create_reel_sound (reel, refs);
737 create_reel_markers (reel);
740 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
743 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
750 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
753 if (_picture_asset) {
754 _picture_asset->hash (set_progress);
758 _sound_asset->hash (set_progress);
762 _atmos_asset->hash (set_progress);
764 } catch (boost::thread_interrupted) {
765 /* set_progress contains an interruption_point, so any of these methods
766 * may throw thread_interrupted, at which point we just give up.
771 ReelWriter::start () const
773 return _period.from.frames_floor (film()->video_frame_rate());
778 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
780 if (!_sound_asset_writer) {
784 DCPOMATIC_ASSERT (audio);
785 _sound_asset_writer->write (audio->data(), audio->frames());
789 shared_ptr<dcp::SubtitleAsset>
790 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
792 shared_ptr<dcp::SubtitleAsset> asset;
794 auto lang = film()->subtitle_languages();
795 if (film()->interop()) {
796 auto s = make_shared<dcp::InteropSubtitleAsset>();
797 s->set_movie_title (film()->name());
798 if (type == TextType::OPEN_SUBTITLE) {
799 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
800 } else if (track->language) {
801 s->set_language (track->language->to_string());
803 s->set_reel_number (raw_convert<string> (_reel_index + 1));
806 auto s = make_shared<dcp::SMPTESubtitleAsset>();
807 s->set_content_title_text (film()->name());
808 s->set_metadata (mxf_metadata());
809 if (type == TextType::OPEN_SUBTITLE && lang.first) {
810 s->set_language (*lang.first);
811 } else if (track && track->language) {
812 s->set_language (dcp::LanguageTag(track->language->to_string()));
814 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
815 s->set_reel_number (_reel_index + 1);
816 s->set_time_code_rate (film()->video_frame_rate());
817 s->set_start_time (dcp::Time ());
818 if (film()->encrypted()) {
819 s->set_key (film()->key());
823 std::make_shared<dcp::SubtitleString>(
824 optional<std::string>(),
831 dcp::Time(0, 0, 0, 0, 24),
832 dcp::Time(0, 0, 1, 0, 24),
854 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
856 shared_ptr<dcp::SubtitleAsset> asset;
859 case TextType::OPEN_SUBTITLE:
860 asset = _subtitle_asset;
862 case TextType::CLOSED_CAPTION:
863 DCPOMATIC_ASSERT (track);
864 asset = _closed_caption_assets[*track];
867 DCPOMATIC_ASSERT (false);
871 asset = empty_text_asset (type, track, false);
875 case TextType::OPEN_SUBTITLE:
876 _subtitle_asset = asset;
878 case TextType::CLOSED_CAPTION:
879 DCPOMATIC_ASSERT (track);
880 _closed_caption_assets[*track] = asset;
883 DCPOMATIC_ASSERT (false);
886 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
887 auto const tcr = 1000;
889 for (auto i: subs.string) {
890 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
891 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
892 asset->add (make_shared<dcp::SubtitleString>(i));
895 for (auto i: subs.bitmap) {
897 make_shared<dcp::SubtitleImage>(
899 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
900 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
901 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
902 dcp::Time(), dcp::Time()
909 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
911 LOG_GENERAL ("Checking existing picture frame %1", frame);
913 /* Read the data from the info file; for 3D we just check the left
914 frames until we find a good one.
916 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
920 /* Read the data from the asset and hash it */
921 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
922 ArrayData data (info.size);
923 size_t const read = fread (data.data(), 1, data.size(), asset_file);
924 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
925 if (read != static_cast<size_t> (data.size ())) {
926 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
930 digester.add (data.data(), data.size());
931 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
932 if (digester.get() != info.hash) {
933 LOG_GENERAL ("Existing frame %1 failed hash check", frame);