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 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 auto const audio_channels = film()->audio_channels();
657 dcp::MainSoundConfiguration msc(field, audio_channels);
658 for (auto i: film()->mapped_audio_channels()) {
659 if (i < audio_channels) {
660 msc.set_mapping(i, static_cast<dcp::Channel>(i));
664 cpl->set_main_sound_configuration(msc);
665 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
666 cpl->set_main_picture_stored_area (film()->frame_size());
668 auto active_area = film()->active_area();
669 if (active_area.width > 0 && active_area.height > 0) {
670 /* It's not allowed to have a zero active area width or height, and the sizes must be multiples of 2 */
671 cpl->set_main_picture_active_area({ active_area.width & ~1, active_area.height & ~1});
674 auto sl = film()->subtitle_languages().second;
676 cpl->set_additional_subtitle_languages(sl);
679 auto signer = Config::instance()->signer_chain();
680 /* We did check earlier, but check again here to be on the safe side */
682 if (!signer->valid (&reason)) {
683 throw InvalidSignerError (reason);
686 dcp.set_issuer(issuer);
687 dcp.set_creator(creator);
688 dcp.set_annotation_text(film()->dcp_name());
690 dcp.write_xml(signer, !film()->limit_to_smpte_bv20(), Config::instance()->dcp_metadata_filename_format());
693 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
696 write_cover_sheet (output_dcp);
701 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
703 auto const cover = film()->file("COVER_SHEET.txt");
704 dcp::File file(cover, "w");
706 throw OpenFileError (cover, errno, OpenFileError::WRITE);
709 auto text = Config::instance()->cover_sheet ();
710 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
711 auto cpls = film()->cpls();
713 boost::algorithm::replace_all (text, "$CPL_FILENAME", cpls[0].cpl_file.filename().string());
715 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
716 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
718 auto audio_language = film()->audio_language();
719 if (audio_language) {
720 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_language->description());
722 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
725 auto subtitle_languages = film()->subtitle_languages();
726 if (subtitle_languages.first) {
727 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
729 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
732 boost::uintmax_t size = 0;
734 auto i = boost::filesystem::recursive_directory_iterator(output_dcp);
735 i != boost::filesystem::recursive_directory_iterator();
737 if (boost::filesystem::is_regular_file (i->path())) {
738 size += boost::filesystem::file_size (i->path());
742 if (size > (1000000000L)) {
743 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
745 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
748 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
749 auto description = String::compose("%1.%2", ch.first, ch.second);
751 if (description == "0.0") {
752 description = _("None");
753 } else if (description == "1.0") {
754 description = _("Mono");
755 } else if (description == "2.0") {
756 description = _("Stereo");
758 boost::algorithm::replace_all (text, "$AUDIO", description);
760 auto const hmsf = film()->length().split(film()->video_frame_rate());
762 if (hmsf.h == 0 && hmsf.m == 0) {
763 length = String::compose("%1s", hmsf.s);
764 } else if (hmsf.h == 0 && hmsf.m > 0) {
765 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
766 } else if (hmsf.h > 0 && hmsf.m > 0) {
767 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
770 boost::algorithm::replace_all (text, "$LENGTH", length);
772 file.checked_write(text.c_str(), text.length());
776 /** @param frame Frame index within the whole DCP.
777 * @return true if we can fake-write this frame.
780 Writer::can_fake_write (Frame frame) const
782 if (film()->encrypted()) {
783 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
787 /* We have to do a proper write of the first frame so that we can set up the JPEG2000
788 parameters in the asset writer.
791 auto const & reel = _reels[video_reel(frame)];
793 /* Make frame relative to the start of the reel */
794 frame -= reel.start ();
795 return (frame != 0 && frame < reel.first_nonexistent_frame());
799 /** @param track Closed caption track if type == TextType::CLOSED_CAPTION */
801 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
803 vector<ReelWriter>::iterator* reel = nullptr;
806 case TextType::OPEN_SUBTITLE:
807 reel = &_subtitle_reel;
808 _have_subtitles = true;
810 case TextType::CLOSED_CAPTION:
811 DCPOMATIC_ASSERT (track);
812 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
813 reel = &_caption_reels[*track];
814 _have_closed_captions.insert (*track);
817 DCPOMATIC_ASSERT (false);
820 DCPOMATIC_ASSERT (*reel != _reels.end());
821 while ((*reel)->period().to <= period.from) {
823 DCPOMATIC_ASSERT (*reel != _reels.end());
824 write_hanging_text (**reel);
827 auto back_off = [this](DCPTimePeriod period) {
828 period.to -= DCPTime::from_frames(2, film()->video_frame_rate());
832 if (period.to > (*reel)->period().to) {
833 /* This text goes off the end of the reel. Store parts of it that should go into
836 for (auto i = std::next(*reel); i != _reels.end(); ++i) {
837 auto overlap = i->period().overlap(period);
839 _hanging_texts.push_back (HangingText{text, type, track, back_off(*overlap)});
842 /* Back off from the reel boundary by a couple of frames to avoid tripping checks
843 * for subtitles being too close together.
845 period.to = (*reel)->period().to;
846 period = back_off(period);
849 (*reel)->write(text, type, track, period, _fonts, _chosen_interop_font);
854 Writer::write (vector<shared_ptr<Font>> fonts)
860 /* Fonts may come in with empty IDs but we don't want to put those in the DCP */
861 auto fix_id = [](string id) {
862 return id.empty() ? "font" : id;
865 if (film()->interop()) {
866 /* Interop will ignore second and subsequent <LoadFont>s so we don't want to
867 * even write them as they upset some validators. Set up _fonts so that every
868 * font used by any subtitle will be written with the same ID.
870 for (size_t i = 0; i < fonts.size(); ++i) {
871 _fonts.put(fonts[i], fix_id(fonts[0]->id()));
873 _chosen_interop_font = fonts[0];
875 set<string> used_ids;
877 /* Return the index of a _N at the end of a string, or string::npos */
878 auto underscore_number_position = [](string s) {
879 auto last_underscore = s.find_last_of("_");
880 if (last_underscore == string::npos) {
884 for (auto i = last_underscore + 1; i < s.size(); ++i) {
885 if (!isdigit(s[i])) {
890 return last_underscore;
893 /* Write fonts to _fonts, changing any duplicate IDs so that they are unique */
894 for (auto font: fonts) {
895 auto id = fix_id(font->id());
896 if (used_ids.find(id) == used_ids.end()) {
897 /* This ID is unique so we can just use it as-is */
898 _fonts.put(font, id);
901 auto end = underscore_number_position(id);
902 if (end == string::npos) {
903 /* This string has no _N suffix, so add one */
905 end = underscore_number_position(id);
910 /* Increment the suffix until we find a unique one */
911 auto number = dcp::raw_convert<int>(id.substr(end));
912 while (used_ids.find(id) != used_ids.end()) {
914 id = String::compose("%1_%2", id.substr(0, end - 1), number);
918 _fonts.put(font, id);
921 DCPOMATIC_ASSERT(_fonts.map().size() == used_ids.size());
927 operator< (QueueItem const & a, QueueItem const & b)
929 if (a.reel != b.reel) {
930 return a.reel < b.reel;
933 if (a.frame != b.frame) {
934 return a.frame < b.frame;
937 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
942 operator== (QueueItem const & a, QueueItem const & b)
944 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
949 Writer::set_encoder_threads (int threads)
951 boost::mutex::scoped_lock lm (_state_mutex);
952 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
953 _maximum_queue_size = threads * 16;
958 Writer::write (ReferencedReelAsset asset)
960 _reel_assets.push_back (asset);
965 Writer::video_reel (int frame) const
967 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
968 size_t reel_index = 0;
969 while (reel_index < _reels.size() && !_reels[reel_index].period().contains(t)) {
973 DCPOMATIC_ASSERT(reel_index < _reels.size ());
979 Writer::set_digest_progress (Job* job, float progress)
981 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
983 _digest_progresses[boost::this_thread::get_id()] = progress;
984 float min_progress = FLT_MAX;
985 for (auto const& i: _digest_progresses) {
986 min_progress = min (min_progress, i.second);
989 job->set_progress (min_progress);
994 boost::this_thread::interruption_point();
998 /** Calculate hashes for any referenced MXF assets which do not already have one */
1000 Writer::calculate_referenced_digests (std::function<void (float)> set_progress)
1003 for (auto const& i: _reel_assets) {
1004 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
1005 if (file && !file->hash()) {
1006 file->asset_ref().asset()->hash (set_progress);
1007 file->set_hash (file->asset_ref().asset()->hash());
1010 } catch (boost::thread_interrupted) {
1011 /* set_progress contains an interruption_point, so any of these methods
1012 * may throw thread_interrupted, at which point we just give up.
1018 Writer::write_hanging_text (ReelWriter& reel)
1020 vector<HangingText> new_hanging_texts;
1021 for (auto i: _hanging_texts) {
1022 if (i.period.from == reel.period().from) {
1023 reel.write(i.text, i.type, i.track, i.period, _fonts, _chosen_interop_font);
1025 new_hanging_texts.push_back (i);
1028 _hanging_texts = new_hanging_texts;