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/>.
23 #include "compose.hpp"
27 #include "dcpomatic_log.h"
28 #include "dcp_video.h"
29 #include "dcp_content_type.h"
30 #include "audio_mapping.h"
34 #include "audio_buffers.h"
36 #include "font_data.h"
38 #include "reel_writer.h"
39 #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;
59 using std::make_shared;
64 using std::shared_ptr;
68 using boost::optional;
69 #if BOOST_VERSION >= 106100
70 using namespace boost::placeholders;
74 using namespace dcpomatic;
77 /** @param j Job to report progress to, or 0.
78 * @param text_only true to enable only the text (subtitle/ccap) parts of the writer.
80 Writer::Writer (weak_ptr<const Film> weak_film, weak_ptr<Job> j, bool text_only)
81 : WeakConstFilm (weak_film)
83 /* These will be reset to sensible values when J2KEncoder is created */
84 , _maximum_frames_in_memory (8)
85 , _maximum_queue_size (8)
86 , _text_only (text_only)
88 auto job = _job.lock ();
91 auto const reels = film()->reels();
93 _reels.push_back (ReelWriter(weak_film, p, job, reel_index++, reels.size(), text_only));
96 _last_written.resize (reels.size());
98 /* We can keep track of the current audio, subtitle and closed caption reels easily because audio
99 and captions arrive to the Writer in sequence. This is not so for video.
101 _audio_reel = _reels.begin ();
102 _subtitle_reel = _reels.begin ();
103 for (auto i: film()->closed_caption_tracks()) {
104 _caption_reels[i] = _reels.begin ();
106 _atmos_reel = _reels.begin ();
108 /* Check that the signer is OK */
110 if (!Config::instance()->signer_chain()->valid(&reason)) {
111 throw InvalidSignerError (reason);
120 _thread = boost::thread (boost::bind(&Writer::thread, this));
121 #ifdef DCPOMATIC_LINUX
122 pthread_setname_np (_thread.native_handle(), "writer");
131 terminate_thread (false);
136 /** Pass a video frame to the writer for writing to disk at some point.
137 * This method can be called with frames out of order.
138 * @param encoded JPEG2000-encoded data.
139 * @param frame Frame index within the DCP.
140 * @param eyes Eyes that this frame image is for.
143 Writer::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
145 boost::mutex::scoped_lock lock (_state_mutex);
147 while (_queued_full_in_memory > _maximum_frames_in_memory) {
148 /* There are too many full frames in memory; wake the main writer thread and
149 wait until it sorts everything out */
150 _empty_condition.notify_all ();
151 _full_condition.wait (lock);
155 qi.type = QueueItem::Type::FULL;
156 qi.encoded = encoded;
157 qi.reel = video_reel (frame);
158 qi.frame = frame - _reels[qi.reel].start ();
160 if (film()->three_d() && eyes == Eyes::BOTH) {
161 /* 2D material in a 3D DCP; fake the 3D */
162 qi.eyes = Eyes::LEFT;
163 _queue.push_back (qi);
164 ++_queued_full_in_memory;
165 qi.eyes = Eyes::RIGHT;
166 _queue.push_back (qi);
167 ++_queued_full_in_memory;
170 _queue.push_back (qi);
171 ++_queued_full_in_memory;
174 /* Now there's something to do: wake anything wait()ing on _empty_condition */
175 _empty_condition.notify_all ();
180 Writer::can_repeat (Frame frame) const
182 return frame > _reels[video_reel(frame)].start();
186 /** Repeat the last frame that was written to a reel as a new frame.
187 * @param frame Frame index within the DCP of the new (repeated) frame.
188 * @param eyes Eyes that this repeated frame image is for.
191 Writer::repeat (Frame frame, Eyes eyes)
193 boost::mutex::scoped_lock lock (_state_mutex);
195 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
196 /* The queue is too big, and the main writer thread can run and fix it, so
197 wake it and wait until it has done.
199 _empty_condition.notify_all ();
200 _full_condition.wait (lock);
204 qi.type = QueueItem::Type::REPEAT;
205 qi.reel = video_reel (frame);
206 qi.frame = frame - _reels[qi.reel].start ();
207 if (film()->three_d() && eyes == Eyes::BOTH) {
208 qi.eyes = Eyes::LEFT;
209 _queue.push_back (qi);
210 qi.eyes = Eyes::RIGHT;
211 _queue.push_back (qi);
214 _queue.push_back (qi);
217 /* Now there's something to do: wake anything wait()ing on _empty_condition */
218 _empty_condition.notify_all ();
223 Writer::fake_write (Frame frame, Eyes eyes)
225 boost::mutex::scoped_lock lock (_state_mutex);
227 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
228 /* The queue is too big, and the main writer thread can run and fix it, so
229 wake it and wait until it has done.
231 _empty_condition.notify_all ();
232 _full_condition.wait (lock);
235 size_t const reel = video_reel (frame);
236 Frame const frame_in_reel = frame - _reels[reel].start ();
239 qi.type = QueueItem::Type::FAKE;
242 shared_ptr<InfoFileHandle> info_file = film()->info_file_handle(_reels[reel].period(), true);
243 qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
247 qi.frame = frame_in_reel;
248 if (film()->three_d() && eyes == Eyes::BOTH) {
249 qi.eyes = Eyes::LEFT;
250 _queue.push_back (qi);
251 qi.eyes = Eyes::RIGHT;
252 _queue.push_back (qi);
255 _queue.push_back (qi);
258 /* Now there's something to do: wake anything wait()ing on _empty_condition */
259 _empty_condition.notify_all ();
263 /** Write some audio frames to the DCP.
264 * @param audio Audio data.
265 * @param time Time of this data within the DCP.
266 * This method is not thread safe.
269 Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
271 DCPOMATIC_ASSERT (audio);
273 int const afr = film()->audio_frame_rate();
275 DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
277 /* The audio we get might span a reel boundary, and if so we have to write it in bits */
282 if (_audio_reel == _reels.end ()) {
283 /* This audio is off the end of the last reel; ignore it */
287 if (end <= _audio_reel->period().to) {
288 /* Easy case: we can write all the audio to this reel */
289 _audio_reel->write (audio);
291 } else if (_audio_reel->period().to <= t) {
292 /* This reel is entirely before the start of our audio; just skip the reel */
295 /* This audio is over a reel boundary; split the audio into two and write the first part */
296 DCPTime part_lengths[2] = {
297 _audio_reel->period().to - t,
298 end - _audio_reel->period().to
301 /* Be careful that part_lengths[0] + part_lengths[1] can't be bigger than audio->frames() */
302 Frame part_frames[2] = {
303 part_lengths[0].frames_ceil(afr),
304 part_lengths[1].frames_floor(afr)
307 DCPOMATIC_ASSERT ((part_frames[0] + part_frames[1]) <= audio->frames());
309 if (part_frames[0]) {
310 shared_ptr<AudioBuffers> part (new AudioBuffers(audio, part_frames[0], 0));
311 _audio_reel->write (part);
314 if (part_frames[1]) {
315 audio.reset (new AudioBuffers(audio, part_frames[1], part_frames[0]));
321 t += part_lengths[0];
328 Writer::write (shared_ptr<const dcp::AtmosFrame> atmos, DCPTime time, AtmosMetadata metadata)
330 if (_atmos_reel->period().to == time) {
332 DCPOMATIC_ASSERT (_atmos_reel != _reels.end());
335 /* We assume that we get a video frame's worth of data here */
336 _atmos_reel->write (atmos, metadata);
340 /** Caller must hold a lock on _state_mutex */
342 Writer::have_sequenced_image_at_queue_head ()
344 if (_queue.empty ()) {
349 auto const & f = _queue.front();
350 return _last_written[f.reel].next(f);
355 Writer::LastWritten::next (QueueItem qi) const
357 if (qi.eyes == Eyes::BOTH) {
359 return qi.frame == (_frame + 1);
364 if (_eyes == Eyes::LEFT && qi.frame == _frame && qi.eyes == Eyes::RIGHT) {
368 if (_eyes == Eyes::RIGHT && qi.frame == (_frame + 1) && qi.eyes == Eyes::LEFT) {
377 Writer::LastWritten::update (QueueItem qi)
388 start_of_thread ("Writer");
392 boost::mutex::scoped_lock lock (_state_mutex);
396 if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
397 /* We've got something to do: go and do it */
401 /* Nothing to do: wait until something happens which may indicate that we do */
402 LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
403 _empty_condition.wait (lock);
404 LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
407 /* We stop here if we have been asked to finish, and if either the queue
408 is empty or we do not have a sequenced image at its head (if this is the
409 case we will never terminate as no new frames will be sent once
412 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
413 /* (Hopefully temporarily) log anything that was not written */
414 if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
415 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
416 for (auto const& i: _queue) {
417 if (i.type == QueueItem::Type::FULL) {
418 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
420 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
427 /* Write any frames that we can write; i.e. those that are in sequence. */
428 while (have_sequenced_image_at_queue_head ()) {
429 auto qi = _queue.front ();
430 _last_written[qi.reel].update (qi);
432 if (qi.type == QueueItem::Type::FULL && qi.encoded) {
433 --_queued_full_in_memory;
438 auto& reel = _reels[qi.reel];
441 case QueueItem::Type::FULL:
442 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
444 qi.encoded.reset (new ArrayData(film()->j2c_path(qi.reel, qi.frame, qi.eyes, false)));
446 reel.write (qi.encoded, qi.frame, qi.eyes);
449 case QueueItem::Type::FAKE:
450 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
451 reel.fake_write (qi.size);
454 case QueueItem::Type::REPEAT:
455 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
456 reel.repeat_write (qi.frame, qi.eyes);
462 _full_condition.notify_all ();
465 while (_queued_full_in_memory > _maximum_frames_in_memory) {
466 /* Too many frames in memory which can't yet be written to the stream.
467 Write some FULL frames to disk.
470 /* Find one from the back of the queue */
472 auto i = _queue.rbegin ();
473 while (i != _queue.rend() && (i->type != QueueItem::Type::FULL || !i->encoded)) {
477 DCPOMATIC_ASSERT (i != _queue.rend());
479 /* For the log message below */
480 int const awaiting = _last_written[_queue.front().reel].frame() + 1;
483 /* i is valid here, even though we don't hold a lock on the mutex,
484 since list iterators are unaffected by insertion and only this
485 thread could erase the last item in the list.
488 LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
490 i->encoded->write_via_temp (
491 film()->j2c_path(i->reel, i->frame, i->eyes, true),
492 film()->j2c_path(i->reel, i->frame, i->eyes, false)
497 --_queued_full_in_memory;
498 _full_condition.notify_all ();
509 Writer::terminate_thread (bool can_throw)
511 boost::this_thread::disable_interruption dis;
513 boost::mutex::scoped_lock lock (_state_mutex);
516 _empty_condition.notify_all ();
517 _full_condition.notify_all ();
531 Writer::calculate_digests ()
533 auto job = _job.lock ();
535 job->sub (_("Computing digests"));
538 boost::asio::io_service service;
539 boost::thread_group pool;
541 auto work = make_shared<boost::asio::io_service::work>(service);
543 int const threads = max (1, Config::instance()->master_encoding_threads());
545 for (int i = 0; i < threads; ++i) {
546 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
549 std::function<void (float)> set_progress;
551 set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
553 set_progress = [](float) {
554 boost::this_thread::interruption_point();
558 for (auto& i: _reels) {
559 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
561 service.post (boost::bind (&Writer::calculate_referenced_digests, this, set_progress));
567 } catch (boost::thread_interrupted) {
568 /* join_all was interrupted, so we need to interrupt the threads
569 * in our pool then try again to join them.
571 pool.interrupt_all ();
579 /** @param output_dcp Path to DCP folder to write */
581 Writer::finish (boost::filesystem::path output_dcp)
583 if (_thread.joinable()) {
584 LOG_GENERAL_NC ("Terminating writer thread");
585 terminate_thread (true);
588 LOG_GENERAL_NC ("Finishing ReelWriters");
590 for (auto& i: _reels) {
591 write_hanging_text (i);
592 i.finish (output_dcp);
595 LOG_GENERAL_NC ("Writing XML");
597 dcp::DCP dcp (output_dcp);
599 auto cpl = make_shared<dcp::CPL>(
601 film()->dcp_content_type()->libdcp_kind(),
602 film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE
607 calculate_digests ();
611 for (auto& i: _reels) {
612 cpl->add (i.create_reel(_reel_assets, _fonts, output_dcp, _have_subtitles, _have_closed_captions));
617 auto creator = Config::instance()->dcp_creator();
618 if (creator.empty()) {
619 creator = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
622 auto issuer = Config::instance()->dcp_issuer();
623 if (issuer.empty()) {
624 issuer = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
627 cpl->set_creator (creator);
628 cpl->set_issuer (issuer);
630 cpl->set_ratings (film()->ratings());
632 vector<dcp::ContentVersion> cv;
633 for (auto i: film()->content_versions()) {
634 cv.push_back (dcp::ContentVersion(i));
636 cpl->set_content_versions (cv);
638 cpl->set_full_content_title_text (film()->name());
639 cpl->set_full_content_title_text_language (film()->name_language());
640 if (film()->release_territory()) {
641 cpl->set_release_territory (*film()->release_territory());
643 cpl->set_version_number (film()->version_number());
644 cpl->set_status (film()->status());
645 if (film()->chain()) {
646 cpl->set_chain (*film()->chain());
648 if (film()->distributor()) {
649 cpl->set_distributor (*film()->distributor());
651 if (film()->facility()) {
652 cpl->set_facility (*film()->facility());
654 if (film()->luminance()) {
655 cpl->set_luminance (*film()->luminance());
658 auto ac = film()->mapped_audio_channels();
659 dcp::MCASoundField field = (
660 find(ac.begin(), ac.end(), static_cast<int>(dcp::Channel::BSL)) != ac.end() ||
661 find(ac.begin(), ac.end(), static_cast<int>(dcp::Channel::BSR)) != ac.end()
662 ) ? dcp::MCASoundField::SEVEN_POINT_ONE : dcp::MCASoundField::FIVE_POINT_ONE;
664 dcp::MainSoundConfiguration msc (field, film()->audio_channels());
666 if (static_cast<int>(i) < film()->audio_channels()) {
667 msc.set_mapping (i, static_cast<dcp::Channel>(i));
671 cpl->set_main_sound_configuration (msc.to_string());
672 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
673 cpl->set_main_picture_stored_area (film()->frame_size());
675 auto active_area = film()->active_area();
676 if (active_area.width > 0 && active_area.height > 0) {
677 /* It's not allowed to have a zero active area width or height */
678 cpl->set_main_picture_active_area (active_area);
681 auto sl = film()->subtitle_languages().second;
683 cpl->set_additional_subtitle_languages(sl);
686 auto signer = Config::instance()->signer_chain();
687 /* We did check earlier, but check again here to be on the safe side */
689 if (!signer->valid (&reason)) {
690 throw InvalidSignerError (reason);
696 dcp::LocalTime().as_string(),
699 Config::instance()->dcp_metadata_filename_format()
703 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
706 write_cover_sheet (output_dcp);
711 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
713 auto const cover = film()->file("COVER_SHEET.txt");
714 auto f = fopen_boost (cover, "w");
716 throw OpenFileError (cover, errno, OpenFileError::WRITE);
719 auto text = Config::instance()->cover_sheet ();
720 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
721 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
722 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
724 auto audio_language = film()->audio_language();
725 if (audio_language) {
726 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_language->description());
728 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
731 auto subtitle_languages = film()->subtitle_languages();
732 if (subtitle_languages.first) {
733 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
735 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
738 boost::uintmax_t size = 0;
740 auto i = boost::filesystem::recursive_directory_iterator(output_dcp);
741 i != boost::filesystem::recursive_directory_iterator();
743 if (boost::filesystem::is_regular_file (i->path())) {
744 size += boost::filesystem::file_size (i->path());
748 if (size > (1000000000L)) {
749 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
751 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
754 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
755 auto description = String::compose("%1.%2", ch.first, ch.second);
757 if (description == "0.0") {
758 description = _("None");
759 } else if (description == "1.0") {
760 description = _("Mono");
761 } else if (description == "2.0") {
762 description = _("Stereo");
764 boost::algorithm::replace_all (text, "$AUDIO", description);
766 auto const hmsf = film()->length().split(film()->video_frame_rate());
768 if (hmsf.h == 0 && hmsf.m == 0) {
769 length = String::compose("%1s", hmsf.s);
770 } else if (hmsf.h == 0 && hmsf.m > 0) {
771 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
772 } else if (hmsf.h > 0 && hmsf.m > 0) {
773 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
776 boost::algorithm::replace_all (text, "$LENGTH", length);
778 checked_fwrite (text.c_str(), text.length(), f, cover);
783 /** @param frame Frame index within the whole DCP.
784 * @return true if we can fake-write this frame.
787 Writer::can_fake_write (Frame frame) const
789 if (film()->encrypted()) {
790 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
794 /* We have to do a proper write of the first frame so that we can set up the JPEG2000
795 parameters in the asset writer.
798 auto const & reel = _reels[video_reel(frame)];
800 /* Make frame relative to the start of the reel */
801 frame -= reel.start ();
802 return (frame != 0 && frame < reel.first_nonexistant_frame());
806 /** @param track Closed caption track if type == TextType::CLOSED_CAPTION */
808 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
810 vector<ReelWriter>::iterator* reel = nullptr;
813 case TextType::OPEN_SUBTITLE:
814 reel = &_subtitle_reel;
815 _have_subtitles = true;
817 case TextType::CLOSED_CAPTION:
818 DCPOMATIC_ASSERT (track);
819 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
820 reel = &_caption_reels[*track];
821 _have_closed_captions.insert (*track);
824 DCPOMATIC_ASSERT (false);
827 DCPOMATIC_ASSERT (*reel != _reels.end());
828 while ((*reel)->period().to <= period.from) {
830 DCPOMATIC_ASSERT (*reel != _reels.end());
831 write_hanging_text (**reel);
834 auto back_off = [this](DCPTimePeriod period) {
835 period.to -= DCPTime::from_frames(2, film()->video_frame_rate());
839 if (period.to > (*reel)->period().to) {
840 /* This text goes off the end of the reel. Store parts of it that should go into
843 for (auto i = std::next(*reel); i != _reels.end(); ++i) {
844 auto overlap = i->period().overlap(period);
846 _hanging_texts.push_back (HangingText{text, type, track, back_off(*overlap)});
849 /* Back off from the reel boundary by a couple of frames to avoid tripping checks
850 * for subtitles being too close together.
852 period.to = (*reel)->period().to;
853 period = back_off(period);
856 (*reel)->write (text, type, track, period);
861 Writer::write (vector<FontData> fonts)
863 /* Just keep a list of unique fonts and we'll deal with them in ::finish */
865 for (auto const& i: fonts) {
867 for (auto& j: _fonts) {
874 _fonts.push_back (i);
881 operator< (QueueItem const & a, QueueItem const & b)
883 if (a.reel != b.reel) {
884 return a.reel < b.reel;
887 if (a.frame != b.frame) {
888 return a.frame < b.frame;
891 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
896 operator== (QueueItem const & a, QueueItem const & b)
898 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
903 Writer::set_encoder_threads (int threads)
905 boost::mutex::scoped_lock lm (_state_mutex);
906 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
907 _maximum_queue_size = threads * 16;
912 Writer::write (ReferencedReelAsset asset)
914 _reel_assets.push_back (asset);
919 Writer::video_reel (int frame) const
921 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
923 while (i < _reels.size() && !_reels[i].period().contains (t)) {
927 DCPOMATIC_ASSERT (i < _reels.size ());
933 Writer::set_digest_progress (Job* job, float progress)
935 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
937 _digest_progresses[boost::this_thread::get_id()] = progress;
938 float min_progress = FLT_MAX;
939 for (auto const& i: _digest_progresses) {
940 min_progress = min (min_progress, i.second);
943 job->set_progress (min_progress);
948 boost::this_thread::interruption_point();
952 /** Calculate hashes for any referenced MXF assets which do not already have one */
954 Writer::calculate_referenced_digests (std::function<void (float)> set_progress)
957 for (auto const& i: _reel_assets) {
958 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
959 if (file && !file->hash()) {
960 file->asset_ref().asset()->hash (set_progress);
961 file->set_hash (file->asset_ref().asset()->hash());
964 } catch (boost::thread_interrupted) {
965 /* set_progress contains an interruption_point, so any of these methods
966 * may throw thread_interrupted, at which point we just give up.
972 Writer::write_hanging_text (ReelWriter& reel)
974 vector<HangingText> new_hanging_texts;
975 for (auto i: _hanging_texts) {
976 if (i.period.from == reel.period().from) {
977 reel.write (i.text, i.type, i.track, i.period);
979 new_hanging_texts.push_back (i);
982 _hanging_texts = new_hanging_texts;