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;
79 ignore_progress (float)
85 /** @param j Job to report progress to, or 0.
86 * @param text_only true to enable only the text (subtitle/ccap) parts of the writer.
88 Writer::Writer (weak_ptr<const Film> weak_film, weak_ptr<Job> j, bool text_only)
89 : WeakConstFilm (weak_film)
91 /* These will be reset to sensible values when J2KEncoder is created */
92 , _maximum_frames_in_memory (8)
93 , _maximum_queue_size (8)
94 , _text_only (text_only)
96 auto job = _job.lock ();
99 auto const reels = film()->reels();
100 for (auto p: reels) {
101 _reels.push_back (ReelWriter(weak_film, p, job, reel_index++, reels.size(), text_only));
104 _last_written.resize (reels.size());
106 /* We can keep track of the current audio, subtitle and closed caption reels easily because audio
107 and captions arrive to the Writer in sequence. This is not so for video.
109 _audio_reel = _reels.begin ();
110 _subtitle_reel = _reels.begin ();
111 for (auto i: film()->closed_caption_tracks()) {
112 _caption_reels[i] = _reels.begin ();
114 _atmos_reel = _reels.begin ();
116 /* Check that the signer is OK */
118 if (!Config::instance()->signer_chain()->valid(&reason)) {
119 throw InvalidSignerError (reason);
128 _thread = boost::thread (boost::bind(&Writer::thread, this));
129 #ifdef DCPOMATIC_LINUX
130 pthread_setname_np (_thread.native_handle(), "writer");
139 terminate_thread (false);
144 /** Pass a video frame to the writer for writing to disk at some point.
145 * This method can be called with frames out of order.
146 * @param encoded JPEG2000-encoded data.
147 * @param frame Frame index within the DCP.
148 * @param eyes Eyes that this frame image is for.
151 Writer::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
153 boost::mutex::scoped_lock lock (_state_mutex);
155 while (_queued_full_in_memory > _maximum_frames_in_memory) {
156 /* There are too many full frames in memory; wake the main writer thread and
157 wait until it sorts everything out */
158 _empty_condition.notify_all ();
159 _full_condition.wait (lock);
163 qi.type = QueueItem::Type::FULL;
164 qi.encoded = encoded;
165 qi.reel = video_reel (frame);
166 qi.frame = frame - _reels[qi.reel].start ();
168 if (film()->three_d() && eyes == Eyes::BOTH) {
169 /* 2D material in a 3D DCP; fake the 3D */
170 qi.eyes = Eyes::LEFT;
171 _queue.push_back (qi);
172 ++_queued_full_in_memory;
173 qi.eyes = Eyes::RIGHT;
174 _queue.push_back (qi);
175 ++_queued_full_in_memory;
178 _queue.push_back (qi);
179 ++_queued_full_in_memory;
182 /* Now there's something to do: wake anything wait()ing on _empty_condition */
183 _empty_condition.notify_all ();
188 Writer::can_repeat (Frame frame) const
190 return frame > _reels[video_reel(frame)].start();
194 /** Repeat the last frame that was written to a reel as a new frame.
195 * @param frame Frame index within the DCP of the new (repeated) frame.
196 * @param eyes Eyes that this repeated frame image is for.
199 Writer::repeat (Frame frame, Eyes eyes)
201 boost::mutex::scoped_lock lock (_state_mutex);
203 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
204 /* The queue is too big, and the main writer thread can run and fix it, so
205 wake it and wait until it has done.
207 _empty_condition.notify_all ();
208 _full_condition.wait (lock);
212 qi.type = QueueItem::Type::REPEAT;
213 qi.reel = video_reel (frame);
214 qi.frame = frame - _reels[qi.reel].start ();
215 if (film()->three_d() && eyes == Eyes::BOTH) {
216 qi.eyes = Eyes::LEFT;
217 _queue.push_back (qi);
218 qi.eyes = Eyes::RIGHT;
219 _queue.push_back (qi);
222 _queue.push_back (qi);
225 /* Now there's something to do: wake anything wait()ing on _empty_condition */
226 _empty_condition.notify_all ();
231 Writer::fake_write (Frame frame, Eyes eyes)
233 boost::mutex::scoped_lock lock (_state_mutex);
235 while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
236 /* The queue is too big, and the main writer thread can run and fix it, so
237 wake it and wait until it has done.
239 _empty_condition.notify_all ();
240 _full_condition.wait (lock);
243 size_t const reel = video_reel (frame);
244 Frame const frame_in_reel = frame - _reels[reel].start ();
247 qi.type = QueueItem::Type::FAKE;
250 shared_ptr<InfoFileHandle> info_file = film()->info_file_handle(_reels[reel].period(), true);
251 qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
255 qi.frame = frame_in_reel;
256 if (film()->three_d() && eyes == Eyes::BOTH) {
257 qi.eyes = Eyes::LEFT;
258 _queue.push_back (qi);
259 qi.eyes = Eyes::RIGHT;
260 _queue.push_back (qi);
263 _queue.push_back (qi);
266 /* Now there's something to do: wake anything wait()ing on _empty_condition */
267 _empty_condition.notify_all ();
271 /** Write some audio frames to the DCP.
272 * @param audio Audio data.
273 * @param time Time of this data within the DCP.
274 * This method is not thread safe.
277 Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
279 DCPOMATIC_ASSERT (audio);
281 int const afr = film()->audio_frame_rate();
283 DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
285 /* The audio we get might span a reel boundary, and if so we have to write it in bits */
290 if (_audio_reel == _reels.end ()) {
291 /* This audio is off the end of the last reel; ignore it */
295 if (end <= _audio_reel->period().to) {
296 /* Easy case: we can write all the audio to this reel */
297 _audio_reel->write (audio);
299 } else if (_audio_reel->period().to <= t) {
300 /* This reel is entirely before the start of our audio; just skip the reel */
303 /* This audio is over a reel boundary; split the audio into two and write the first part */
304 DCPTime part_lengths[2] = {
305 _audio_reel->period().to - t,
306 end - _audio_reel->period().to
309 /* Be careful that part_lengths[0] + part_lengths[1] can't be bigger than audio->frames() */
310 Frame part_frames[2] = {
311 part_lengths[0].frames_ceil(afr),
312 part_lengths[1].frames_floor(afr)
315 DCPOMATIC_ASSERT ((part_frames[0] + part_frames[1]) <= audio->frames());
317 if (part_frames[0]) {
318 shared_ptr<AudioBuffers> part (new AudioBuffers(audio, part_frames[0], 0));
319 _audio_reel->write (part);
322 if (part_frames[1]) {
323 audio.reset (new AudioBuffers(audio, part_frames[1], part_frames[0]));
329 t += part_lengths[0];
336 Writer::write (shared_ptr<const dcp::AtmosFrame> atmos, DCPTime time, AtmosMetadata metadata)
338 if (_atmos_reel->period().to == time) {
340 DCPOMATIC_ASSERT (_atmos_reel != _reels.end());
343 /* We assume that we get a video frame's worth of data here */
344 _atmos_reel->write (atmos, metadata);
348 /** Caller must hold a lock on _state_mutex */
350 Writer::have_sequenced_image_at_queue_head ()
352 if (_queue.empty ()) {
357 auto const & f = _queue.front();
358 return _last_written[f.reel].next(f);
363 Writer::LastWritten::next (QueueItem qi) const
365 if (qi.eyes == Eyes::BOTH) {
367 return qi.frame == (_frame + 1);
372 if (_eyes == Eyes::LEFT && qi.frame == _frame && qi.eyes == Eyes::RIGHT) {
376 if (_eyes == Eyes::RIGHT && qi.frame == (_frame + 1) && qi.eyes == Eyes::LEFT) {
385 Writer::LastWritten::update (QueueItem qi)
396 start_of_thread ("Writer");
400 boost::mutex::scoped_lock lock (_state_mutex);
404 if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
405 /* We've got something to do: go and do it */
409 /* Nothing to do: wait until something happens which may indicate that we do */
410 LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
411 _empty_condition.wait (lock);
412 LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
415 /* We stop here if we have been asked to finish, and if either the queue
416 is empty or we do not have a sequenced image at its head (if this is the
417 case we will never terminate as no new frames will be sent once
420 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
421 /* (Hopefully temporarily) log anything that was not written */
422 if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
423 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
424 for (auto const& i: _queue) {
425 if (i.type == QueueItem::Type::FULL) {
426 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
428 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
435 /* Write any frames that we can write; i.e. those that are in sequence. */
436 while (have_sequenced_image_at_queue_head ()) {
437 auto qi = _queue.front ();
438 _last_written[qi.reel].update (qi);
440 if (qi.type == QueueItem::Type::FULL && qi.encoded) {
441 --_queued_full_in_memory;
446 auto& reel = _reels[qi.reel];
449 case QueueItem::Type::FULL:
450 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
452 qi.encoded.reset (new ArrayData(film()->j2c_path(qi.reel, qi.frame, qi.eyes, false)));
454 reel.write (qi.encoded, qi.frame, qi.eyes);
457 case QueueItem::Type::FAKE:
458 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
459 reel.fake_write (qi.size);
462 case QueueItem::Type::REPEAT:
463 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
464 reel.repeat_write (qi.frame, qi.eyes);
470 _full_condition.notify_all ();
473 while (_queued_full_in_memory > _maximum_frames_in_memory) {
474 /* Too many frames in memory which can't yet be written to the stream.
475 Write some FULL frames to disk.
478 /* Find one from the back of the queue */
480 auto i = _queue.rbegin ();
481 while (i != _queue.rend() && (i->type != QueueItem::Type::FULL || !i->encoded)) {
485 DCPOMATIC_ASSERT (i != _queue.rend());
487 /* For the log message below */
488 int const awaiting = _last_written[_queue.front().reel].frame() + 1;
491 /* i is valid here, even though we don't hold a lock on the mutex,
492 since list iterators are unaffected by insertion and only this
493 thread could erase the last item in the list.
496 LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
498 i->encoded->write_via_temp (
499 film()->j2c_path(i->reel, i->frame, i->eyes, true),
500 film()->j2c_path(i->reel, i->frame, i->eyes, false)
505 --_queued_full_in_memory;
506 _full_condition.notify_all ();
517 Writer::terminate_thread (bool can_throw)
519 boost::this_thread::disable_interruption dis;
521 boost::mutex::scoped_lock lock (_state_mutex);
524 _empty_condition.notify_all ();
525 _full_condition.notify_all ();
538 /** @param output_dcp Path to DCP folder to write */
540 Writer::finish (boost::filesystem::path output_dcp)
542 if (_thread.joinable()) {
543 LOG_GENERAL_NC ("Terminating writer thread");
544 terminate_thread (true);
547 LOG_GENERAL_NC ("Finishing ReelWriters");
549 for (auto& i: _reels) {
550 write_hanging_text (i);
551 i.finish (output_dcp);
554 LOG_GENERAL_NC ("Writing XML");
556 dcp::DCP dcp (output_dcp);
558 auto cpl = make_shared<dcp::CPL>(
560 film()->dcp_content_type()->libdcp_kind(),
561 film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE
566 /* Calculate digests for each reel in parallel */
568 auto job = _job.lock ();
570 job->sub (_("Computing digests"));
573 boost::asio::io_service service;
574 boost::thread_group pool;
576 auto work = make_shared<boost::asio::io_service::work>(service);
578 int const threads = max (1, Config::instance()->master_encoding_threads ());
580 for (int i = 0; i < threads; ++i) {
581 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
584 boost::function<void (float)> set_progress;
586 set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
588 set_progress = &ignore_progress;
591 for (auto& i: _reels) {
592 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
594 service.post (boost::bind (&Writer::calculate_referenced_digests, this, set_progress));
602 for (auto& i: _reels) {
603 cpl->add (i.create_reel(_reel_assets, _fonts, output_dcp, _have_subtitles, _have_closed_captions));
608 auto creator = Config::instance()->dcp_creator();
609 if (creator.empty()) {
610 creator = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
613 auto issuer = Config::instance()->dcp_issuer();
614 if (issuer.empty()) {
615 issuer = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
618 cpl->set_ratings (film()->ratings());
620 vector<dcp::ContentVersion> cv;
621 for (auto i: film()->content_versions()) {
622 cv.push_back (dcp::ContentVersion(i));
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());
646 auto ac = film()->mapped_audio_channels();
647 dcp::MCASoundField field = (
648 find(ac.begin(), ac.end(), static_cast<int>(dcp::Channel::BSL)) != ac.end() ||
649 find(ac.begin(), ac.end(), static_cast<int>(dcp::Channel::BSR)) != ac.end()
650 ) ? dcp::MCASoundField::SEVEN_POINT_ONE : dcp::MCASoundField::FIVE_POINT_ONE;
652 dcp::MainSoundConfiguration msc (field, film()->audio_channels());
654 if (static_cast<int>(i) < film()->audio_channels()) {
655 msc.set_mapping (i, static_cast<dcp::Channel>(i));
659 cpl->set_main_sound_configuration (msc.to_string());
660 cpl->set_main_sound_sample_rate (film()->audio_frame_rate());
661 cpl->set_main_picture_stored_area (film()->frame_size());
663 auto active_area = film()->active_area();
664 if (active_area.width > 0 && active_area.height > 0) {
665 /* It's not allowed to have a zero active area width or height */
666 cpl->set_main_picture_active_area (active_area);
669 auto sl = film()->subtitle_languages().second;
671 cpl->set_additional_subtitle_languages(sl);
674 auto signer = Config::instance()->signer_chain();
675 /* We did check earlier, but check again here to be on the safe side */
677 if (!signer->valid (&reason)) {
678 throw InvalidSignerError (reason);
684 dcp::LocalTime().as_string(),
687 Config::instance()->dcp_metadata_filename_format()
691 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
694 write_cover_sheet (output_dcp);
699 Writer::write_cover_sheet (boost::filesystem::path output_dcp)
701 auto const cover = film()->file("COVER_SHEET.txt");
702 auto f = fopen_boost (cover, "w");
704 throw OpenFileError (cover, errno, OpenFileError::WRITE);
707 auto text = Config::instance()->cover_sheet ();
708 boost::algorithm::replace_all (text, "$CPL_NAME", film()->name());
709 boost::algorithm::replace_all (text, "$TYPE", film()->dcp_content_type()->pretty_name());
710 boost::algorithm::replace_all (text, "$CONTAINER", film()->container()->container_nickname());
712 auto audio_languages = film()->audio_languages();
713 if (!audio_languages.empty()) {
714 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", audio_languages.front().description());
716 boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _("None"));
719 auto subtitle_languages = film()->subtitle_languages();
720 if (subtitle_languages.first) {
721 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.first->description());
723 boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _("None"));
726 boost::uintmax_t size = 0;
728 auto i = boost::filesystem::recursive_directory_iterator(output_dcp);
729 i != boost::filesystem::recursive_directory_iterator();
731 if (boost::filesystem::is_regular_file (i->path())) {
732 size += boost::filesystem::file_size (i->path());
736 if (size > (1000000000L)) {
737 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1GB", dcp::locale_convert<string>(size / 1000000000.0, 1, true)));
739 boost::algorithm::replace_all (text, "$SIZE", String::compose("%1MB", dcp::locale_convert<string>(size / 1000000.0, 1, true)));
742 auto ch = audio_channel_types (film()->mapped_audio_channels(), film()->audio_channels());
743 auto description = String::compose("%1.%2", ch.first, ch.second);
745 if (description == "0.0") {
746 description = _("None");
747 } else if (description == "1.0") {
748 description = _("Mono");
749 } else if (description == "2.0") {
750 description = _("Stereo");
752 boost::algorithm::replace_all (text, "$AUDIO", description);
754 auto const hmsf = film()->length().split(film()->video_frame_rate());
756 if (hmsf.h == 0 && hmsf.m == 0) {
757 length = String::compose("%1s", hmsf.s);
758 } else if (hmsf.h == 0 && hmsf.m > 0) {
759 length = String::compose("%1m%2s", hmsf.m, hmsf.s);
760 } else if (hmsf.h > 0 && hmsf.m > 0) {
761 length = String::compose("%1h%2m%3s", hmsf.h, hmsf.m, hmsf.s);
764 boost::algorithm::replace_all (text, "$LENGTH", length);
766 checked_fwrite (text.c_str(), text.length(), f, cover);
771 /** @param frame Frame index within the whole DCP.
772 * @return true if we can fake-write this frame.
775 Writer::can_fake_write (Frame frame) const
777 if (film()->encrypted()) {
778 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
782 /* We have to do a proper write of the first frame so that we can set up the JPEG2000
783 parameters in the asset writer.
786 auto const & reel = _reels[video_reel(frame)];
788 /* Make frame relative to the start of the reel */
789 frame -= reel.start ();
790 return (frame != 0 && frame < reel.first_nonexistant_frame());
794 /** @param track Closed caption track if type == TextType::CLOSED_CAPTION */
796 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
798 vector<ReelWriter>::iterator* reel = nullptr;
801 case TextType::OPEN_SUBTITLE:
802 reel = &_subtitle_reel;
803 _have_subtitles = true;
805 case TextType::CLOSED_CAPTION:
806 DCPOMATIC_ASSERT (track);
807 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
808 reel = &_caption_reels[*track];
809 _have_closed_captions.insert (*track);
812 DCPOMATIC_ASSERT (false);
815 DCPOMATIC_ASSERT (*reel != _reels.end());
816 while ((*reel)->period().to <= period.from) {
818 DCPOMATIC_ASSERT (*reel != _reels.end());
819 write_hanging_text (**reel);
822 auto back_off = [this](DCPTimePeriod period) {
823 period.to -= DCPTime::from_frames(2, film()->video_frame_rate());
827 if (period.to > (*reel)->period().to) {
828 /* This text goes off the end of the reel. Store parts of it that should go into
831 for (auto i = std::next(*reel); i != _reels.end(); ++i) {
832 auto overlap = i->period().overlap(period);
834 _hanging_texts.push_back (HangingText{text, type, track, back_off(*overlap)});
837 /* Back off from the reel boundary by a couple of frames to avoid tripping checks
838 * for subtitles being too close together.
840 period.to = (*reel)->period().to;
841 period = back_off(period);
844 (*reel)->write (text, type, track, period);
849 Writer::write (vector<FontData> fonts)
851 /* Just keep a list of unique fonts and we'll deal with them in ::finish */
853 for (auto const& i: fonts) {
855 for (auto& j: _fonts) {
862 _fonts.push_back (i);
869 operator< (QueueItem const & a, QueueItem const & b)
871 if (a.reel != b.reel) {
872 return a.reel < b.reel;
875 if (a.frame != b.frame) {
876 return a.frame < b.frame;
879 return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
884 operator== (QueueItem const & a, QueueItem const & b)
886 return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
891 Writer::set_encoder_threads (int threads)
893 boost::mutex::scoped_lock lm (_state_mutex);
894 _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
895 _maximum_queue_size = threads * 16;
900 Writer::write (ReferencedReelAsset asset)
902 _reel_assets.push_back (asset);
907 Writer::video_reel (int frame) const
909 auto t = DCPTime::from_frames (frame, film()->video_frame_rate());
911 while (i < _reels.size() && !_reels[i].period().contains (t)) {
915 DCPOMATIC_ASSERT (i < _reels.size ());
921 Writer::set_digest_progress (Job* job, float progress)
923 boost::mutex::scoped_lock lm (_digest_progresses_mutex);
925 _digest_progresses[boost::this_thread::get_id()] = progress;
926 float min_progress = FLT_MAX;
927 for (auto const& i: _digest_progresses) {
928 min_progress = min (min_progress, i.second);
931 job->set_progress (min_progress);
938 /** Calculate hashes for any referenced MXF assets which do not already have one */
940 Writer::calculate_referenced_digests (boost::function<void (float)> set_progress)
942 for (auto const& i: _reel_assets) {
943 auto file = dynamic_pointer_cast<dcp::ReelFileAsset>(i.asset);
944 if (file && !file->hash()) {
945 file->asset_ref().asset()->hash (set_progress);
946 file->set_hash (file->asset_ref().asset()->hash());
953 Writer::write_hanging_text (ReelWriter& reel)
955 vector<HangingText> new_hanging_texts;
956 for (auto i: _hanging_texts) {
957 if (i.period.from == reel.period().from) {
958 reel.write (i.text, i.type, i.track, i.period);
960 new_hanging_texts.push_back (i);
963 _hanging_texts = new_hanging_texts;