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 /* We don't have a sound asset of our own; hopefully we have one to reference */
519 BOOST_FOREACH (ReferencedReelAsset j, refs) {
520 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
521 if (k && j.period == _period) {
522 reel_sound_asset = k;
523 /* If we have a hash for this asset in the CPL, assume that it is correct */
525 k->asset_ref()->set_hash (k->hash().get());
531 DCPOMATIC_ASSERT (reel_sound_asset);
532 if (reel_sound_asset->actual_duration() != period_duration) {
534 "Reel sound asset has length %1 but reel period is %2",
535 reel_sound_asset->actual_duration(),
538 if (reel_sound_asset->actual_duration() != period_duration) {
539 throw ProgrammingError (
541 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
546 reel->add (reel_sound_asset);
548 maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
549 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
550 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
551 i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
553 a->set_annotation_text (i->first.name);
554 a->set_language (i->first.language);
557 map<dcp::Marker, DCPTime> markers = _film->markers ();
558 map<dcp::Marker, DCPTime> reel_markers;
559 for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
560 if (_period.contains(i->second)) {
561 reel_markers[i->first] = i->second;
565 if (!reel_markers.empty ()) {
566 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
567 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
569 DCPTime relative = i->second - _period.from;
570 relative.split (_film->video_frame_rate(), h, m, s, f);
571 ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
580 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
582 if (_picture_asset) {
583 _picture_asset->hash (set_progress);
587 _sound_asset->hash (set_progress);
592 ReelWriter::start () const
594 return _period.from.frames_floor (_film->video_frame_rate());
599 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
601 if (!_sound_asset_writer) {
605 DCPOMATIC_ASSERT (audio);
606 _sound_asset_writer->write (audio->data(), audio->frames());
610 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
612 shared_ptr<dcp::SubtitleAsset> asset;
615 case TEXT_OPEN_SUBTITLE:
616 asset = _subtitle_asset;
618 case TEXT_CLOSED_CAPTION:
619 DCPOMATIC_ASSERT (track);
620 asset = _closed_caption_assets[*track];
623 DCPOMATIC_ASSERT (false);
627 string lang = _film->subtitle_language ();
628 if (_film->interop ()) {
629 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
630 s->set_movie_title (_film->name ());
631 if (type == TEXT_OPEN_SUBTITLE) {
632 s->set_language (lang.empty() ? "Unknown" : lang);
634 s->set_language (track->language);
636 s->set_reel_number (raw_convert<string> (_reel_index + 1));
639 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
640 s->set_content_title_text (_film->name ());
641 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
642 s->set_language (lang);
644 s->set_language (track->language);
646 s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
647 s->set_reel_number (_reel_index + 1);
648 s->set_time_code_rate (_film->video_frame_rate ());
649 s->set_start_time (dcp::Time ());
650 if (_film->encrypted ()) {
651 s->set_key (_film->key ());
658 case TEXT_OPEN_SUBTITLE:
659 _subtitle_asset = asset;
661 case TEXT_CLOSED_CAPTION:
662 DCPOMATIC_ASSERT (track);
663 _closed_caption_assets[*track] = asset;
666 DCPOMATIC_ASSERT (false);
669 BOOST_FOREACH (StringText i, subs.string) {
670 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
671 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
672 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
673 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
676 BOOST_FOREACH (BitmapText i, subs.bitmap) {
678 shared_ptr<dcp::Subtitle>(
679 new dcp::SubtitleImage(
681 dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
682 dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
683 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
684 dcp::Time(), dcp::Time()
692 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
694 LOG_GENERAL ("Checking existing picture frame %1", frame);
696 /* Read the data from the info file; for 3D we just check the left
697 frames until we find a good one.
699 dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
703 /* Read the data from the asset and hash it */
704 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
705 Data data (info.size);
706 size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
707 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
708 if (read != static_cast<size_t> (data.size ())) {
709 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
713 digester.add (data.data().get(), data.size());
714 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
715 if (digester.get() != info.hash) {
716 LOG_GENERAL ("Existing frame %1 failed hash check", frame);