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