2 Copyright (C) 2012-2019 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"
30 #include "audio_buffers.h"
32 #include <dcp/mono_picture_asset.h>
33 #include <dcp/stereo_picture_asset.h>
34 #include <dcp/sound_asset.h>
35 #include <dcp/sound_asset_writer.h>
37 #include <dcp/reel_mono_picture_asset.h>
38 #include <dcp/reel_stereo_picture_asset.h>
39 #include <dcp/reel_sound_asset.h>
40 #include <dcp/reel_subtitle_asset.h>
41 #include <dcp/reel_closed_caption_asset.h>
42 #include <dcp/reel_markers_asset.h>
45 #include <dcp/certificate_chain.h>
46 #include <dcp/interop_subtitle_asset.h>
47 #include <dcp/smpte_subtitle_asset.h>
48 #include <dcp/raw_convert.h>
49 #include <dcp/subtitle_image.h>
50 #include <boost/foreach.hpp>
59 using boost::shared_ptr;
60 using boost::optional;
61 using boost::dynamic_pointer_cast;
63 using dcp::raw_convert;
64 using namespace dcpomatic;
66 int const ReelWriter::_info_size = 48;
68 /** @param job Related job, or 0 */
69 ReelWriter::ReelWriter (
70 shared_ptr<const Film> film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, optional<string> content_summary
74 , _last_written_video_frame (-1)
75 , _last_written_eyes (EYES_RIGHT)
76 , _reel_index (reel_index)
77 , _reel_count (reel_count)
78 , _content_summary (content_summary)
81 /* Create or find our picture asset in a subdirectory, named
82 according to those film's parameters which affect the video
83 output. We will hard-link it into the DCP later.
86 dcp::Standard const standard = _film->interop() ? dcp::INTEROP : dcp::SMPTE;
88 boost::filesystem::path const asset =
89 _film->internal_video_asset_dir() / _film->internal_video_asset_filename(_period);
91 _first_nonexistant_frame = check_existing_picture_asset (asset);
93 if (_first_nonexistant_frame < period.duration().frames_round(_film->video_frame_rate())) {
94 /* We do not have a complete picture asset. If there is an
95 existing asset, break any hard links to it as we are about
96 to change its contents (if only by changing the IDs); see
99 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
101 job->sub (_("Copying old video file"));
102 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
104 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
106 boost::filesystem::remove (asset);
107 boost::filesystem::rename (asset.string() + ".tmp", asset);
111 if (_film->three_d ()) {
112 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
114 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
117 _picture_asset->set_size (_film->frame_size());
119 if (_film->encrypted ()) {
120 _picture_asset->set_key (_film->key());
121 _picture_asset->set_context_id (_film->context_id());
124 _picture_asset->set_file (asset);
125 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
127 /* We already have a complete picture asset that we can just re-use */
128 /* XXX: what about if the encryption key changes? */
129 if (_film->three_d ()) {
130 _picture_asset.reset (new dcp::StereoPictureAsset(asset));
132 _picture_asset.reset (new dcp::MonoPictureAsset(asset));
136 if (_film->audio_channels ()) {
138 new dcp::SoundAsset (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels (), standard)
141 if (_film->encrypted ()) {
142 _sound_asset->set_key (_film->key ());
145 DCPOMATIC_ASSERT (_film->directory());
147 /* Write the sound asset into the film directory so that we leave the creation
148 of the DCP directory until the last minute.
150 _sound_asset_writer = _sound_asset->start_write (
151 _film->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary)
156 /** @param frame reel-relative frame */
158 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
160 shared_ptr<InfoFileHandle> handle = _film->info_file_handle(_period, false);
161 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
162 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
163 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
164 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
168 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
170 dcp::FrameInfo frame_info;
171 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
172 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
173 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
175 char hash_buffer[33];
176 checked_fread (hash_buffer, 32, info->get(), info->file());
177 hash_buffer[32] = '\0';
178 frame_info.hash = hash_buffer;
184 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
188 return frame * _info_size;
190 return frame * _info_size * 2;
192 return frame * _info_size * 2 + _info_size;
194 DCPOMATIC_ASSERT (false);
197 DCPOMATIC_ASSERT (false);
201 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
203 shared_ptr<Job> job = _job.lock ();
206 job->sub (_("Checking existing image data"));
209 /* Try to open the existing asset */
210 FILE* asset_file = fopen_boost (asset, "rb");
212 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
215 LOG_GENERAL ("Opened existing asset at %1", asset.string());
218 shared_ptr<InfoFileHandle> info_file;
221 info_file = _film->info_file_handle (_period, true);
222 } catch (OpenFileError) {
223 LOG_GENERAL_NC ("Could not open film info file");
228 /* Offset of the last dcp::FrameInfo in the info file */
229 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
230 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
232 Frame first_nonexistant_frame;
233 if (_film->three_d ()) {
234 /* Start looking at the last left frame */
235 first_nonexistant_frame = n / 2;
237 first_nonexistant_frame = n;
240 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
241 --first_nonexistant_frame;
244 if (!_film->three_d() && first_nonexistant_frame > 0) {
245 /* If we are doing 3D we might have found a good L frame with no R, so only
246 do this if we're in 2D and we've just found a good B(oth) frame.
248 ++first_nonexistant_frame;
251 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
255 return first_nonexistant_frame;
259 ReelWriter::write (optional<Data> encoded, Frame frame, Eyes eyes)
261 if (!_picture_asset_writer) {
262 /* We're not writing any data */
266 dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data().get (), encoded->size());
267 write_frame_info (frame, eyes, fin);
268 _last_written[eyes] = encoded;
269 _last_written_video_frame = frame;
270 _last_written_eyes = eyes;
274 ReelWriter::fake_write (Frame frame, Eyes eyes, int size)
276 if (!_picture_asset_writer) {
277 /* We're not writing any data */
281 _picture_asset_writer->fake_write (size);
282 _last_written_video_frame = frame;
283 _last_written_eyes = eyes;
287 ReelWriter::repeat_write (Frame frame, Eyes eyes)
289 if (!_picture_asset_writer) {
290 /* We're not writing any data */
294 dcp::FrameInfo fin = _picture_asset_writer->write (
295 _last_written[eyes]->data().get(),
296 _last_written[eyes]->size()
298 write_frame_info (frame, eyes, fin);
299 _last_written_video_frame = frame;
300 _last_written_eyes = eyes;
304 ReelWriter::finish ()
306 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
307 /* Nothing was written to the picture asset */
308 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
309 _picture_asset.reset ();
312 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
313 /* Nothing was written to the sound asset */
314 _sound_asset.reset ();
317 /* Hard-link any video asset file into the DCP */
318 if (_picture_asset) {
319 DCPOMATIC_ASSERT (_picture_asset->file());
320 boost::filesystem::path video_from = _picture_asset->file().get();
321 boost::filesystem::path video_to;
322 video_to /= _film->dir (_film->dcp_name());
323 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
324 /* There may be an existing "to" file if we are recreating a DCP in the same place without
327 boost::system::error_code ec;
328 boost::filesystem::remove (video_to, ec);
330 boost::filesystem::create_hard_link (video_from, video_to, ec);
332 LOG_WARNING_NC ("Hard-link failed; copying instead");
333 shared_ptr<Job> job = _job.lock ();
335 job->sub (_("Copying video file into DCP"));
337 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
338 } catch (exception& e) {
339 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
340 throw FileError (e.what(), video_from);
343 boost::filesystem::copy_file (video_from, video_to, ec);
345 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
346 throw FileError (ec.message(), video_from);
351 _picture_asset->set_file (video_to);
354 /* Move the audio asset into the DCP */
356 boost::filesystem::path audio_to;
357 audio_to /= _film->dir (_film->dcp_name ());
358 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
361 boost::system::error_code ec;
362 boost::filesystem::rename (_film->file (aaf), audio_to, ec);
365 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
369 _sound_asset->set_file (audio_to);
376 shared_ptr<dcp::SubtitleAsset> asset,
377 int64_t picture_duration,
378 shared_ptr<dcp::Reel> reel,
379 list<ReferencedReelAsset> const & refs,
380 list<shared_ptr<Font> > const & fonts,
381 shared_ptr<const Film> film,
385 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
387 shared_ptr<T> reel_asset;
390 boost::filesystem::path liberation_normal;
392 liberation_normal = shared_path() / "LiberationSans-Regular.ttf";
393 if (!boost::filesystem::exists (liberation_normal)) {
394 /* Hack for unit tests */
395 liberation_normal = shared_path() / "fonts" / "LiberationSans-Regular.ttf";
397 } catch (boost::filesystem::filesystem_error& e) {
401 if (!boost::filesystem::exists(liberation_normal)) {
402 liberation_normal = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf";
405 /* Add the font to the subtitle content */
406 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
407 asset->add_font (j->id(), j->file().get_value_or(liberation_normal));
410 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
411 boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
412 boost::filesystem::create_directories (directory);
413 asset->write (directory / ("sub_" + asset->id() + ".xml"));
415 /* All our assets should be the same length; use the picture asset length here
416 as a reference to set the subtitle one. We'll use the duration rather than
417 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
418 just interested in its presentation length.
420 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
423 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
430 dcp::Fraction (film->video_frame_rate(), 1),
436 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
437 BOOST_FOREACH (ReferencedReelAsset j, refs) {
438 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
439 if (k && j.period == period) {
441 /* If we have a hash for this asset in the CPL, assume that it is correct */
443 k->asset_ref()->set_hash (k->hash().get());
450 if (reel_asset->actual_duration() != period_duration) {
451 throw ProgrammingError (
453 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
456 reel->add (reel_asset);
462 shared_ptr<dcp::Reel>
463 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
465 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
467 shared_ptr<dcp::Reel> reel (new dcp::Reel ());
469 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
471 if (_picture_asset) {
472 /* We have made a picture asset of our own. Put it into the reel */
473 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
475 reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
478 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
480 reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
483 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
484 /* We don't have a picture asset of our own; hopefully we have one to reference */
485 BOOST_FOREACH (ReferencedReelAsset j, refs) {
486 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
488 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
490 if (k && j.period == _period) {
491 reel_picture_asset = k;
496 Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
498 DCPOMATIC_ASSERT (reel_picture_asset);
499 if (reel_picture_asset->duration() != period_duration) {
500 throw ProgrammingError (
502 String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
505 reel->add (reel_picture_asset);
507 /* If we have a hash for this asset in the CPL, assume that it is correct */
508 if (reel_picture_asset->hash()) {
509 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
512 shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
515 /* We have made a sound asset of our own. Put it into the reel */
516 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
518 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
519 /* We don't have a sound asset of our own; hopefully we have one to reference */
520 BOOST_FOREACH (ReferencedReelAsset j, refs) {
521 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
523 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
525 if (k && j.period == _period) {
526 reel_sound_asset = k;
527 /* If we have a hash for this asset in the CPL, assume that it is correct */
529 k->asset_ref()->set_hash (k->hash().get());
535 DCPOMATIC_ASSERT (reel_sound_asset);
536 if (reel_sound_asset->actual_duration() != period_duration) {
538 "Reel sound asset has length %1 but reel period is %2",
539 reel_sound_asset->actual_duration(),
542 if (reel_sound_asset->actual_duration() != period_duration) {
543 throw ProgrammingError (
545 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
550 reel->add (reel_sound_asset);
552 maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
553 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
554 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
555 i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
557 a->set_annotation_text (i->first.name);
558 a->set_language (i->first.language);
561 map<dcp::Marker, DCPTime> markers = _film->markers ();
562 map<dcp::Marker, DCPTime> reel_markers;
563 for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
564 if (_period.contains(i->second)) {
565 reel_markers[i->first] = i->second;
569 if (!reel_markers.empty ()) {
570 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
571 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
573 DCPTime relative = i->second - _period.from;
574 relative.split (_film->video_frame_rate(), h, m, s, f);
575 ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
584 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
586 if (_picture_asset) {
587 _picture_asset->hash (set_progress);
591 _sound_asset->hash (set_progress);
596 ReelWriter::start () const
598 return _period.from.frames_floor (_film->video_frame_rate());
603 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
605 if (!_sound_asset_writer) {
609 DCPOMATIC_ASSERT (audio);
610 _sound_asset_writer->write (audio->data(), audio->frames());
614 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
616 shared_ptr<dcp::SubtitleAsset> asset;
619 case TEXT_OPEN_SUBTITLE:
620 asset = _subtitle_asset;
622 case TEXT_CLOSED_CAPTION:
623 DCPOMATIC_ASSERT (track);
624 asset = _closed_caption_assets[*track];
627 DCPOMATIC_ASSERT (false);
631 string lang = _film->subtitle_language ();
632 if (_film->interop ()) {
633 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
634 s->set_movie_title (_film->name ());
635 if (type == TEXT_OPEN_SUBTITLE) {
636 s->set_language (lang.empty() ? "Unknown" : lang);
638 s->set_language (track->language);
640 s->set_reel_number (raw_convert<string> (_reel_index + 1));
643 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
644 s->set_content_title_text (_film->name ());
645 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
646 s->set_language (lang);
648 s->set_language (track->language);
650 s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
651 s->set_reel_number (_reel_index + 1);
652 s->set_time_code_rate (_film->video_frame_rate ());
653 s->set_start_time (dcp::Time ());
654 if (_film->encrypted ()) {
655 s->set_key (_film->key ());
662 case TEXT_OPEN_SUBTITLE:
663 _subtitle_asset = asset;
665 case TEXT_CLOSED_CAPTION:
666 DCPOMATIC_ASSERT (track);
667 _closed_caption_assets[*track] = asset;
670 DCPOMATIC_ASSERT (false);
673 BOOST_FOREACH (StringText i, subs.string) {
674 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
675 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
676 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
677 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
680 BOOST_FOREACH (BitmapText i, subs.bitmap) {
682 shared_ptr<dcp::Subtitle>(
683 new dcp::SubtitleImage(
685 dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
686 dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
687 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
688 dcp::Time(), dcp::Time()
696 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
698 LOG_GENERAL ("Checking existing picture frame %1", frame);
700 /* Read the data from the info file; for 3D we just check the left
701 frames until we find a good one.
703 dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
707 /* Read the data from the asset and hash it */
708 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
709 Data data (info.size);
710 size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
711 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
712 if (read != static_cast<size_t> (data.size ())) {
713 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
717 digester.add (data.data().get(), data.size());
718 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
719 if (digester.get() != info.hash) {
720 LOG_GENERAL ("Existing frame %1 failed hash check", frame);