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 weak_job 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> weak_job, 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 item = _queue.rbegin();
458 while (item != _queue.rend() && (item->type != QueueItem::Type::FULL || !item->encoded)) {
462 DCPOMATIC_ASSERT(item != _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", item->frame, awaiting);
475 item->encoded->write_via_temp(
476 film()->j2c_path(item->reel, item->frame, item->eyes, true),
477 film()->j2c_path(item->reel, item->frame, item->eyes, false)
481 item->encoded.reset();
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& reel: _reels) {
576 write_hanging_text(reel);
577 reel.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 /* Make sure we don't end up writing an empty <LabelText> node as some validators
620 * complain about that.
622 cv.push_back(!i.empty() ? dcp::ContentVersion(i) : dcp::ContentVersion("1"));
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 (channel_is_mapped(film(), dcp::Channel::BSL) || channel_is_mapped(film(), dcp::Channel::BSR)) {
654 field = dcp::MCASoundField::SEVEN_POINT_ONE;
656 field = dcp::MCASoundField::FIVE_POINT_ONE;
659 auto const audio_channels = film()->audio_channels();
660 dcp::MainSoundConfiguration msc(field, audio_channels);
661 for (auto i: film()->mapped_audio_channels()) {
662 if (i < audio_channels) {
663 msc.set_mapping(i, static_cast<dcp::Channel>(i));
667 cpl->set_main_sound_configuration(msc);
668 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
669 cpl->set_main_picture_stored_area (film()->frame_size());
671 auto active_area = film()->active_area();
672 if (active_area.width > 0 && active_area.height > 0) {
673 /* It's not allowed to have a zero active area width or height, and the sizes must be multiples of 2 */
674 cpl->set_main_picture_active_area({ active_area.width & ~1, active_area.height & ~1});
677 auto sl = film()->subtitle_languages().second;
679 cpl->set_additional_subtitle_languages(sl);
682 auto signer = Config::instance()->signer_chain();
683 /* We did check earlier, but check again here to be on the safe side */
685 if (!signer->valid (&reason)) {
686 throw InvalidSignerError (reason);
689 dcp.set_issuer(issuer);
690 dcp.set_creator(creator);
691 dcp.set_annotation_text(film()->dcp_name());
693 dcp.write_xml(signer, !film()->limit_to_smpte_bv20(), Config::instance()->dcp_metadata_filename_format());
696 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
699 write_cover_sheet (output_dcp);
704 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
706 auto const cover = film()->file("COVER_SHEET.txt");
707 dcp::File file(cover, "w");
709 throw OpenFileError (cover, errno, OpenFileError::WRITE);
712 auto text = Config::instance()->cover_sheet ();
713 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
714 auto cpls = film()->cpls();
716 boost::algorithm::replace_all (text, "$CPL_FILENAME", cpls[0].cpl_file.filename().string());
718 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
719 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
721 auto audio_language = film()->audio_language();
722 if (audio_language) {
723 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_language->description());
725 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
728 auto subtitle_languages = film()->subtitle_languages();
729 if (subtitle_languages.first) {
730 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
732 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
735 boost::uintmax_t size = 0;
737 auto i = boost::filesystem::recursive_directory_iterator(output_dcp);
738 i != boost::filesystem::recursive_directory_iterator();
740 if (boost::filesystem::is_regular_file (i->path())) {
741 size += boost::filesystem::file_size (i->path());
745 if (size > (1000000000L)) {
746 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
748 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
751 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
752 auto description = String::compose("%1.%2", ch.first, ch.second);
754 if (description == "0.0") {
755 description = _("None");
756 } else if (description == "1.0") {
757 description = _("Mono");
758 } else if (description == "2.0") {
759 description = _("Stereo");
761 boost::algorithm::replace_all (text, "$AUDIO", description);
763 auto const hmsf = film()->length().split(film()->video_frame_rate());
765 if (hmsf.h == 0 && hmsf.m == 0) {
766 length = String::compose("%1s", hmsf.s);
767 } else if (hmsf.h == 0 && hmsf.m > 0) {
768 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
769 } else if (hmsf.h > 0 && hmsf.m > 0) {
770 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
773 boost::algorithm::replace_all (text, "$LENGTH", length);
775 file.checked_write(text.c_str(), text.length());
779 /** @param frame Frame index within the whole DCP.
780 * @return true if we can fake-write this frame.
783 Writer::can_fake_write (Frame frame) const
785 if (film()->encrypted()) {
786 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
790 /* We have to do a proper write of the first frame so that we can set up the JPEG2000
791 parameters in the asset writer.
794 auto const & reel = _reels[video_reel(frame)];
796 /* Make frame relative to the start of the reel */
797 frame -= reel.start ();
798 return (frame != 0 && frame < reel.first_nonexistent_frame());
802 /** @param track Closed caption track if type == TextType::CLOSED_CAPTION */
804 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
806 vector<ReelWriter>::iterator* reel = nullptr;
809 case TextType::OPEN_SUBTITLE:
810 reel = &_subtitle_reel;
811 _have_subtitles = true;
813 case TextType::CLOSED_CAPTION:
814 DCPOMATIC_ASSERT (track);
815 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
816 reel = &_caption_reels[*track];
817 _have_closed_captions.insert (*track);
820 DCPOMATIC_ASSERT (false);
823 DCPOMATIC_ASSERT (*reel != _reels.end());
824 while ((*reel)->period().to <= period.from) {
826 DCPOMATIC_ASSERT (*reel != _reels.end());
827 write_hanging_text (**reel);
830 auto back_off = [this](DCPTimePeriod period) {
831 period.to -= DCPTime::from_frames(2, film()->video_frame_rate());
835 if (period.to > (*reel)->period().to) {
836 /* This text goes off the end of the reel. Store parts of it that should go into
839 for (auto i = std::next(*reel); i != _reels.end(); ++i) {
840 auto overlap = i->period().overlap(period);
842 _hanging_texts.push_back (HangingText{text, type, track, back_off(*overlap)});
845 /* Back off from the reel boundary by a couple of frames to avoid tripping checks
846 * for subtitles being too close together.
848 period.to = (*reel)->period().to;
849 period = back_off(period);
852 (*reel)->write(text, type, track, period, _fonts, _chosen_interop_font);
857 Writer::write (vector<shared_ptr<Font>> fonts)
863 /* Fonts may come in with empty IDs but we don't want to put those in the DCP */
864 auto fix_id = [](string id) {
865 return id.empty() ? "font" : id;
868 if (film()->interop()) {
869 /* Interop will ignore second and subsequent <LoadFont>s so we don't want to
870 * even write them as they upset some validators. Set up _fonts so that every
871 * font used by any subtitle will be written with the same ID.
873 for (size_t i = 0; i < fonts.size(); ++i) {
874 _fonts.put(fonts[i], fix_id(fonts[0]->id()));
876 _chosen_interop_font = fonts[0];
878 set<string> used_ids;
880 /* Return the index of a _N at the end of a string, or string::npos */
881 auto underscore_number_position = [](string s) {
882 auto last_underscore = s.find_last_of("_");
883 if (last_underscore == string::npos) {
887 for (auto i = last_underscore + 1; i < s.size(); ++i) {
888 if (!isdigit(s[i])) {
893 return last_underscore;
896 /* Write fonts to _fonts, changing any duplicate IDs so that they are unique */
897 for (auto font: fonts) {
898 auto id = fix_id(font->id());
899 if (used_ids.find(id) == used_ids.end()) {
900 /* This ID is unique so we can just use it as-is */
901 _fonts.put(font, id);
904 auto end = underscore_number_position(id);
905 if (end == string::npos) {
906 /* This string has no _N suffix, so add one */
908 end = underscore_number_position(id);
913 /* Increment the suffix until we find a unique one */
914 auto number = dcp::raw_convert<int>(id.substr(end));
915 while (used_ids.find(id) != used_ids.end()) {
917 id = String::compose("%1_%2", id.substr(0, end - 1), number);
921 _fonts.put(font, id);
924 DCPOMATIC_ASSERT(_fonts.map().size() == used_ids.size());
930 operator< (QueueItem const & a, QueueItem const & b)
932 if (a.reel != b.reel) {
933 return a.reel < b.reel;
936 if (a.frame != b.frame) {
937 return a.frame < b.frame;
940 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
945 operator== (QueueItem const & a, QueueItem const & b)
947 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
952 Writer::set_encoder_threads (int threads)
954 boost::mutex::scoped_lock lm (_state_mutex);
955 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
956 _maximum_queue_size = threads * 16;
961 Writer::write (ReferencedReelAsset asset)
963 _reel_assets.push_back (asset);
968 Writer::video_reel (int frame) const
970 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
971 size_t reel_index = 0;
972 while (reel_index < _reels.size() && !_reels[reel_index].period().contains(t)) {
976 DCPOMATIC_ASSERT(reel_index < _reels.size ());
982 Writer::set_digest_progress (Job* job, float progress)
984 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
986 _digest_progresses[boost::this_thread::get_id()] = progress;
987 float min_progress = FLT_MAX;
988 for (auto const& i: _digest_progresses) {
989 min_progress = min (min_progress, i.second);
992 job->set_progress (min_progress);
997 boost::this_thread::interruption_point();
1001 /** Calculate hashes for any referenced MXF assets which do not already have one */
1003 Writer::calculate_referenced_digests (std::function<void (float)> set_progress)
1006 for (auto const& i: _reel_assets) {
1007 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
1008 if (file && !file->hash()) {
1009 file->asset_ref().asset()->hash (set_progress);
1010 file->set_hash (file->asset_ref().asset()->hash());
1013 } catch (boost::thread_interrupted) {
1014 /* set_progress contains an interruption_point, so any of these methods
1015 * may throw thread_interrupted, at which point we just give up.
1021 Writer::write_hanging_text (ReelWriter& reel)
1023 vector<HangingText> new_hanging_texts;
1024 for (auto i: _hanging_texts) {
1025 if (i.period.from == reel.period().from) {
1026 reel.write(i.text, i.type, i.track, i.period, _fonts, _chosen_interop_font);
1028 new_hanging_texts.push_back (i);
1031 _hanging_texts = new_hanging_texts;