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 , _reel_index (reel_index)
75 , _reel_count (reel_count)
76 , _content_summary (content_summary)
79 /* Create or find our picture asset in a subdirectory, named
80 according to those film's parameters which affect the video
81 output. We will hard-link it into the DCP later.
84 dcp::Standard const standard = _film->interop() ? dcp::INTEROP : dcp::SMPTE;
86 boost::filesystem::path const asset =
87 _film->internal_video_asset_dir() / _film->internal_video_asset_filename(_period);
89 _first_nonexistant_frame = check_existing_picture_asset (asset);
91 if (_first_nonexistant_frame < period.duration().frames_round(_film->video_frame_rate())) {
92 /* We do not have a complete picture asset. If there is an
93 existing asset, break any hard links to it as we are about
94 to change its contents (if only by changing the IDs); see
97 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
99 job->sub (_("Copying old video file"));
100 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
102 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
104 boost::filesystem::remove (asset);
105 boost::filesystem::rename (asset.string() + ".tmp", asset);
109 if (_film->three_d ()) {
110 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
112 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(_film->video_frame_rate(), 1), standard));
115 _picture_asset->set_size (_film->frame_size());
117 if (_film->encrypted ()) {
118 _picture_asset->set_key (_film->key());
119 _picture_asset->set_context_id (_film->context_id());
122 _picture_asset->set_file (asset);
123 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
125 /* We already have a complete picture asset that we can just re-use */
126 /* XXX: what about if the encryption key changes? */
127 if (_film->three_d ()) {
128 _picture_asset.reset (new dcp::StereoPictureAsset(asset));
130 _picture_asset.reset (new dcp::MonoPictureAsset(asset));
134 if (_film->audio_channels ()) {
136 new dcp::SoundAsset (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels (), standard)
139 if (_film->encrypted ()) {
140 _sound_asset->set_key (_film->key ());
143 DCPOMATIC_ASSERT (_film->directory());
145 /* Write the sound asset into the film directory so that we leave the creation
146 of the DCP directory until the last minute.
148 _sound_asset_writer = _sound_asset->start_write (
149 _film->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary)
154 /** @param frame reel-relative frame */
156 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
158 shared_ptr<InfoFileHandle> handle = _film->info_file_handle(_period, false);
159 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
160 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
161 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
162 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
166 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
168 dcp::FrameInfo frame_info;
169 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
170 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
171 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
173 char hash_buffer[33];
174 checked_fread (hash_buffer, 32, info->get(), info->file());
175 hash_buffer[32] = '\0';
176 frame_info.hash = hash_buffer;
182 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
186 return frame * _info_size;
188 return frame * _info_size * 2;
190 return frame * _info_size * 2 + _info_size;
192 DCPOMATIC_ASSERT (false);
195 DCPOMATIC_ASSERT (false);
199 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
201 shared_ptr<Job> job = _job.lock ();
204 job->sub (_("Checking existing image data"));
207 /* Try to open the existing asset */
208 FILE* asset_file = fopen_boost (asset, "rb");
210 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
213 LOG_GENERAL ("Opened existing asset at %1", asset.string());
216 shared_ptr<InfoFileHandle> info_file;
219 info_file = _film->info_file_handle (_period, true);
220 } catch (OpenFileError) {
221 LOG_GENERAL_NC ("Could not open film info file");
226 /* Offset of the last dcp::FrameInfo in the info file */
227 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
228 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
230 Frame first_nonexistant_frame;
231 if (_film->three_d ()) {
232 /* Start looking at the last left frame */
233 first_nonexistant_frame = n / 2;
235 first_nonexistant_frame = n;
238 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
239 --first_nonexistant_frame;
242 if (!_film->three_d() && first_nonexistant_frame > 0) {
243 /* If we are doing 3D we might have found a good L frame with no R, so only
244 do this if we're in 2D and we've just found a good B(oth) frame.
246 ++first_nonexistant_frame;
249 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
253 return first_nonexistant_frame;
257 ReelWriter::write (optional<Data> encoded, Frame frame, Eyes eyes)
259 if (!_picture_asset_writer) {
260 /* We're not writing any data */
264 dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data().get (), encoded->size());
265 write_frame_info (frame, eyes, fin);
266 _last_written[eyes] = encoded;
270 ReelWriter::fake_write (int size)
272 if (!_picture_asset_writer) {
273 /* We're not writing any data */
277 _picture_asset_writer->fake_write (size);
281 ReelWriter::repeat_write (Frame frame, Eyes eyes)
283 if (!_picture_asset_writer) {
284 /* We're not writing any data */
288 dcp::FrameInfo fin = _picture_asset_writer->write (
289 _last_written[eyes]->data().get(),
290 _last_written[eyes]->size()
292 write_frame_info (frame, eyes, fin);
296 ReelWriter::finish ()
298 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
299 /* Nothing was written to the picture asset */
300 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
301 _picture_asset.reset ();
304 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
305 /* Nothing was written to the sound asset */
306 _sound_asset.reset ();
309 /* Hard-link any video asset file into the DCP */
310 if (_picture_asset) {
311 DCPOMATIC_ASSERT (_picture_asset->file());
312 boost::filesystem::path video_from = _picture_asset->file().get();
313 boost::filesystem::path video_to;
314 video_to /= _film->dir (_film->dcp_name());
315 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
316 /* There may be an existing "to" file if we are recreating a DCP in the same place without
319 boost::system::error_code ec;
320 boost::filesystem::remove (video_to, ec);
322 boost::filesystem::create_hard_link (video_from, video_to, ec);
324 LOG_WARNING_NC ("Hard-link failed; copying instead");
325 shared_ptr<Job> job = _job.lock ();
327 job->sub (_("Copying video file into DCP"));
329 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
330 } catch (exception& e) {
331 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
332 throw FileError (e.what(), video_from);
335 boost::filesystem::copy_file (video_from, video_to, ec);
337 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
338 throw FileError (ec.message(), video_from);
343 _picture_asset->set_file (video_to);
346 /* Move the audio asset into the DCP */
348 boost::filesystem::path audio_to;
349 audio_to /= _film->dir (_film->dcp_name ());
350 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
353 boost::system::error_code ec;
354 boost::filesystem::rename (_film->file (aaf), audio_to, ec);
357 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
361 _sound_asset->set_file (audio_to);
368 shared_ptr<dcp::SubtitleAsset> asset,
369 int64_t picture_duration,
370 shared_ptr<dcp::Reel> reel,
371 list<ReferencedReelAsset> const & refs,
372 list<shared_ptr<Font> > const & fonts,
373 shared_ptr<const Film> film,
377 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
379 shared_ptr<T> reel_asset;
382 boost::filesystem::path liberation_normal;
384 liberation_normal = shared_path() / "LiberationSans-Regular.ttf";
385 if (!boost::filesystem::exists (liberation_normal)) {
386 /* Hack for unit tests */
387 liberation_normal = shared_path() / "fonts" / "LiberationSans-Regular.ttf";
389 } catch (boost::filesystem::filesystem_error& e) {
393 if (!boost::filesystem::exists(liberation_normal)) {
394 liberation_normal = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf";
397 /* Add the font to the subtitle content */
398 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
399 asset->add_font (j->id(), j->file().get_value_or(liberation_normal));
402 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
403 boost::filesystem::path directory = film->dir (film->dcp_name ()) / asset->id ();
404 boost::filesystem::create_directories (directory);
405 asset->write (directory / ("sub_" + asset->id() + ".xml"));
407 /* All our assets should be the same length; use the picture asset length here
408 as a reference to set the subtitle one. We'll use the duration rather than
409 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
410 just interested in its presentation length.
412 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
415 film->dir(film->dcp_name()) / ("sub_" + asset->id() + ".mxf")
422 dcp::Fraction (film->video_frame_rate(), 1),
428 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
429 BOOST_FOREACH (ReferencedReelAsset j, refs) {
430 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
431 if (k && j.period == period) {
433 /* If we have a hash for this asset in the CPL, assume that it is correct */
435 k->asset_ref()->set_hash (k->hash().get());
442 if (reel_asset->actual_duration() != period_duration) {
443 throw ProgrammingError (
445 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
448 reel->add (reel_asset);
454 shared_ptr<dcp::Reel>
455 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts)
457 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
459 shared_ptr<dcp::Reel> reel (new dcp::Reel ());
461 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset;
463 if (_picture_asset) {
464 /* We have made a picture asset of our own. Put it into the reel */
465 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
467 reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
470 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
472 reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
475 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
476 /* We don't have a picture asset of our own; hopefully we have one to reference */
477 BOOST_FOREACH (ReferencedReelAsset j, refs) {
478 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
480 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
482 if (k && j.period == _period) {
483 reel_picture_asset = k;
488 Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
490 DCPOMATIC_ASSERT (reel_picture_asset);
491 if (reel_picture_asset->duration() != period_duration) {
492 throw ProgrammingError (
494 String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
497 reel->add (reel_picture_asset);
499 /* If we have a hash for this asset in the CPL, assume that it is correct */
500 if (reel_picture_asset->hash()) {
501 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
504 shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
507 /* We have made a sound asset of our own. Put it into the reel */
508 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
510 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
511 /* We don't have a sound asset of our own; hopefully we have one to reference */
512 BOOST_FOREACH (ReferencedReelAsset j, refs) {
513 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
515 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
517 if (k && j.period == _period) {
518 reel_sound_asset = k;
519 /* If we have a hash for this asset in the CPL, assume that it is correct */
521 k->asset_ref()->set_hash (k->hash().get());
527 DCPOMATIC_ASSERT (reel_sound_asset);
528 if (reel_sound_asset->actual_duration() != period_duration) {
530 "Reel sound asset has length %1 but reel period is %2",
531 reel_sound_asset->actual_duration(),
534 if (reel_sound_asset->actual_duration() != period_duration) {
535 throw ProgrammingError (
537 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
542 reel->add (reel_sound_asset);
544 maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
545 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
546 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
547 i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
549 a->set_annotation_text (i->first.name);
550 a->set_language (i->first.language);
553 map<dcp::Marker, DCPTime> markers = _film->markers ();
554 map<dcp::Marker, DCPTime> reel_markers;
555 for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
556 if (_period.contains(i->second)) {
557 reel_markers[i->first] = i->second;
561 if (!reel_markers.empty ()) {
562 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
563 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
565 DCPTime relative = i->second - _period.from;
566 relative.split (_film->video_frame_rate(), h, m, s, f);
567 ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
576 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
578 if (_picture_asset) {
579 _picture_asset->hash (set_progress);
583 _sound_asset->hash (set_progress);
588 ReelWriter::start () const
590 return _period.from.frames_floor (_film->video_frame_rate());
595 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
597 if (!_sound_asset_writer) {
601 DCPOMATIC_ASSERT (audio);
602 _sound_asset_writer->write (audio->data(), audio->frames());
606 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
608 shared_ptr<dcp::SubtitleAsset> asset;
611 case TEXT_OPEN_SUBTITLE:
612 asset = _subtitle_asset;
614 case TEXT_CLOSED_CAPTION:
615 DCPOMATIC_ASSERT (track);
616 asset = _closed_caption_assets[*track];
619 DCPOMATIC_ASSERT (false);
623 string lang = _film->subtitle_language ();
624 if (_film->interop ()) {
625 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
626 s->set_movie_title (_film->name ());
627 if (type == TEXT_OPEN_SUBTITLE) {
628 s->set_language (lang.empty() ? "Unknown" : lang);
630 s->set_language (track->language);
632 s->set_reel_number (raw_convert<string> (_reel_index + 1));
635 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
636 s->set_content_title_text (_film->name ());
637 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
638 s->set_language (lang);
640 s->set_language (track->language);
642 s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
643 s->set_reel_number (_reel_index + 1);
644 s->set_time_code_rate (_film->video_frame_rate ());
645 s->set_start_time (dcp::Time ());
646 if (_film->encrypted ()) {
647 s->set_key (_film->key ());
654 case TEXT_OPEN_SUBTITLE:
655 _subtitle_asset = asset;
657 case TEXT_CLOSED_CAPTION:
658 DCPOMATIC_ASSERT (track);
659 _closed_caption_assets[*track] = asset;
662 DCPOMATIC_ASSERT (false);
665 BOOST_FOREACH (StringText i, subs.string) {
666 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
667 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
668 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
669 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
672 BOOST_FOREACH (BitmapText i, subs.bitmap) {
674 shared_ptr<dcp::Subtitle>(
675 new dcp::SubtitleImage(
677 dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
678 dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
679 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
680 dcp::Time(), dcp::Time()
688 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
690 LOG_GENERAL ("Checking existing picture frame %1", frame);
692 /* Read the data from the info file; for 3D we just check the left
693 frames until we find a good one.
695 dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
699 /* Read the data from the asset and hash it */
700 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
701 Data data (info.size);
702 size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
703 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
704 if (read != static_cast<size_t> (data.size ())) {
705 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
709 digester.add (data.data().get(), data.size());
710 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
711 if (digester.get() != info.hash) {
712 LOG_GENERAL ("Existing frame %1 failed hash check", frame);