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_file_asset.h>
52 /* OS X strikes again */
57 using std::dynamic_pointer_cast;
58 using std::make_shared;
61 using std::shared_ptr;
66 using boost::optional;
67 #if BOOST_VERSION >= 106100
68 using namespace boost::placeholders;
72 using namespace dcpomatic;
75 /** @param j Job to report progress to, or 0.
76 * @param text_only true to enable only the text (subtitle/ccap) parts of the writer.
78 Writer::Writer (weak_ptr<const Film> weak_film, weak_ptr<Job> j, bool text_only)
79 : WeakConstFilm (weak_film)
81 /* These will be reset to sensible values when J2KEncoder is created */
82 , _maximum_frames_in_memory (8)
83 , _maximum_queue_size (8)
84 , _text_only (text_only)
86 auto job = _job.lock ();
89 auto const reels = film()->reels();
91 _reels.push_back (ReelWriter(weak_film, p, job, reel_index++, reels.size(), text_only));
94 _last_written.resize (reels.size());
96 /* We can keep track of the current audio, subtitle and closed caption reels easily because audio
97 and captions arrive to the Writer in sequence. This is not so for video.
99 _audio_reel = _reels.begin ();
100 _subtitle_reel = _reels.begin ();
101 for (auto i: film()->closed_caption_tracks()) {
102 _caption_reels[i] = _reels.begin ();
104 _atmos_reel = _reels.begin ();
106 /* Check that the signer is OK */
108 if (!Config::instance()->signer_chain()->valid(&reason)) {
109 throw InvalidSignerError (reason);
118 _thread = boost::thread (boost::bind(&Writer::thread, this));
119 #ifdef DCPOMATIC_LINUX
120 pthread_setname_np (_thread.native_handle(), "writer");
129 terminate_thread (false);
134 /** Pass a video frame to the writer for writing to disk at some point.
135 * This method can be called with frames out of order.
136 * @param encoded JPEG2000-encoded data.
137 * @param frame Frame index within the DCP.
138 * @param eyes Eyes that this frame image is for.
141 Writer::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
143 boost::mutex::scoped_lock lock (_state_mutex);
145 while (_queued_full_in_memory > _maximum_frames_in_memory) {
146 /* There are too many full frames in memory; wake the main writer thread and
147 wait until it sorts everything out */
148 _empty_condition.notify_all ();
149 _full_condition.wait (lock);
153 qi.type = QueueItem::Type::FULL;
154 qi.encoded = encoded;
155 qi.reel = video_reel (frame);
156 qi.frame = frame - _reels[qi.reel].start ();
158 DCPOMATIC_ASSERT((film()->three_d() && eyes != Eyes::BOTH) || (!film()->three_d() && eyes == Eyes::BOTH));
161 _queue.push_back(qi);
162 ++_queued_full_in_memory;
164 /* Now there's something to do: wake anything wait()ing on _empty_condition */
165 _empty_condition.notify_all ();
170 Writer::can_repeat (Frame frame) const
172 return frame > _reels[video_reel(frame)].start();
176 /** Repeat the last frame that was written to a reel as a new frame.
177 * @param frame Frame index within the DCP of the new (repeated) frame.
178 * @param eyes Eyes that this repeated frame image is for.
181 Writer::repeat (Frame frame, Eyes eyes)
183 boost::mutex::scoped_lock lock (_state_mutex);
185 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
186 /* The queue is too big, and the main writer thread can run and fix it, so
187 wake it and wait until it has done.
189 _empty_condition.notify_all ();
190 _full_condition.wait (lock);
194 qi.type = QueueItem::Type::REPEAT;
195 qi.reel = video_reel (frame);
196 qi.frame = frame - _reels[qi.reel].start ();
197 if (film()->three_d() && eyes == Eyes::BOTH) {
198 qi.eyes = Eyes::LEFT;
199 _queue.push_back (qi);
200 qi.eyes = Eyes::RIGHT;
201 _queue.push_back (qi);
204 _queue.push_back (qi);
207 /* Now there's something to do: wake anything wait()ing on _empty_condition */
208 _empty_condition.notify_all ();
213 Writer::fake_write (Frame frame, Eyes eyes)
215 boost::mutex::scoped_lock lock (_state_mutex);
217 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
218 /* The queue is too big, and the main writer thread can run and fix it, so
219 wake it and wait until it has done.
221 _empty_condition.notify_all ();
222 _full_condition.wait (lock);
225 size_t const reel = video_reel (frame);
226 Frame const frame_in_reel = frame - _reels[reel].start ();
229 qi.type = QueueItem::Type::FAKE;
232 shared_ptr<InfoFileHandle> info_file = film()->info_file_handle(_reels[reel].period(), true);
233 qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
236 DCPOMATIC_ASSERT((film()->three_d() && eyes != Eyes::BOTH) || (!film()->three_d() && eyes == Eyes::BOTH));
239 qi.frame = frame_in_reel;
241 _queue.push_back(qi);
243 /* Now there's something to do: wake anything wait()ing on _empty_condition */
244 _empty_condition.notify_all ();
248 /** Write some audio frames to the DCP.
249 * @param audio Audio data.
250 * @param time Time of this data within the DCP.
251 * This method is not thread safe.
254 Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
256 DCPOMATIC_ASSERT (audio);
258 int const afr = film()->audio_frame_rate();
260 DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
262 /* The audio we get might span a reel boundary, and if so we have to write it in bits */
267 if (_audio_reel == _reels.end ()) {
268 /* This audio is off the end of the last reel; ignore it */
272 if (end <= _audio_reel->period().to) {
273 /* Easy case: we can write all the audio to this reel */
274 _audio_reel->write (audio);
276 } else if (_audio_reel->period().to <= t) {
277 /* This reel is entirely before the start of our audio; just skip the reel */
280 /* This audio is over a reel boundary; split the audio into two and write the first part */
281 DCPTime part_lengths[2] = {
282 _audio_reel->period().to - t,
283 end - _audio_reel->period().to
286 /* Be careful that part_lengths[0] + part_lengths[1] can't be bigger than audio->frames() */
287 Frame part_frames[2] = {
288 part_lengths[0].frames_ceil(afr),
289 part_lengths[1].frames_floor(afr)
292 DCPOMATIC_ASSERT ((part_frames[0] + part_frames[1]) <= audio->frames());
294 if (part_frames[0]) {
295 auto part = make_shared<AudioBuffers>(audio, part_frames[0], 0);
296 _audio_reel->write (part);
299 if (part_frames[1]) {
300 audio = make_shared<AudioBuffers>(audio, part_frames[1], part_frames[0]);
306 t += part_lengths[0];
313 Writer::write (shared_ptr<const dcp::AtmosFrame> atmos, DCPTime time, AtmosMetadata metadata)
315 if (_atmos_reel->period().to == time) {
317 DCPOMATIC_ASSERT (_atmos_reel != _reels.end());
320 /* We assume that we get a video frame's worth of data here */
321 _atmos_reel->write (atmos, metadata);
325 /** Caller must hold a lock on _state_mutex */
327 Writer::have_sequenced_image_at_queue_head ()
329 if (_queue.empty ()) {
334 auto const & f = _queue.front();
335 return _last_written[f.reel].next(f);
340 Writer::LastWritten::next (QueueItem qi) const
342 if (qi.eyes == Eyes::BOTH) {
344 return qi.frame == (_frame + 1);
349 if (_eyes == Eyes::LEFT && qi.frame == _frame && qi.eyes == Eyes::RIGHT) {
353 if (_eyes == Eyes::RIGHT && qi.frame == (_frame + 1) && qi.eyes == Eyes::LEFT) {
362 Writer::LastWritten::update (QueueItem qi)
373 start_of_thread ("Writer");
377 boost::mutex::scoped_lock lock (_state_mutex);
381 if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
382 /* We've got something to do: go and do it */
386 /* Nothing to do: wait until something happens which may indicate that we do */
387 LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
388 _empty_condition.wait (lock);
389 LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
392 /* We stop here if we have been asked to finish, and if either the queue
393 is empty or we do not have a sequenced image at its head (if this is the
394 case we will never terminate as no new frames will be sent once
397 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
398 /* (Hopefully temporarily) log anything that was not written */
399 if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
400 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
401 for (auto const& i: _queue) {
402 if (i.type == QueueItem::Type::FULL) {
403 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
405 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
412 /* Write any frames that we can write; i.e. those that are in sequence. */
413 while (have_sequenced_image_at_queue_head ()) {
414 auto qi = _queue.front ();
415 _last_written[qi.reel].update (qi);
417 if (qi.type == QueueItem::Type::FULL && qi.encoded) {
418 --_queued_full_in_memory;
423 auto& reel = _reels[qi.reel];
426 case QueueItem::Type::FULL:
427 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
429 qi.encoded.reset (new ArrayData(film()->j2c_path(qi.reel, qi.frame, qi.eyes, false)));
431 reel.write (qi.encoded, qi.frame, qi.eyes);
434 case QueueItem::Type::FAKE:
435 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
436 reel.fake_write (qi.size);
439 case QueueItem::Type::REPEAT:
440 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
441 reel.repeat_write (qi.frame, qi.eyes);
447 _full_condition.notify_all ();
450 while (_queued_full_in_memory > _maximum_frames_in_memory) {
451 /* Too many frames in memory which can't yet be written to the stream.
452 Write some FULL frames to disk.
455 /* Find one from the back of the queue */
457 auto i = _queue.rbegin ();
458 while (i != _queue.rend() && (i->type != QueueItem::Type::FULL || !i->encoded)) {
462 DCPOMATIC_ASSERT (i != _queue.rend());
464 /* For the log message below */
465 int const awaiting = _last_written[_queue.front().reel].frame() + 1;
468 /* i is valid here, even though we don't hold a lock on the mutex,
469 since list iterators are unaffected by insertion and only this
470 thread could erase the last item in the list.
473 LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
475 i->encoded->write_via_temp (
476 film()->j2c_path(i->reel, i->frame, i->eyes, true),
477 film()->j2c_path(i->reel, i->frame, i->eyes, false)
482 --_queued_full_in_memory;
483 _full_condition.notify_all ();
494 Writer::terminate_thread (bool can_throw)
496 boost::this_thread::disable_interruption dis;
498 boost::mutex::scoped_lock lock (_state_mutex);
501 _empty_condition.notify_all ();
502 _full_condition.notify_all ();
516 Writer::calculate_digests ()
518 auto job = _job.lock ();
520 job->sub (_("Computing digests"));
523 boost::asio::io_service service;
524 boost::thread_group pool;
526 auto work = make_shared<boost::asio::io_service::work>(service);
528 int const threads = max (1, Config::instance()->master_encoding_threads());
530 for (int i = 0; i < threads; ++i) {
531 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
534 std::function<void (float)> set_progress;
536 set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
538 set_progress = [](float) {
539 boost::this_thread::interruption_point();
543 for (auto& i: _reels) {
544 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
546 service.post (boost::bind (&Writer::calculate_referenced_digests, this, set_progress));
552 } catch (boost::thread_interrupted) {
553 /* join_all was interrupted, so we need to interrupt the threads
554 * in our pool then try again to join them.
556 pool.interrupt_all ();
564 /** @param output_dcp Path to DCP folder to write */
566 Writer::finish (boost::filesystem::path output_dcp)
568 if (_thread.joinable()) {
569 LOG_GENERAL_NC ("Terminating writer thread");
570 terminate_thread (true);
573 LOG_GENERAL_NC ("Finishing ReelWriters");
575 for (auto& i: _reels) {
576 write_hanging_text (i);
577 i.finish (output_dcp);
580 LOG_GENERAL_NC ("Writing XML");
582 dcp::DCP dcp (output_dcp);
584 auto cpl = make_shared<dcp::CPL>(
586 film()->dcp_content_type()->libdcp_kind(),
587 film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE
592 calculate_digests ();
596 for (auto& i: _reels) {
597 cpl->add(i.create_reel(_reel_assets, output_dcp, _have_subtitles, _have_closed_captions));
602 auto creator = Config::instance()->dcp_creator();
603 if (creator.empty()) {
604 creator = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
607 auto issuer = Config::instance()->dcp_issuer();
608 if (issuer.empty()) {
609 issuer = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
612 cpl->set_creator (creator);
613 cpl->set_issuer (issuer);
615 cpl->set_ratings (film()->ratings());
617 vector<dcp::ContentVersion> cv;
618 for (auto i: film()->content_versions()) {
619 cv.push_back (dcp::ContentVersion(i));
622 cv = { dcp::ContentVersion("1") };
624 cpl->set_content_versions (cv);
626 cpl->set_full_content_title_text (film()->name());
627 cpl->set_full_content_title_text_language (film()->name_language());
628 if (film()->release_territory()) {
629 cpl->set_release_territory (*film()->release_territory());
631 cpl->set_version_number (film()->version_number());
632 cpl->set_status (film()->status());
633 if (film()->chain()) {
634 cpl->set_chain (*film()->chain());
636 if (film()->distributor()) {
637 cpl->set_distributor (*film()->distributor());
639 if (film()->facility()) {
640 cpl->set_facility (*film()->facility());
642 if (film()->luminance()) {
643 cpl->set_luminance (*film()->luminance());
645 if (film()->sign_language_video_language()) {
646 cpl->set_sign_language_video_language (*film()->sign_language_video_language());
649 dcp::MCASoundField field;
650 if (channel_is_mapped(film(), dcp::Channel::BSL) || channel_is_mapped(film(), dcp::Channel::BSR)) {
651 field = dcp::MCASoundField::SEVEN_POINT_ONE;
653 field = dcp::MCASoundField::FIVE_POINT_ONE;
656 dcp::MainSoundConfiguration msc(field, MAX_DCP_AUDIO_CHANNELS);
657 for (auto i: film()->mapped_audio_channels()) {
658 msc.set_mapping(i, static_cast<dcp::Channel>(i));
661 cpl->set_main_sound_configuration(msc);
662 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
663 cpl->set_main_picture_stored_area (film()->frame_size());
665 auto active_area = film()->active_area();
666 if (active_area.width > 0 && active_area.height > 0) {
667 /* It's not allowed to have a zero active area width or height, and the sizes must be multiples of 2 */
668 cpl->set_main_picture_active_area({ active_area.width & ~1, active_area.height & ~1});
671 auto sl = film()->subtitle_languages().second;
673 cpl->set_additional_subtitle_languages(sl);
676 auto signer = Config::instance()->signer_chain();
677 /* We did check earlier, but check again here to be on the safe side */
679 if (!signer->valid (&reason)) {
680 throw InvalidSignerError (reason);
683 dcp.set_issuer(issuer);
684 dcp.set_creator(creator);
685 dcp.set_annotation_text(film()->dcp_name());
687 dcp.write_xml(signer, !film()->limit_to_smpte_bv20(), Config::instance()->dcp_metadata_filename_format());
690 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
693 write_cover_sheet (output_dcp);
698 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
700 auto const cover = film()->file("COVER_SHEET.txt");
701 dcp::File f(cover, "w");
703 throw OpenFileError (cover, errno, OpenFileError::WRITE);
706 auto text = Config::instance()->cover_sheet ();
707 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
708 auto cpls = film()->cpls();
710 boost::algorithm::replace_all (text, "$CPL_FILENAME", cpls[0].cpl_file.filename().string());
712 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
713 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
715 auto audio_language = film()->audio_language();
716 if (audio_language) {
717 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_language->description());
719 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
722 auto subtitle_languages = film()->subtitle_languages();
723 if (subtitle_languages.first) {
724 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
726 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
729 boost::uintmax_t size = 0;
731 auto i = boost::filesystem::recursive_directory_iterator(output_dcp);
732 i != boost::filesystem::recursive_directory_iterator();
734 if (boost::filesystem::is_regular_file (i->path())) {
735 size += boost::filesystem::file_size (i->path());
739 if (size > (1000000000L)) {
740 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
742 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
745 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
746 auto description = String::compose("%1.%2", ch.first, ch.second);
748 if (description == "0.0") {
749 description = _("None");
750 } else if (description == "1.0") {
751 description = _("Mono");
752 } else if (description == "2.0") {
753 description = _("Stereo");
755 boost::algorithm::replace_all (text, "$AUDIO", description);
757 auto const hmsf = film()->length().split(film()->video_frame_rate());
759 if (hmsf.h == 0 && hmsf.m == 0) {
760 length = String::compose("%1s", hmsf.s);
761 } else if (hmsf.h == 0 && hmsf.m > 0) {
762 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
763 } else if (hmsf.h > 0 && hmsf.m > 0) {
764 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
767 boost::algorithm::replace_all (text, "$LENGTH", length);
769 f.checked_write(text.c_str(), text.length());
773 /** @param frame Frame index within the whole DCP.
774 * @return true if we can fake-write this frame.
777 Writer::can_fake_write (Frame frame) const
779 if (film()->encrypted()) {
780 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
784 /* We have to do a proper write of the first frame so that we can set up the JPEG2000
785 parameters in the asset writer.
788 auto const & reel = _reels[video_reel(frame)];
790 /* Make frame relative to the start of the reel */
791 frame -= reel.start ();
792 return (frame != 0 && frame < reel.first_nonexistent_frame());
796 /** @param track Closed caption track if type == TextType::CLOSED_CAPTION */
798 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
800 vector<ReelWriter>::iterator* reel = nullptr;
803 case TextType::OPEN_SUBTITLE:
804 reel = &_subtitle_reel;
805 _have_subtitles = true;
807 case TextType::CLOSED_CAPTION:
808 DCPOMATIC_ASSERT (track);
809 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
810 reel = &_caption_reels[*track];
811 _have_closed_captions.insert (*track);
814 DCPOMATIC_ASSERT (false);
817 DCPOMATIC_ASSERT (*reel != _reels.end());
818 while ((*reel)->period().to <= period.from) {
820 DCPOMATIC_ASSERT (*reel != _reels.end());
821 write_hanging_text (**reel);
824 auto back_off = [this](DCPTimePeriod period) {
825 period.to -= DCPTime::from_frames(2, film()->video_frame_rate());
829 if (period.to > (*reel)->period().to) {
830 /* This text goes off the end of the reel. Store parts of it that should go into
833 for (auto i = std::next(*reel); i != _reels.end(); ++i) {
834 auto overlap = i->period().overlap(period);
836 _hanging_texts.push_back (HangingText{text, type, track, back_off(*overlap)});
839 /* Back off from the reel boundary by a couple of frames to avoid tripping checks
840 * for subtitles being too close together.
842 period.to = (*reel)->period().to;
843 period = back_off(period);
846 (*reel)->write(text, type, track, period, _fonts, _chosen_interop_font);
851 Writer::write (vector<shared_ptr<Font>> fonts)
857 /* Fonts may come in with empty IDs but we don't want to put those in the DCP */
858 auto fix_id = [](string id) {
859 return id.empty() ? "font" : id;
862 if (film()->interop()) {
863 /* Interop will ignore second and subsequent <LoadFont>s so we don't want to
864 * even write them as they upset some validators. Set up _fonts so that every
865 * font used by any subtitle will be written with the same ID.
867 for (size_t i = 0; i < fonts.size(); ++i) {
868 _fonts.put(fonts[i], fix_id(fonts[0]->id()));
870 _chosen_interop_font = fonts[0];
872 set<string> used_ids;
874 /* Return the index of a _N at the end of a string, or string::npos */
875 auto underscore_number_position = [](string s) {
876 auto last_underscore = s.find_last_of("_");
877 if (last_underscore == string::npos) {
881 for (auto i = last_underscore + 1; i < s.size(); ++i) {
882 if (!isdigit(s[i])) {
887 return last_underscore;
890 /* Write fonts to _fonts, changing any duplicate IDs so that they are unique */
891 for (auto font: fonts) {
892 auto id = fix_id(font->id());
893 if (used_ids.find(id) == used_ids.end()) {
894 /* This ID is unique so we can just use it as-is */
895 _fonts.put(font, id);
898 auto end = underscore_number_position(id);
899 if (end == string::npos) {
900 /* This string has no _N suffix, so add one */
902 end = underscore_number_position(id);
907 /* Increment the suffix until we find a unique one */
908 auto number = dcp::raw_convert<int>(id.substr(end));
909 while (used_ids.find(id) != used_ids.end()) {
911 id = String::compose("%1_%2", id.substr(0, end - 1), number);
915 _fonts.put(font, id);
918 DCPOMATIC_ASSERT(_fonts.map().size() == used_ids.size());
924 operator< (QueueItem const & a, QueueItem const & b)
926 if (a.reel != b.reel) {
927 return a.reel < b.reel;
930 if (a.frame != b.frame) {
931 return a.frame < b.frame;
934 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
939 operator== (QueueItem const & a, QueueItem const & b)
941 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
946 Writer::set_encoder_threads (int threads)
948 boost::mutex::scoped_lock lm (_state_mutex);
949 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
950 _maximum_queue_size = threads * 16;
955 Writer::write (ReferencedReelAsset asset)
957 _reel_assets.push_back (asset);
962 Writer::video_reel (int frame) const
964 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
966 while (i < _reels.size() && !_reels[i].period().contains (t)) {
970 DCPOMATIC_ASSERT (i < _reels.size ());
976 Writer::set_digest_progress (Job* job, float progress)
978 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
980 _digest_progresses[boost::this_thread::get_id()] = progress;
981 float min_progress = FLT_MAX;
982 for (auto const& i: _digest_progresses) {
983 min_progress = min (min_progress, i.second);
986 job->set_progress (min_progress);
991 boost::this_thread::interruption_point();
995 /** Calculate hashes for any referenced MXF assets which do not already have one */
997 Writer::calculate_referenced_digests (std::function<void (float)> set_progress)
1000 for (auto const& i: _reel_assets) {
1001 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
1002 if (file && !file->hash()) {
1003 file->asset_ref().asset()->hash (set_progress);
1004 file->set_hash (file->asset_ref().asset()->hash());
1007 } catch (boost::thread_interrupted) {
1008 /* set_progress contains an interruption_point, so any of these methods
1009 * may throw thread_interrupted, at which point we just give up.
1015 Writer::write_hanging_text (ReelWriter& reel)
1017 vector<HangingText> new_hanging_texts;
1018 for (auto i: _hanging_texts) {
1019 if (i.period.from == reel.period().from) {
1020 reel.write(i.text, i.type, i.track, i.period, _fonts, _chosen_interop_font);
1022 new_hanging_texts.push_back (i);
1025 _hanging_texts = new_hanging_texts;