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"
35 #include "reel_writer.h"
36 #include "text_content.h"
41 #include <dcp/locale_convert.h>
42 #include <dcp/raw_convert.h>
43 #include <dcp/reel_file_asset.h>
51 /* OS X strikes again */
56 using std::dynamic_pointer_cast;
57 using std::make_shared;
60 using std::shared_ptr;
65 using boost::optional;
66 #if BOOST_VERSION >= 106100
67 using namespace boost::placeholders;
71 using namespace dcpomatic;
74 /** @param j Job to report progress to, or 0.
75 * @param text_only true to enable only the text (subtitle/ccap) parts of the writer.
77 Writer::Writer (weak_ptr<const Film> weak_film, weak_ptr<Job> j, bool text_only)
78 : WeakConstFilm (weak_film)
80 /* These will be reset to sensible values when J2KEncoder is created */
81 , _maximum_frames_in_memory (8)
82 , _maximum_queue_size (8)
83 , _text_only (text_only)
85 auto job = _job.lock ();
88 auto const reels = film()->reels();
90 _reels.push_back (ReelWriter(weak_film, p, job, reel_index++, reels.size(), text_only));
93 _last_written.resize (reels.size());
95 /* We can keep track of the current audio, subtitle and closed caption reels easily because audio
96 and captions arrive to the Writer in sequence. This is not so for video.
98 _audio_reel = _reels.begin ();
99 _subtitle_reel = _reels.begin ();
100 for (auto i: film()->closed_caption_tracks()) {
101 _caption_reels[i] = _reels.begin ();
103 _atmos_reel = _reels.begin ();
105 /* Check that the signer is OK */
107 if (!Config::instance()->signer_chain()->valid(&reason)) {
108 throw InvalidSignerError (reason);
117 _thread = boost::thread (boost::bind(&Writer::thread, this));
118 #ifdef DCPOMATIC_LINUX
119 pthread_setname_np (_thread.native_handle(), "writer");
128 terminate_thread (false);
133 /** Pass a video frame to the writer for writing to disk at some point.
134 * This method can be called with frames out of order.
135 * @param encoded JPEG2000-encoded data.
136 * @param frame Frame index within the DCP.
137 * @param eyes Eyes that this frame image is for.
140 Writer::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
142 boost::mutex::scoped_lock lock (_state_mutex);
144 while (_queued_full_in_memory > _maximum_frames_in_memory) {
145 /* There are too many full frames in memory; wake the main writer thread and
146 wait until it sorts everything out */
147 _empty_condition.notify_all ();
148 _full_condition.wait (lock);
152 qi.type = QueueItem::Type::FULL;
153 qi.encoded = encoded;
154 qi.reel = video_reel (frame);
155 qi.frame = frame - _reels[qi.reel].start ();
157 DCPOMATIC_ASSERT((film()->three_d() && eyes != Eyes::BOTH) || (!film()->three_d() && eyes == Eyes::BOTH));
160 _queue.push_back(qi);
161 ++_queued_full_in_memory;
163 /* Now there's something to do: wake anything wait()ing on _empty_condition */
164 _empty_condition.notify_all ();
169 Writer::can_repeat (Frame frame) const
171 return frame > _reels[video_reel(frame)].start();
175 /** Repeat the last frame that was written to a reel as a new frame.
176 * @param frame Frame index within the DCP of the new (repeated) frame.
177 * @param eyes Eyes that this repeated frame image is for.
180 Writer::repeat (Frame frame, Eyes eyes)
182 boost::mutex::scoped_lock lock (_state_mutex);
184 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
185 /* The queue is too big, and the main writer thread can run and fix it, so
186 wake it and wait until it has done.
188 _empty_condition.notify_all ();
189 _full_condition.wait (lock);
193 qi.type = QueueItem::Type::REPEAT;
194 qi.reel = video_reel (frame);
195 qi.frame = frame - _reels[qi.reel].start ();
196 if (film()->three_d() && eyes == Eyes::BOTH) {
197 qi.eyes = Eyes::LEFT;
198 _queue.push_back (qi);
199 qi.eyes = Eyes::RIGHT;
200 _queue.push_back (qi);
203 _queue.push_back (qi);
206 /* Now there's something to do: wake anything wait()ing on _empty_condition */
207 _empty_condition.notify_all ();
212 Writer::fake_write (Frame frame, Eyes eyes)
214 boost::mutex::scoped_lock lock (_state_mutex);
216 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
217 /* The queue is too big, and the main writer thread can run and fix it, so
218 wake it and wait until it has done.
220 _empty_condition.notify_all ();
221 _full_condition.wait (lock);
224 size_t const reel = video_reel (frame);
225 Frame const frame_in_reel = frame - _reels[reel].start ();
228 qi.type = QueueItem::Type::FAKE;
231 shared_ptr<InfoFileHandle> info_file = film()->info_file_handle(_reels[reel].period(), true);
232 qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
235 DCPOMATIC_ASSERT((film()->three_d() && eyes != Eyes::BOTH) || (!film()->three_d() && eyes == Eyes::BOTH));
238 qi.frame = frame_in_reel;
240 _queue.push_back(qi);
242 /* Now there's something to do: wake anything wait()ing on _empty_condition */
243 _empty_condition.notify_all ();
247 /** Write some audio frames to the DCP.
248 * @param audio Audio data.
249 * @param time Time of this data within the DCP.
250 * This method is not thread safe.
253 Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
255 DCPOMATIC_ASSERT (audio);
257 int const afr = film()->audio_frame_rate();
259 DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
261 /* The audio we get might span a reel boundary, and if so we have to write it in bits */
266 if (_audio_reel == _reels.end ()) {
267 /* This audio is off the end of the last reel; ignore it */
271 if (end <= _audio_reel->period().to) {
272 /* Easy case: we can write all the audio to this reel */
273 _audio_reel->write (audio);
275 } else if (_audio_reel->period().to <= t) {
276 /* This reel is entirely before the start of our audio; just skip the reel */
279 /* This audio is over a reel boundary; split the audio into two and write the first part */
280 DCPTime part_lengths[2] = {
281 _audio_reel->period().to - t,
282 end - _audio_reel->period().to
285 /* Be careful that part_lengths[0] + part_lengths[1] can't be bigger than audio->frames() */
286 Frame part_frames[2] = {
287 part_lengths[0].frames_ceil(afr),
288 part_lengths[1].frames_floor(afr)
291 DCPOMATIC_ASSERT ((part_frames[0] + part_frames[1]) <= audio->frames());
293 if (part_frames[0]) {
294 auto part = make_shared<AudioBuffers>(audio, part_frames[0], 0);
295 _audio_reel->write (part);
298 if (part_frames[1]) {
299 audio = make_shared<AudioBuffers>(audio, part_frames[1], part_frames[0]);
305 t += part_lengths[0];
312 Writer::write (shared_ptr<const dcp::AtmosFrame> atmos, DCPTime time, AtmosMetadata metadata)
314 if (_atmos_reel->period().to == time) {
316 DCPOMATIC_ASSERT (_atmos_reel != _reels.end());
319 /* We assume that we get a video frame's worth of data here */
320 _atmos_reel->write (atmos, metadata);
324 /** Caller must hold a lock on _state_mutex */
326 Writer::have_sequenced_image_at_queue_head ()
328 if (_queue.empty ()) {
333 auto const & f = _queue.front();
334 return _last_written[f.reel].next(f);
339 Writer::LastWritten::next (QueueItem qi) const
341 if (qi.eyes == Eyes::BOTH) {
343 return qi.frame == (_frame + 1);
348 if (_eyes == Eyes::LEFT && qi.frame == _frame && qi.eyes == Eyes::RIGHT) {
352 if (_eyes == Eyes::RIGHT && qi.frame == (_frame + 1) && qi.eyes == Eyes::LEFT) {
361 Writer::LastWritten::update (QueueItem qi)
372 start_of_thread ("Writer");
376 boost::mutex::scoped_lock lock (_state_mutex);
380 if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
381 /* We've got something to do: go and do it */
385 /* Nothing to do: wait until something happens which may indicate that we do */
386 LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
387 _empty_condition.wait (lock);
388 LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
391 /* We stop here if we have been asked to finish, and if either the queue
392 is empty or we do not have a sequenced image at its head (if this is the
393 case we will never terminate as no new frames will be sent once
396 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
397 /* (Hopefully temporarily) log anything that was not written */
398 if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
399 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
400 for (auto const& i: _queue) {
401 if (i.type == QueueItem::Type::FULL) {
402 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
404 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
411 /* Write any frames that we can write; i.e. those that are in sequence. */
412 while (have_sequenced_image_at_queue_head ()) {
413 auto qi = _queue.front ();
414 _last_written[qi.reel].update (qi);
416 if (qi.type == QueueItem::Type::FULL && qi.encoded) {
417 --_queued_full_in_memory;
422 auto& reel = _reels[qi.reel];
425 case QueueItem::Type::FULL:
426 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
428 qi.encoded.reset (new ArrayData(film()->j2c_path(qi.reel, qi.frame, qi.eyes, false)));
430 reel.write (qi.encoded, qi.frame, qi.eyes);
433 case QueueItem::Type::FAKE:
434 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
435 reel.fake_write (qi.size);
438 case QueueItem::Type::REPEAT:
439 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
440 reel.repeat_write (qi.frame, qi.eyes);
446 _full_condition.notify_all ();
449 while (_queued_full_in_memory > _maximum_frames_in_memory) {
450 /* Too many frames in memory which can't yet be written to the stream.
451 Write some FULL frames to disk.
454 /* Find one from the back of the queue */
456 auto i = _queue.rbegin ();
457 while (i != _queue.rend() && (i->type != QueueItem::Type::FULL || !i->encoded)) {
461 DCPOMATIC_ASSERT (i != _queue.rend());
463 /* For the log message below */
464 int const awaiting = _last_written[_queue.front().reel].frame() + 1;
467 /* i is valid here, even though we don't hold a lock on the mutex,
468 since list iterators are unaffected by insertion and only this
469 thread could erase the last item in the list.
472 LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
474 i->encoded->write_via_temp (
475 film()->j2c_path(i->reel, i->frame, i->eyes, true),
476 film()->j2c_path(i->reel, i->frame, i->eyes, false)
481 --_queued_full_in_memory;
482 _full_condition.notify_all ();
493 Writer::terminate_thread (bool can_throw)
495 boost::this_thread::disable_interruption dis;
497 boost::mutex::scoped_lock lock (_state_mutex);
500 _empty_condition.notify_all ();
501 _full_condition.notify_all ();
515 Writer::calculate_digests ()
517 auto job = _job.lock ();
519 job->sub (_("Computing digests"));
522 boost::asio::io_service service;
523 boost::thread_group pool;
525 auto work = make_shared<boost::asio::io_service::work>(service);
527 int const threads = max (1, Config::instance()->master_encoding_threads());
529 for (int i = 0; i < threads; ++i) {
530 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
533 std::function<void (float)> set_progress;
535 set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
537 set_progress = [](float) {
538 boost::this_thread::interruption_point();
542 for (auto& i: _reels) {
543 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
545 service.post (boost::bind (&Writer::calculate_referenced_digests, this, set_progress));
551 } catch (boost::thread_interrupted) {
552 /* join_all was interrupted, so we need to interrupt the threads
553 * in our pool then try again to join them.
555 pool.interrupt_all ();
563 /** @param output_dcp Path to DCP folder to write */
565 Writer::finish (boost::filesystem::path output_dcp)
567 if (_thread.joinable()) {
568 LOG_GENERAL_NC ("Terminating writer thread");
569 terminate_thread (true);
572 LOG_GENERAL_NC ("Finishing ReelWriters");
574 for (auto& i: _reels) {
575 write_hanging_text (i);
576 i.finish (output_dcp);
579 LOG_GENERAL_NC ("Writing XML");
581 dcp::DCP dcp (output_dcp);
583 auto cpl = make_shared<dcp::CPL>(
585 film()->dcp_content_type()->libdcp_kind(),
586 film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE
591 calculate_digests ();
595 for (auto& i: _reels) {
596 cpl->add (i.create_reel(_reel_assets, _fonts, _chosen_interop_font, output_dcp, _have_subtitles, _have_closed_captions));
601 auto creator = Config::instance()->dcp_creator();
602 if (creator.empty()) {
603 creator = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
606 auto issuer = Config::instance()->dcp_issuer();
607 if (issuer.empty()) {
608 issuer = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
611 cpl->set_creator (creator);
612 cpl->set_issuer (issuer);
614 cpl->set_ratings (film()->ratings());
616 vector<dcp::ContentVersion> cv;
617 for (auto i: film()->content_versions()) {
618 cv.push_back (dcp::ContentVersion(i));
621 cv = { dcp::ContentVersion("1") };
623 cpl->set_content_versions (cv);
625 cpl->set_full_content_title_text (film()->name());
626 cpl->set_full_content_title_text_language (film()->name_language());
627 if (film()->release_territory()) {
628 cpl->set_release_territory (*film()->release_territory());
630 cpl->set_version_number (film()->version_number());
631 cpl->set_status (film()->status());
632 if (film()->chain()) {
633 cpl->set_chain (*film()->chain());
635 if (film()->distributor()) {
636 cpl->set_distributor (*film()->distributor());
638 if (film()->facility()) {
639 cpl->set_facility (*film()->facility());
641 if (film()->luminance()) {
642 cpl->set_luminance (*film()->luminance());
644 if (film()->sign_language_video_language()) {
645 cpl->set_sign_language_video_language (*film()->sign_language_video_language());
648 dcp::MCASoundField field;
649 if (film()->audio_channels() <= 6) {
650 field = dcp::MCASoundField::FIVE_POINT_ONE;
652 field = dcp::MCASoundField::SEVEN_POINT_ONE;
655 dcp::MainSoundConfiguration msc(field, MAX_DCP_AUDIO_CHANNELS);
656 for (auto i: film()->mapped_audio_channels()) {
657 msc.set_mapping(i, static_cast<dcp::Channel>(i));
660 cpl->set_main_sound_configuration(msc);
661 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
662 cpl->set_main_picture_stored_area (film()->frame_size());
664 auto active_area = film()->active_area();
665 if (active_area.width > 0 && active_area.height > 0) {
666 /* It's not allowed to have a zero active area width or height, and the sizes must be multiples of 2 */
667 cpl->set_main_picture_active_area({ active_area.width & ~1, active_area.height & ~1});
670 auto sl = film()->subtitle_languages().second;
672 cpl->set_additional_subtitle_languages(sl);
675 auto signer = Config::instance()->signer_chain();
676 /* We did check earlier, but check again here to be on the safe side */
678 if (!signer->valid (&reason)) {
679 throw InvalidSignerError (reason);
682 dcp.set_issuer(issuer);
683 dcp.set_creator(creator);
684 dcp.set_annotation_text(film()->dcp_name());
686 dcp.write_xml(signer, !film()->limit_to_smpte_bv20(), Config::instance()->dcp_metadata_filename_format());
689 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
692 write_cover_sheet (output_dcp);
697 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
699 auto const cover = film()->file("COVER_SHEET.txt");
700 dcp::File f(cover, "w");
702 throw OpenFileError (cover, errno, OpenFileError::WRITE);
705 auto text = Config::instance()->cover_sheet ();
706 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
707 auto cpls = film()->cpls();
709 boost::algorithm::replace_all (text, "$CPL_FILENAME", cpls[0].cpl_file.filename().string());
711 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
712 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
714 auto audio_language = film()->audio_language();
715 if (audio_language) {
716 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_language->description());
718 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
721 auto subtitle_languages = film()->subtitle_languages();
722 if (subtitle_languages.first) {
723 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
725 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
728 boost::uintmax_t size = 0;
730 auto i = boost::filesystem::recursive_directory_iterator(output_dcp);
731 i != boost::filesystem::recursive_directory_iterator();
733 if (boost::filesystem::is_regular_file (i->path())) {
734 size += boost::filesystem::file_size (i->path());
738 if (size > (1000000000L)) {
739 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
741 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
744 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
745 auto description = String::compose("%1.%2", ch.first, ch.second);
747 if (description == "0.0") {
748 description = _("None");
749 } else if (description == "1.0") {
750 description = _("Mono");
751 } else if (description == "2.0") {
752 description = _("Stereo");
754 boost::algorithm::replace_all (text, "$AUDIO", description);
756 auto const hmsf = film()->length().split(film()->video_frame_rate());
758 if (hmsf.h == 0 && hmsf.m == 0) {
759 length = String::compose("%1s", hmsf.s);
760 } else if (hmsf.h == 0 && hmsf.m > 0) {
761 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
762 } else if (hmsf.h > 0 && hmsf.m > 0) {
763 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
766 boost::algorithm::replace_all (text, "$LENGTH", length);
768 f.checked_write(text.c_str(), text.length());
772 /** @param frame Frame index within the whole DCP.
773 * @return true if we can fake-write this frame.
776 Writer::can_fake_write (Frame frame) const
778 if (film()->encrypted()) {
779 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
783 /* We have to do a proper write of the first frame so that we can set up the JPEG2000
784 parameters in the asset writer.
787 auto const & reel = _reels[video_reel(frame)];
789 /* Make frame relative to the start of the reel */
790 frame -= reel.start ();
791 return (frame != 0 && frame < reel.first_nonexistent_frame());
795 /** @param track Closed caption track if type == TextType::CLOSED_CAPTION */
797 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
799 vector<ReelWriter>::iterator* reel = nullptr;
802 case TextType::OPEN_SUBTITLE:
803 reel = &_subtitle_reel;
804 _have_subtitles = true;
806 case TextType::CLOSED_CAPTION:
807 DCPOMATIC_ASSERT (track);
808 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
809 reel = &_caption_reels[*track];
810 _have_closed_captions.insert (*track);
813 DCPOMATIC_ASSERT (false);
816 DCPOMATIC_ASSERT (*reel != _reels.end());
817 while ((*reel)->period().to <= period.from) {
819 DCPOMATIC_ASSERT (*reel != _reels.end());
820 write_hanging_text (**reel);
823 auto back_off = [this](DCPTimePeriod period) {
824 period.to -= DCPTime::from_frames(2, film()->video_frame_rate());
828 if (period.to > (*reel)->period().to) {
829 /* This text goes off the end of the reel. Store parts of it that should go into
832 for (auto i = std::next(*reel); i != _reels.end(); ++i) {
833 auto overlap = i->period().overlap(period);
835 _hanging_texts.push_back (HangingText{text, type, track, back_off(*overlap)});
838 /* Back off from the reel boundary by a couple of frames to avoid tripping checks
839 * for subtitles being too close together.
841 period.to = (*reel)->period().to;
842 period = back_off(period);
845 (*reel)->write(text, type, track, period, _fonts);
850 Writer::write (vector<shared_ptr<Font>> fonts)
856 /* Fonts may come in with empty IDs but we don't want to put those in the DCP */
857 auto fix_id = [](string id) {
858 return id.empty() ? "font" : id;
861 if (film()->interop()) {
862 /* Interop will ignore second and subsequent <LoadFont>s so we don't want to
863 * even write them as they upset some validators. Set up _fonts so that every
864 * font used by any subtitle will be written with the same ID.
866 for (size_t i = 0; i < fonts.size(); ++i) {
867 _fonts.put(fonts[i], fix_id(fonts[0]->id()));
869 _chosen_interop_font = fonts[0];
871 set<string> used_ids;
873 /* Return the index of a _N at the end of a string, or string::npos */
874 auto underscore_number_position = [](string s) {
875 auto last_underscore = s.find_last_of("_");
876 if (last_underscore == string::npos) {
880 for (auto i = last_underscore + 1; i < s.size(); ++i) {
881 if (!isdigit(s[i])) {
886 return last_underscore;
889 /* Write fonts to _fonts, changing any duplicate IDs so that they are unique */
890 for (auto font: fonts) {
891 auto id = fix_id(font->id());
892 if (used_ids.find(id) == used_ids.end()) {
893 /* This ID is unique so we can just use it as-is */
894 _fonts.put(font, id);
897 auto end = underscore_number_position(id);
898 if (end == string::npos) {
899 /* This string has no _N suffix, so add one */
901 end = underscore_number_position(id);
906 /* Increment the suffix until we find a unique one */
907 auto number = dcp::raw_convert<int>(id.substr(end));
908 while (used_ids.find(id) != used_ids.end()) {
910 id = String::compose("%1_%2", id.substr(0, end - 1), number);
914 _fonts.put(font, id);
917 DCPOMATIC_ASSERT(_fonts.map().size() == used_ids.size());
923 operator< (QueueItem const & a, QueueItem const & b)
925 if (a.reel != b.reel) {
926 return a.reel < b.reel;
929 if (a.frame != b.frame) {
930 return a.frame < b.frame;
933 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
938 operator== (QueueItem const & a, QueueItem const & b)
940 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
945 Writer::set_encoder_threads (int threads)
947 boost::mutex::scoped_lock lm (_state_mutex);
948 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
949 _maximum_queue_size = threads * 16;
954 Writer::write (ReferencedReelAsset asset)
956 _reel_assets.push_back (asset);
961 Writer::video_reel (int frame) const
963 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
965 while (i < _reels.size() && !_reels[i].period().contains (t)) {
969 DCPOMATIC_ASSERT (i < _reels.size ());
975 Writer::set_digest_progress (Job* job, float progress)
977 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
979 _digest_progresses[boost::this_thread::get_id()] = progress;
980 float min_progress = FLT_MAX;
981 for (auto const& i: _digest_progresses) {
982 min_progress = min (min_progress, i.second);
985 job->set_progress (min_progress);
990 boost::this_thread::interruption_point();
994 /** Calculate hashes for any referenced MXF assets which do not already have one */
996 Writer::calculate_referenced_digests (std::function<void (float)> set_progress)
999 for (auto const& i: _reel_assets) {
1000 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
1001 if (file && !file->hash()) {
1002 file->asset_ref().asset()->hash (set_progress);
1003 file->set_hash (file->asset_ref().asset()->hash());
1006 } catch (boost::thread_interrupted) {
1007 /* set_progress contains an interruption_point, so any of these methods
1008 * may throw thread_interrupted, at which point we just give up.
1014 Writer::write_hanging_text (ReelWriter& reel)
1016 vector<HangingText> new_hanging_texts;
1017 for (auto i: _hanging_texts) {
1018 if (i.period.from == reel.period().from) {
1019 reel.write (i.text, i.type, i.track, i.period, _fonts);
1021 new_hanging_texts.push_back (i);
1024 _hanging_texts = new_hanging_texts;