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 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
473 if (_picture_asset) {
474 /* We have made a picture asset of our own. Put it into the reel */
475 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
477 reel_picture_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
480 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
482 reel_picture_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
485 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
486 /* We don't have a picture asset of our own; hopefully we have one to reference */
487 BOOST_FOREACH (ReferencedReelAsset j, refs) {
488 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
490 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
492 if (k && j.period == _period) {
493 reel_picture_asset = k;
498 Frame const period_duration = _period.duration().frames_round(_film->video_frame_rate());
500 DCPOMATIC_ASSERT (reel_picture_asset);
501 if (reel_picture_asset->duration() != period_duration) {
502 throw ProgrammingError (
504 String::compose ("%1 vs %2", reel_picture_asset->actual_duration(), period_duration)
507 reel->add (reel_picture_asset);
509 /* If we have a hash for this asset in the CPL, assume that it is correct */
510 if (reel_picture_asset->hash()) {
511 reel_picture_asset->asset_ref()->set_hash (reel_picture_asset->hash().get());
514 shared_ptr<dcp::ReelSoundAsset> reel_sound_asset;
517 /* We have made a sound asset of our own. Put it into the reel */
518 reel_sound_asset.reset (new dcp::ReelSoundAsset (_sound_asset, 0));
520 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
521 /* We don't have a sound asset of our own; hopefully we have one to reference */
522 BOOST_FOREACH (ReferencedReelAsset j, refs) {
523 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
525 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
527 if (k && j.period == _period) {
528 reel_sound_asset = k;
529 /* If we have a hash for this asset in the CPL, assume that it is correct */
531 k->asset_ref()->set_hash (k->hash().get());
537 DCPOMATIC_ASSERT (reel_sound_asset);
538 if (reel_sound_asset->actual_duration() != period_duration) {
540 "Reel sound asset has length %1 but reel period is %2",
541 reel_sound_asset->actual_duration(),
544 if (reel_sound_asset->actual_duration() != period_duration) {
545 throw ProgrammingError (
547 String::compose ("%1 vs %2", reel_sound_asset->actual_duration(), period_duration)
552 reel->add (reel_sound_asset);
554 maybe_add_text<dcp::ReelSubtitleAsset> (_subtitle_asset, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period);
555 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
556 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
557 i->second, reel_picture_asset->actual_duration(), reel, refs, fonts, _film, _period
559 a->set_annotation_text (i->first.name);
560 a->set_language (i->first.language);
563 map<dcp::Marker, DCPTime> markers = _film->markers ();
564 map<dcp::Marker, DCPTime> reel_markers;
565 for (map<dcp::Marker, DCPTime>::const_iterator i = markers.begin(); i != markers.end(); ++i) {
566 if (_period.contains(i->second)) {
567 reel_markers[i->first] = i->second;
571 if (!reel_markers.empty ()) {
572 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(_film->video_frame_rate(), 1), 0));
573 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
575 DCPTime relative = i->second - _period.from;
576 relative.split (_film->video_frame_rate(), h, m, s, f);
577 ma->set (i->first, dcp::Time(h, m, s, f, _film->video_frame_rate()));
586 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
588 if (_picture_asset) {
589 _picture_asset->hash (set_progress);
593 _sound_asset->hash (set_progress);
598 ReelWriter::start () const
600 return _period.from.frames_floor (_film->video_frame_rate());
605 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
607 if (!_sound_asset_writer) {
611 DCPOMATIC_ASSERT (audio);
612 _sound_asset_writer->write (audio->data(), audio->frames());
616 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
618 shared_ptr<dcp::SubtitleAsset> asset;
621 case TEXT_OPEN_SUBTITLE:
622 asset = _subtitle_asset;
624 case TEXT_CLOSED_CAPTION:
625 DCPOMATIC_ASSERT (track);
626 asset = _closed_caption_assets[*track];
629 DCPOMATIC_ASSERT (false);
633 string lang = _film->subtitle_language ();
634 if (_film->interop ()) {
635 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
636 s->set_movie_title (_film->name ());
637 if (type == TEXT_OPEN_SUBTITLE) {
638 s->set_language (lang.empty() ? "Unknown" : lang);
640 s->set_language (track->language);
642 s->set_reel_number (raw_convert<string> (_reel_index + 1));
645 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
646 s->set_content_title_text (_film->name ());
647 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
648 s->set_language (lang);
650 s->set_language (track->language);
652 s->set_edit_rate (dcp::Fraction (_film->video_frame_rate (), 1));
653 s->set_reel_number (_reel_index + 1);
654 s->set_time_code_rate (_film->video_frame_rate ());
655 s->set_start_time (dcp::Time ());
656 if (_film->encrypted ()) {
657 s->set_key (_film->key ());
664 case TEXT_OPEN_SUBTITLE:
665 _subtitle_asset = asset;
667 case TEXT_CLOSED_CAPTION:
668 DCPOMATIC_ASSERT (track);
669 _closed_caption_assets[*track] = asset;
672 DCPOMATIC_ASSERT (false);
675 BOOST_FOREACH (StringText i, subs.string) {
676 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
677 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
678 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
679 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
682 BOOST_FOREACH (BitmapText i, subs.bitmap) {
684 shared_ptr<dcp::Subtitle>(
685 new dcp::SubtitleImage(
687 dcp::Time(period.from.seconds() - _period.from.seconds(), _film->video_frame_rate()),
688 dcp::Time(period.to.seconds() - _period.from.seconds(), _film->video_frame_rate()),
689 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
690 dcp::Time(), dcp::Time()
698 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
700 LOG_GENERAL ("Checking existing picture frame %1", frame);
702 /* Read the data from the info file; for 3D we just check the left
703 frames until we find a good one.
705 dcp::FrameInfo const info = read_frame_info (info_file, frame, _film->three_d () ? EYES_LEFT : EYES_BOTH);
709 /* Read the data from the asset and hash it */
710 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
711 Data data (info.size);
712 size_t const read = fread (data.data().get(), 1, data.size(), asset_file);
713 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
714 if (read != static_cast<size_t> (data.size ())) {
715 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
719 digester.add (data.data().get(), data.size());
720 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
721 if (digester.get() != info.hash) {
722 LOG_GENERAL ("Existing frame %1 failed hash check", frame);