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