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 /* Add the font to the subtitle content */
443 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
444 asset->add_font (j->id(), j->file().get_value_or(default_font_file()));
447 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
448 boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
449 boost::filesystem::create_directories (directory);
450 asset->write (directory / ("sub_" + asset->id() + ".xml"));
452 /* All our assets should be the same length; use the picture asset length here
453 as a reference to set the subtitle one. We'll use the duration rather than
454 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
455 just interested in its presentation length.
457 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
460 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
467 dcp::Fraction (film->video_frame_rate(), 1),
473 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
474 BOOST_FOREACH (ReferencedReelAsset j, refs) {
475 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
476 if (k && j.period == period) {
478 /* If we have a hash for this asset in the CPL, assume that it is correct */
480 k->asset_ref()->set_hash (k->hash().get());
487 if (reel_asset->actual_duration() != period_duration) {
488 throw ProgrammingError (
490 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
493 reel->add (reel_asset);
499 shared_ptr<dcp::Reel>
500 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
502 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
504 shared_ptr<dcp::Reel> reel (new dcp::Reel ());
506 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
508 if (_picture_asset) {
509 /* We have made a picture asset of our own. Put it into the reel */
510 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
512 reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
515 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
517 reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
520 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
521 /* We don't have a picture asset of our own; hopefully we have one to reference */
522 BOOST_FOREACH (ReferencedReelAsset j, refs) {
523 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
525 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
527 if (k && j.period == _period) {
528 reel_picture_asset = k;
533 Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
535 DCPOMATIC_ASSERT (reel_picture_asset);
536 if (reel_picture_asset->duration() != period_duration) {
537 throw ProgrammingError (
539 String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
542 reel->add (reel_picture_asset);
544 /* If we have a hash for this asset in the CPL, assume that it is correct */
545 if (reel_picture_asset->hash()) {
546 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
549 shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
552 /* We have made a sound asset of our own. Put it into the reel */
553 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
555 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
556 /* We don't have a sound asset of our own; hopefully we have one to reference */
557 BOOST_FOREACH (ReferencedReelAsset j, refs) {
558 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
560 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
562 if (k && j.period == _period) {
563 reel_sound_asset = k;
564 /* If we have a hash for this asset in the CPL, assume that it is correct */
566 k->asset_ref()->set_hash (k->hash().get());
572 DCPOMATIC_ASSERT (reel_sound_asset);
573 if (reel_sound_asset->actual_duration() != period_duration) {
575 "Reel sound asset has length %1 but reel period is %2",
576 reel_sound_asset->actual_duration(),
579 if (reel_sound_asset->actual_duration() != period_duration) {
580 throw ProgrammingError (
582 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
587 reel->add (reel_sound_asset);
589 maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
590 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
591 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
592 i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
594 a->set_annotation_text (i->first.name);
595 a->set_language (i->first.language);
598 map<dcp::Marker, DCPTime> markers = _film->markers ();
599 map<dcp::Marker, DCPTime> reel_markers;
600 for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
601 if (_period.contains(i->second)) {
602 reel_markers[i->first] = i->second;
606 if (!reel_markers.empty ()) {
607 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
608 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
610 DCPTime relative = i->second - _period.from;
611 relative.split (_film->video_frame_rate(), h, m, s, f);
612 ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
618 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
625 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
627 if (_picture_asset) {
628 _picture_asset->hash (set_progress);
632 _sound_asset->hash (set_progress);
636 _atmos_asset->hash (set_progress);
641 ReelWriter::start () const
643 return _period.from.frames_floor (_film->video_frame_rate());
648 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
650 if (!_sound_asset_writer) {
654 DCPOMATIC_ASSERT (audio);
655 _sound_asset_writer->write (audio->data(), audio->frames());
659 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
661 shared_ptr<dcp::SubtitleAsset> asset;
664 case TEXT_OPEN_SUBTITLE:
665 asset = _subtitle_asset;
667 case TEXT_CLOSED_CAPTION:
668 DCPOMATIC_ASSERT (track);
669 asset = _closed_caption_assets[*track];
672 DCPOMATIC_ASSERT (false);
676 string lang = _film->subtitle_language ();
677 if (_film->interop ()) {
678 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
679 s->set_movie_title (_film->name ());
680 if (type == TEXT_OPEN_SUBTITLE) {
681 s->set_language (lang.empty() ? "Unknown" : lang);
683 s->set_language (track->language);
685 s->set_reel_number (raw_convert<string> (_reel_index + 1));
688 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
689 s->set_content_title_text (_film->name ());
690 s->set_metadata (mxf_metadata());
691 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
692 s->set_language (lang);
694 s->set_language (track->language);
696 s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
697 s->set_reel_number (_reel_index + 1);
698 s->set_time_code_rate (_film->video_frame_rate ());
699 s->set_start_time (dcp::Time ());
700 if (_film->encrypted ()) {
701 s->set_key (_film->key ());
708 case TEXT_OPEN_SUBTITLE:
709 _subtitle_asset = asset;
711 case TEXT_CLOSED_CAPTION:
712 DCPOMATIC_ASSERT (track);
713 _closed_caption_assets[*track] = asset;
716 DCPOMATIC_ASSERT (false);
719 BOOST_FOREACH (StringText i, subs.string) {
720 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
721 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
722 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
723 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
726 BOOST_FOREACH (BitmapText i, subs.bitmap) {
728 shared_ptr<dcp::Subtitle>(
729 new dcp::SubtitleImage(
731 dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
732 dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
733 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
734 dcp::Time(), dcp::Time()
742 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
744 LOG_GENERAL ("Checking existing picture frame %1", frame);
746 /* Read the data from the info file; for 3D we just check the left
747 frames until we find a good one.
749 dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
753 /* Read the data from the asset and hash it */
754 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
755 Data data (info.size);
756 size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
757 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
758 if (read != static_cast<size_t> (data.size ())) {
759 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
763 digester.add (data.data().get(), data.size());
764 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
765 if (digester.get() != info.hash) {
766 LOG_GENERAL ("Existing frame %1 failed hash check", frame);