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>
63 using boost::shared_ptr;
64 using boost::optional;
65 using boost::dynamic_pointer_cast;
67 using dcp::raw_convert;
68 using namespace dcpomatic;
70 int const ReelWriter::_info_size = 48;
72 static dcp::MXFMetadata
75 dcp::MXFMetadata meta;
76 Config* config = Config::instance();
77 if (!config->dcp_company_name().empty()) {
78 meta.company_name = config->dcp_company_name ();
80 if (!config->dcp_product_name().empty()) {
81 meta.product_name = config->dcp_product_name ();
83 if (!config->dcp_product_version().empty()) {
84 meta.product_version = config->dcp_product_version ();
89 /** @param job Related job, or 0 */
90 ReelWriter::ReelWriter (
91 shared_ptr<const Film> film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, optional<string> content_summary
95 , _reel_index (reel_index)
96 , _reel_count (reel_count)
97 , _content_summary (content_summary)
100 /* Create or find our picture asset in a subdirectory, named
101 according to those film's parameters which affect the video
102 output. We will hard-link it into the DCP later.
105 dcp::Standard const standard = _film->interop() ? dcp::INTEROP : dcp::SMPTE;
107 boost::filesystem::path const asset =
108 _film->internal_video_asset_dir() / _film->internal_video_asset_filename(_period);
110 _first_nonexistant_frame = check_existing_picture_asset (asset);
112 if (_first_nonexistant_frame < period.duration().frames_round(_film->video_frame_rate())) {
113 /* We do not have a complete picture asset. If there is an
114 existing asset, break any hard links to it as we are about
115 to change its contents (if only by changing the IDs); see
118 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
120 job->sub (_("Copying old video file"));
121 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
123 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
125 boost::filesystem::remove (asset);
126 boost::filesystem::rename (asset.string() + ".tmp", asset);
130 if (_film->three_d ()) {
131 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
133 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
136 _picture_asset->set_size (_film->frame_size());
137 _picture_asset->set_metadata (mxf_metadata());
139 if (_film->encrypted ()) {
140 _picture_asset->set_key (_film->key());
141 _picture_asset->set_context_id (_film->context_id());
144 _picture_asset->set_file (asset);
145 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
147 /* We already have a complete picture asset that we can just re-use */
148 /* XXX: what about if the encryption key changes? */
149 if (_film->three_d ()) {
150 _picture_asset.reset (new dcp::StereoPictureAsset(asset));
152 _picture_asset.reset (new dcp::MonoPictureAsset(asset));
156 if (_film->audio_channels ()) {
158 new dcp::SoundAsset (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels (), standard)
161 _sound_asset->set_metadata (mxf_metadata());
163 if (_film->encrypted ()) {
164 _sound_asset->set_key (_film->key ());
167 DCPOMATIC_ASSERT (_film->directory());
169 /* Write the sound asset into the film directory so that we leave the creation
170 of the DCP directory until the last minute.
172 _sound_asset_writer = _sound_asset->start_write (
173 _film->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
174 _film->contains_atmos_content()
179 /** @param frame reel-relative frame */
181 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
183 shared_ptr<InfoFileHandle> handle = _film->info_file_handle(_period, false);
184 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
185 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
186 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
187 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
191 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
193 dcp::FrameInfo frame_info;
194 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
195 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
196 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
198 char hash_buffer[33];
199 checked_fread (hash_buffer, 32, info->get(), info->file());
200 hash_buffer[32] = '\0';
201 frame_info.hash = hash_buffer;
207 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
211 return frame * _info_size;
213 return frame * _info_size * 2;
215 return frame * _info_size * 2 + _info_size;
217 DCPOMATIC_ASSERT (false);
220 DCPOMATIC_ASSERT (false);
224 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
226 shared_ptr<Job> job = _job.lock ();
229 job->sub (_("Checking existing image data"));
232 /* Try to open the existing asset */
233 FILE* asset_file = fopen_boost (asset, "rb");
235 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
238 LOG_GENERAL ("Opened existing asset at %1", asset.string());
241 shared_ptr<InfoFileHandle> info_file;
244 info_file = _film->info_file_handle (_period, true);
245 } catch (OpenFileError) {
246 LOG_GENERAL_NC ("Could not open film info file");
251 /* Offset of the last dcp::FrameInfo in the info file */
252 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
253 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
255 Frame first_nonexistant_frame;
256 if (_film->three_d ()) {
257 /* Start looking at the last left frame */
258 first_nonexistant_frame = n / 2;
260 first_nonexistant_frame = n;
263 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
264 --first_nonexistant_frame;
267 if (!_film->three_d() && first_nonexistant_frame > 0) {
268 /* If we are doing 3D we might have found a good L frame with no R, so only
269 do this if we're in 2D and we've just found a good B(oth) frame.
271 ++first_nonexistant_frame;
274 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
278 return first_nonexistant_frame;
282 ReelWriter::write (optional<Data> encoded, Frame frame, Eyes eyes)
284 if (!_picture_asset_writer) {
285 /* We're not writing any data */
289 dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data().get (), encoded->size());
290 write_frame_info (frame, eyes, fin);
291 _last_written[eyes] = encoded;
296 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
299 _atmos_asset = metadata.create (dcp::Fraction(_film->video_frame_rate(), 1));
300 if (_film->encrypted()) {
301 _atmos_asset->set_key(_film->key());
303 _atmos_asset_writer = _atmos_asset->start_write (
304 _film->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
307 _atmos_asset_writer->write (atmos);
312 ReelWriter::fake_write (int size)
314 if (!_picture_asset_writer) {
315 /* We're not writing any data */
319 _picture_asset_writer->fake_write (size);
323 ReelWriter::repeat_write (Frame frame, Eyes eyes)
325 if (!_picture_asset_writer) {
326 /* We're not writing any data */
330 dcp::FrameInfo fin = _picture_asset_writer->write (
331 _last_written[eyes]->data().get(),
332 _last_written[eyes]->size()
334 write_frame_info (frame, eyes, fin);
338 ReelWriter::finish ()
340 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
341 /* Nothing was written to the picture asset */
342 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
343 _picture_asset.reset ();
346 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
347 /* Nothing was written to the sound asset */
348 _sound_asset.reset ();
351 /* Hard-link any video asset file into the DCP */
352 if (_picture_asset) {
353 DCPOMATIC_ASSERT (_picture_asset->file());
354 boost::filesystem::path video_from = _picture_asset->file().get();
355 boost::filesystem::path video_to;
356 video_to /= _film->dir (_film->dcp_name());
357 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
358 /* There may be an existing "to" file if we are recreating a DCP in the same place without
361 boost::system::error_code ec;
362 boost::filesystem::remove (video_to, ec);
364 boost::filesystem::create_hard_link (video_from, video_to, ec);
366 LOG_WARNING_NC ("Hard-link failed; copying instead");
367 shared_ptr<Job> job = _job.lock ();
369 job->sub (_("Copying video file into DCP"));
371 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
372 } catch (exception& e) {
373 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
374 throw FileError (e.what(), video_from);
377 boost::filesystem::copy_file (video_from, video_to, ec);
379 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
380 throw FileError (ec.message(), video_from);
385 _picture_asset->set_file (video_to);
388 /* Move the audio asset into the DCP */
390 boost::filesystem::path audio_to;
391 audio_to /= _film->dir (_film->dcp_name ());
392 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
395 boost::system::error_code ec;
396 boost::filesystem::rename (_film->file (aaf), audio_to, ec);
399 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
403 _sound_asset->set_file (audio_to);
407 _atmos_asset_writer->finalize ();
408 boost::filesystem::path atmos_to;
409 atmos_to /= _film->dir (_film->dcp_name());
410 string const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
413 boost::system::error_code ec;
414 boost::filesystem::rename (_film->file(aaf), atmos_to, ec);
417 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
421 _atmos_asset->set_file (atmos_to);
428 shared_ptr<dcp::SubtitleAsset> asset,
429 int64_t picture_duration,
430 shared_ptr<dcp::Reel> reel,
431 list<ReferencedReelAsset> const & refs,
432 list<shared_ptr<Font> > const & fonts,
433 shared_ptr<const Film> film,
437 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
439 shared_ptr<T> reel_asset;
442 boost::filesystem::path liberation_normal;
444 liberation_normal = shared_path() / "LiberationSans-Regular.ttf";
445 if (!boost::filesystem::exists (liberation_normal)) {
446 /* Hack for unit tests */
447 liberation_normal = shared_path() / "fonts" / "LiberationSans-Regular.ttf";
449 } catch (boost::filesystem::filesystem_error& e) {
453 if (!boost::filesystem::exists(liberation_normal)) {
454 liberation_normal = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf";
457 /* Add the font to the subtitle content */
458 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
459 asset->add_font (j->id(), j->file().get_value_or(liberation_normal));
462 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
463 boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
464 boost::filesystem::create_directories (directory);
465 asset->write (directory / ("sub_" + asset->id() + ".xml"));
467 /* All our assets should be the same length; use the picture asset length here
468 as a reference to set the subtitle one. We'll use the duration rather than
469 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
470 just interested in its presentation length.
472 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
475 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
482 dcp::Fraction (film->video_frame_rate(), 1),
488 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
489 BOOST_FOREACH (ReferencedReelAsset j, refs) {
490 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
491 if (k && j.period == period) {
493 /* If we have a hash for this asset in the CPL, assume that it is correct */
495 k->asset_ref()->set_hash (k->hash().get());
502 if (reel_asset->actual_duration() != period_duration) {
503 throw ProgrammingError (
505 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
508 reel->add (reel_asset);
514 shared_ptr<dcp::Reel>
515 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
517 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
519 shared_ptr<dcp::Reel> reel (new dcp::Reel ());
521 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
523 if (_picture_asset) {
524 /* We have made a picture asset of our own. Put it into the reel */
525 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
527 reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
530 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
532 reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
535 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
536 /* We don't have a picture asset of our own; hopefully we have one to reference */
537 BOOST_FOREACH (ReferencedReelAsset j, refs) {
538 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
540 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
542 if (k && j.period == _period) {
543 reel_picture_asset = k;
548 Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
550 DCPOMATIC_ASSERT (reel_picture_asset);
551 if (reel_picture_asset->duration() != period_duration) {
552 throw ProgrammingError (
554 String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
557 reel->add (reel_picture_asset);
559 /* If we have a hash for this asset in the CPL, assume that it is correct */
560 if (reel_picture_asset->hash()) {
561 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
564 shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
567 /* We have made a sound asset of our own. Put it into the reel */
568 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
570 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
571 /* We don't have a sound asset of our own; hopefully we have one to reference */
572 BOOST_FOREACH (ReferencedReelAsset j, refs) {
573 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
575 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
577 if (k && j.period == _period) {
578 reel_sound_asset = k;
579 /* If we have a hash for this asset in the CPL, assume that it is correct */
581 k->asset_ref()->set_hash (k->hash().get());
587 DCPOMATIC_ASSERT (reel_sound_asset);
588 if (reel_sound_asset->actual_duration() != period_duration) {
590 "Reel sound asset has length %1 but reel period is %2",
591 reel_sound_asset->actual_duration(),
594 if (reel_sound_asset->actual_duration() != period_duration) {
595 throw ProgrammingError (
597 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
602 reel->add (reel_sound_asset);
604 maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
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
609 a->set_annotation_text (i->first.name);
610 a->set_language (i->first.language);
613 map<dcp::Marker, DCPTime> markers = _film->markers ();
614 map<dcp::Marker, DCPTime> reel_markers;
615 for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
616 if (_period.contains(i->second)) {
617 reel_markers[i->first] = i->second;
621 if (!reel_markers.empty ()) {
622 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
623 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
625 DCPTime relative = i->second - _period.from;
626 relative.split (_film->video_frame_rate(), h, m, s, f);
627 ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
633 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
640 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
642 if (_picture_asset) {
643 _picture_asset->hash (set_progress);
647 _sound_asset->hash (set_progress);
651 _atmos_asset->hash (set_progress);
656 ReelWriter::start () const
658 return _period.from.frames_floor (_film->video_frame_rate());
663 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
665 if (!_sound_asset_writer) {
669 DCPOMATIC_ASSERT (audio);
670 _sound_asset_writer->write (audio->data(), audio->frames());
674 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
676 shared_ptr<dcp::SubtitleAsset> asset;
679 case TEXT_OPEN_SUBTITLE:
680 asset = _subtitle_asset;
682 case TEXT_CLOSED_CAPTION:
683 DCPOMATIC_ASSERT (track);
684 asset = _closed_caption_assets[*track];
687 DCPOMATIC_ASSERT (false);
691 string lang = _film->subtitle_language ();
692 if (_film->interop ()) {
693 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
694 s->set_movie_title (_film->name ());
695 if (type == TEXT_OPEN_SUBTITLE) {
696 s->set_language (lang.empty() ? "Unknown" : lang);
698 s->set_language (track->language);
700 s->set_reel_number (raw_convert<string> (_reel_index + 1));
703 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
704 s->set_content_title_text (_film->name ());
705 s->set_metadata (mxf_metadata());
706 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
707 s->set_language (lang);
709 s->set_language (track->language);
711 s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
712 s->set_reel_number (_reel_index + 1);
713 s->set_time_code_rate (_film->video_frame_rate ());
714 s->set_start_time (dcp::Time ());
715 if (_film->encrypted ()) {
716 s->set_key (_film->key ());
723 case TEXT_OPEN_SUBTITLE:
724 _subtitle_asset = asset;
726 case TEXT_CLOSED_CAPTION:
727 DCPOMATIC_ASSERT (track);
728 _closed_caption_assets[*track] = asset;
731 DCPOMATIC_ASSERT (false);
734 BOOST_FOREACH (StringText i, subs.string) {
735 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
736 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
737 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
738 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
741 BOOST_FOREACH (BitmapText i, subs.bitmap) {
743 shared_ptr<dcp::Subtitle>(
744 new dcp::SubtitleImage(
746 dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
747 dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
748 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
749 dcp::Time(), dcp::Time()
757 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
759 LOG_GENERAL ("Checking existing picture frame %1", frame);
761 /* Read the data from the info file; for 3D we just check the left
762 frames until we find a good one.
764 dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
768 /* Read the data from the asset and hash it */
769 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
770 Data data (info.size);
771 size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
772 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
773 if (read != static_cast<size_t> (data.size ())) {
774 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
778 digester.add (data.data().get(), data.size());
779 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
780 if (digester.get() != info.hash) {
781 LOG_GENERAL ("Existing frame %1 failed hash check", frame);