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"
31 #include "font_data.h"
35 #include "reel_writer.h"
36 #include "text_content.h"
41 #include <dcp/locale_convert.h>
42 #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;
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 _audio_reel->write (make_shared<AudioBuffers>(audio, part_frames[0], 0));
309 if (part_frames[1]) {
310 audio = make_shared<AudioBuffers>(audio, part_frames[1], part_frames[0]);
316 t += part_lengths[0];
323 Writer::write (shared_ptr<const dcp::AtmosFrame> atmos, DCPTime time, AtmosMetadata metadata)
325 if (_atmos_reel->period().to == time) {
327 DCPOMATIC_ASSERT (_atmos_reel != _reels.end());
330 /* We assume that we get a video frame's worth of data here */
331 _atmos_reel->write (atmos, metadata);
335 /** Caller must hold a lock on _state_mutex */
337 Writer::have_sequenced_image_at_queue_head ()
339 if (_queue.empty ()) {
344 auto const & f = _queue.front();
345 return _last_written[f.reel].next(f);
350 Writer::LastWritten::next (QueueItem qi) const
352 if (qi.eyes == Eyes::BOTH) {
354 return qi.frame == (_frame + 1);
359 if (_eyes == Eyes::LEFT && qi.frame == _frame && qi.eyes == Eyes::RIGHT) {
363 if (_eyes == Eyes::RIGHT && qi.frame == (_frame + 1) && qi.eyes == Eyes::LEFT) {
372 Writer::LastWritten::update (QueueItem qi)
383 start_of_thread ("Writer");
387 boost::mutex::scoped_lock lock (_state_mutex);
391 if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
392 /* We've got something to do: go and do it */
396 /* Nothing to do: wait until something happens which may indicate that we do */
397 LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
398 _empty_condition.wait (lock);
399 LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
402 /* We stop here if we have been asked to finish, and if either the queue
403 is empty or we do not have a sequenced image at its head (if this is the
404 case we will never terminate as no new frames will be sent once
407 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
408 /* (Hopefully temporarily) log anything that was not written */
409 if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
410 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
411 for (auto const& i: _queue) {
412 if (i.type == QueueItem::Type::FULL) {
413 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
415 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
422 /* Write any frames that we can write; i.e. those that are in sequence. */
423 while (have_sequenced_image_at_queue_head ()) {
424 auto qi = _queue.front ();
425 _last_written[qi.reel].update (qi);
427 if (qi.type == QueueItem::Type::FULL && qi.encoded) {
428 --_queued_full_in_memory;
433 auto& reel = _reels[qi.reel];
436 case QueueItem::Type::FULL:
437 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
439 qi.encoded.reset (new ArrayData(film()->j2c_path(qi.reel, qi.frame, qi.eyes, false)));
441 reel.write (qi.encoded, qi.frame, qi.eyes);
444 case QueueItem::Type::FAKE:
445 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
446 reel.fake_write (qi.size);
449 case QueueItem::Type::REPEAT:
450 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
451 reel.repeat_write (qi.frame, qi.eyes);
457 _full_condition.notify_all ();
460 while (_queued_full_in_memory > _maximum_frames_in_memory) {
461 /* Too many frames in memory which can't yet be written to the stream.
462 Write some FULL frames to disk.
465 /* Find one from the back of the queue */
467 auto i = _queue.rbegin ();
468 while (i != _queue.rend() && (i->type != QueueItem::Type::FULL || !i->encoded)) {
472 DCPOMATIC_ASSERT (i != _queue.rend());
474 /* For the log message below */
475 int const awaiting = _last_written[_queue.front().reel].frame() + 1;
478 /* i is valid here, even though we don't hold a lock on the mutex,
479 since list iterators are unaffected by insertion and only this
480 thread could erase the last item in the list.
483 LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
485 i->encoded->write_via_temp (
486 film()->j2c_path(i->reel, i->frame, i->eyes, true),
487 film()->j2c_path(i->reel, i->frame, i->eyes, false)
492 --_queued_full_in_memory;
493 _full_condition.notify_all ();
504 Writer::terminate_thread (bool can_throw)
506 boost::this_thread::disable_interruption dis;
508 boost::mutex::scoped_lock lock (_state_mutex);
511 _empty_condition.notify_all ();
512 _full_condition.notify_all ();
526 Writer::calculate_digests ()
528 auto job = _job.lock ();
530 job->sub (_("Computing digests"));
533 boost::asio::io_service service;
534 boost::thread_group pool;
536 auto work = make_shared<boost::asio::io_service::work>(service);
538 int const threads = max (1, Config::instance()->master_encoding_threads());
540 for (int i = 0; i < threads; ++i) {
541 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
544 std::function<void (float)> set_progress;
546 set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
548 set_progress = [](float) {
549 boost::this_thread::interruption_point();
553 for (auto& i: _reels) {
554 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
556 service.post (boost::bind (&Writer::calculate_referenced_digests, this, set_progress));
562 } catch (boost::thread_interrupted) {
563 /* join_all was interrupted, so we need to interrupt the threads
564 * in our pool then try again to join them.
566 pool.interrupt_all ();
574 /** @param output_dcp Path to DCP folder to write */
576 Writer::finish (boost::filesystem::path output_dcp)
578 if (_thread.joinable()) {
579 LOG_GENERAL_NC ("Terminating writer thread");
580 terminate_thread (true);
583 LOG_GENERAL_NC ("Finishing ReelWriters");
585 for (auto& i: _reels) {
586 write_hanging_text (i);
587 i.finish (output_dcp);
590 LOG_GENERAL_NC ("Writing XML");
592 dcp::DCP dcp (output_dcp);
594 auto cpl = make_shared<dcp::CPL>(
596 film()->dcp_content_type()->libdcp_kind(),
597 film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE
602 calculate_digests ();
606 for (auto& i: _reels) {
607 cpl->add (i.create_reel(_reel_assets, _fonts, output_dcp, _have_subtitles, _have_closed_captions));
612 auto creator = Config::instance()->dcp_creator();
613 if (creator.empty()) {
614 creator = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
617 auto issuer = Config::instance()->dcp_issuer();
618 if (issuer.empty()) {
619 issuer = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
622 cpl->set_creator (creator);
623 cpl->set_issuer (issuer);
625 cpl->set_ratings (film()->ratings());
627 vector<dcp::ContentVersion> cv;
628 for (auto i: film()->content_versions()) {
629 cv.push_back (dcp::ContentVersion(i));
632 cv = { dcp::ContentVersion("1") };
634 cpl->set_content_versions (cv);
636 cpl->set_full_content_title_text (film()->name());
637 cpl->set_full_content_title_text_language (film()->name_language());
638 if (film()->release_territory()) {
639 cpl->set_release_territory (*film()->release_territory());
641 cpl->set_version_number (film()->version_number());
642 cpl->set_status (film()->status());
643 if (film()->chain()) {
644 cpl->set_chain (*film()->chain());
646 if (film()->distributor()) {
647 cpl->set_distributor (*film()->distributor());
649 if (film()->facility()) {
650 cpl->set_facility (*film()->facility());
652 if (film()->luminance()) {
653 cpl->set_luminance (*film()->luminance());
655 if (film()->sign_language_video_language()) {
656 cpl->set_sign_language_video_language (*film()->sign_language_video_language());
659 auto ac = film()->mapped_audio_channels();
660 dcp::MCASoundField field = (
661 find(ac.begin(), ac.end(), static_cast<int>(dcp::Channel::BSL)) != ac.end() ||
662 find(ac.begin(), ac.end(), static_cast<int>(dcp::Channel::BSR)) != ac.end()
663 ) ? dcp::MCASoundField::SEVEN_POINT_ONE : dcp::MCASoundField::FIVE_POINT_ONE;
665 dcp::MainSoundConfiguration msc (field, film()->audio_channels());
667 if (static_cast<int>(i) < film()->audio_channels()) {
668 msc.set_mapping (i, static_cast<dcp::Channel>(i));
672 cpl->set_main_sound_configuration (msc.to_string());
673 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
674 cpl->set_main_picture_stored_area (film()->frame_size());
676 auto active_area = film()->active_area();
677 if (active_area.width > 0 && active_area.height > 0) {
678 /* It's not allowed to have a zero active area width or height */
679 cpl->set_main_picture_active_area (active_area);
682 auto sl = film()->subtitle_languages().second;
684 cpl->set_additional_subtitle_languages(sl);
687 auto signer = Config::instance()->signer_chain();
688 /* We did check earlier, but check again here to be on the safe side */
690 if (!signer->valid (&reason)) {
691 throw InvalidSignerError (reason);
697 dcp::LocalTime().as_string(),
700 Config::instance()->dcp_metadata_filename_format()
704 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
707 write_cover_sheet (output_dcp);
712 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
714 auto const cover = film()->file("COVER_SHEET.txt");
715 auto f = fopen_boost (cover, "w");
717 throw OpenFileError (cover, errno, OpenFileError::WRITE);
720 auto text = Config::instance()->cover_sheet ();
721 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
722 auto cpls = film()->cpls();
724 boost::algorithm::replace_all (text, "$CPL_FILENAME", cpls[0].cpl_file.filename().string());
726 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
727 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
729 auto audio_language = film()->audio_language();
730 if (audio_language) {
731 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_language->description());
733 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
736 auto subtitle_languages = film()->subtitle_languages();
737 if (subtitle_languages.first) {
738 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
740 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
743 boost::uintmax_t size = 0;
745 auto i = boost::filesystem::recursive_directory_iterator(output_dcp);
746 i != boost::filesystem::recursive_directory_iterator();
748 if (boost::filesystem::is_regular_file (i->path())) {
749 size += boost::filesystem::file_size (i->path());
753 if (size > (1000000000L)) {
754 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
756 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
759 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
760 auto description = String::compose("%1.%2", ch.first, ch.second);
762 if (description == "0.0") {
763 description = _("None");
764 } else if (description == "1.0") {
765 description = _("Mono");
766 } else if (description == "2.0") {
767 description = _("Stereo");
769 boost::algorithm::replace_all (text, "$AUDIO", description);
771 auto const hmsf = film()->length().split(film()->video_frame_rate());
773 if (hmsf.h == 0 && hmsf.m == 0) {
774 length = String::compose("%1s", hmsf.s);
775 } else if (hmsf.h == 0 && hmsf.m > 0) {
776 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
777 } else if (hmsf.h > 0 && hmsf.m > 0) {
778 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
781 boost::algorithm::replace_all (text, "$LENGTH", length);
783 checked_fwrite (text.c_str(), text.length(), f, cover);
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_nonexistant_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);
866 Writer::write (vector<FontData> fonts)
868 /* Just keep a list of unique fonts and we'll deal with them in ::finish */
870 for (auto const& i: fonts) {
872 for (auto& j: _fonts) {
879 _fonts.push_back (i);
886 operator< (QueueItem const & a, QueueItem const & b)
888 if (a.reel != b.reel) {
889 return a.reel < b.reel;
892 if (a.frame != b.frame) {
893 return a.frame < b.frame;
896 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
901 operator== (QueueItem const & a, QueueItem const & b)
903 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
908 Writer::set_encoder_threads (int threads)
910 boost::mutex::scoped_lock lm (_state_mutex);
911 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
912 _maximum_queue_size = threads * 16;
917 Writer::write (ReferencedReelAsset asset)
919 _reel_assets.push_back (asset);
924 Writer::video_reel (int frame) const
926 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
928 while (i < _reels.size() && !_reels[i].period().contains (t)) {
932 DCPOMATIC_ASSERT (i < _reels.size ());
938 Writer::set_digest_progress (Job* job, float progress)
940 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
942 _digest_progresses[boost::this_thread::get_id()] = progress;
943 float min_progress = FLT_MAX;
944 for (auto const& i: _digest_progresses) {
945 min_progress = min (min_progress, i.second);
948 job->set_progress (min_progress);
953 boost::this_thread::interruption_point();
957 /** Calculate hashes for any referenced MXF assets which do not already have one */
959 Writer::calculate_referenced_digests (std::function<void (float)> set_progress)
962 for (auto const& i: _reel_assets) {
963 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
964 if (file && !file->hash()) {
965 file->asset_ref().asset()->hash (set_progress);
966 file->set_hash (file->asset_ref().asset()->hash());
969 } catch (boost::thread_interrupted) {
970 /* set_progress contains an interruption_point, so any of these methods
971 * may throw thread_interrupted, at which point we just give up.
977 Writer::write_hanging_text (ReelWriter& reel)
979 vector<HangingText> new_hanging_texts;
980 for (auto i: _hanging_texts) {
981 if (i.period.from == reel.period().from) {
982 reel.write (i.text, i.type, i.track, i.period);
984 new_hanging_texts.push_back (i);
987 _hanging_texts = new_hanging_texts;