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/mono_picture_asset.h>
34 #include <dcp/stereo_picture_asset.h>
35 #include <dcp/sound_asset.h>
36 #include <dcp/sound_asset_writer.h>
38 #include <dcp/reel_mono_picture_asset.h>
39 #include <dcp/reel_stereo_picture_asset.h>
40 #include <dcp/reel_sound_asset.h>
41 #include <dcp/reel_subtitle_asset.h>
42 #include <dcp/reel_closed_caption_asset.h>
43 #include <dcp/reel_markers_asset.h>
46 #include <dcp/certificate_chain.h>
47 #include <dcp/interop_subtitle_asset.h>
48 #include <dcp/smpte_subtitle_asset.h>
49 #include <dcp/raw_convert.h>
50 #include <dcp/subtitle_image.h>
51 #include <boost/foreach.hpp>
60 using boost::shared_ptr;
61 using boost::optional;
62 using boost::dynamic_pointer_cast;
64 using dcp::raw_convert;
65 using namespace dcpomatic;
67 int const ReelWriter::_info_size = 48;
69 static dcp::MXFMetadata
72 dcp::MXFMetadata meta;
73 Config* config = Config::instance();
74 if (!config->dcp_company_name().empty()) {
75 meta.company_name = config->dcp_company_name ();
77 if (!config->dcp_product_name().empty()) {
78 meta.product_name = config->dcp_product_name ();
80 if (!config->dcp_product_version().empty()) {
81 meta.product_version = config->dcp_product_version ();
86 /** @param job Related job, or 0 */
87 ReelWriter::ReelWriter (
88 shared_ptr<const Film> film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, optional<string> content_summary
92 , _reel_index (reel_index)
93 , _reel_count (reel_count)
94 , _content_summary (content_summary)
97 /* Create or find our picture asset in a subdirectory, named
98 according to those film's parameters which affect the video
99 output. We will hard-link it into the DCP later.
102 dcp::Standard const standard = _film->interop() ? dcp::INTEROP : dcp::SMPTE;
104 boost::filesystem::path const asset =
105 _film->internal_video_asset_dir() / _film->internal_video_asset_filename(_period);
107 _first_nonexistant_frame = check_existing_picture_asset (asset);
109 if (_first_nonexistant_frame < period.duration().frames_round(_film->video_frame_rate())) {
110 /* We do not have a complete picture asset. If there is an
111 existing asset, break any hard links to it as we are about
112 to change its contents (if only by changing the IDs); see
115 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
117 job->sub (_("Copying old video file"));
118 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
120 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
122 boost::filesystem::remove (asset);
123 boost::filesystem::rename (asset.string() + ".tmp", asset);
127 if (_film->three_d ()) {
128 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
130 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
133 _picture_asset->set_size (_film->frame_size());
134 _picture_asset->set_metadata (mxf_metadata());
136 if (_film->encrypted ()) {
137 _picture_asset->set_key (_film->key());
138 _picture_asset->set_context_id (_film->context_id());
141 _picture_asset->set_file (asset);
142 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
144 /* We already have a complete picture asset that we can just re-use */
145 /* XXX: what about if the encryption key changes? */
146 if (_film->three_d ()) {
147 _picture_asset.reset (new dcp::StereoPictureAsset(asset));
149 _picture_asset.reset (new dcp::MonoPictureAsset(asset));
153 if (_film->audio_channels ()) {
155 new dcp::SoundAsset (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels (), standard)
158 _sound_asset->set_metadata (mxf_metadata());
160 if (_film->encrypted ()) {
161 _sound_asset->set_key (_film->key ());
164 DCPOMATIC_ASSERT (_film->directory());
166 /* Write the sound asset into the film directory so that we leave the creation
167 of the DCP directory until the last minute.
169 _sound_asset_writer = _sound_asset->start_write (
170 _film->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary)
175 /** @param frame reel-relative frame */
177 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
179 shared_ptr<InfoFileHandle> handle = _film->info_file_handle(_period, false);
180 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
181 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
182 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
183 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
187 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
189 dcp::FrameInfo frame_info;
190 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
191 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
192 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
194 char hash_buffer[33];
195 checked_fread (hash_buffer, 32, info->get(), info->file());
196 hash_buffer[32] = '\0';
197 frame_info.hash = hash_buffer;
203 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
207 return frame * _info_size;
209 return frame * _info_size * 2;
211 return frame * _info_size * 2 + _info_size;
213 DCPOMATIC_ASSERT (false);
216 DCPOMATIC_ASSERT (false);
220 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
222 shared_ptr<Job> job = _job.lock ();
225 job->sub (_("Checking existing image data"));
228 /* Try to open the existing asset */
229 FILE* asset_file = fopen_boost (asset, "rb");
231 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
234 LOG_GENERAL ("Opened existing asset at %1", asset.string());
237 shared_ptr<InfoFileHandle> info_file;
240 info_file = _film->info_file_handle (_period, true);
241 } catch (OpenFileError) {
242 LOG_GENERAL_NC ("Could not open film info file");
247 /* Offset of the last dcp::FrameInfo in the info file */
248 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
249 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
251 Frame first_nonexistant_frame;
252 if (_film->three_d ()) {
253 /* Start looking at the last left frame */
254 first_nonexistant_frame = n / 2;
256 first_nonexistant_frame = n;
259 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
260 --first_nonexistant_frame;
263 if (!_film->three_d() && first_nonexistant_frame > 0) {
264 /* If we are doing 3D we might have found a good L frame with no R, so only
265 do this if we're in 2D and we've just found a good B(oth) frame.
267 ++first_nonexistant_frame;
270 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
274 return first_nonexistant_frame;
278 ReelWriter::write (optional<Data> encoded, Frame frame, Eyes eyes)
280 if (!_picture_asset_writer) {
281 /* We're not writing any data */
285 dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data().get (), encoded->size());
286 write_frame_info (frame, eyes, fin);
287 _last_written[eyes] = encoded;
291 ReelWriter::fake_write (int size)
293 if (!_picture_asset_writer) {
294 /* We're not writing any data */
298 _picture_asset_writer->fake_write (size);
302 ReelWriter::repeat_write (Frame frame, Eyes eyes)
304 if (!_picture_asset_writer) {
305 /* We're not writing any data */
309 dcp::FrameInfo fin = _picture_asset_writer->write (
310 _last_written[eyes]->data().get(),
311 _last_written[eyes]->size()
313 write_frame_info (frame, eyes, fin);
317 ReelWriter::finish ()
319 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
320 /* Nothing was written to the picture asset */
321 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
322 _picture_asset.reset ();
325 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
326 /* Nothing was written to the sound asset */
327 _sound_asset.reset ();
330 /* Hard-link any video asset file into the DCP */
331 if (_picture_asset) {
332 DCPOMATIC_ASSERT (_picture_asset->file());
333 boost::filesystem::path video_from = _picture_asset->file().get();
334 boost::filesystem::path video_to;
335 video_to /= _film->dir (_film->dcp_name());
336 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
337 /* There may be an existing "to" file if we are recreating a DCP in the same place without
340 boost::system::error_code ec;
341 boost::filesystem::remove (video_to, ec);
343 boost::filesystem::create_hard_link (video_from, video_to, ec);
345 LOG_WARNING_NC ("Hard-link failed; copying instead");
346 shared_ptr<Job> job = _job.lock ();
348 job->sub (_("Copying video file into DCP"));
350 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
351 } catch (exception& e) {
352 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
353 throw FileError (e.what(), video_from);
356 boost::filesystem::copy_file (video_from, video_to, ec);
358 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
359 throw FileError (ec.message(), video_from);
364 _picture_asset->set_file (video_to);
367 /* Move the audio asset into the DCP */
369 boost::filesystem::path audio_to;
370 audio_to /= _film->dir (_film->dcp_name ());
371 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
374 boost::system::error_code ec;
375 boost::filesystem::rename (_film->file (aaf), audio_to, ec);
378 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
382 _sound_asset->set_file (audio_to);
389 shared_ptr<dcp::SubtitleAsset> asset,
390 int64_t picture_duration,
391 shared_ptr<dcp::Reel> reel,
392 list<ReferencedReelAsset> const & refs,
393 list<shared_ptr<Font> > const & fonts,
394 shared_ptr<const Film> film,
398 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
400 shared_ptr<T> reel_asset;
403 boost::filesystem::path liberation_normal;
405 liberation_normal = shared_path() / "LiberationSans-Regular.ttf";
406 if (!boost::filesystem::exists (liberation_normal)) {
407 /* Hack for unit tests */
408 liberation_normal = shared_path() / "fonts" / "LiberationSans-Regular.ttf";
410 } catch (boost::filesystem::filesystem_error& e) {
414 if (!boost::filesystem::exists(liberation_normal)) {
415 liberation_normal = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf";
418 /* Add the font to the subtitle content */
419 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
420 asset->add_font (j->id(), j->file().get_value_or(liberation_normal));
423 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
424 boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
425 boost::filesystem::create_directories (directory);
426 asset->write (directory / ("sub_" + asset->id() + ".xml"));
428 /* All our assets should be the same length; use the picture asset length here
429 as a reference to set the subtitle one. We'll use the duration rather than
430 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
431 just interested in its presentation length.
433 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
436 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
443 dcp::Fraction (film->video_frame_rate(), 1),
449 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
450 BOOST_FOREACH (ReferencedReelAsset j, refs) {
451 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
452 if (k && j.period == period) {
454 /* If we have a hash for this asset in the CPL, assume that it is correct */
456 k->asset_ref()->set_hash (k->hash().get());
463 if (reel_asset->actual_duration() != period_duration) {
464 throw ProgrammingError (
466 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
469 reel->add (reel_asset);
475 shared_ptr<dcp::Reel>
476 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
478 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
480 shared_ptr<dcp::Reel> reel (new dcp::Reel ());
482 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
484 if (_picture_asset) {
485 /* We have made a picture asset of our own. Put it into the reel */
486 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
488 reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
491 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
493 reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
496 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
497 /* We don't have a picture asset of our own; hopefully we have one to reference */
498 BOOST_FOREACH (ReferencedReelAsset j, refs) {
499 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
501 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
503 if (k && j.period == _period) {
504 reel_picture_asset = k;
509 Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
511 DCPOMATIC_ASSERT (reel_picture_asset);
512 if (reel_picture_asset->duration() != period_duration) {
513 throw ProgrammingError (
515 String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
518 reel->add (reel_picture_asset);
520 /* If we have a hash for this asset in the CPL, assume that it is correct */
521 if (reel_picture_asset->hash()) {
522 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
525 shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
528 /* We have made a sound asset of our own. Put it into the reel */
529 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
531 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
532 /* We don't have a sound asset of our own; hopefully we have one to reference */
533 BOOST_FOREACH (ReferencedReelAsset j, refs) {
534 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
536 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
538 if (k && j.period == _period) {
539 reel_sound_asset = k;
540 /* If we have a hash for this asset in the CPL, assume that it is correct */
542 k->asset_ref()->set_hash (k->hash().get());
548 DCPOMATIC_ASSERT (reel_sound_asset);
549 if (reel_sound_asset->actual_duration() != period_duration) {
551 "Reel sound asset has length %1 but reel period is %2",
552 reel_sound_asset->actual_duration(),
555 if (reel_sound_asset->actual_duration() != period_duration) {
556 throw ProgrammingError (
558 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
563 reel->add (reel_sound_asset);
565 maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
566 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
567 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
568 i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
570 a->set_annotation_text (i->first.name);
571 a->set_language (i->first.language);
574 map<dcp::Marker, DCPTime> markers = _film->markers ();
575 map<dcp::Marker, DCPTime> reel_markers;
576 for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
577 if (_period.contains(i->second)) {
578 reel_markers[i->first] = i->second;
582 if (!reel_markers.empty ()) {
583 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
584 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
586 DCPTime relative = i->second - _period.from;
587 relative.split (_film->video_frame_rate(), h, m, s, f);
588 ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
597 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
599 if (_picture_asset) {
600 _picture_asset->hash (set_progress);
604 _sound_asset->hash (set_progress);
609 ReelWriter::start () const
611 return _period.from.frames_floor (_film->video_frame_rate());
616 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
618 if (!_sound_asset_writer) {
622 DCPOMATIC_ASSERT (audio);
623 _sound_asset_writer->write (audio->data(), audio->frames());
627 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
629 shared_ptr<dcp::SubtitleAsset> asset;
632 case TEXT_OPEN_SUBTITLE:
633 asset = _subtitle_asset;
635 case TEXT_CLOSED_CAPTION:
636 DCPOMATIC_ASSERT (track);
637 asset = _closed_caption_assets[*track];
640 DCPOMATIC_ASSERT (false);
644 string lang = _film->subtitle_language ();
645 if (_film->interop ()) {
646 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
647 s->set_movie_title (_film->name ());
648 if (type == TEXT_OPEN_SUBTITLE) {
649 s->set_language (lang.empty() ? "Unknown" : lang);
651 s->set_language (track->language);
653 s->set_reel_number (raw_convert<string> (_reel_index + 1));
656 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
657 s->set_content_title_text (_film->name ());
658 s->set_metadata (mxf_metadata());
659 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
660 s->set_language (lang);
662 s->set_language (track->language);
664 s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
665 s->set_reel_number (_reel_index + 1);
666 s->set_time_code_rate (_film->video_frame_rate ());
667 s->set_start_time (dcp::Time ());
668 if (_film->encrypted ()) {
669 s->set_key (_film->key ());
676 case TEXT_OPEN_SUBTITLE:
677 _subtitle_asset = asset;
679 case TEXT_CLOSED_CAPTION:
680 DCPOMATIC_ASSERT (track);
681 _closed_caption_assets[*track] = asset;
684 DCPOMATIC_ASSERT (false);
687 BOOST_FOREACH (StringText i, subs.string) {
688 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
689 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
690 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
691 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
694 BOOST_FOREACH (BitmapText i, subs.bitmap) {
696 shared_ptr<dcp::Subtitle>(
697 new dcp::SubtitleImage(
699 dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
700 dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
701 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
702 dcp::Time(), dcp::Time()
710 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
712 LOG_GENERAL ("Checking existing picture frame %1", frame);
714 /* Read the data from the info file; for 3D we just check the left
715 frames until we find a good one.
717 dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
721 /* Read the data from the asset and hash it */
722 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
723 Data data (info.size);
724 size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
725 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
726 if (read != static_cast<size_t> (data.size ())) {
727 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
731 digester.add (data.data().get(), data.size());
732 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
733 if (digester.get() != info.hash) {
734 LOG_GENERAL ("Existing frame %1 failed hash check", frame);