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>
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)
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());
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 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
212 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
213 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
214 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
219 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
221 dcp::FrameInfo frame_info;
222 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
223 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
224 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
226 char hash_buffer[33];
227 checked_fread (hash_buffer, 32, info->get(), info->file());
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 auto asset_file = fopen_boost (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");
281 /* Offset of the last dcp::FrameInfo in the info file */
282 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
283 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
285 Frame first_nonexistant_frame;
286 if (film()->three_d()) {
287 /* Start looking at the last left frame */
288 first_nonexistant_frame = n / 2;
290 first_nonexistant_frame = n;
293 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
294 --first_nonexistant_frame;
297 if (!film()->three_d() && first_nonexistant_frame > 0) {
298 /* If we are doing 3D we might have found a good L frame with no R, so only
299 do this if we're in 2D and we've just found a good B(oth) frame.
301 ++first_nonexistant_frame;
304 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
308 return first_nonexistant_frame;
313 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
315 if (!_picture_asset_writer) {
316 /* We're not writing any data */
320 auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
321 write_frame_info (frame, eyes, fin);
322 _last_written[static_cast<int>(eyes)] = encoded;
327 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
330 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
331 if (film()->encrypted()) {
332 _atmos_asset->set_key(film()->key());
334 _atmos_asset_writer = _atmos_asset->start_write (
335 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
338 _atmos_asset_writer->write (atmos);
343 ReelWriter::fake_write (int size)
345 if (!_picture_asset_writer) {
346 /* We're not writing any data */
350 _picture_asset_writer->fake_write (size);
355 ReelWriter::repeat_write (Frame frame, Eyes eyes)
357 if (!_picture_asset_writer) {
358 /* We're not writing any data */
362 auto fin = _picture_asset_writer->write (
363 _last_written[static_cast<int>(eyes)]->data(),
364 _last_written[static_cast<int>(eyes)]->size()
366 write_frame_info (frame, eyes, fin);
371 ReelWriter::finish (boost::filesystem::path output_dcp)
373 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
374 /* Nothing was written to the picture asset */
375 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
376 _picture_asset.reset ();
379 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
380 /* Nothing was written to the sound asset */
381 _sound_asset.reset ();
384 /* Hard-link any video asset file into the DCP */
385 if (_picture_asset) {
386 DCPOMATIC_ASSERT (_picture_asset->file());
387 boost::filesystem::path video_from = _picture_asset->file().get();
388 boost::filesystem::path video_to = output_dcp;
389 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
390 /* There may be an existing "to" file if we are recreating a DCP in the same place without
393 boost::system::error_code ec;
394 boost::filesystem::remove (video_to, ec);
396 boost::filesystem::create_hard_link (video_from, video_to, ec);
398 LOG_WARNING_NC ("Hard-link failed; copying instead");
399 auto job = _job.lock ();
401 job->sub (_("Copying video file into DCP"));
403 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
404 } catch (exception& e) {
405 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
406 throw FileError (e.what(), video_from);
409 boost::filesystem::copy_file (video_from, video_to, ec);
411 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
412 throw FileError (ec.message(), video_from);
417 _picture_asset->set_file (video_to);
420 /* Move the audio asset into the DCP */
422 boost::filesystem::path audio_to = output_dcp;
423 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
426 boost::system::error_code ec;
427 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
430 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
434 _sound_asset->set_file (audio_to);
438 _atmos_asset_writer->finalize ();
439 boost::filesystem::path atmos_to = output_dcp;
440 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
443 boost::system::error_code ec;
444 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
447 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
451 _atmos_asset->set_file (atmos_to);
456 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
457 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
459 template <class Interop, class SMPTE, class Result>
462 shared_ptr<dcp::SubtitleAsset> asset,
463 int64_t picture_duration,
464 shared_ptr<dcp::Reel> reel,
465 list<ReferencedReelAsset> const & refs,
466 vector<FontData> const & fonts,
467 dcp::ArrayData default_font,
468 shared_ptr<const Film> film,
469 DCPTimePeriod period,
470 boost::filesystem::path output_dcp,
474 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
476 shared_ptr<Result> reel_asset;
479 /* Add the font to the subtitle content */
480 for (auto const& j: fonts) {
481 asset->add_font (j.id, j.data.get_value_or(default_font));
484 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
485 auto directory = output_dcp / interop->id ();
486 boost::filesystem::create_directories (directory);
487 interop->write (directory / ("sub_" + interop->id() + ".xml"));
488 reel_asset = make_shared<Interop> (
490 dcp::Fraction(film->video_frame_rate(), 1),
494 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
495 /* All our assets should be the same length; use the picture asset length here
496 as a reference to set the subtitle one. We'll use the duration rather than
497 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
498 just interested in its presentation length.
500 smpte->set_intrinsic_duration(picture_duration);
502 output_dcp / ("sub_" + asset->id() + ".mxf")
504 reel_asset = make_shared<SMPTE> (
506 dcp::Fraction(film->video_frame_rate(), 1),
513 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
515 auto k = dynamic_pointer_cast<Result> (j.asset);
516 if (k && j.period == period) {
518 /* If we have a hash for this asset in the CPL, assume that it is correct */
520 k->asset_ref()->set_hash (k->hash().get());
527 if (!text_only && reel_asset->actual_duration() != period_duration) {
528 throw ProgrammingError (
530 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
533 reel->add (reel_asset);
540 shared_ptr<dcp::ReelPictureAsset>
541 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
543 shared_ptr<dcp::ReelPictureAsset> reel_asset;
545 if (_picture_asset) {
546 /* We have made a picture asset of our own. Put it into the reel */
547 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
549 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
552 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
554 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
557 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
558 /* We don't have a picture asset of our own; hopefully we have one to reference */
560 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
562 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
564 if (k && j.period == _period) {
570 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
572 DCPOMATIC_ASSERT (reel_asset);
573 if (reel_asset->duration() != period_duration) {
574 throw ProgrammingError (
576 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
579 reel->add (reel_asset);
581 /* If we have a hash for this asset in the CPL, assume that it is correct */
582 if (reel_asset->hash()) {
583 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
591 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
593 shared_ptr<dcp::ReelSoundAsset> reel_asset;
596 /* We have made a sound asset of our own. Put it into the reel */
597 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
599 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
600 /* We don't have a sound asset of our own; hopefully we have one to reference */
602 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
604 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
606 if (k && j.period == _period) {
608 /* If we have a hash for this asset in the CPL, assume that it is correct */
610 k->asset_ref()->set_hash (k->hash().get());
616 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
618 DCPOMATIC_ASSERT (reel_asset);
619 if (reel_asset->actual_duration() != period_duration) {
621 "Reel sound asset has length %1 but reel period is %2",
622 reel_asset->actual_duration(),
625 if (reel_asset->actual_duration() != period_duration) {
626 throw ProgrammingError (
628 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
633 reel->add (reel_asset);
638 ReelWriter::create_reel_text (
639 shared_ptr<dcp::Reel> reel,
640 list<ReferencedReelAsset> const & refs,
641 vector<FontData> const& fonts,
643 boost::filesystem::path output_dcp,
644 bool ensure_subtitles,
645 set<DCPTextTrack> ensure_closed_captions
648 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
649 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
653 /* We have a subtitle asset that we either made or are referencing */
654 if (auto main_language = film()->subtitle_languages().first) {
655 subtitle->set_language (*main_language);
657 } else if (ensure_subtitles) {
658 /* We had no subtitle asset, but we've been asked to make sure there is one */
659 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
660 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
673 for (auto const& i: _closed_caption_assets) {
674 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
675 i.second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
677 DCPOMATIC_ASSERT (a);
678 a->set_annotation_text (i.first.name);
679 if (i.first.language) {
680 a->set_language (i.first.language.get());
683 ensure_closed_captions.erase (i.first);
686 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
687 for (auto i: ensure_closed_captions) {
688 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
689 empty_text_asset(TextType::CLOSED_CAPTION, i, true), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
691 DCPOMATIC_ASSERT (a);
692 a->set_annotation_text (i.name);
694 a->set_language (i.language.get());
701 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
703 auto markers = film()->markers();
704 film()->add_ffoc_lfoc(markers);
705 Film::Markers reel_markers;
706 for (auto const& i: markers) {
707 if (_period.contains(i.second)) {
708 reel_markers[i.first] = i.second;
712 if (!reel_markers.empty ()) {
713 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration(), 0);
714 for (auto const& i: reel_markers) {
715 DCPTime relative = i.second - _period.from;
716 auto hmsf = relative.split (film()->video_frame_rate());
717 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
724 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
725 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
727 shared_ptr<dcp::Reel>
728 ReelWriter::create_reel (
729 list<ReferencedReelAsset> const & refs,
730 vector<FontData> const & fonts,
731 boost::filesystem::path output_dcp,
732 bool ensure_subtitles,
733 set<DCPTextTrack> ensure_closed_captions
736 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
738 auto reel = make_shared<dcp::Reel>();
740 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
741 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
742 * how big they are, we don't care about that.
744 int64_t duration = 0;
746 auto reel_picture_asset = create_reel_picture (reel, refs);
747 duration = reel_picture_asset->actual_duration ();
748 create_reel_sound (reel, refs);
749 create_reel_markers (reel);
752 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
755 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
763 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
766 if (_picture_asset) {
767 _picture_asset->hash (set_progress);
771 _sound_asset->hash (set_progress);
775 _atmos_asset->hash (set_progress);
777 } catch (boost::thread_interrupted) {
778 /* set_progress contains an interruption_point, so any of these methods
779 * may throw thread_interrupted, at which point we just give up.
785 ReelWriter::start () const
787 return _period.from.frames_floor (film()->video_frame_rate());
792 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
794 if (!_sound_asset_writer) {
798 DCPOMATIC_ASSERT (audio);
799 _sound_asset_writer->write (audio->data(), audio->frames());
803 shared_ptr<dcp::SubtitleAsset>
804 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
806 shared_ptr<dcp::SubtitleAsset> asset;
808 auto lang = film()->subtitle_languages();
809 if (film()->interop()) {
810 auto s = make_shared<dcp::InteropSubtitleAsset>();
811 s->set_movie_title (film()->name());
812 if (type == TextType::OPEN_SUBTITLE) {
813 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
814 } else if (track->language) {
815 s->set_language (track->language->to_string());
817 s->set_reel_number (raw_convert<string> (_reel_index + 1));
820 auto s = make_shared<dcp::SMPTESubtitleAsset>();
821 s->set_content_title_text (film()->name());
822 s->set_metadata (mxf_metadata());
823 if (type == TextType::OPEN_SUBTITLE && lang.first) {
824 s->set_language (*lang.first);
825 } else if (track && track->language) {
826 s->set_language (dcp::LanguageTag(track->language->to_string()));
828 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
829 s->set_reel_number (_reel_index + 1);
830 s->set_time_code_rate (film()->video_frame_rate());
831 s->set_start_time (dcp::Time ());
832 if (film()->encrypted()) {
833 s->set_key (film()->key());
837 std::make_shared<dcp::SubtitleString>(
838 optional<std::string>(),
845 dcp::Time(0, 0, 0, 0, 24),
846 dcp::Time(0, 0, 1, 0, 24),
869 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
871 shared_ptr<dcp::SubtitleAsset> asset;
874 case TextType::OPEN_SUBTITLE:
875 asset = _subtitle_asset;
877 case TextType::CLOSED_CAPTION:
878 DCPOMATIC_ASSERT (track);
879 asset = _closed_caption_assets[*track];
882 DCPOMATIC_ASSERT (false);
886 asset = empty_text_asset (type, track, false);
890 case TextType::OPEN_SUBTITLE:
891 _subtitle_asset = asset;
893 case TextType::CLOSED_CAPTION:
894 DCPOMATIC_ASSERT (track);
895 _closed_caption_assets[*track] = asset;
898 DCPOMATIC_ASSERT (false);
901 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
902 auto const tcr = 1000;
904 for (auto i: subs.string) {
905 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
906 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
907 asset->add (make_shared<dcp::SubtitleString>(i));
910 for (auto i: subs.bitmap) {
912 make_shared<dcp::SubtitleImage>(
913 image_as_png(i.image),
914 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
915 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
916 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP,
917 dcp::Time(), dcp::Time()
925 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
927 LOG_GENERAL ("Checking existing picture frame %1", frame);
929 /* Read the data from the info file; for 3D we just check the left
930 frames until we find a good one.
932 auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
936 /* Read the data from the asset and hash it */
937 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
938 ArrayData data (info.size);
939 size_t const read = fread (data.data(), 1, data.size(), asset_file);
940 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
941 if (read != static_cast<size_t> (data.size ())) {
942 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
946 digester.add (data.data(), data.size());
947 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
948 if (digester.get() != info.hash) {
949 LOG_GENERAL ("Existing frame %1 failed hash check", frame);