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/mono_picture_asset.h>
36 #include <dcp/stereo_picture_asset.h>
37 #include <dcp/sound_asset.h>
38 #include <dcp/sound_asset_writer.h>
40 #include <dcp/reel_atmos_asset.h>
41 #include <dcp/reel_mono_picture_asset.h>
42 #include <dcp/reel_stereo_picture_asset.h>
43 #include <dcp/reel_sound_asset.h>
44 #include <dcp/reel_subtitle_asset.h>
45 #include <dcp/reel_closed_caption_asset.h>
46 #include <dcp/reel_markers_asset.h>
49 #include <dcp/certificate_chain.h>
50 #include <dcp/interop_subtitle_asset.h>
51 #include <dcp/smpte_subtitle_asset.h>
52 #include <dcp/raw_convert.h>
53 #include <dcp/subtitle_image.h>
54 #include <boost/foreach.hpp>
65 using std::shared_ptr;
66 using boost::optional;
67 using std::dynamic_pointer_cast;
68 #if BOOST_VERSION >= 106100
69 using namespace boost::placeholders;
74 using dcp::raw_convert;
75 using namespace dcpomatic;
77 int const ReelWriter::_info_size = 48;
79 static dcp::MXFMetadata
82 dcp::MXFMetadata meta;
83 Config* config = Config::instance();
84 if (!config->dcp_company_name().empty()) {
85 meta.company_name = config->dcp_company_name ();
87 if (!config->dcp_product_name().empty()) {
88 meta.product_name = config->dcp_product_name ();
90 if (!config->dcp_product_version().empty()) {
91 meta.product_version = config->dcp_product_version ();
96 /** @param job Related job, or 0.
97 * @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
98 * (no picture nor sound) and not give errors in that case. This is used by the hints system to check the potential sizes of
99 * subtitle / closed caption files.
101 ReelWriter::ReelWriter (
102 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
104 : WeakConstFilm (weak_film)
106 , _reel_index (reel_index)
107 , _reel_count (reel_count)
108 , _content_summary (film()->content_summary(period))
110 , _text_only (text_only)
112 /* Create or find our picture asset in a subdirectory, named
113 according to those film's parameters which affect the video
114 output. We will hard-link it into the DCP later.
117 dcp::Standard const standard = film()->interop() ? dcp::INTEROP : dcp::SMPTE;
119 boost::filesystem::path const asset =
120 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
122 _first_nonexistant_frame = check_existing_picture_asset (asset);
124 if (_first_nonexistant_frame < period.duration().frames_round(film()->video_frame_rate())) {
125 /* We do not have a complete picture asset. If there is an
126 existing asset, break any hard links to it as we are about
127 to change its contents (if only by changing the IDs); see
130 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
132 job->sub (_("Copying old video file"));
133 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
135 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
137 boost::filesystem::remove (asset);
138 boost::filesystem::rename (asset.string() + ".tmp", asset);
142 if (film()->three_d()) {
143 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
145 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
148 _picture_asset->set_size (film()->frame_size());
149 _picture_asset->set_metadata (mxf_metadata());
151 if (film()->encrypted()) {
152 _picture_asset->set_key (film()->key());
153 _picture_asset->set_context_id (film()->context_id());
156 _picture_asset->set_file (asset);
157 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
158 } else if (!text_only) {
159 /* We already have a complete picture asset that we can just re-use */
160 /* XXX: what about if the encryption key changes? */
161 if (film()->three_d()) {
162 _picture_asset.reset (new dcp::StereoPictureAsset(asset));
164 _picture_asset.reset (new dcp::MonoPictureAsset(asset));
168 if (film()->audio_channels()) {
170 new dcp::SoundAsset (dcp::Fraction(film()->video_frame_rate(), 1), film()->audio_frame_rate(), film()->audio_channels(), film()->audio_language(), standard)
173 _sound_asset->set_metadata (mxf_metadata());
175 if (film()->encrypted()) {
176 _sound_asset->set_key (film()->key());
179 DCPOMATIC_ASSERT (film()->directory());
181 vector<dcp::Channel> active;
182 BOOST_FOREACH (int i, film()->mapped_audio_channels()) {
183 active.push_back (static_cast<dcp::Channel>(i));
186 /* Write the sound asset into the film directory so that we leave the creation
187 of the DCP directory until the last minute.
189 _sound_asset_writer = _sound_asset->start_write (
190 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
192 film()->contains_atmos_content()
196 _default_font = dcp::ArrayData(default_font_file());
199 /** @param frame reel-relative frame */
201 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
203 shared_ptr<InfoFileHandle> handle = film()->info_file_handle(_period, false);
204 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
205 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
206 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
207 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
211 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
213 dcp::FrameInfo frame_info;
214 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
215 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
216 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
218 char hash_buffer[33];
219 checked_fread (hash_buffer, 32, info->get(), info->file());
220 hash_buffer[32] = '\0';
221 frame_info.hash = hash_buffer;
227 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
231 return frame * _info_size;
233 return frame * _info_size * 2;
235 return frame * _info_size * 2 + _info_size;
237 DCPOMATIC_ASSERT (false);
240 DCPOMATIC_ASSERT (false);
244 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
246 shared_ptr<Job> job = _job.lock ();
249 job->sub (_("Checking existing image data"));
252 /* Try to open the existing asset */
253 FILE* asset_file = fopen_boost (asset, "rb");
255 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
258 LOG_GENERAL ("Opened existing asset at %1", asset.string());
261 shared_ptr<InfoFileHandle> info_file;
264 info_file = film()->info_file_handle (_period, true);
265 } catch (OpenFileError &) {
266 LOG_GENERAL_NC ("Could not open film info file");
271 /* Offset of the last dcp::FrameInfo in the info file */
272 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
273 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
275 Frame first_nonexistant_frame;
276 if (film()->three_d()) {
277 /* Start looking at the last left frame */
278 first_nonexistant_frame = n / 2;
280 first_nonexistant_frame = n;
283 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
284 --first_nonexistant_frame;
287 if (!film()->three_d() && first_nonexistant_frame > 0) {
288 /* If we are doing 3D we might have found a good L frame with no R, so only
289 do this if we're in 2D and we've just found a good B(oth) frame.
291 ++first_nonexistant_frame;
294 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
298 return first_nonexistant_frame;
302 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
304 if (!_picture_asset_writer) {
305 /* We're not writing any data */
309 dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data(), encoded->size());
310 write_frame_info (frame, eyes, fin);
311 _last_written[eyes] = encoded;
316 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
319 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
320 if (film()->encrypted()) {
321 _atmos_asset->set_key(film()->key());
323 _atmos_asset_writer = _atmos_asset->start_write (
324 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
327 _atmos_asset_writer->write (atmos);
332 ReelWriter::fake_write (int size)
334 if (!_picture_asset_writer) {
335 /* We're not writing any data */
339 _picture_asset_writer->fake_write (size);
343 ReelWriter::repeat_write (Frame frame, Eyes eyes)
345 if (!_picture_asset_writer) {
346 /* We're not writing any data */
350 dcp::FrameInfo fin = _picture_asset_writer->write (
351 _last_written[eyes]->data(),
352 _last_written[eyes]->size()
354 write_frame_info (frame, eyes, fin);
358 ReelWriter::finish (boost::filesystem::path output_dcp)
360 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
361 /* Nothing was written to the picture asset */
362 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
363 _picture_asset.reset ();
366 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
367 /* Nothing was written to the sound asset */
368 _sound_asset.reset ();
371 /* Hard-link any video asset file into the DCP */
372 if (_picture_asset) {
373 DCPOMATIC_ASSERT (_picture_asset->file());
374 boost::filesystem::path video_from = _picture_asset->file().get();
375 boost::filesystem::path video_to = output_dcp;
376 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
377 /* There may be an existing "to" file if we are recreating a DCP in the same place without
380 boost::system::error_code ec;
381 boost::filesystem::remove (video_to, ec);
383 boost::filesystem::create_hard_link (video_from, video_to, ec);
385 LOG_WARNING_NC ("Hard-link failed; copying instead");
386 shared_ptr<Job> job = _job.lock ();
388 job->sub (_("Copying video file into DCP"));
390 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
391 } catch (exception& e) {
392 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
393 throw FileError (e.what(), video_from);
396 boost::filesystem::copy_file (video_from, video_to, ec);
398 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
399 throw FileError (ec.message(), video_from);
404 _picture_asset->set_file (video_to);
407 /* Move the audio asset into the DCP */
409 boost::filesystem::path audio_to = output_dcp;
410 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
413 boost::system::error_code ec;
414 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
417 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
421 _sound_asset->set_file (audio_to);
425 _atmos_asset_writer->finalize ();
426 boost::filesystem::path atmos_to = output_dcp;
427 string const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
430 boost::system::error_code ec;
431 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
434 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
438 _atmos_asset->set_file (atmos_to);
445 shared_ptr<dcp::SubtitleAsset> asset,
446 int64_t picture_duration,
447 shared_ptr<dcp::Reel> reel,
448 list<ReferencedReelAsset> const & refs,
449 vector<FontData> const & fonts,
450 dcp::ArrayData default_font,
451 shared_ptr<const Film> film,
452 DCPTimePeriod period,
453 boost::filesystem::path output_dcp,
457 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
459 shared_ptr<T> reel_asset;
462 /* Add the font to the subtitle content */
463 BOOST_FOREACH (FontData const& j, fonts) {
464 asset->add_font (j.id, j.data.get_value_or(default_font));
467 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
468 boost::filesystem::path directory = output_dcp / asset->id ();
469 boost::filesystem::create_directories (directory);
470 asset->write (directory / ("sub_" + asset->id() + ".xml"));
472 /* All our assets should be the same length; use the picture asset length here
473 as a reference to set the subtitle one. We'll use the duration rather than
474 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
475 just interested in its presentation length.
477 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
480 output_dcp / ("sub_" + asset->id() + ".mxf")
487 dcp::Fraction (film->video_frame_rate(), 1),
493 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
494 BOOST_FOREACH (ReferencedReelAsset j, refs) {
495 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
496 if (k && j.period == period) {
498 /* If we have a hash for this asset in the CPL, assume that it is correct */
500 k->asset_ref()->set_hash (k->hash().get());
507 if (!text_only && reel_asset->actual_duration() != period_duration) {
508 throw ProgrammingError (
510 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
513 reel->add (reel_asset);
520 shared_ptr<dcp::ReelPictureAsset>
521 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
523 shared_ptr<dcp::ReelPictureAsset> reel_asset;
525 if (_picture_asset) {
526 /* We have made a picture asset of our own. Put it into the reel */
527 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
529 reel_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
532 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
534 reel_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
537 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
538 /* We don't have a picture asset of our own; hopefully we have one to reference */
539 BOOST_FOREACH (ReferencedReelAsset j, refs) {
540 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
542 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
544 if (k && j.period == _period) {
550 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
552 DCPOMATIC_ASSERT (reel_asset);
553 if (reel_asset->duration() != period_duration) {
554 throw ProgrammingError (
556 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
559 reel->add (reel_asset);
561 /* If we have a hash for this asset in the CPL, assume that it is correct */
562 if (reel_asset->hash()) {
563 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
571 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
573 shared_ptr<dcp::ReelSoundAsset> reel_asset;
576 /* We have made a sound asset of our own. Put it into the reel */
577 reel_asset.reset (new dcp::ReelSoundAsset(_sound_asset, 0));
579 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
580 /* We don't have a sound asset of our own; hopefully we have one to reference */
581 BOOST_FOREACH (ReferencedReelAsset j, refs) {
582 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
584 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
586 if (k && j.period == _period) {
588 /* If we have a hash for this asset in the CPL, assume that it is correct */
590 k->asset_ref()->set_hash (k->hash().get());
596 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
598 DCPOMATIC_ASSERT (reel_asset);
599 if (reel_asset->actual_duration() != period_duration) {
601 "Reel sound asset has length %1 but reel period is %2",
602 reel_asset->actual_duration(),
605 if (reel_asset->actual_duration() != period_duration) {
606 throw ProgrammingError (
608 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
613 reel->add (reel_asset);
618 ReelWriter::create_reel_text (
619 shared_ptr<dcp::Reel> reel,
620 list<ReferencedReelAsset> const & refs,
621 vector<FontData> const& fonts,
623 boost::filesystem::path output_dcp,
624 bool ensure_subtitles,
625 set<DCPTextTrack> ensure_closed_captions
628 shared_ptr<dcp::ReelSubtitleAsset> subtitle = maybe_add_text<dcp::ReelSubtitleAsset> (
629 _subtitle_asset, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
633 /* We have a subtitle asset that we either made or are referencing */
634 if (!film()->subtitle_languages().empty()) {
635 subtitle->set_language (film()->subtitle_languages().front());
637 } else if (ensure_subtitles) {
638 /* We had no subtitle asset, but we've been asked to make sure there is one */
639 subtitle = maybe_add_text<dcp::ReelSubtitleAsset>(
640 empty_text_asset(TEXT_OPEN_SUBTITLE, optional<DCPTextTrack>()),
653 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
654 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
655 i->second, duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
657 DCPOMATIC_ASSERT (a);
658 a->set_annotation_text (i->first.name);
659 if (!i->first.language.empty()) {
660 a->set_language (dcp::LanguageTag(i->first.language));
663 ensure_closed_captions.erase (i->first);
666 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
667 BOOST_FOREACH (DCPTextTrack i, ensure_closed_captions) {
668 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
669 empty_text_asset(TEXT_CLOSED_CAPTION, i), duration, reel, refs, fonts, _default_font, film(), _period, output_dcp, _text_only
671 DCPOMATIC_ASSERT (a);
672 a->set_annotation_text (i.name);
673 if (!i.language.empty()) {
674 a->set_language (dcp::LanguageTag(i.language));
682 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
684 Film::Markers markers = film()->markers();
685 film()->add_ffoc_lfoc(markers);
686 Film::Markers reel_markers;
687 for (Film::Markers::const_iterator i = markers.begin(); i != markers.end(); ++i) {
688 if (_period.contains(i->second)) {
689 reel_markers[i->first] = i->second;
693 if (!reel_markers.empty ()) {
694 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(film()->video_frame_rate(), 1), 0));
695 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
697 DCPTime relative = i->second - _period.from;
698 relative.split (film()->video_frame_rate(), h, m, s, f);
699 ma->set (i->first, dcp::Time(h, m, s, f, film()->video_frame_rate()));
706 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
707 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
709 shared_ptr<dcp::Reel>
710 ReelWriter::create_reel (
711 list<ReferencedReelAsset> const & refs,
712 vector<FontData> const & fonts,
713 boost::filesystem::path output_dcp,
714 bool ensure_subtitles,
715 set<DCPTextTrack> ensure_closed_captions
718 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
720 shared_ptr<dcp::Reel> reel (new dcp::Reel());
722 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
723 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
724 * how big they are, we don't care about that.
726 int64_t duration = 0;
728 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset = create_reel_picture (reel, refs);
729 duration = reel_picture_asset->actual_duration ();
730 create_reel_sound (reel, refs);
731 create_reel_markers (reel);
734 create_reel_text (reel, refs, fonts, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
737 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
744 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
746 if (_picture_asset) {
747 _picture_asset->hash (set_progress);
751 _sound_asset->hash (set_progress);
755 _atmos_asset->hash (set_progress);
760 ReelWriter::start () const
762 return _period.from.frames_floor (film()->video_frame_rate());
767 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
769 if (!_sound_asset_writer) {
773 DCPOMATIC_ASSERT (audio);
774 _sound_asset_writer->write (audio->data(), audio->frames());
778 shared_ptr<dcp::SubtitleAsset>
779 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track) const
781 shared_ptr<dcp::SubtitleAsset> asset;
783 vector<dcp::LanguageTag> lang = film()->subtitle_languages();
784 if (film()->interop()) {
785 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
786 s->set_movie_title (film()->name());
787 if (type == TEXT_OPEN_SUBTITLE) {
788 s->set_language (lang.empty() ? "Unknown" : lang.front().to_string());
789 } else if (!track->language.empty()) {
790 s->set_language (track->language);
792 s->set_reel_number (raw_convert<string> (_reel_index + 1));
795 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
796 s->set_content_title_text (film()->name());
797 s->set_metadata (mxf_metadata());
798 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
799 s->set_language (lang.front());
800 } else if (track && !track->language.empty()) {
801 s->set_language (dcp::LanguageTag(track->language));
803 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
804 s->set_reel_number (_reel_index + 1);
805 s->set_time_code_rate (film()->video_frame_rate());
806 s->set_start_time (dcp::Time ());
807 if (film()->encrypted()) {
808 s->set_key (film()->key());
818 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
820 shared_ptr<dcp::SubtitleAsset> asset;
823 case TEXT_OPEN_SUBTITLE:
824 asset = _subtitle_asset;
826 case TEXT_CLOSED_CAPTION:
827 DCPOMATIC_ASSERT (track);
828 asset = _closed_caption_assets[*track];
831 DCPOMATIC_ASSERT (false);
835 asset = empty_text_asset (type, track);
839 case TEXT_OPEN_SUBTITLE:
840 _subtitle_asset = asset;
842 case TEXT_CLOSED_CAPTION:
843 DCPOMATIC_ASSERT (track);
844 _closed_caption_assets[*track] = asset;
847 DCPOMATIC_ASSERT (false);
850 BOOST_FOREACH (StringText i, subs.string) {
851 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
852 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
853 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
854 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
857 BOOST_FOREACH (BitmapText i, subs.bitmap) {
859 shared_ptr<dcp::Subtitle>(
860 new dcp::SubtitleImage(
862 dcp::Time(period.from.seconds() - _period.from.seconds(), film()->video_frame_rate()),
863 dcp::Time(period.to.seconds() - _period.from.seconds(), film()->video_frame_rate()),
864 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
865 dcp::Time(), dcp::Time()
873 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
875 LOG_GENERAL ("Checking existing picture frame %1", frame);
877 /* Read the data from the info file; for 3D we just check the left
878 frames until we find a good one.
880 dcp::FrameInfo const info = read_frame_info (info_file, frame, film()->three_d() ? EYES_LEFT : EYES_BOTH);
884 /* Read the data from the asset and hash it */
885 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
886 ArrayData data (info.size);
887 size_t const read = fread (data.data(), 1, data.size(), asset_file);
888 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
889 if (read != static_cast<size_t> (data.size ())) {
890 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
894 digester.add (data.data(), data.size());
895 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
896 if (digester.get() != info.hash) {
897 LOG_GENERAL ("Existing frame %1 failed hash check", frame);