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 if (film()->three_d() && eyes == Eyes::BOTH) {
157 /* 2D material in a 3D DCP; fake the 3D */
158 qi.eyes = Eyes::LEFT;
159 _queue.push_back (qi);
160 ++_queued_full_in_memory;
161 qi.eyes = Eyes::RIGHT;
162 _queue.push_back (qi);
163 ++_queued_full_in_memory;
166 _queue.push_back (qi);
167 ++_queued_full_in_memory;
170 /* Now there's something to do: wake anything wait()ing on _empty_condition */
171 _empty_condition.notify_all ();
176 Writer::can_repeat (Frame frame) const
178 return frame > _reels[video_reel(frame)].start();
182 /** Repeat the last frame that was written to a reel as a new frame.
183 * @param frame Frame index within the DCP of the new (repeated) frame.
184 * @param eyes Eyes that this repeated frame image is for.
187 Writer::repeat (Frame frame, Eyes eyes)
189 boost::mutex::scoped_lock lock (_state_mutex);
191 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
192 /* The queue is too big, and the main writer thread can run and fix it, so
193 wake it and wait until it has done.
195 _empty_condition.notify_all ();
196 _full_condition.wait (lock);
200 qi.type = QueueItem::Type::REPEAT;
201 qi.reel = video_reel (frame);
202 qi.frame = frame - _reels[qi.reel].start ();
203 if (film()->three_d() && eyes == Eyes::BOTH) {
204 qi.eyes = Eyes::LEFT;
205 _queue.push_back (qi);
206 qi.eyes = Eyes::RIGHT;
207 _queue.push_back (qi);
210 _queue.push_back (qi);
213 /* Now there's something to do: wake anything wait()ing on _empty_condition */
214 _empty_condition.notify_all ();
219 Writer::fake_write (Frame frame, Eyes eyes)
221 boost::mutex::scoped_lock lock (_state_mutex);
223 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
224 /* The queue is too big, and the main writer thread can run and fix it, so
225 wake it and wait until it has done.
227 _empty_condition.notify_all ();
228 _full_condition.wait (lock);
231 size_t const reel = video_reel (frame);
232 Frame const frame_in_reel = frame - _reels[reel].start ();
235 qi.type = QueueItem::Type::FAKE;
238 shared_ptr<InfoFileHandle> info_file = film()->info_file_handle(_reels[reel].period(), true);
239 qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
243 qi.frame = frame_in_reel;
244 if (film()->three_d() && eyes == Eyes::BOTH) {
245 qi.eyes = Eyes::LEFT;
246 _queue.push_back (qi);
247 qi.eyes = Eyes::RIGHT;
248 _queue.push_back (qi);
251 _queue.push_back (qi);
254 /* Now there's something to do: wake anything wait()ing on _empty_condition */
255 _empty_condition.notify_all ();
259 /** Write some audio frames to the DCP.
260 * @param audio Audio data.
261 * @param time Time of this data within the DCP.
262 * This method is not thread safe.
265 Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
267 DCPOMATIC_ASSERT (audio);
269 int const afr = film()->audio_frame_rate();
271 DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
273 /* The audio we get might span a reel boundary, and if so we have to write it in bits */
278 if (_audio_reel == _reels.end ()) {
279 /* This audio is off the end of the last reel; ignore it */
283 if (end <= _audio_reel->period().to) {
284 /* Easy case: we can write all the audio to this reel */
285 _audio_reel->write (audio);
287 } else if (_audio_reel->period().to <= t) {
288 /* This reel is entirely before the start of our audio; just skip the reel */
291 /* This audio is over a reel boundary; split the audio into two and write the first part */
292 DCPTime part_lengths[2] = {
293 _audio_reel->period().to - t,
294 end - _audio_reel->period().to
297 /* Be careful that part_lengths[0] + part_lengths[1] can't be bigger than audio->frames() */
298 Frame part_frames[2] = {
299 part_lengths[0].frames_ceil(afr),
300 part_lengths[1].frames_floor(afr)
303 DCPOMATIC_ASSERT ((part_frames[0] + part_frames[1]) <= audio->frames());
305 if (part_frames[0]) {
306 auto part = make_shared<AudioBuffers>(audio, part_frames[0], 0);
307 _audio_reel->write (part);
310 if (part_frames[1]) {
311 audio = make_shared<AudioBuffers>(audio, part_frames[1], part_frames[0]);
317 t += part_lengths[0];
324 Writer::write (shared_ptr<const dcp::AtmosFrame> atmos, DCPTime time, AtmosMetadata metadata)
326 if (_atmos_reel->period().to == time) {
328 DCPOMATIC_ASSERT (_atmos_reel != _reels.end());
331 /* We assume that we get a video frame's worth of data here */
332 _atmos_reel->write (atmos, metadata);
336 /** Caller must hold a lock on _state_mutex */
338 Writer::have_sequenced_image_at_queue_head ()
340 if (_queue.empty ()) {
345 auto const & f = _queue.front();
346 return _last_written[f.reel].next(f);
351 Writer::LastWritten::next (QueueItem qi) const
353 if (qi.eyes == Eyes::BOTH) {
355 return qi.frame == (_frame + 1);
360 if (_eyes == Eyes::LEFT && qi.frame == _frame && qi.eyes == Eyes::RIGHT) {
364 if (_eyes == Eyes::RIGHT && qi.frame == (_frame + 1) && qi.eyes == Eyes::LEFT) {
373 Writer::LastWritten::update (QueueItem qi)
384 start_of_thread ("Writer");
388 boost::mutex::scoped_lock lock (_state_mutex);
392 if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
393 /* We've got something to do: go and do it */
397 /* Nothing to do: wait until something happens which may indicate that we do */
398 LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
399 _empty_condition.wait (lock);
400 LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
403 /* We stop here if we have been asked to finish, and if either the queue
404 is empty or we do not have a sequenced image at its head (if this is the
405 case we will never terminate as no new frames will be sent once
408 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
409 /* (Hopefully temporarily) log anything that was not written */
410 if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
411 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
412 for (auto const& i: _queue) {
413 if (i.type == QueueItem::Type::FULL) {
414 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
416 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
423 /* Write any frames that we can write; i.e. those that are in sequence. */
424 while (have_sequenced_image_at_queue_head ()) {
425 auto qi = _queue.front ();
426 _last_written[qi.reel].update (qi);
428 if (qi.type == QueueItem::Type::FULL && qi.encoded) {
429 --_queued_full_in_memory;
434 auto& reel = _reels[qi.reel];
437 case QueueItem::Type::FULL:
438 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
440 qi.encoded.reset (new ArrayData(film()->j2c_path(qi.reel, qi.frame, qi.eyes, false)));
442 reel.write (qi.encoded, qi.frame, qi.eyes);
445 case QueueItem::Type::FAKE:
446 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
447 reel.fake_write (qi.size);
450 case QueueItem::Type::REPEAT:
451 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
452 reel.repeat_write (qi.frame, qi.eyes);
458 _full_condition.notify_all ();
461 while (_queued_full_in_memory > _maximum_frames_in_memory) {
462 /* Too many frames in memory which can't yet be written to the stream.
463 Write some FULL frames to disk.
466 /* Find one from the back of the queue */
468 auto i = _queue.rbegin ();
469 while (i != _queue.rend() && (i->type != QueueItem::Type::FULL || !i->encoded)) {
473 DCPOMATIC_ASSERT (i != _queue.rend());
475 /* For the log message below */
476 int const awaiting = _last_written[_queue.front().reel].frame() + 1;
479 /* i is valid here, even though we don't hold a lock on the mutex,
480 since list iterators are unaffected by insertion and only this
481 thread could erase the last item in the list.
484 LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
486 i->encoded->write_via_temp (
487 film()->j2c_path(i->reel, i->frame, i->eyes, true),
488 film()->j2c_path(i->reel, i->frame, i->eyes, false)
493 --_queued_full_in_memory;
494 _full_condition.notify_all ();
505 Writer::terminate_thread (bool can_throw)
507 boost::this_thread::disable_interruption dis;
509 boost::mutex::scoped_lock lock (_state_mutex);
512 _empty_condition.notify_all ();
513 _full_condition.notify_all ();
527 Writer::calculate_digests ()
529 auto job = _job.lock ();
531 job->sub (_("Computing digests"));
534 boost::asio::io_service service;
535 boost::thread_group pool;
537 auto work = make_shared<boost::asio::io_service::work>(service);
539 int const threads = max (1, Config::instance()->master_encoding_threads());
541 for (int i = 0; i < threads; ++i) {
542 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
545 std::function<void (float)> set_progress;
547 set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
549 set_progress = [](float) {
550 boost::this_thread::interruption_point();
554 for (auto& i: _reels) {
555 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
557 service.post (boost::bind (&Writer::calculate_referenced_digests, this, set_progress));
563 } catch (boost::thread_interrupted) {
564 /* join_all was interrupted, so we need to interrupt the threads
565 * in our pool then try again to join them.
567 pool.interrupt_all ();
575 /** @param output_dcp Path to DCP folder to write */
577 Writer::finish (boost::filesystem::path output_dcp)
579 if (_thread.joinable()) {
580 LOG_GENERAL_NC ("Terminating writer thread");
581 terminate_thread (true);
584 LOG_GENERAL_NC ("Finishing ReelWriters");
586 for (auto& i: _reels) {
587 write_hanging_text (i);
588 i.finish (output_dcp);
591 LOG_GENERAL_NC ("Writing XML");
593 dcp::DCP dcp (output_dcp);
595 auto cpl = make_shared<dcp::CPL>(
597 film()->dcp_content_type()->libdcp_kind(),
598 film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE
603 calculate_digests ();
607 for (auto& i: _reels) {
608 cpl->add (i.create_reel(_reel_assets, _fonts, _chosen_interop_font, output_dcp, _have_subtitles, _have_closed_captions));
613 auto creator = Config::instance()->dcp_creator();
614 if (creator.empty()) {
615 creator = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
618 auto issuer = Config::instance()->dcp_issuer();
619 if (issuer.empty()) {
620 issuer = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
623 cpl->set_creator (creator);
624 cpl->set_issuer (issuer);
626 cpl->set_ratings (film()->ratings());
628 vector<dcp::ContentVersion> cv;
629 for (auto i: film()->content_versions()) {
630 cv.push_back (dcp::ContentVersion(i));
633 cv = { dcp::ContentVersion("1") };
635 cpl->set_content_versions (cv);
637 cpl->set_full_content_title_text (film()->name());
638 cpl->set_full_content_title_text_language (film()->name_language());
639 if (film()->release_territory()) {
640 cpl->set_release_territory (*film()->release_territory());
642 cpl->set_version_number (film()->version_number());
643 cpl->set_status (film()->status());
644 if (film()->chain()) {
645 cpl->set_chain (*film()->chain());
647 if (film()->distributor()) {
648 cpl->set_distributor (*film()->distributor());
650 if (film()->facility()) {
651 cpl->set_facility (*film()->facility());
653 if (film()->luminance()) {
654 cpl->set_luminance (*film()->luminance());
656 if (film()->sign_language_video_language()) {
657 cpl->set_sign_language_video_language (*film()->sign_language_video_language());
660 dcp::MCASoundField field;
661 if (film()->audio_channels() == 2) {
662 field = dcp::MCASoundField::STEREO;
663 } else if (film()->audio_channels() <= 6) {
664 field = dcp::MCASoundField::FIVE_POINT_ONE;
666 field = dcp::MCASoundField::SEVEN_POINT_ONE;
669 dcp::MainSoundConfiguration msc (field, film()->audio_channels());
670 for (auto i: film()->mapped_audio_channels()) {
671 if (static_cast<int>(i) < film()->audio_channels()) {
672 msc.set_mapping (i, static_cast<dcp::Channel>(i));
676 cpl->set_main_sound_configuration (msc.to_string());
677 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
678 cpl->set_main_picture_stored_area (film()->frame_size());
680 auto active_area = film()->active_area();
681 if (active_area.width > 0 && active_area.height > 0) {
682 /* It's not allowed to have a zero active area width or height, and the sizes must be multiples of 2 */
683 cpl->set_main_picture_active_area({ active_area.width & ~1, active_area.height & ~1});
686 auto sl = film()->subtitle_languages().second;
688 cpl->set_additional_subtitle_languages(sl);
691 auto signer = Config::instance()->signer_chain();
692 /* We did check earlier, but check again here to be on the safe side */
694 if (!signer->valid (&reason)) {
695 throw InvalidSignerError (reason);
698 dcp.set_issuer(issuer);
699 dcp.set_creator(creator);
700 dcp.set_annotation_text(film()->dcp_name());
702 dcp.write_xml (signer, Config::instance()->dcp_metadata_filename_format());
705 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
708 write_cover_sheet (output_dcp);
713 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
715 auto const cover = film()->file("COVER_SHEET.txt");
716 dcp::File f(cover, "w");
718 throw OpenFileError (cover, errno, OpenFileError::WRITE);
721 auto text = Config::instance()->cover_sheet ();
722 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
723 auto cpls = film()->cpls();
725 boost::algorithm::replace_all (text, "$CPL_FILENAME", cpls[0].cpl_file.filename().string());
727 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
728 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
730 auto audio_language = film()->audio_language();
731 if (audio_language) {
732 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_language->description());
734 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
737 auto subtitle_languages = film()->subtitle_languages();
738 if (subtitle_languages.first) {
739 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
741 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
744 boost::uintmax_t size = 0;
746 auto i = boost::filesystem::recursive_directory_iterator(output_dcp);
747 i != boost::filesystem::recursive_directory_iterator();
749 if (boost::filesystem::is_regular_file (i->path())) {
750 size += boost::filesystem::file_size (i->path());
754 if (size > (1000000000L)) {
755 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
757 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
760 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
761 auto description = String::compose("%1.%2", ch.first, ch.second);
763 if (description == "0.0") {
764 description = _("None");
765 } else if (description == "1.0") {
766 description = _("Mono");
767 } else if (description == "2.0") {
768 description = _("Stereo");
770 boost::algorithm::replace_all (text, "$AUDIO", description);
772 auto const hmsf = film()->length().split(film()->video_frame_rate());
774 if (hmsf.h == 0 && hmsf.m == 0) {
775 length = String::compose("%1s", hmsf.s);
776 } else if (hmsf.h == 0 && hmsf.m > 0) {
777 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
778 } else if (hmsf.h > 0 && hmsf.m > 0) {
779 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
782 boost::algorithm::replace_all (text, "$LENGTH", length);
784 f.checked_write(text.c_str(), text.length());
788 /** @param frame Frame index within the whole DCP.
789 * @return true if we can fake-write this frame.
792 Writer::can_fake_write (Frame frame) const
794 if (film()->encrypted()) {
795 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
799 /* We have to do a proper write of the first frame so that we can set up the JPEG2000
800 parameters in the asset writer.
803 auto const & reel = _reels[video_reel(frame)];
805 /* Make frame relative to the start of the reel */
806 frame -= reel.start ();
807 return (frame != 0 && frame < reel.first_nonexistent_frame());
811 /** @param track Closed caption track if type == TextType::CLOSED_CAPTION */
813 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
815 vector<ReelWriter>::iterator* reel = nullptr;
818 case TextType::OPEN_SUBTITLE:
819 reel = &_subtitle_reel;
820 _have_subtitles = true;
822 case TextType::CLOSED_CAPTION:
823 DCPOMATIC_ASSERT (track);
824 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
825 reel = &_caption_reels[*track];
826 _have_closed_captions.insert (*track);
829 DCPOMATIC_ASSERT (false);
832 DCPOMATIC_ASSERT (*reel != _reels.end());
833 while ((*reel)->period().to <= period.from) {
835 DCPOMATIC_ASSERT (*reel != _reels.end());
836 write_hanging_text (**reel);
839 auto back_off = [this](DCPTimePeriod period) {
840 period.to -= DCPTime::from_frames(2, film()->video_frame_rate());
844 if (period.to > (*reel)->period().to) {
845 /* This text goes off the end of the reel. Store parts of it that should go into
848 for (auto i = std::next(*reel); i != _reels.end(); ++i) {
849 auto overlap = i->period().overlap(period);
851 _hanging_texts.push_back (HangingText{text, type, track, back_off(*overlap)});
854 /* Back off from the reel boundary by a couple of frames to avoid tripping checks
855 * for subtitles being too close together.
857 period.to = (*reel)->period().to;
858 period = back_off(period);
861 (*reel)->write(text, type, track, period, _fonts);
866 Writer::write (vector<shared_ptr<Font>> fonts)
872 /* Fonts may come in with empty IDs but we don't want to put those in the DCP */
873 auto fix_id = [](string id) {
874 return id.empty() ? "font" : id;
877 if (film()->interop()) {
878 /* Interop will ignore second and subsequent <LoadFont>s so we don't want to
879 * even write them as they upset some validators. Set up _fonts so that every
880 * font used by any subtitle will be written with the same ID.
882 for (size_t i = 0; i < fonts.size(); ++i) {
883 _fonts.put(fonts[i], fix_id(fonts[0]->id()));
885 _chosen_interop_font = fonts[0];
887 set<string> used_ids;
889 /* Return the index of a _N at the end of a string, or string::npos */
890 auto underscore_number_position = [](string s) {
891 auto last_underscore = s.find_last_of("_");
892 if (last_underscore == string::npos) {
896 for (auto i = last_underscore + 1; i < s.size(); ++i) {
897 if (!isdigit(s[i])) {
902 return last_underscore;
905 /* Write fonts to _fonts, changing any duplicate IDs so that they are unique */
906 for (auto font: fonts) {
907 auto id = fix_id(font->id());
908 if (used_ids.find(id) == used_ids.end()) {
909 /* This ID is unique so we can just use it as-is */
910 _fonts.put(font, id);
913 auto end = underscore_number_position(id);
914 if (end == string::npos) {
915 /* This string has no _N suffix, so add one */
917 end = underscore_number_position(id);
922 /* Increment the suffix until we find a unique one */
923 auto number = dcp::raw_convert<int>(id.substr(end));
924 while (used_ids.find(id) != used_ids.end()) {
926 id = String::compose("%1_%2", id.substr(0, end - 1), number);
930 _fonts.put(font, id);
933 DCPOMATIC_ASSERT(_fonts.map().size() == used_ids.size());
939 operator< (QueueItem const & a, QueueItem const & b)
941 if (a.reel != b.reel) {
942 return a.reel < b.reel;
945 if (a.frame != b.frame) {
946 return a.frame < b.frame;
949 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
954 operator== (QueueItem const & a, QueueItem const & b)
956 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
961 Writer::set_encoder_threads (int threads)
963 boost::mutex::scoped_lock lm (_state_mutex);
964 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
965 _maximum_queue_size = threads * 16;
970 Writer::write (ReferencedReelAsset asset)
972 _reel_assets.push_back (asset);
977 Writer::video_reel (int frame) const
979 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
981 while (i < _reels.size() && !_reels[i].period().contains (t)) {
985 DCPOMATIC_ASSERT (i < _reels.size ());
991 Writer::set_digest_progress (Job* job, float progress)
993 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
995 _digest_progresses[boost::this_thread::get_id()] = progress;
996 float min_progress = FLT_MAX;
997 for (auto const& i: _digest_progresses) {
998 min_progress = min (min_progress, i.second);
1001 job->set_progress (min_progress);
1006 boost::this_thread::interruption_point();
1010 /** Calculate hashes for any referenced MXF assets which do not already have one */
1012 Writer::calculate_referenced_digests (std::function<void (float)> set_progress)
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 (set_progress);
1019 file->set_hash (file->asset_ref().asset()->hash());
1022 } catch (boost::thread_interrupted) {
1023 /* set_progress contains an interruption_point, so any of these methods
1024 * may throw thread_interrupted, at which point we just give up.
1030 Writer::write_hanging_text (ReelWriter& reel)
1032 vector<HangingText> new_hanging_texts;
1033 for (auto i: _hanging_texts) {
1034 if (i.period.from == reel.period().from) {
1035 reel.write (i.text, i.type, i.track, i.period, _fonts);
1037 new_hanging_texts.push_back (i);
1040 _hanging_texts = new_hanging_texts;