8682437b37888682959a96bf0cd286f9d6ac29f9
[dcpomatic.git] / src / lib / writer.cc
1 /*
2     Copyright (C) 2012-2019 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
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.
10
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.
15
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/>.
18
19 */
20
21 #include "writer.h"
22 #include "compose.hpp"
23 #include "film.h"
24 #include "ratio.h"
25 #include "log.h"
26 #include "dcpomatic_log.h"
27 #include "dcp_video.h"
28 #include "dcp_content_type.h"
29 #include "audio_mapping.h"
30 #include "config.h"
31 #include "job.h"
32 #include "cross.h"
33 #include "audio_buffers.h"
34 #include "version.h"
35 #include "font.h"
36 #include "util.h"
37 #include "reel_writer.h"
38 #include "text_content.h"
39 #include <dcp/cpl.h>
40 #include <dcp/locale_convert.h>
41 #include <boost/foreach.hpp>
42 #include <fstream>
43 #include <cerrno>
44 #include <iostream>
45 #include <cfloat>
46
47 #include "i18n.h"
48
49 /* OS X strikes again */
50 #undef set_key
51
52 using std::make_pair;
53 using std::pair;
54 using std::string;
55 using std::list;
56 using std::cout;
57 using std::map;
58 using std::min;
59 using std::max;
60 using std::vector;
61 using boost::shared_ptr;
62 using boost::weak_ptr;
63 using boost::dynamic_pointer_cast;
64 using boost::optional;
65 using dcp::Data;
66 using namespace dcpomatic;
67
68 Writer::Writer (shared_ptr<const Film> film, weak_ptr<Job> j)
69         : _film (film)
70         , _job (j)
71         , _finish (false)
72         , _queued_full_in_memory (0)
73         /* These will be reset to sensible values when J2KEncoder is created */
74         , _maximum_frames_in_memory (8)
75         , _maximum_queue_size (8)
76         , _full_written (0)
77         , _fake_written (0)
78         , _repeat_written (0)
79         , _pushed_to_disk (0)
80 {
81         shared_ptr<Job> job = _job.lock ();
82         DCPOMATIC_ASSERT (job);
83
84         int reel_index = 0;
85         list<DCPTimePeriod> const reels = _film->reels ();
86         BOOST_FOREACH (DCPTimePeriod p, reels) {
87                 _reels.push_back (ReelWriter (film, p, job, reel_index++, reels.size(), _film->content_summary(p)));
88         }
89
90         _last_written.resize (reels.size());
91
92         /* We can keep track of the current audio, subtitle and closed caption reels easily because audio
93            and captions arrive to the Writer in sequence.  This is not so for video.
94         */
95         _audio_reel = _reels.begin ();
96         _subtitle_reel = _reels.begin ();
97         BOOST_FOREACH (DCPTextTrack i, _film->closed_caption_tracks()) {
98                 _caption_reels[i] = _reels.begin ();
99         }
100
101         /* Check that the signer is OK */
102         string reason;
103         if (!Config::instance()->signer_chain()->valid(&reason)) {
104                 throw InvalidSignerError (reason);
105         }
106 }
107
108 void
109 Writer::start ()
110 {
111         _thread = boost::thread (boost::bind(&Writer::thread, this));
112 #ifdef DCPOMATIC_LINUX
113         pthread_setname_np (_thread.native_handle(), "writer");
114 #endif
115 }
116
117 Writer::~Writer ()
118 {
119         terminate_thread (false);
120 }
121
122 /** Pass a video frame to the writer for writing to disk at some point.
123  *  This method can be called with frames out of order.
124  *  @param encoded JPEG2000-encoded data.
125  *  @param frame Frame index within the DCP.
126  *  @param eyes Eyes that this frame image is for.
127  */
128 void
129 Writer::write (Data encoded, Frame frame, Eyes eyes)
130 {
131         boost::mutex::scoped_lock lock (_state_mutex);
132
133         while (_queued_full_in_memory > _maximum_frames_in_memory) {
134                 /* There are too many full frames in memory; wake the main writer thread and
135                    wait until it sorts everything out */
136                 _empty_condition.notify_all ();
137                 _full_condition.wait (lock);
138         }
139
140         QueueItem qi;
141         qi.type = QueueItem::FULL;
142         qi.encoded = encoded;
143         qi.reel = video_reel (frame);
144         qi.frame = frame - _reels[qi.reel].start ();
145
146         if (_film->three_d() && eyes == EYES_BOTH) {
147                 /* 2D material in a 3D DCP; fake the 3D */
148                 qi.eyes = EYES_LEFT;
149                 _queue.push_back (qi);
150                 ++_queued_full_in_memory;
151                 qi.eyes = EYES_RIGHT;
152                 _queue.push_back (qi);
153                 ++_queued_full_in_memory;
154         } else {
155                 qi.eyes = eyes;
156                 _queue.push_back (qi);
157                 ++_queued_full_in_memory;
158         }
159
160         /* Now there's something to do: wake anything wait()ing on _empty_condition */
161         _empty_condition.notify_all ();
162 }
163
164 bool
165 Writer::can_repeat (Frame frame) const
166 {
167         return frame > _reels[video_reel(frame)].start();
168 }
169
170 /** Repeat the last frame that was written to a reel as a new frame.
171  *  @param frame Frame index within the DCP of the new (repeated) frame.
172  *  @param eyes Eyes that this repeated frame image is for.
173  */
174 void
175 Writer::repeat (Frame frame, Eyes eyes)
176 {
177         boost::mutex::scoped_lock lock (_state_mutex);
178
179         while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
180                 /* The queue is too big, and the main writer thread can run and fix it, so
181                    wake it and wait until it has done.
182                 */
183                 _empty_condition.notify_all ();
184                 _full_condition.wait (lock);
185         }
186
187         QueueItem qi;
188         qi.type = QueueItem::REPEAT;
189         qi.reel = video_reel (frame);
190         qi.frame = frame - _reels[qi.reel].start ();
191         if (_film->three_d() && eyes == EYES_BOTH) {
192                 qi.eyes = EYES_LEFT;
193                 _queue.push_back (qi);
194                 qi.eyes = EYES_RIGHT;
195                 _queue.push_back (qi);
196         } else {
197                 qi.eyes = eyes;
198                 _queue.push_back (qi);
199         }
200
201         /* Now there's something to do: wake anything wait()ing on _empty_condition */
202         _empty_condition.notify_all ();
203 }
204
205 void
206 Writer::fake_write (Frame frame, Eyes eyes)
207 {
208         boost::mutex::scoped_lock lock (_state_mutex);
209
210         while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
211                 /* The queue is too big, and the main writer thread can run and fix it, so
212                    wake it and wait until it has done.
213                 */
214                 _empty_condition.notify_all ();
215                 _full_condition.wait (lock);
216         }
217
218         size_t const reel = video_reel (frame);
219         Frame const frame_in_reel = frame - _reels[reel].start ();
220
221         QueueItem qi;
222         qi.type = QueueItem::FAKE;
223
224         {
225                 shared_ptr<InfoFileHandle> info_file = _film->info_file_handle(_reels[reel].period(), true);
226                 qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
227         }
228
229         qi.reel = reel;
230         qi.frame = frame_in_reel;
231         if (_film->three_d() && eyes == EYES_BOTH) {
232                 qi.eyes = EYES_LEFT;
233                 _queue.push_back (qi);
234                 qi.eyes = EYES_RIGHT;
235                 _queue.push_back (qi);
236         } else {
237                 qi.eyes = eyes;
238                 _queue.push_back (qi);
239         }
240
241         /* Now there's something to do: wake anything wait()ing on _empty_condition */
242         _empty_condition.notify_all ();
243 }
244
245 /** Write some audio frames to the DCP.
246  *  @param audio Audio data.
247  *  @param time Time of this data within the DCP.
248  *  This method is not thread safe.
249  */
250 void
251 Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
252 {
253         DCPOMATIC_ASSERT (audio);
254
255         int const afr = _film->audio_frame_rate();
256
257         DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
258
259         /* The audio we get might span a reel boundary, and if so we have to write it in bits */
260
261         DCPTime t = time;
262         while (t < end) {
263
264                 if (_audio_reel == _reels.end ()) {
265                         /* This audio is off the end of the last reel; ignore it */
266                         return;
267                 }
268
269                 if (end <= _audio_reel->period().to) {
270                         /* Easy case: we can write all the audio to this reel */
271                         _audio_reel->write (audio);
272                         t = end;
273                 } else if (_audio_reel->period().to <= t) {
274                         /* This reel is entirely before the start of our audio; just skip the reel */
275                         ++_audio_reel;
276                 } else {
277                         /* This audio is over a reel boundary; split the audio into two and write the first part */
278                         DCPTime part_lengths[2] = {
279                                 _audio_reel->period().to - t,
280                                 end - _audio_reel->period().to
281                         };
282
283                         Frame part_frames[2] = {
284                                 part_lengths[0].frames_ceil(afr),
285                                 part_lengths[1].frames_ceil(afr)
286                         };
287
288                         if (part_frames[0]) {
289                                 shared_ptr<AudioBuffers> part (new AudioBuffers(audio, part_frames[0], 0));
290                                 _audio_reel->write (part);
291                         }
292
293                         if (part_frames[1]) {
294                                 audio.reset (new AudioBuffers(audio, part_frames[1], part_frames[0]));
295                         } else {
296                                 audio.reset ();
297                         }
298
299                         ++_audio_reel;
300                         t += part_lengths[0];
301                 }
302         }
303 }
304
305
306 /** Caller must hold a lock on _state_mutex */
307 bool
308 Writer::have_sequenced_image_at_queue_head ()
309 {
310         if (_queue.empty ()) {
311                 return false;
312         }
313
314         _queue.sort ();
315         QueueItem const & f = _queue.front();
316         return _last_written[f.reel].next(f);
317 }
318
319
320 bool
321 Writer::LastWritten::next (QueueItem qi) const
322 {
323         if (qi.eyes == EYES_BOTH) {
324                 /* 2D */
325                 return qi.frame == (_frame + 1);
326         }
327
328         /* 3D */
329
330         if (_eyes == EYES_LEFT && qi.frame == _frame && qi.eyes == EYES_RIGHT) {
331                 return true;
332         }
333
334         if (_eyes == EYES_RIGHT && qi.frame == (_frame + 1) && qi.eyes == EYES_LEFT) {
335                 return true;
336         }
337
338         return false;
339 }
340
341
342 void
343 Writer::LastWritten::update (QueueItem qi)
344 {
345         _frame = qi.frame;
346         _eyes = qi.eyes;
347 }
348
349
350 void
351 Writer::thread ()
352 try
353 {
354         while (true)
355         {
356                 boost::mutex::scoped_lock lock (_state_mutex);
357
358                 while (true) {
359
360                         if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
361                                 /* We've got something to do: go and do it */
362                                 break;
363                         }
364
365                         /* Nothing to do: wait until something happens which may indicate that we do */
366                         LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
367                         _empty_condition.wait (lock);
368                         LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
369                 }
370
371                 if (_finish && _queue.empty()) {
372                         return;
373                 }
374
375                 /* We stop here if we have been asked to finish, and if either the queue
376                    is empty or we do not have a sequenced image at its head (if this is the
377                    case we will never terminate as no new frames will be sent once
378                    _finish is true).
379                 */
380                 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
381                         /* (Hopefully temporarily) log anything that was not written */
382                         if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
383                                 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
384                                 BOOST_FOREACH (QueueItem const& i, _queue) {
385                                         if (i.type == QueueItem::FULL) {
386                                                 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i.frame, (int) i.eyes);
387                                         } else {
388                                                 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
389                                         }
390                                 }
391                         }
392                         return;
393                 }
394
395                 /* Write any frames that we can write; i.e. those that are in sequence. */
396                 while (have_sequenced_image_at_queue_head ()) {
397                         QueueItem qi = _queue.front ();
398                         _last_written[qi.reel].update (qi);
399                         _queue.pop_front ();
400                         if (qi.type == QueueItem::FULL && qi.encoded) {
401                                 --_queued_full_in_memory;
402                         }
403
404                         lock.unlock ();
405
406                         ReelWriter& reel = _reels[qi.reel];
407
408                         switch (qi.type) {
409                         case QueueItem::FULL:
410                                 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
411                                 if (!qi.encoded) {
412                                         qi.encoded = Data (_film->j2c_path (qi.reel, qi.frame, qi.eyes, false));
413                                 }
414                                 reel.write (qi.encoded, qi.frame, qi.eyes);
415                                 ++_full_written;
416                                 break;
417                         case QueueItem::FAKE:
418                                 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
419                                 reel.fake_write (qi.size);
420                                 ++_fake_written;
421                                 break;
422                         case QueueItem::REPEAT:
423                                 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
424                                 reel.repeat_write (qi.frame, qi.eyes);
425                                 ++_repeat_written;
426                                 break;
427                         }
428
429                         lock.lock ();
430                         _full_condition.notify_all ();
431                 }
432
433                 while (_queued_full_in_memory > _maximum_frames_in_memory) {
434                         /* Too many frames in memory which can't yet be written to the stream.
435                            Write some FULL frames to disk.
436                         */
437
438                         /* Find one from the back of the queue */
439                         _queue.sort ();
440                         list<QueueItem>::reverse_iterator i = _queue.rbegin ();
441                         while (i != _queue.rend() && (i->type != QueueItem::FULL || !i->encoded)) {
442                                 ++i;
443                         }
444
445                         DCPOMATIC_ASSERT (i != _queue.rend());
446                         ++_pushed_to_disk;
447                         /* For the log message below */
448                         int const awaiting = _last_written[_queue.front().reel].frame() + 1;
449                         lock.unlock ();
450
451                         /* i is valid here, even though we don't hold a lock on the mutex,
452                            since list iterators are unaffected by insertion and only this
453                            thread could erase the last item in the list.
454                         */
455
456                         LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
457
458                         i->encoded->write_via_temp (
459                                 _film->j2c_path (i->reel, i->frame, i->eyes, true),
460                                 _film->j2c_path (i->reel, i->frame, i->eyes, false)
461                                 );
462
463                         lock.lock ();
464                         i->encoded.reset ();
465                         --_queued_full_in_memory;
466                         _full_condition.notify_all ();
467                 }
468         }
469 }
470 catch (...)
471 {
472         store_current ();
473 }
474
475 void
476 Writer::terminate_thread (bool can_throw)
477 {
478         boost::mutex::scoped_lock lock (_state_mutex);
479         if (!_thread.joinable()) {
480                 return;
481         }
482
483         _finish = true;
484         _empty_condition.notify_all ();
485         _full_condition.notify_all ();
486         lock.unlock ();
487
488         _thread.join ();
489
490         if (can_throw) {
491                 rethrow ();
492         }
493 }
494
495 void
496 Writer::finish ()
497 {
498         if (!_thread.joinable()) {
499                 return;
500         }
501
502         LOG_GENERAL_NC ("Terminating writer thread");
503
504         terminate_thread (true);
505
506         LOG_GENERAL_NC ("Finishing ReelWriters");
507
508         BOOST_FOREACH (ReelWriter& i, _reels) {
509                 i.finish ();
510         }
511
512         LOG_GENERAL_NC ("Writing XML");
513
514         dcp::DCP dcp (_film->dir (_film->dcp_name()));
515
516         shared_ptr<dcp::CPL> cpl (
517                 new dcp::CPL (
518                         _film->dcp_name(),
519                         _film->dcp_content_type()->libdcp_kind ()
520                         )
521                 );
522
523         dcp.add (cpl);
524
525         /* Calculate digests for each reel in parallel */
526
527         shared_ptr<Job> job = _job.lock ();
528         job->sub (_("Computing digests"));
529
530         boost::asio::io_service service;
531         boost::thread_group pool;
532
533         shared_ptr<boost::asio::io_service::work> work (new boost::asio::io_service::work (service));
534
535         int const threads = max (1, Config::instance()->master_encoding_threads ());
536
537         for (int i = 0; i < threads; ++i) {
538                 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
539         }
540
541         BOOST_FOREACH (ReelWriter& i, _reels) {
542                 boost::function<void (float)> set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
543                 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
544         }
545
546         work.reset ();
547         pool.join_all ();
548         service.stop ();
549
550         /* Add reels to CPL */
551
552         BOOST_FOREACH (ReelWriter& i, _reels) {
553                 cpl->add (i.create_reel (_reel_assets, _fonts));
554         }
555
556         dcp::XMLMetadata meta;
557         meta.annotation_text = cpl->annotation_text ();
558         meta.creator = Config::instance()->dcp_creator ();
559         if (meta.creator.empty ()) {
560                 meta.creator = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
561         }
562         meta.issuer = Config::instance()->dcp_issuer ();
563         if (meta.issuer.empty ()) {
564                 meta.issuer = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
565         }
566         meta.set_issue_date_now ();
567
568         cpl->set_metadata (meta);
569         cpl->set_ratings (vector_to_list(_film->ratings()));
570         cpl->set_content_version_label_text (_film->content_version());
571
572         shared_ptr<const dcp::CertificateChain> signer;
573         signer = Config::instance()->signer_chain ();
574         /* We did check earlier, but check again here to be on the safe side */
575         string reason;
576         if (!signer->valid (&reason)) {
577                 throw InvalidSignerError (reason);
578         }
579
580         dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer, Config::instance()->dcp_metadata_filename_format());
581
582         LOG_GENERAL (
583                 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
584                 );
585
586         write_cover_sheet ();
587 }
588
589 void
590 Writer::write_cover_sheet ()
591 {
592         boost::filesystem::path const cover = _film->file ("COVER_SHEET.txt");
593         FILE* f = fopen_boost (cover, "w");
594         if (!f) {
595                 throw OpenFileError (cover, errno, OpenFileError::WRITE);
596         }
597
598         string text = Config::instance()->cover_sheet ();
599         boost::algorithm::replace_all (text, "$CPL_NAME", _film->name());
600         boost::algorithm::replace_all (text, "$TYPE", _film->dcp_content_type()->pretty_name());
601         boost::algorithm::replace_all (text, "$CONTAINER", _film->container()->container_nickname());
602         boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _film->isdcf_metadata().audio_language);
603
604         optional<string> subtitle_language;
605         BOOST_FOREACH (shared_ptr<Content> i, _film->content()) {
606                 BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
607                         if (j->type() == TEXT_OPEN_SUBTITLE && j->use()) {
608                                 subtitle_language = j->language ();
609                         }
610                 }
611         }
612         boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_language.get_value_or("None"));
613
614         boost::uintmax_t size = 0;
615         for (
616                 boost::filesystem::recursive_directory_iterator i = boost::filesystem::recursive_directory_iterator(_film->dir(_film->dcp_name()));
617                 i != boost::filesystem::recursive_directory_iterator();
618                 ++i) {
619                 if (boost::filesystem::is_regular_file (i->path ())) {
620                         size += boost::filesystem::file_size (i->path ());
621                 }
622         }
623
624         if (size > (1000000000L)) {
625                 boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1GB", dcp::locale_convert<string> (size / 1000000000.0, 1, true)));
626         } else {
627                 boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1MB", dcp::locale_convert<string> (size / 1000000.0, 1, true)));
628         }
629
630         pair<int, int> ch = audio_channel_types (_film->mapped_audio_channels(), _film->audio_channels());
631         string description = String::compose("%1.%2", ch.first, ch.second);
632
633         if (description == "0.0") {
634                 description = _("None");
635         } else if (description == "1.0") {
636                 description = _("Mono");
637         } else if (description == "2.0") {
638                 description = _("Stereo");
639         }
640         boost::algorithm::replace_all (text, "$AUDIO", description);
641
642         int h, m, s, fr;
643         _film->length().split (_film->video_frame_rate(), h, m, s, fr);
644         string length;
645         if (h == 0 && m == 0) {
646                 length = String::compose("%1s", s);
647         } else if (h == 0 && m > 0) {
648                 length = String::compose("%1m%2s", m, s);
649         } else if (h > 0 && m > 0) {
650                 length = String::compose("%1h%2m%3s", h, m, s);
651         }
652
653         boost::algorithm::replace_all (text, "$LENGTH", length);
654
655         checked_fwrite (text.c_str(), text.length(), f, cover);
656         fclose (f);
657 }
658
659 /** @param frame Frame index within the whole DCP.
660  *  @return true if we can fake-write this frame.
661  */
662 bool
663 Writer::can_fake_write (Frame frame) const
664 {
665         if (_film->encrypted()) {
666                 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
667                 return false;
668         }
669
670         /* We have to do a proper write of the first frame so that we can set up the JPEG2000
671            parameters in the asset writer.
672         */
673
674         ReelWriter const & reel = _reels[video_reel(frame)];
675
676         /* Make frame relative to the start of the reel */
677         frame -= reel.start ();
678         return (frame != 0 && frame < reel.first_nonexistant_frame());
679 }
680
681 /** @param track Closed caption track if type == TEXT_CLOSED_CAPTION */
682 void
683 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
684 {
685         vector<ReelWriter>::iterator* reel = 0;
686
687         switch (type) {
688         case TEXT_OPEN_SUBTITLE:
689                 reel = &_subtitle_reel;
690                 break;
691         case TEXT_CLOSED_CAPTION:
692                 DCPOMATIC_ASSERT (track);
693                 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
694                 reel = &_caption_reels[*track];
695                 break;
696         default:
697                 DCPOMATIC_ASSERT (false);
698         }
699
700         DCPOMATIC_ASSERT (*reel != _reels.end());
701         while ((*reel)->period().to <= period.from) {
702                 ++(*reel);
703                 DCPOMATIC_ASSERT (*reel != _reels.end());
704         }
705
706         (*reel)->write (text, type, track, period);
707 }
708
709 void
710 Writer::write (list<shared_ptr<Font> > fonts)
711 {
712         /* Just keep a list of unique fonts and we'll deal with them in ::finish */
713
714         BOOST_FOREACH (shared_ptr<Font> i, fonts) {
715                 bool got = false;
716                 BOOST_FOREACH (shared_ptr<Font> j, _fonts) {
717                         if (*i == *j) {
718                                 got = true;
719                         }
720                 }
721
722                 if (!got) {
723                         _fonts.push_back (i);
724                 }
725         }
726 }
727
728 bool
729 operator< (QueueItem const & a, QueueItem const & b)
730 {
731         if (a.reel != b.reel) {
732                 return a.reel < b.reel;
733         }
734
735         if (a.frame != b.frame) {
736                 return a.frame < b.frame;
737         }
738
739         return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
740 }
741
742 bool
743 operator== (QueueItem const & a, QueueItem const & b)
744 {
745         return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
746 }
747
748 void
749 Writer::set_encoder_threads (int threads)
750 {
751         boost::mutex::scoped_lock lm (_state_mutex);
752         _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
753         _maximum_queue_size = threads * 16;
754 }
755
756 void
757 Writer::write (ReferencedReelAsset asset)
758 {
759         _reel_assets.push_back (asset);
760 }
761
762 size_t
763 Writer::video_reel (int frame) const
764 {
765         DCPTime t = DCPTime::from_frames (frame, _film->video_frame_rate ());
766         size_t i = 0;
767         while (i < _reels.size() && !_reels[i].period().contains (t)) {
768                 ++i;
769         }
770
771         DCPOMATIC_ASSERT (i < _reels.size ());
772         return i;
773 }
774
775 void
776 Writer::set_digest_progress (Job* job, float progress)
777 {
778         boost::mutex::scoped_lock lm (_digest_progresses_mutex);
779
780         _digest_progresses[boost::this_thread::get_id()] = progress;
781         float min_progress = FLT_MAX;
782         for (map<boost::thread::id, float>::const_iterator i = _digest_progresses.begin(); i != _digest_progresses.end(); ++i) {
783                 min_progress = min (min_progress, i->second);
784         }
785
786         job->set_progress (min_progress);
787
788         Waker waker;
789         waker.nudge ();
790 }