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"
27 #include "dcp_content_type.h"
28 #include "dcp_video.h"
29 #include "dcpomatic_log.h"
34 #include "reel_writer.h"
35 #include "text_content.h"
40 #include <dcp/locale_convert.h>
41 #include <dcp/raw_convert.h>
42 #include <dcp/reel_file_asset.h>
50 /* OS X strikes again */
55 using std::dynamic_pointer_cast;
56 using std::make_shared;
59 using std::shared_ptr;
64 using boost::optional;
65 #if BOOST_VERSION >= 106100
66 using namespace boost::placeholders;
70 using namespace dcpomatic;
73 /** @param j Job to report progress to, or 0.
74 * @param text_only true to enable only the text (subtitle/ccap) parts of the writer.
76 Writer::Writer (weak_ptr<const Film> weak_film, weak_ptr<Job> j, bool text_only)
77 : WeakConstFilm (weak_film)
79 /* These will be reset to sensible values when J2KEncoder is created */
80 , _maximum_frames_in_memory (8)
81 , _maximum_queue_size (8)
82 , _text_only (text_only)
84 auto job = _job.lock ();
87 auto const reels = film()->reels();
89 _reels.push_back (ReelWriter(weak_film, p, job, reel_index++, reels.size(), text_only));
92 _last_written.resize (reels.size());
94 /* We can keep track of the current audio, subtitle and closed caption reels easily because audio
95 and captions arrive to the Writer in sequence. This is not so for video.
97 _audio_reel = _reels.begin ();
98 _subtitle_reel = _reels.begin ();
99 for (auto i: film()->closed_caption_tracks()) {
100 _caption_reels[i] = _reels.begin ();
102 _atmos_reel = _reels.begin ();
104 /* Check that the signer is OK */
106 if (!Config::instance()->signer_chain()->valid(&reason)) {
107 throw InvalidSignerError (reason);
116 _thread = boost::thread (boost::bind(&Writer::thread, this));
117 #ifdef DCPOMATIC_LINUX
118 pthread_setname_np (_thread.native_handle(), "writer");
127 terminate_thread (false);
132 /** Pass a video frame to the writer for writing to disk at some point.
133 * This method can be called with frames out of order.
134 * @param encoded JPEG2000-encoded data.
135 * @param frame Frame index within the DCP.
136 * @param eyes Eyes that this frame image is for.
139 Writer::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
141 boost::mutex::scoped_lock lock (_state_mutex);
143 while (_queued_full_in_memory > _maximum_frames_in_memory) {
144 /* There are too many full frames in memory; wake the main writer thread and
145 wait until it sorts everything out */
146 _empty_condition.notify_all ();
147 _full_condition.wait (lock);
151 qi.type = QueueItem::Type::FULL;
152 qi.encoded = encoded;
153 qi.reel = video_reel (frame);
154 qi.frame = frame - _reels[qi.reel].start ();
156 DCPOMATIC_ASSERT((film()->three_d() && eyes != Eyes::BOTH) || (!film()->three_d() && eyes == Eyes::BOTH));
159 _queue.push_back(qi);
160 ++_queued_full_in_memory;
162 /* Now there's something to do: wake anything wait()ing on _empty_condition */
163 _empty_condition.notify_all ();
168 Writer::can_repeat (Frame frame) const
170 return frame > _reels[video_reel(frame)].start();
174 /** Repeat the last frame that was written to a reel as a new frame.
175 * @param frame Frame index within the DCP of the new (repeated) frame.
176 * @param eyes Eyes that this repeated frame image is for.
179 Writer::repeat (Frame frame, Eyes eyes)
181 boost::mutex::scoped_lock lock (_state_mutex);
183 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
184 /* The queue is too big, and the main writer thread can run and fix it, so
185 wake it and wait until it has done.
187 _empty_condition.notify_all ();
188 _full_condition.wait (lock);
192 qi.type = QueueItem::Type::REPEAT;
193 qi.reel = video_reel (frame);
194 qi.frame = frame - _reels[qi.reel].start ();
195 if (film()->three_d() && eyes == Eyes::BOTH) {
196 qi.eyes = Eyes::LEFT;
197 _queue.push_back (qi);
198 qi.eyes = Eyes::RIGHT;
199 _queue.push_back (qi);
202 _queue.push_back (qi);
205 /* Now there's something to do: wake anything wait()ing on _empty_condition */
206 _empty_condition.notify_all ();
211 Writer::fake_write (Frame frame, Eyes eyes)
213 boost::mutex::scoped_lock lock (_state_mutex);
215 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
216 /* The queue is too big, and the main writer thread can run and fix it, so
217 wake it and wait until it has done.
219 _empty_condition.notify_all ();
220 _full_condition.wait (lock);
223 size_t const reel = video_reel (frame);
224 Frame const frame_in_reel = frame - _reels[reel].start ();
227 qi.type = QueueItem::Type::FAKE;
230 shared_ptr<InfoFileHandle> info_file = film()->info_file_handle(_reels[reel].period(), true);
231 qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
235 qi.frame = frame_in_reel;
236 if (film()->three_d() && eyes == Eyes::BOTH) {
237 qi.eyes = Eyes::LEFT;
238 _queue.push_back (qi);
239 qi.eyes = Eyes::RIGHT;
240 _queue.push_back (qi);
243 _queue.push_back (qi);
246 /* Now there's something to do: wake anything wait()ing on _empty_condition */
247 _empty_condition.notify_all ();
251 /** Write some audio frames to the DCP.
252 * @param audio Audio data.
253 * @param time Time of this data within the DCP.
254 * This method is not thread safe.
257 Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
259 DCPOMATIC_ASSERT (audio);
261 int const afr = film()->audio_frame_rate();
263 DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
265 /* The audio we get might span a reel boundary, and if so we have to write it in bits */
270 if (_audio_reel == _reels.end ()) {
271 /* This audio is off the end of the last reel; ignore it */
275 if (end <= _audio_reel->period().to) {
276 /* Easy case: we can write all the audio to this reel */
277 _audio_reel->write (audio);
279 } else if (_audio_reel->period().to <= t) {
280 /* This reel is entirely before the start of our audio; just skip the reel */
283 /* This audio is over a reel boundary; split the audio into two and write the first part */
284 DCPTime part_lengths[2] = {
285 _audio_reel->period().to - t,
286 end - _audio_reel->period().to
289 /* Be careful that part_lengths[0] + part_lengths[1] can't be bigger than audio->frames() */
290 Frame part_frames[2] = {
291 part_lengths[0].frames_ceil(afr),
292 part_lengths[1].frames_floor(afr)
295 DCPOMATIC_ASSERT ((part_frames[0] + part_frames[1]) <= audio->frames());
297 if (part_frames[0]) {
298 auto part = make_shared<AudioBuffers>(audio, part_frames[0], 0);
299 _audio_reel->write (part);
302 if (part_frames[1]) {
303 audio = make_shared<AudioBuffers>(audio, part_frames[1], part_frames[0]);
309 t += part_lengths[0];
316 Writer::write (shared_ptr<const dcp::AtmosFrame> atmos, DCPTime time, AtmosMetadata metadata)
318 if (_atmos_reel->period().to == time) {
320 DCPOMATIC_ASSERT (_atmos_reel != _reels.end());
323 /* We assume that we get a video frame's worth of data here */
324 _atmos_reel->write (atmos, metadata);
328 /** Caller must hold a lock on _state_mutex */
330 Writer::have_sequenced_image_at_queue_head ()
332 if (_queue.empty ()) {
337 auto const & f = _queue.front();
338 return _last_written[f.reel].next(f);
343 Writer::LastWritten::next (QueueItem qi) const
345 if (qi.eyes == Eyes::BOTH) {
347 return qi.frame == (_frame + 1);
352 if (_eyes == Eyes::LEFT && qi.frame == _frame && qi.eyes == Eyes::RIGHT) {
356 if (_eyes == Eyes::RIGHT && qi.frame == (_frame + 1) && qi.eyes == Eyes::LEFT) {
365 Writer::LastWritten::update (QueueItem qi)
376 start_of_thread ("Writer");
380 boost::mutex::scoped_lock lock (_state_mutex);
384 if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
385 /* We've got something to do: go and do it */
389 /* Nothing to do: wait until something happens which may indicate that we do */
390 LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
391 _empty_condition.wait (lock);
392 LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
395 /* We stop here if we have been asked to finish, and if either the queue
396 is empty or we do not have a sequenced image at its head (if this is the
397 case we will never terminate as no new frames will be sent once
400 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
401 /* (Hopefully temporarily) log anything that was not written */
402 if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
403 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
404 for (auto const& i: _queue) {
405 if (i.type == QueueItem::Type::FULL) {
406 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
408 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
415 /* Write any frames that we can write; i.e. those that are in sequence. */
416 while (have_sequenced_image_at_queue_head ()) {
417 auto qi = _queue.front ();
418 _last_written[qi.reel].update (qi);
420 if (qi.type == QueueItem::Type::FULL && qi.encoded) {
421 --_queued_full_in_memory;
426 auto& reel = _reels[qi.reel];
429 case QueueItem::Type::FULL:
430 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
432 qi.encoded.reset (new ArrayData(film()->j2c_path(qi.reel, qi.frame, qi.eyes, false)));
434 reel.write (qi.encoded, qi.frame, qi.eyes);
437 case QueueItem::Type::FAKE:
438 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
439 reel.fake_write (qi.size);
442 case QueueItem::Type::REPEAT:
443 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
444 reel.repeat_write (qi.frame, qi.eyes);
450 _full_condition.notify_all ();
453 while (_queued_full_in_memory > _maximum_frames_in_memory) {
454 /* Too many frames in memory which can't yet be written to the stream.
455 Write some FULL frames to disk.
458 /* Find one from the back of the queue */
460 auto i = _queue.rbegin ();
461 while (i != _queue.rend() && (i->type != QueueItem::Type::FULL || !i->encoded)) {
465 DCPOMATIC_ASSERT (i != _queue.rend());
467 /* For the log message below */
468 int const awaiting = _last_written[_queue.front().reel].frame() + 1;
471 /* i is valid here, even though we don't hold a lock on the mutex,
472 since list iterators are unaffected by insertion and only this
473 thread could erase the last item in the list.
476 LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
478 i->encoded->write_via_temp (
479 film()->j2c_path(i->reel, i->frame, i->eyes, true),
480 film()->j2c_path(i->reel, i->frame, i->eyes, false)
485 --_queued_full_in_memory;
486 _full_condition.notify_all ();
497 Writer::terminate_thread (bool can_throw)
499 boost::this_thread::disable_interruption dis;
501 boost::mutex::scoped_lock lock (_state_mutex);
504 _empty_condition.notify_all ();
505 _full_condition.notify_all ();
519 Writer::calculate_digests ()
521 auto job = _job.lock ();
523 job->sub (_("Computing digests"));
526 boost::asio::io_service service;
527 boost::thread_group pool;
529 auto work = make_shared<boost::asio::io_service::work>(service);
531 int const threads = max (1, Config::instance()->master_encoding_threads());
533 for (int i = 0; i < threads; ++i) {
534 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
537 std::function<void (float)> set_progress;
539 set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
541 set_progress = [](float) {
542 boost::this_thread::interruption_point();
546 for (auto& i: _reels) {
547 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
549 service.post (boost::bind (&Writer::calculate_referenced_digests, this, set_progress));
555 } catch (boost::thread_interrupted) {
556 /* join_all was interrupted, so we need to interrupt the threads
557 * in our pool then try again to join them.
559 pool.interrupt_all ();
567 /** @param output_dcp Path to DCP folder to write */
569 Writer::finish (boost::filesystem::path output_dcp)
571 if (_thread.joinable()) {
572 LOG_GENERAL_NC ("Terminating writer thread");
573 terminate_thread (true);
576 LOG_GENERAL_NC ("Finishing ReelWriters");
578 for (auto& i: _reels) {
579 write_hanging_text (i);
580 i.finish (output_dcp);
583 LOG_GENERAL_NC ("Writing XML");
585 dcp::DCP dcp (output_dcp);
587 auto cpl = make_shared<dcp::CPL>(
589 film()->dcp_content_type()->libdcp_kind(),
590 film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE
595 calculate_digests ();
599 for (auto& i: _reels) {
600 cpl->add (i.create_reel(_reel_assets, _fonts, _chosen_interop_font, output_dcp, _have_subtitles, _have_closed_captions));
605 auto creator = Config::instance()->dcp_creator();
606 if (creator.empty()) {
607 creator = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
610 auto issuer = Config::instance()->dcp_issuer();
611 if (issuer.empty()) {
612 issuer = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
615 cpl->set_creator (creator);
616 cpl->set_issuer (issuer);
618 cpl->set_ratings (film()->ratings());
620 vector<dcp::ContentVersion> cv;
621 for (auto i: film()->content_versions()) {
622 cv.push_back (dcp::ContentVersion(i));
625 cv = { dcp::ContentVersion("1") };
627 cpl->set_content_versions (cv);
629 cpl->set_full_content_title_text (film()->name());
630 cpl->set_full_content_title_text_language (film()->name_language());
631 if (film()->release_territory()) {
632 cpl->set_release_territory (*film()->release_territory());
634 cpl->set_version_number (film()->version_number());
635 cpl->set_status (film()->status());
636 if (film()->chain()) {
637 cpl->set_chain (*film()->chain());
639 if (film()->distributor()) {
640 cpl->set_distributor (*film()->distributor());
642 if (film()->facility()) {
643 cpl->set_facility (*film()->facility());
645 if (film()->luminance()) {
646 cpl->set_luminance (*film()->luminance());
648 if (film()->sign_language_video_language()) {
649 cpl->set_sign_language_video_language (*film()->sign_language_video_language());
652 dcp::MCASoundField field;
653 if (film()->audio_channels() == 2) {
654 field = dcp::MCASoundField::STEREO;
655 } else if (film()->audio_channels() <= 6) {
656 field = dcp::MCASoundField::FIVE_POINT_ONE;
658 field = dcp::MCASoundField::SEVEN_POINT_ONE;
661 dcp::MainSoundConfiguration msc (field, film()->audio_channels());
662 for (auto i: film()->mapped_audio_channels()) {
663 if (static_cast<int>(i) < film()->audio_channels()) {
664 msc.set_mapping (i, static_cast<dcp::Channel>(i));
668 cpl->set_main_sound_configuration (msc.to_string());
669 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
670 cpl->set_main_picture_stored_area (film()->frame_size());
672 auto active_area = film()->active_area();
673 if (active_area.width > 0 && active_area.height > 0) {
674 /* It's not allowed to have a zero active area width or height, and the sizes must be multiples of 2 */
675 cpl->set_main_picture_active_area({ active_area.width & ~1, active_area.height & ~1});
678 auto sl = film()->subtitle_languages().second;
680 cpl->set_additional_subtitle_languages(sl);
683 auto signer = Config::instance()->signer_chain();
684 /* We did check earlier, but check again here to be on the safe side */
686 if (!signer->valid (&reason)) {
687 throw InvalidSignerError (reason);
690 dcp.set_issuer(issuer);
691 dcp.set_creator(creator);
692 dcp.set_annotation_text(film()->dcp_name());
694 dcp.write_xml(signer, !film()->limit_to_smpte_bv20(), Config::instance()->dcp_metadata_filename_format());
697 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
700 write_cover_sheet (output_dcp);
705 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
707 auto const cover = film()->file("COVER_SHEET.txt");
708 dcp::File f(cover, "w");
710 throw OpenFileError (cover, errno, OpenFileError::WRITE);
713 auto text = Config::instance()->cover_sheet ();
714 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
715 auto cpls = film()->cpls();
717 boost::algorithm::replace_all (text, "$CPL_FILENAME", cpls[0].cpl_file.filename().string());
719 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
720 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
722 auto audio_language = film()->audio_language();
723 if (audio_language) {
724 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_language->description());
726 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
729 auto subtitle_languages = film()->subtitle_languages();
730 if (subtitle_languages.first) {
731 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
733 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
736 boost::uintmax_t size = 0;
738 auto i = boost::filesystem::recursive_directory_iterator(output_dcp);
739 i != boost::filesystem::recursive_directory_iterator();
741 if (boost::filesystem::is_regular_file (i->path())) {
742 size += boost::filesystem::file_size (i->path());
746 if (size > (1000000000L)) {
747 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
749 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
752 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
753 auto description = String::compose("%1.%2", ch.first, ch.second);
755 if (description == "0.0") {
756 description = _("None");
757 } else if (description == "1.0") {
758 description = _("Mono");
759 } else if (description == "2.0") {
760 description = _("Stereo");
762 boost::algorithm::replace_all (text, "$AUDIO", description);
764 auto const hmsf = film()->length().split(film()->video_frame_rate());
766 if (hmsf.h == 0 && hmsf.m == 0) {
767 length = String::compose("%1s", hmsf.s);
768 } else if (hmsf.h == 0 && hmsf.m > 0) {
769 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
770 } else if (hmsf.h > 0 && hmsf.m > 0) {
771 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
774 boost::algorithm::replace_all (text, "$LENGTH", length);
776 f.checked_write(text.c_str(), text.length());
780 /** @param frame Frame index within the whole DCP.
781 * @return true if we can fake-write this frame.
784 Writer::can_fake_write (Frame frame) const
786 if (film()->encrypted()) {
787 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
791 /* We have to do a proper write of the first frame so that we can set up the JPEG2000
792 parameters in the asset writer.
795 auto const & reel = _reels[video_reel(frame)];
797 /* Make frame relative to the start of the reel */
798 frame -= reel.start ();
799 return (frame != 0 && frame < reel.first_nonexistent_frame());
803 /** @param track Closed caption track if type == TextType::CLOSED_CAPTION */
805 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
807 vector<ReelWriter>::iterator* reel = nullptr;
810 case TextType::OPEN_SUBTITLE:
811 reel = &_subtitle_reel;
812 _have_subtitles = true;
814 case TextType::CLOSED_CAPTION:
815 DCPOMATIC_ASSERT (track);
816 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
817 reel = &_caption_reels[*track];
818 _have_closed_captions.insert (*track);
821 DCPOMATIC_ASSERT (false);
824 DCPOMATIC_ASSERT (*reel != _reels.end());
825 while ((*reel)->period().to <= period.from) {
827 DCPOMATIC_ASSERT (*reel != _reels.end());
828 write_hanging_text (**reel);
831 auto back_off = [this](DCPTimePeriod period) {
832 period.to -= DCPTime::from_frames(2, film()->video_frame_rate());
836 if (period.to > (*reel)->period().to) {
837 /* This text goes off the end of the reel. Store parts of it that should go into
840 for (auto i = std::next(*reel); i != _reels.end(); ++i) {
841 auto overlap = i->period().overlap(period);
843 _hanging_texts.push_back (HangingText{text, type, track, back_off(*overlap)});
846 /* Back off from the reel boundary by a couple of frames to avoid tripping checks
847 * for subtitles being too close together.
849 period.to = (*reel)->period().to;
850 period = back_off(period);
853 (*reel)->write(text, type, track, period, _fonts);
858 Writer::write (vector<shared_ptr<Font>> fonts)
864 /* Fonts may come in with empty IDs but we don't want to put those in the DCP */
865 auto fix_id = [](string id) {
866 return id.empty() ? "font" : id;
869 if (film()->interop()) {
870 /* Interop will ignore second and subsequent <LoadFont>s so we don't want to
871 * even write them as they upset some validators. Set up _fonts so that every
872 * font used by any subtitle will be written with the same ID.
874 for (size_t i = 0; i < fonts.size(); ++i) {
875 _fonts.put(fonts[i], fix_id(fonts[0]->id()));
877 _chosen_interop_font = fonts[0];
879 set<string> used_ids;
881 /* Return the index of a _N at the end of a string, or string::npos */
882 auto underscore_number_position = [](string s) {
883 auto last_underscore = s.find_last_of("_");
884 if (last_underscore == string::npos) {
888 for (auto i = last_underscore + 1; i < s.size(); ++i) {
889 if (!isdigit(s[i])) {
894 return last_underscore;
897 /* Write fonts to _fonts, changing any duplicate IDs so that they are unique */
898 for (auto font: fonts) {
899 auto id = fix_id(font->id());
900 if (used_ids.find(id) == used_ids.end()) {
901 /* This ID is unique so we can just use it as-is */
902 _fonts.put(font, id);
905 auto end = underscore_number_position(id);
906 if (end == string::npos) {
907 /* This string has no _N suffix, so add one */
909 end = underscore_number_position(id);
914 /* Increment the suffix until we find a unique one */
915 auto number = dcp::raw_convert<int>(id.substr(end));
916 while (used_ids.find(id) != used_ids.end()) {
918 id = String::compose("%1_%2", id.substr(0, end - 1), number);
922 _fonts.put(font, id);
925 DCPOMATIC_ASSERT(_fonts.map().size() == used_ids.size());
931 operator< (QueueItem const & a, QueueItem const & b)
933 if (a.reel != b.reel) {
934 return a.reel < b.reel;
937 if (a.frame != b.frame) {
938 return a.frame < b.frame;
941 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
946 operator== (QueueItem const & a, QueueItem const & b)
948 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
953 Writer::set_encoder_threads (int threads)
955 boost::mutex::scoped_lock lm (_state_mutex);
956 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
957 _maximum_queue_size = threads * 16;
962 Writer::write (ReferencedReelAsset asset)
964 _reel_assets.push_back (asset);
969 Writer::video_reel (int frame) const
971 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
973 while (i < _reels.size() && !_reels[i].period().contains (t)) {
977 DCPOMATIC_ASSERT (i < _reels.size ());
983 Writer::set_digest_progress (Job* job, float progress)
985 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
987 _digest_progresses[boost::this_thread::get_id()] = progress;
988 float min_progress = FLT_MAX;
989 for (auto const& i: _digest_progresses) {
990 min_progress = min (min_progress, i.second);
993 job->set_progress (min_progress);
998 boost::this_thread::interruption_point();
1002 /** Calculate hashes for any referenced MXF assets which do not already have one */
1004 Writer::calculate_referenced_digests (std::function<void (float)> set_progress)
1007 for (auto const& i: _reel_assets) {
1008 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
1009 if (file && !file->hash()) {
1010 file->asset_ref().asset()->hash (set_progress);
1011 file->set_hash (file->asset_ref().asset()->hash());
1014 } catch (boost::thread_interrupted) {
1015 /* set_progress contains an interruption_point, so any of these methods
1016 * may throw thread_interrupted, at which point we just give up.
1022 Writer::write_hanging_text (ReelWriter& reel)
1024 vector<HangingText> new_hanging_texts;
1025 for (auto i: _hanging_texts) {
1026 if (i.period.from == reel.period().from) {
1027 reel.write (i.text, i.type, i.track, i.period, _fonts);
1029 new_hanging_texts.push_back (i);
1032 _hanging_texts = new_hanging_texts;