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)
178 /** @param frame reel-relative frame */
180 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
182 shared_ptr<InfoFileHandle> handle = _film->info_file_handle(_period, false);
183 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
184 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
185 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
186 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
190 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
192 dcp::FrameInfo frame_info;
193 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
194 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
195 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
197 char hash_buffer[33];
198 checked_fread (hash_buffer, 32, info->get(), info->file());
199 hash_buffer[32] = '\0';
200 frame_info.hash = hash_buffer;
206 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
210 return frame * _info_size;
212 return frame * _info_size * 2;
214 return frame * _info_size * 2 + _info_size;
216 DCPOMATIC_ASSERT (false);
219 DCPOMATIC_ASSERT (false);
223 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
225 shared_ptr<Job> job = _job.lock ();
228 job->sub (_("Checking existing image data"));
231 /* Try to open the existing asset */
232 FILE* asset_file = fopen_boost (asset, "rb");
234 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
237 LOG_GENERAL ("Opened existing asset at %1", asset.string());
240 shared_ptr<InfoFileHandle> info_file;
243 info_file = _film->info_file_handle (_period, true);
244 } catch (OpenFileError) {
245 LOG_GENERAL_NC ("Could not open film info file");
250 /* Offset of the last dcp::FrameInfo in the info file */
251 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
252 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
254 Frame first_nonexistant_frame;
255 if (_film->three_d ()) {
256 /* Start looking at the last left frame */
257 first_nonexistant_frame = n / 2;
259 first_nonexistant_frame = n;
262 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
263 --first_nonexistant_frame;
266 if (!_film->three_d() && first_nonexistant_frame > 0) {
267 /* If we are doing 3D we might have found a good L frame with no R, so only
268 do this if we're in 2D and we've just found a good B(oth) frame.
270 ++first_nonexistant_frame;
273 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
277 return first_nonexistant_frame;
281 ReelWriter::write (optional<Data> encoded, Frame frame, Eyes eyes)
283 if (!_picture_asset_writer) {
284 /* We're not writing any data */
288 dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data().get (), encoded->size());
289 write_frame_info (frame, eyes, fin);
290 _last_written[eyes] = encoded;
295 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
298 _atmos_asset = metadata.create (dcp::Fraction(_film->video_frame_rate(), 1));
299 if (_film->encrypted()) {
300 _atmos_asset->set_key(_film->key());
302 _atmos_asset_writer = _atmos_asset->start_write (
303 _film->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
306 _atmos_asset_writer->write (atmos);
311 ReelWriter::fake_write (int size)
313 if (!_picture_asset_writer) {
314 /* We're not writing any data */
318 _picture_asset_writer->fake_write (size);
322 ReelWriter::repeat_write (Frame frame, Eyes eyes)
324 if (!_picture_asset_writer) {
325 /* We're not writing any data */
329 dcp::FrameInfo fin = _picture_asset_writer->write (
330 _last_written[eyes]->data().get(),
331 _last_written[eyes]->size()
333 write_frame_info (frame, eyes, fin);
337 ReelWriter::finish ()
339 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
340 /* Nothing was written to the picture asset */
341 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
342 _picture_asset.reset ();
345 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
346 /* Nothing was written to the sound asset */
347 _sound_asset.reset ();
350 /* Hard-link any video asset file into the DCP */
351 if (_picture_asset) {
352 DCPOMATIC_ASSERT (_picture_asset->file());
353 boost::filesystem::path video_from = _picture_asset->file().get();
354 boost::filesystem::path video_to;
355 video_to /= _film->dir (_film->dcp_name());
356 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
357 /* There may be an existing "to" file if we are recreating a DCP in the same place without
360 boost::system::error_code ec;
361 boost::filesystem::remove (video_to, ec);
363 boost::filesystem::create_hard_link (video_from, video_to, ec);
365 LOG_WARNING_NC ("Hard-link failed; copying instead");
366 shared_ptr<Job> job = _job.lock ();
368 job->sub (_("Copying video file into DCP"));
370 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
371 } catch (exception& e) {
372 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
373 throw FileError (e.what(), video_from);
376 boost::filesystem::copy_file (video_from, video_to, ec);
378 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
379 throw FileError (ec.message(), video_from);
384 _picture_asset->set_file (video_to);
387 /* Move the audio asset into the DCP */
389 boost::filesystem::path audio_to;
390 audio_to /= _film->dir (_film->dcp_name ());
391 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
394 boost::system::error_code ec;
395 boost::filesystem::rename (_film->file (aaf), audio_to, ec);
398 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
402 _sound_asset->set_file (audio_to);
406 _atmos_asset_writer->finalize ();
407 boost::filesystem::path atmos_to;
408 atmos_to /= _film->dir (_film->dcp_name());
409 string const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
412 boost::system::error_code ec;
413 boost::filesystem::rename (_film->file(aaf), atmos_to, ec);
416 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
420 _atmos_asset->set_file (atmos_to);
427 shared_ptr<dcp::SubtitleAsset> asset,
428 int64_t picture_duration,
429 shared_ptr<dcp::Reel> reel,
430 list<ReferencedReelAsset> const & refs,
431 list<shared_ptr<Font> > const & fonts,
432 shared_ptr<const Film> film,
436 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
438 shared_ptr<T> reel_asset;
441 boost::filesystem::path liberation_normal;
443 liberation_normal = shared_path() / "LiberationSans-Regular.ttf";
444 if (!boost::filesystem::exists (liberation_normal)) {
445 /* Hack for unit tests */
446 liberation_normal = shared_path() / "fonts" / "LiberationSans-Regular.ttf";
448 } catch (boost::filesystem::filesystem_error& e) {
452 if (!boost::filesystem::exists(liberation_normal)) {
453 liberation_normal = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf";
456 /* Add the font to the subtitle content */
457 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
458 asset->add_font (j->id(), j->file().get_value_or(liberation_normal));
461 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
462 boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
463 boost::filesystem::create_directories (directory);
464 asset->write (directory / ("sub_" + asset->id() + ".xml"));
466 /* All our assets should be the same length; use the picture asset length here
467 as a reference to set the subtitle one. We'll use the duration rather than
468 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
469 just interested in its presentation length.
471 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
474 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
481 dcp::Fraction (film->video_frame_rate(), 1),
487 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
488 BOOST_FOREACH (ReferencedReelAsset j, refs) {
489 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
490 if (k && j.period == period) {
492 /* If we have a hash for this asset in the CPL, assume that it is correct */
494 k->asset_ref()->set_hash (k->hash().get());
501 if (reel_asset->actual_duration() != period_duration) {
502 throw ProgrammingError (
504 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
507 reel->add (reel_asset);
513 shared_ptr<dcp::Reel>
514 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
516 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
518 shared_ptr<dcp::Reel> reel (new dcp::Reel ());
520 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
522 if (_picture_asset) {
523 /* We have made a picture asset of our own. Put it into the reel */
524 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
526 reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
529 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
531 reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
534 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
535 /* We don't have a picture asset of our own; hopefully we have one to reference */
536 BOOST_FOREACH (ReferencedReelAsset j, refs) {
537 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
539 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
541 if (k && j.period == _period) {
542 reel_picture_asset = k;
547 Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
549 DCPOMATIC_ASSERT (reel_picture_asset);
550 if (reel_picture_asset->duration() != period_duration) {
551 throw ProgrammingError (
553 String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
556 reel->add (reel_picture_asset);
558 /* If we have a hash for this asset in the CPL, assume that it is correct */
559 if (reel_picture_asset->hash()) {
560 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
563 shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
566 /* We have made a sound asset of our own. Put it into the reel */
567 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
569 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
570 /* We don't have a sound asset of our own; hopefully we have one to reference */
571 BOOST_FOREACH (ReferencedReelAsset j, refs) {
572 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
574 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
576 if (k && j.period == _period) {
577 reel_sound_asset = k;
578 /* If we have a hash for this asset in the CPL, assume that it is correct */
580 k->asset_ref()->set_hash (k->hash().get());
586 DCPOMATIC_ASSERT (reel_sound_asset);
587 if (reel_sound_asset->actual_duration() != period_duration) {
589 "Reel sound asset has length %1 but reel period is %2",
590 reel_sound_asset->actual_duration(),
593 if (reel_sound_asset->actual_duration() != period_duration) {
594 throw ProgrammingError (
596 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
601 reel->add (reel_sound_asset);
603 maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
604 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
605 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
606 i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
608 a->set_annotation_text (i->first.name);
609 a->set_language (i->first.language);
612 map<dcp::Marker, DCPTime> markers = _film->markers ();
613 map<dcp::Marker, DCPTime> reel_markers;
614 for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
615 if (_period.contains(i->second)) {
616 reel_markers[i->first] = i->second;
620 if (!reel_markers.empty ()) {
621 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
622 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
624 DCPTime relative = i->second - _period.from;
625 relative.split (_film->video_frame_rate(), h, m, s, f);
626 ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
632 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
639 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
641 if (_picture_asset) {
642 _picture_asset->hash (set_progress);
646 _sound_asset->hash (set_progress);
650 _atmos_asset->hash (set_progress);
655 ReelWriter::start () const
657 return _period.from.frames_floor (_film->video_frame_rate());
662 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
664 if (!_sound_asset_writer) {
668 DCPOMATIC_ASSERT (audio);
669 _sound_asset_writer->write (audio->data(), audio->frames());
673 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
675 shared_ptr<dcp::SubtitleAsset> asset;
678 case TEXT_OPEN_SUBTITLE:
679 asset = _subtitle_asset;
681 case TEXT_CLOSED_CAPTION:
682 DCPOMATIC_ASSERT (track);
683 asset = _closed_caption_assets[*track];
686 DCPOMATIC_ASSERT (false);
690 string lang = _film->subtitle_language ();
691 if (_film->interop ()) {
692 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
693 s->set_movie_title (_film->name ());
694 if (type == TEXT_OPEN_SUBTITLE) {
695 s->set_language (lang.empty() ? "Unknown" : lang);
697 s->set_language (track->language);
699 s->set_reel_number (raw_convert<string> (_reel_index + 1));
702 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
703 s->set_content_title_text (_film->name ());
704 s->set_metadata (mxf_metadata());
705 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
706 s->set_language (lang);
708 s->set_language (track->language);
710 s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
711 s->set_reel_number (_reel_index + 1);
712 s->set_time_code_rate (_film->video_frame_rate ());
713 s->set_start_time (dcp::Time ());
714 if (_film->encrypted ()) {
715 s->set_key (_film->key ());
722 case TEXT_OPEN_SUBTITLE:
723 _subtitle_asset = asset;
725 case TEXT_CLOSED_CAPTION:
726 DCPOMATIC_ASSERT (track);
727 _closed_caption_assets[*track] = asset;
730 DCPOMATIC_ASSERT (false);
733 BOOST_FOREACH (StringText i, subs.string) {
734 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
735 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
736 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
737 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
740 BOOST_FOREACH (BitmapText i, subs.bitmap) {
742 shared_ptr<dcp::Subtitle>(
743 new dcp::SubtitleImage(
745 dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
746 dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
747 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
748 dcp::Time(), dcp::Time()
756 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
758 LOG_GENERAL ("Checking existing picture frame %1", frame);
760 /* Read the data from the info file; for 3D we just check the left
761 frames until we find a good one.
763 dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
767 /* Read the data from the asset and hash it */
768 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
769 Data data (info.size);
770 size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
771 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
772 if (read != static_cast<size_t> (data.size ())) {
773 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
777 digester.add (data.data().get(), data.size());
778 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
779 if (digester.get() != info.hash) {
780 LOG_GENERAL ("Existing frame %1 failed hash check", frame);