2 Copyright (C) 2012-2021 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/>.
22 #include "audio_buffers.h"
23 #include "audio_mapping.h"
24 #include "compose.hpp"
26 #include "constants.h"
28 #include "dcp_content_type.h"
29 #include "dcp_video.h"
30 #include "dcpomatic_log.h"
32 #include "film_util.h"
36 #include "reel_writer.h"
37 #include "text_content.h"
42 #include <dcp/locale_convert.h>
43 #include <dcp/raw_convert.h>
44 #include <dcp/reel_closed_caption_asset.h>
45 #include <dcp/reel_file_asset.h>
46 #include <dcp/reel_subtitle_asset.h>
54 /* OS X strikes again */
59 using std::dynamic_pointer_cast;
60 using std::make_shared;
63 using std::shared_ptr;
68 using boost::optional;
69 #if BOOST_VERSION >= 106100
70 using namespace boost::placeholders;
74 using namespace dcpomatic;
77 /** @param weak_job Job to report progress to, or 0.
78 * @param text_only true to enable only the text (subtitle/ccap) parts of the writer.
80 Writer::Writer(weak_ptr<const Film> weak_film, weak_ptr<Job> weak_job, bool text_only)
81 : WeakConstFilm (weak_film)
83 /* These will be reset to sensible values when J2KEncoder is created */
84 , _maximum_frames_in_memory (8)
85 , _maximum_queue_size (8)
86 , _text_only (text_only)
88 auto job = _job.lock ();
91 auto const reels = film()->reels();
93 _reels.push_back (ReelWriter(weak_film, p, job, reel_index++, reels.size(), text_only));
96 _last_written.resize (reels.size());
98 /* We can keep track of the current audio, subtitle and closed caption reels easily because audio
99 and captions arrive to the Writer in sequence. This is not so for video.
101 _audio_reel = _reels.begin ();
102 _subtitle_reel = _reels.begin ();
103 for (auto i: film()->closed_caption_tracks()) {
104 _caption_reels[i] = _reels.begin ();
106 _atmos_reel = _reels.begin ();
108 /* Check that the signer is OK */
110 if (!Config::instance()->signer_chain()->valid(&reason)) {
111 throw InvalidSignerError (reason);
120 _thread = boost::thread (boost::bind(&Writer::thread, this));
121 #ifdef DCPOMATIC_LINUX
122 pthread_setname_np (_thread.native_handle(), "writer");
131 terminate_thread (false);
136 /** Pass a video frame to the writer for writing to disk at some point.
137 * This method can be called with frames out of order.
138 * @param encoded JPEG2000-encoded data.
139 * @param frame Frame index within the DCP.
140 * @param eyes Eyes that this frame image is for.
143 Writer::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
145 boost::mutex::scoped_lock lock (_state_mutex);
147 while (_queued_full_in_memory > _maximum_frames_in_memory) {
148 /* There are too many full frames in memory; wake the main writer thread and
149 wait until it sorts everything out */
150 _empty_condition.notify_all ();
151 _full_condition.wait (lock);
155 qi.type = QueueItem::Type::FULL;
156 qi.encoded = encoded;
157 qi.reel = video_reel (frame);
158 qi.frame = frame - _reels[qi.reel].start ();
160 DCPOMATIC_ASSERT((film()->three_d() && eyes != Eyes::BOTH) || (!film()->three_d() && eyes == Eyes::BOTH));
163 _queue.push_back(qi);
164 ++_queued_full_in_memory;
166 /* Now there's something to do: wake anything wait()ing on _empty_condition */
167 _empty_condition.notify_all ();
172 Writer::can_repeat (Frame frame) const
174 return frame > _reels[video_reel(frame)].start();
178 /** Repeat the last frame that was written to a reel as a new frame.
179 * @param frame Frame index within the DCP of the new (repeated) frame.
180 * @param eyes Eyes that this repeated frame image is for.
183 Writer::repeat (Frame frame, Eyes eyes)
185 boost::mutex::scoped_lock lock (_state_mutex);
187 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
188 /* The queue is too big, and the main writer thread can run and fix it, so
189 wake it and wait until it has done.
191 _empty_condition.notify_all ();
192 _full_condition.wait (lock);
196 qi.type = QueueItem::Type::REPEAT;
197 qi.reel = video_reel (frame);
198 qi.frame = frame - _reels[qi.reel].start ();
199 if (film()->three_d() && eyes == Eyes::BOTH) {
200 qi.eyes = Eyes::LEFT;
201 _queue.push_back (qi);
202 qi.eyes = Eyes::RIGHT;
203 _queue.push_back (qi);
206 _queue.push_back (qi);
209 /* Now there's something to do: wake anything wait()ing on _empty_condition */
210 _empty_condition.notify_all ();
215 Writer::fake_write (Frame frame, Eyes eyes)
217 boost::mutex::scoped_lock lock (_state_mutex);
219 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
220 /* The queue is too big, and the main writer thread can run and fix it, so
221 wake it and wait until it has done.
223 _empty_condition.notify_all ();
224 _full_condition.wait (lock);
227 size_t const reel = video_reel (frame);
228 Frame const frame_in_reel = frame - _reels[reel].start ();
231 qi.type = QueueItem::Type::FAKE;
234 shared_ptr<InfoFileHandle> info_file = film()->info_file_handle(_reels[reel].period(), true);
235 qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
238 DCPOMATIC_ASSERT((film()->three_d() && eyes != Eyes::BOTH) || (!film()->three_d() && eyes == Eyes::BOTH));
241 qi.frame = frame_in_reel;
243 _queue.push_back(qi);
245 /* Now there's something to do: wake anything wait()ing on _empty_condition */
246 _empty_condition.notify_all ();
250 /** Write some audio frames to the DCP.
251 * @param audio Audio data.
252 * @param time Time of this data within the DCP.
253 * This method is not thread safe.
256 Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
258 DCPOMATIC_ASSERT (audio);
260 int const afr = film()->audio_frame_rate();
262 DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
264 /* The audio we get might span a reel boundary, and if so we have to write it in bits */
269 if (_audio_reel == _reels.end ()) {
270 /* This audio is off the end of the last reel; ignore it */
274 if (end <= _audio_reel->period().to) {
275 /* Easy case: we can write all the audio to this reel */
276 _audio_reel->write (audio);
278 } else if (_audio_reel->period().to <= t) {
279 /* This reel is entirely before the start of our audio; just skip the reel */
282 /* This audio is over a reel boundary; split the audio into two and write the first part */
283 DCPTime part_lengths[2] = {
284 _audio_reel->period().to - t,
285 end - _audio_reel->period().to
288 /* Be careful that part_lengths[0] + part_lengths[1] can't be bigger than audio->frames() */
289 Frame part_frames[2] = {
290 part_lengths[0].frames_ceil(afr),
291 part_lengths[1].frames_floor(afr)
294 DCPOMATIC_ASSERT ((part_frames[0] + part_frames[1]) <= audio->frames());
296 if (part_frames[0]) {
297 auto part = make_shared<AudioBuffers>(audio, part_frames[0], 0);
298 _audio_reel->write (part);
301 if (part_frames[1]) {
302 audio = make_shared<AudioBuffers>(audio, part_frames[1], part_frames[0]);
308 t += part_lengths[0];
315 Writer::write (shared_ptr<const dcp::AtmosFrame> atmos, DCPTime time, AtmosMetadata metadata)
317 if (_atmos_reel->period().to == time) {
319 DCPOMATIC_ASSERT (_atmos_reel != _reels.end());
322 /* We assume that we get a video frame's worth of data here */
323 _atmos_reel->write (atmos, metadata);
327 /** Caller must hold a lock on _state_mutex */
329 Writer::have_sequenced_image_at_queue_head ()
331 if (_queue.empty ()) {
336 auto const & f = _queue.front();
337 return _last_written[f.reel].next(f);
342 Writer::LastWritten::next (QueueItem qi) const
344 if (qi.eyes == Eyes::BOTH) {
346 return qi.frame == (_frame + 1);
351 if (_eyes == Eyes::LEFT && qi.frame == _frame && qi.eyes == Eyes::RIGHT) {
355 if (_eyes == Eyes::RIGHT && qi.frame == (_frame + 1) && qi.eyes == Eyes::LEFT) {
364 Writer::LastWritten::update (QueueItem qi)
375 start_of_thread ("Writer");
379 boost::mutex::scoped_lock lock (_state_mutex);
383 if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
384 /* We've got something to do: go and do it */
388 /* Nothing to do: wait until something happens which may indicate that we do */
389 LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
390 _empty_condition.wait (lock);
391 LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
394 /* We stop here if we have been asked to finish, and if either the queue
395 is empty or we do not have a sequenced image at its head (if this is the
396 case we will never terminate as no new frames will be sent once
399 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
400 /* (Hopefully temporarily) log anything that was not written */
401 if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
402 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
403 for (auto const& i: _queue) {
404 if (i.type == QueueItem::Type::FULL) {
405 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
407 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
414 /* Write any frames that we can write; i.e. those that are in sequence. */
415 while (have_sequenced_image_at_queue_head ()) {
416 auto qi = _queue.front ();
417 _last_written[qi.reel].update (qi);
419 if (qi.type == QueueItem::Type::FULL && qi.encoded) {
420 --_queued_full_in_memory;
425 auto& reel = _reels[qi.reel];
428 case QueueItem::Type::FULL:
429 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
431 qi.encoded.reset (new ArrayData(film()->j2c_path(qi.reel, qi.frame, qi.eyes, false)));
433 reel.write (qi.encoded, qi.frame, qi.eyes);
436 case QueueItem::Type::FAKE:
437 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
438 reel.fake_write (qi.size);
441 case QueueItem::Type::REPEAT:
442 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
443 reel.repeat_write (qi.frame, qi.eyes);
449 _full_condition.notify_all ();
452 while (_queued_full_in_memory > _maximum_frames_in_memory) {
453 /* Too many frames in memory which can't yet be written to the stream.
454 Write some FULL frames to disk.
457 /* Find one from the back of the queue */
459 auto item = _queue.rbegin();
460 while (item != _queue.rend() && (item->type != QueueItem::Type::FULL || !item->encoded)) {
464 DCPOMATIC_ASSERT(item != _queue.rend());
466 /* For the log message below */
467 int const awaiting = _last_written[_queue.front().reel].frame() + 1;
470 /* i is valid here, even though we don't hold a lock on the mutex,
471 since list iterators are unaffected by insertion and only this
472 thread could erase the last item in the list.
475 LOG_GENERAL("Writer full; pushes %1 to disk while awaiting %2", item->frame, awaiting);
477 item->encoded->write_via_temp(
478 film()->j2c_path(item->reel, item->frame, item->eyes, true),
479 film()->j2c_path(item->reel, item->frame, item->eyes, false)
483 item->encoded.reset();
484 --_queued_full_in_memory;
485 _full_condition.notify_all ();
496 Writer::terminate_thread (bool can_throw)
498 boost::this_thread::disable_interruption dis;
500 boost::mutex::scoped_lock lock (_state_mutex);
503 _empty_condition.notify_all ();
504 _full_condition.notify_all ();
518 Writer::calculate_digests ()
520 auto job = _job.lock ();
522 job->sub (_("Computing digests"));
525 boost::asio::io_service service;
526 boost::thread_group pool;
528 auto work = make_shared<boost::asio::io_service::work>(service);
530 int const threads = max (1, Config::instance()->master_encoding_threads());
532 for (int i = 0; i < threads; ++i) {
533 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
536 std::function<void (int, int64_t, int64_t)> set_progress;
538 set_progress = boost::bind(&Writer::set_digest_progress, this, job.get(), _1, _2, _3);
540 set_progress = [](int, int64_t, int64_t) {
541 boost::this_thread::interruption_point();
547 for (auto& i: _reels) {
550 &ReelWriter::calculate_digests,
552 std::function<void (int64_t, int64_t)>(boost::bind(set_progress, index, _1, _2))
558 &Writer::calculate_referenced_digests,
560 std::function<void (int64_t, int64_t)>(boost::bind(set_progress, index, _1, _2))
567 } catch (boost::thread_interrupted) {
568 /* join_all was interrupted, so we need to interrupt the threads
569 * in our pool then try again to join them.
571 pool.interrupt_all ();
579 /** @param output_dcp Path to DCP folder to write */
581 Writer::finish (boost::filesystem::path output_dcp)
583 if (_thread.joinable()) {
584 LOG_GENERAL_NC ("Terminating writer thread");
585 terminate_thread (true);
588 LOG_GENERAL_NC ("Finishing ReelWriters");
590 for (auto& reel: _reels) {
591 write_hanging_text(reel);
592 reel.finish(output_dcp);
595 LOG_GENERAL_NC ("Writing XML");
597 dcp::DCP dcp (output_dcp);
599 auto cpl = make_shared<dcp::CPL>(
601 film()->dcp_content_type()->libdcp_kind(),
602 film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE
607 calculate_digests ();
611 for (auto& i: _reels) {
612 cpl->add(i.create_reel(_reel_assets, output_dcp, _have_subtitles, _have_closed_captions));
617 auto creator = Config::instance()->dcp_creator();
618 if (creator.empty()) {
619 creator = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
622 auto issuer = Config::instance()->dcp_issuer();
623 if (issuer.empty()) {
624 issuer = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
627 cpl->set_creator (creator);
628 cpl->set_issuer (issuer);
630 cpl->set_ratings (film()->ratings());
632 vector<dcp::ContentVersion> cv;
633 for (auto i: film()->content_versions()) {
634 /* Make sure we don't end up writing an empty <LabelText> node as some validators
635 * complain about that.
637 cv.push_back(!i.empty() ? dcp::ContentVersion(i) : dcp::ContentVersion("1"));
640 cv = { dcp::ContentVersion("1") };
642 cpl->set_content_versions (cv);
644 cpl->set_full_content_title_text (film()->name());
645 cpl->set_full_content_title_text_language (film()->name_language());
646 if (film()->release_territory()) {
647 cpl->set_release_territory (*film()->release_territory());
649 cpl->set_version_number (film()->version_number());
650 cpl->set_status (film()->status());
651 if (film()->chain()) {
652 cpl->set_chain (*film()->chain());
654 if (film()->distributor()) {
655 cpl->set_distributor (*film()->distributor());
657 if (film()->facility()) {
658 cpl->set_facility (*film()->facility());
660 if (film()->luminance()) {
661 cpl->set_luminance (*film()->luminance());
663 if (film()->sign_language_video_language()) {
664 cpl->set_sign_language_video_language (*film()->sign_language_video_language());
667 dcp::MCASoundField field;
668 if (channel_is_mapped(film(), dcp::Channel::BSL) || channel_is_mapped(film(), dcp::Channel::BSR)) {
669 field = dcp::MCASoundField::SEVEN_POINT_ONE;
671 field = dcp::MCASoundField::FIVE_POINT_ONE;
674 auto const audio_channels = film()->audio_channels();
675 dcp::MainSoundConfiguration msc(field, audio_channels);
676 for (auto i: film()->mapped_audio_channels()) {
677 if (i < audio_channels) {
678 msc.set_mapping(i, static_cast<dcp::Channel>(i));
682 cpl->set_main_sound_configuration(msc);
683 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
684 cpl->set_main_picture_stored_area (film()->frame_size());
686 auto active_area = film()->active_area();
687 if (active_area.width > 0 && active_area.height > 0) {
688 /* It's not allowed to have a zero active area width or height, and the sizes must be multiples of 2 */
689 cpl->set_main_picture_active_area({ active_area.width & ~1, active_area.height & ~1});
692 auto sl = film()->subtitle_languages().second;
694 cpl->set_additional_subtitle_languages(sl);
697 auto signer = Config::instance()->signer_chain();
698 /* We did check earlier, but check again here to be on the safe side */
700 if (!signer->valid (&reason)) {
701 throw InvalidSignerError (reason);
704 dcp.set_issuer(issuer);
705 dcp.set_creator(creator);
706 dcp.set_annotation_text(film()->dcp_name());
708 dcp.write_xml(signer, !film()->limit_to_smpte_bv20(), Config::instance()->dcp_metadata_filename_format());
711 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
714 write_cover_sheet (output_dcp);
719 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
721 auto const cover = film()->file("COVER_SHEET.txt");
722 dcp::File file(cover, "w");
724 throw OpenFileError (cover, errno, OpenFileError::WRITE);
727 auto text = Config::instance()->cover_sheet ();
728 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
729 auto cpls = film()->cpls();
731 boost::algorithm::replace_all (text, "$CPL_FILENAME", cpls[0].cpl_file.filename().string());
733 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
734 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
736 auto audio_language = film()->audio_language();
737 if (audio_language) {
738 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_language->description());
740 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
743 auto subtitle_languages = film()->subtitle_languages();
744 if (subtitle_languages.first) {
745 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
747 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
750 boost::uintmax_t size = 0;
752 auto i = dcp::filesystem::recursive_directory_iterator(output_dcp);
753 i != dcp::filesystem::recursive_directory_iterator();
755 if (dcp::filesystem::is_regular_file(i->path())) {
756 size += dcp::filesystem::file_size(i->path());
760 if (size > (1000000000L)) {
761 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
763 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
766 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
767 auto description = String::compose("%1.%2", ch.first, ch.second);
769 if (description == "0.0") {
770 description = _("None");
771 } else if (description == "1.0") {
772 description = _("Mono");
773 } else if (description == "2.0") {
774 description = _("Stereo");
776 boost::algorithm::replace_all (text, "$AUDIO", description);
778 auto const hmsf = film()->length().split(film()->video_frame_rate());
780 if (hmsf.h == 0 && hmsf.m == 0) {
781 length = String::compose("%1s", hmsf.s);
782 } else if (hmsf.h == 0 && hmsf.m > 0) {
783 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
784 } else if (hmsf.h > 0 && hmsf.m > 0) {
785 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
788 boost::algorithm::replace_all (text, "$LENGTH", length);
790 file.checked_write(text.c_str(), text.length());
794 /** @param frame Frame index within the whole DCP.
795 * @return true if we can fake-write this frame.
798 Writer::can_fake_write (Frame frame) const
800 if (film()->encrypted()) {
801 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
805 /* We have to do a proper write of the first frame so that we can set up the JPEG2000
806 parameters in the asset writer.
809 auto const & reel = _reels[video_reel(frame)];
811 /* Make frame relative to the start of the reel */
812 frame -= reel.start ();
813 return (frame != 0 && frame < reel.first_nonexistent_frame());
817 /** @param track Closed caption track if type == TextType::CLOSED_CAPTION */
819 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
821 vector<ReelWriter>::iterator* reel = nullptr;
824 case TextType::OPEN_SUBTITLE:
825 reel = &_subtitle_reel;
826 _have_subtitles = true;
828 case TextType::CLOSED_CAPTION:
829 DCPOMATIC_ASSERT (track);
830 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
831 reel = &_caption_reels[*track];
832 _have_closed_captions.insert (*track);
835 DCPOMATIC_ASSERT (false);
838 DCPOMATIC_ASSERT (*reel != _reels.end());
839 while ((*reel)->period().to <= period.from) {
841 DCPOMATIC_ASSERT (*reel != _reels.end());
842 write_hanging_text (**reel);
845 auto back_off = [this](DCPTimePeriod period) {
846 period.to -= DCPTime::from_frames(2, film()->video_frame_rate());
850 if (period.to > (*reel)->period().to) {
851 /* This text goes off the end of the reel. Store parts of it that should go into
854 for (auto i = std::next(*reel); i != _reels.end(); ++i) {
855 auto overlap = i->period().overlap(period);
857 _hanging_texts.push_back (HangingText{text, type, track, back_off(*overlap)});
860 /* Back off from the reel boundary by a couple of frames to avoid tripping checks
861 * for subtitles being too close together.
863 period.to = (*reel)->period().to;
864 period = back_off(period);
867 (*reel)->write(text, type, track, period, _fonts, _chosen_interop_font);
872 Writer::write (vector<shared_ptr<Font>> fonts)
878 /* Fonts may come in with empty IDs but we don't want to put those in the DCP */
879 auto fix_id = [](string id) {
880 return id.empty() ? "font" : id;
883 if (film()->interop()) {
884 /* Interop will ignore second and subsequent <LoadFont>s so we don't want to
885 * even write them as they upset some validators. Set up _fonts so that every
886 * font used by any subtitle will be written with the same ID.
888 for (size_t i = 0; i < fonts.size(); ++i) {
889 _fonts.put(fonts[i], fix_id(fonts[0]->id()));
891 _chosen_interop_font = fonts[0];
893 for (auto font: fonts) {
894 _fonts.put(font, fix_id(font->id()));
901 operator< (QueueItem const & a, QueueItem const & b)
903 if (a.reel != b.reel) {
904 return a.reel < b.reel;
907 if (a.frame != b.frame) {
908 return a.frame < b.frame;
911 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
916 operator== (QueueItem const & a, QueueItem const & b)
918 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
923 Writer::set_encoder_threads (int threads)
925 boost::mutex::scoped_lock lm (_state_mutex);
926 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
927 _maximum_queue_size = threads * 16;
932 Writer::write (ReferencedReelAsset asset)
934 _reel_assets.push_back (asset);
936 if (dynamic_pointer_cast<dcp::ReelSubtitleAsset>(asset.asset)) {
937 _have_subtitles = true;
938 } else if (auto ccap = dynamic_pointer_cast<dcp::ReelClosedCaptionAsset>(asset.asset)) {
939 /* This feels quite fragile. We have a referenced reel and want to know if it's
940 * part of a given closed-caption track so that we can fill if it has any
941 * missing reels. I guess for that purpose almost any DCPTextTrack values are
942 * fine so long as they are consistent.
945 track.name = ccap->annotation_text().get_value_or("");
946 track.language = dcp::LanguageTag(ccap->language().get_value_or("en-US"));
947 if (_have_closed_captions.find(track) == _have_closed_captions.end()) {
948 _have_closed_captions.insert(track);
955 Writer::video_reel (int frame) const
957 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
958 size_t reel_index = 0;
959 while (reel_index < _reels.size() && !_reels[reel_index].period().contains(t)) {
963 DCPOMATIC_ASSERT(reel_index < _reels.size ());
968 /** Update job progress with information about the progress of a single digest calculation
970 * @param id Unique identifier for the thread whose progress has changed.
971 * @param done Number of bytes that this thread has processed.
972 * @param size Total number of bytes that this thread must process.
975 Writer::set_digest_progress(Job* job, int id, int64_t done, int64_t size)
977 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
979 /* Update the progress for this thread */
980 _digest_progresses[id] = std::make_pair(done, size);
982 /* Get the total progress across all threads and use it to set job progress */
983 int64_t total_done = 0;
984 int64_t total_size = 0;
985 for (auto const& i: _digest_progresses) {
986 total_done += i.second.first;
987 total_size += i.second.second;
990 job->set_progress(float(total_done) / total_size);
995 boost::this_thread::interruption_point();
999 /** Calculate hashes for any referenced MXF assets which do not already have one */
1001 Writer::calculate_referenced_digests(std::function<void (int64_t, int64_t)> set_progress)
1004 int64_t total_size = 0;
1005 for (auto const& i: _reel_assets) {
1006 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
1007 if (file && !file->hash()) {
1008 auto filename = file->asset_ref().asset()->file();
1009 DCPOMATIC_ASSERT(filename);
1010 total_size += boost::filesystem::file_size(*filename);
1014 int64_t total_done = 0;
1015 for (auto const& i: _reel_assets) {
1016 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
1017 if (file && !file->hash()) {
1018 file->asset_ref().asset()->hash([&total_done, total_size, set_progress](int64_t done, int64_t) {
1019 set_progress(total_done + done, total_size);
1021 total_done += boost::filesystem::file_size(*file->asset_ref().asset()->file());
1022 file->set_hash (file->asset_ref().asset()->hash());
1025 } catch (boost::thread_interrupted) {
1026 /* set_progress contains an interruption_point, so any of these methods
1027 * may throw thread_interrupted, at which point we just give up.
1033 Writer::write_hanging_text (ReelWriter& reel)
1035 vector<HangingText> new_hanging_texts;
1036 for (auto i: _hanging_texts) {
1037 if (i.period.from == reel.period().from) {
1038 reel.write(i.text, i.type, i.track, i.period, _fonts, _chosen_interop_font);
1040 new_hanging_texts.push_back (i);
1043 _hanging_texts = new_hanging_texts;