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"
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>
64 using boost::shared_ptr;
65 using boost::optional;
66 using boost::dynamic_pointer_cast;
67 #if BOOST_VERSION >= 106100
68 using namespace boost::placeholders;
72 using dcp::raw_convert;
73 using namespace dcpomatic;
75 int const ReelWriter::_info_size = 48;
77 static dcp::MXFMetadata
80 dcp::MXFMetadata meta;
81 Config* config = Config::instance();
82 if (!config->dcp_company_name().empty()) {
83 meta.company_name = config->dcp_company_name ();
85 if (!config->dcp_product_name().empty()) {
86 meta.product_name = config->dcp_product_name ();
88 if (!config->dcp_product_version().empty()) {
89 meta.product_version = config->dcp_product_version ();
94 /** @param job Related job, or 0 */
95 ReelWriter::ReelWriter (
96 shared_ptr<const Film> film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count
100 , _reel_index (reel_index)
101 , _reel_count (reel_count)
102 , _content_summary (film->content_summary(period))
105 /* Create or find our picture asset in a subdirectory, named
106 according to those film's parameters which affect the video
107 output. We will hard-link it into the DCP later.
110 dcp::Standard const standard = _film->interop() ? dcp::INTEROP : dcp::SMPTE;
112 boost::filesystem::path const asset =
113 _film->internal_video_asset_dir() / _film->internal_video_asset_filename(_period);
115 _first_nonexistant_frame = check_existing_picture_asset (asset);
117 if (_first_nonexistant_frame < period.duration().frames_round(_film->video_frame_rate())) {
118 /* We do not have a complete picture asset. If there is an
119 existing asset, break any hard links to it as we are about
120 to change its contents (if only by changing the IDs); see
123 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
125 job->sub (_("Copying old video file"));
126 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
128 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
130 boost::filesystem::remove (asset);
131 boost::filesystem::rename (asset.string() + ".tmp", asset);
135 if (_film->three_d ()) {
136 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
138 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
141 _picture_asset->set_size (_film->frame_size());
142 _picture_asset->set_metadata (mxf_metadata());
144 if (_film->encrypted ()) {
145 _picture_asset->set_key (_film->key());
146 _picture_asset->set_context_id (_film->context_id());
149 _picture_asset->set_file (asset);
150 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
152 /* We already have a complete picture asset that we can just re-use */
153 /* XXX: what about if the encryption key changes? */
154 if (_film->three_d ()) {
155 _picture_asset.reset (new dcp::StereoPictureAsset(asset));
157 _picture_asset.reset (new dcp::MonoPictureAsset(asset));
161 if (_film->audio_channels ()) {
163 new dcp::SoundAsset (dcp::Fraction(_film->video_frame_rate(), 1), _film->audio_frame_rate(), _film->audio_channels(), _film->audio_language(), standard)
166 _sound_asset->set_metadata (mxf_metadata());
168 if (_film->encrypted ()) {
169 _sound_asset->set_key (_film->key ());
172 DCPOMATIC_ASSERT (_film->directory());
174 vector<dcp::Channel> active;
175 BOOST_FOREACH (int i, _film->mapped_audio_channels()) {
176 active.push_back (static_cast<dcp::Channel>(i));
179 /* Write the sound asset into the film directory so that we leave the creation
180 of the DCP directory until the last minute.
182 _sound_asset_writer = _sound_asset->start_write (
183 _film->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
185 _film->contains_atmos_content()
190 /** @param frame reel-relative frame */
192 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
194 shared_ptr<InfoFileHandle> handle = _film->info_file_handle(_period, false);
195 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
196 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
197 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
198 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
202 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
204 dcp::FrameInfo frame_info;
205 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
206 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
207 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
209 char hash_buffer[33];
210 checked_fread (hash_buffer, 32, info->get(), info->file());
211 hash_buffer[32] = '\0';
212 frame_info.hash = hash_buffer;
218 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
222 return frame * _info_size;
224 return frame * _info_size * 2;
226 return frame * _info_size * 2 + _info_size;
228 DCPOMATIC_ASSERT (false);
231 DCPOMATIC_ASSERT (false);
235 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
237 shared_ptr<Job> job = _job.lock ();
240 job->sub (_("Checking existing image data"));
243 /* Try to open the existing asset */
244 FILE* asset_file = fopen_boost (asset, "rb");
246 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
249 LOG_GENERAL ("Opened existing asset at %1", asset.string());
252 shared_ptr<InfoFileHandle> info_file;
255 info_file = _film->info_file_handle (_period, true);
256 } catch (OpenFileError &) {
257 LOG_GENERAL_NC ("Could not open film info file");
262 /* Offset of the last dcp::FrameInfo in the info file */
263 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
264 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
266 Frame first_nonexistant_frame;
267 if (_film->three_d ()) {
268 /* Start looking at the last left frame */
269 first_nonexistant_frame = n / 2;
271 first_nonexistant_frame = n;
274 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
275 --first_nonexistant_frame;
278 if (!_film->three_d() && first_nonexistant_frame > 0) {
279 /* If we are doing 3D we might have found a good L frame with no R, so only
280 do this if we're in 2D and we've just found a good B(oth) frame.
282 ++first_nonexistant_frame;
285 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
289 return first_nonexistant_frame;
293 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
295 if (!_picture_asset_writer) {
296 /* We're not writing any data */
300 dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data(), encoded->size());
301 write_frame_info (frame, eyes, fin);
302 _last_written[eyes] = encoded;
307 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
310 _atmos_asset = metadata.create (dcp::Fraction(_film->video_frame_rate(), 1));
311 if (_film->encrypted()) {
312 _atmos_asset->set_key(_film->key());
314 _atmos_asset_writer = _atmos_asset->start_write (
315 _film->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
318 _atmos_asset_writer->write (atmos);
323 ReelWriter::fake_write (int size)
325 if (!_picture_asset_writer) {
326 /* We're not writing any data */
330 _picture_asset_writer->fake_write (size);
334 ReelWriter::repeat_write (Frame frame, Eyes eyes)
336 if (!_picture_asset_writer) {
337 /* We're not writing any data */
341 dcp::FrameInfo fin = _picture_asset_writer->write (
342 _last_written[eyes]->data(),
343 _last_written[eyes]->size()
345 write_frame_info (frame, eyes, fin);
349 ReelWriter::finish ()
351 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
352 /* Nothing was written to the picture asset */
353 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
354 _picture_asset.reset ();
357 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
358 /* Nothing was written to the sound asset */
359 _sound_asset.reset ();
362 /* Hard-link any video asset file into the DCP */
363 if (_picture_asset) {
364 DCPOMATIC_ASSERT (_picture_asset->file());
365 boost::filesystem::path video_from = _picture_asset->file().get();
366 boost::filesystem::path video_to;
367 video_to /= _film->dir (_film->dcp_name());
368 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
369 /* There may be an existing "to" file if we are recreating a DCP in the same place without
372 boost::system::error_code ec;
373 boost::filesystem::remove (video_to, ec);
375 boost::filesystem::create_hard_link (video_from, video_to, ec);
377 LOG_WARNING_NC ("Hard-link failed; copying instead");
378 shared_ptr<Job> job = _job.lock ();
380 job->sub (_("Copying video file into DCP"));
382 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
383 } catch (exception& e) {
384 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
385 throw FileError (e.what(), video_from);
388 boost::filesystem::copy_file (video_from, video_to, ec);
390 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
391 throw FileError (ec.message(), video_from);
396 _picture_asset->set_file (video_to);
399 /* Move the audio asset into the DCP */
401 boost::filesystem::path audio_to;
402 audio_to /= _film->dir (_film->dcp_name ());
403 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
406 boost::system::error_code ec;
407 boost::filesystem::rename (_film->file (aaf), audio_to, ec);
410 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
414 _sound_asset->set_file (audio_to);
418 _atmos_asset_writer->finalize ();
419 boost::filesystem::path atmos_to;
420 atmos_to /= _film->dir (_film->dcp_name());
421 string const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
424 boost::system::error_code ec;
425 boost::filesystem::rename (_film->file(aaf), atmos_to, ec);
428 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
432 _atmos_asset->set_file (atmos_to);
439 shared_ptr<dcp::SubtitleAsset> asset,
440 int64_t picture_duration,
441 shared_ptr<dcp::Reel> reel,
442 list<ReferencedReelAsset> const & refs,
443 list<shared_ptr<Font> > const & fonts,
444 shared_ptr<const Film> film,
448 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
450 shared_ptr<T> reel_asset;
453 /* Add the font to the subtitle content */
454 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
455 asset->add_font (j->id(), j->file().get_value_or(default_font_file()));
458 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
459 boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
460 boost::filesystem::create_directories (directory);
461 asset->write (directory / ("sub_" + asset->id() + ".xml"));
463 /* All our assets should be the same length; use the picture asset length here
464 as a reference to set the subtitle one. We'll use the duration rather than
465 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
466 just interested in its presentation length.
468 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
471 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
478 dcp::Fraction (film->video_frame_rate(), 1),
484 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
485 BOOST_FOREACH (ReferencedReelAsset j, refs) {
486 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
487 if (k && j.period == period) {
489 /* If we have a hash for this asset in the CPL, assume that it is correct */
491 k->asset_ref()->set_hash (k->hash().get());
498 if (reel_asset->actual_duration() != period_duration) {
499 throw ProgrammingError (
501 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
504 reel->add (reel_asset);
510 shared_ptr<dcp::Reel>
511 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
513 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
515 shared_ptr<dcp::Reel> reel (new dcp::Reel ());
517 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
519 if (_picture_asset) {
520 /* We have made a picture asset of our own. Put it into the reel */
521 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
523 reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
526 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
528 reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
531 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
532 /* We don't have a picture asset of our own; hopefully we have one to reference */
533 BOOST_FOREACH (ReferencedReelAsset j, refs) {
534 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
536 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
538 if (k && j.period == _period) {
539 reel_picture_asset = k;
544 Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
546 DCPOMATIC_ASSERT (reel_picture_asset);
547 if (reel_picture_asset->duration() != period_duration) {
548 throw ProgrammingError (
550 String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
553 reel->add (reel_picture_asset);
555 /* If we have a hash for this asset in the CPL, assume that it is correct */
556 if (reel_picture_asset->hash()) {
557 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
560 shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
563 /* We have made a sound asset of our own. Put it into the reel */
564 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
566 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
567 /* We don't have a sound asset of our own; hopefully we have one to reference */
568 BOOST_FOREACH (ReferencedReelAsset j, refs) {
569 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
571 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
573 if (k && j.period == _period) {
574 reel_sound_asset = k;
575 /* If we have a hash for this asset in the CPL, assume that it is correct */
577 k->asset_ref()->set_hash (k->hash().get());
583 DCPOMATIC_ASSERT (reel_sound_asset);
584 if (reel_sound_asset->actual_duration() != period_duration) {
586 "Reel sound asset has length %1 but reel period is %2",
587 reel_sound_asset->actual_duration(),
590 if (reel_sound_asset->actual_duration() != period_duration) {
591 throw ProgrammingError (
593 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
598 reel->add (reel_sound_asset);
600 shared_ptr<dcp::ReelSubtitleAsset> subtitle = maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
601 if (subtitle && !_film->subtitle_languages().empty()) {
602 subtitle->set_language (_film->subtitle_languages().front());
605 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
606 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
607 i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
610 a->set_annotation_text (i->first.name);
611 if (!i->first.language.empty()) {
612 a->set_language (dcp::LanguageTag(i->first.language));
617 Film::Markers markers = _film->markers ();
618 _film->add_ffoc_lfoc (markers);
619 Film::Markers reel_markers;
620 for (Film::Markers::const_iterator i = markers.begin(); i != markers.end(); ++i) {
621 if (_period.contains(i->second)) {
622 reel_markers[i->first] = i->second;
626 if (!reel_markers.empty ()) {
627 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
628 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
630 DCPTime relative = i->second - _period.from;
631 relative.split (_film->video_frame_rate(), h, m, s, f);
632 ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
638 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
645 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
647 if (_picture_asset) {
648 _picture_asset->hash (set_progress);
652 _sound_asset->hash (set_progress);
656 _atmos_asset->hash (set_progress);
661 ReelWriter::start () const
663 return _period.from.frames_floor (_film->video_frame_rate());
668 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
670 if (!_sound_asset_writer) {
674 DCPOMATIC_ASSERT (audio);
675 _sound_asset_writer->write (audio->data(), audio->frames());
679 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
681 shared_ptr<dcp::SubtitleAsset> asset;
684 case TEXT_OPEN_SUBTITLE:
685 asset = _subtitle_asset;
687 case TEXT_CLOSED_CAPTION:
688 DCPOMATIC_ASSERT (track);
689 asset = _closed_caption_assets[*track];
692 DCPOMATIC_ASSERT (false);
696 vector<dcp::LanguageTag> lang = _film->subtitle_languages ();
697 if (_film->interop ()) {
698 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
699 s->set_movie_title (_film->name ());
700 if (type == TEXT_OPEN_SUBTITLE) {
701 s->set_language (lang.empty() ? "Unknown" : lang.front().to_string());
702 } else if (!track->language.empty()) {
703 s->set_language (track->language);
705 s->set_reel_number (raw_convert<string> (_reel_index + 1));
708 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
709 s->set_content_title_text (_film->name ());
710 s->set_metadata (mxf_metadata());
711 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
712 s->set_language (lang.front());
713 } else if (track && !track->language.empty()) {
714 s->set_language (dcp::LanguageTag(track->language));
716 s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
717 s->set_reel_number (_reel_index + 1);
718 s->set_time_code_rate (_film->video_frame_rate ());
719 s->set_start_time (dcp::Time ());
720 if (_film->encrypted ()) {
721 s->set_key (_film->key ());
728 case TEXT_OPEN_SUBTITLE:
729 _subtitle_asset = asset;
731 case TEXT_CLOSED_CAPTION:
732 DCPOMATIC_ASSERT (track);
733 _closed_caption_assets[*track] = asset;
736 DCPOMATIC_ASSERT (false);
739 BOOST_FOREACH (StringText i, subs.string) {
740 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
741 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
742 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
743 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
746 BOOST_FOREACH (BitmapText i, subs.bitmap) {
748 shared_ptr<dcp::Subtitle>(
749 new dcp::SubtitleImage(
751 dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
752 dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
753 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
754 dcp::Time(), dcp::Time()
762 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
764 LOG_GENERAL ("Checking existing picture frame %1", frame);
766 /* Read the data from the info file; for 3D we just check the left
767 frames until we find a good one.
769 dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
773 /* Read the data from the asset and hash it */
774 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
775 ArrayData data (info.size);
776 size_t const read = fread (data.data(), 1, data.size(), asset_file);
777 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
778 if (read != static_cast<size_t> (data.size ())) {
779 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
783 digester.add (data.data(), data.size());
784 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
785 if (digester.get() != info.hash) {
786 LOG_GENERAL ("Existing frame %1 failed hash check", frame);