Debug.
[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                                 LOG_GENERAL_NC("copy from #10");
289                                 part->copy_from (audio.get(), part_frames[0], 0, 0);
290                                 _audio_reel->write (part);
291                         }
292
293                         if (part_frames[1]) {
294                                 shared_ptr<AudioBuffers> part (new AudioBuffers (audio->channels(), part_frames[1]));
295                                 LOG_GENERAL_NC("copy from #11");
296                                 part->copy_from (audio.get(), part_frames[1], part_frames[0], 0);
297                                 audio = part;
298                         } else {
299                                 audio.reset ();
300                         }
301
302                         ++_audio_reel;
303                         t += part_lengths[0];
304                 }
305         }
306 }
307
308 /** This must be called from Writer::thread() with an appropriate lock held */
309 bool
310 Writer::have_sequenced_image_at_queue_head ()
311 {
312         if (_queue.empty ()) {
313                 return false;
314         }
315
316         _queue.sort ();
317
318         QueueItem const & f = _queue.front();
319         ReelWriter const & reel = _reels[f.reel];
320
321         /* The queue should contain only EYES_LEFT/EYES_RIGHT pairs or EYES_BOTH */
322
323         if (f.eyes == EYES_BOTH) {
324                 /* 2D */
325                 return f.frame == (reel.last_written_video_frame() + 1);
326         }
327
328         /* 3D */
329
330         if (reel.last_written_eyes() == EYES_LEFT && f.frame == reel.last_written_video_frame() && f.eyes == EYES_RIGHT) {
331                 return true;
332         }
333
334         if (reel.last_written_eyes() == EYES_RIGHT && f.frame == (reel.last_written_video_frame() + 1) && f.eyes == EYES_LEFT) {
335                 return true;
336         }
337
338         return false;
339 }
340
341 void
342 Writer::thread ()
343 try
344 {
345         while (true)
346         {
347                 boost::mutex::scoped_lock lock (_state_mutex);
348
349                 while (true) {
350
351                         if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) {
352                                 /* We've got something to do: go and do it */
353                                 break;
354                         }
355
356                         /* Nothing to do: wait until something happens which may indicate that we do */
357                         LOG_TIMING (N_("writer-sleep queue=%1"), _queue.size());
358                         _empty_condition.wait (lock);
359                         LOG_TIMING (N_("writer-wake queue=%1"), _queue.size());
360                 }
361
362                 if (_finish && _queue.empty()) {
363                         return;
364                 }
365
366                 /* We stop here if we have been asked to finish, and if either the queue
367                    is empty or we do not have a sequenced image at its head (if this is the
368                    case we will never terminate as no new frames will be sent once
369                    _finish is true).
370                 */
371                 if (_finish && (!have_sequenced_image_at_queue_head() || _queue.empty())) {
372                         /* (Hopefully temporarily) log anything that was not written */
373                         if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
374                                 LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
375                                 for (list<QueueItem>::const_iterator i = _queue.begin(); i != _queue.end(); ++i) {
376                                         if (i->type == QueueItem::FULL) {
377                                                 LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i->frame, (int) i->eyes);
378                                         } else {
379                                                 LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i->size, i->frame, (int) i->eyes);
380                                         }
381                                 }
382                         }
383                         return;
384                 }
385
386                 /* Write any frames that we can write; i.e. those that are in sequence. */
387                 while (have_sequenced_image_at_queue_head ()) {
388                         QueueItem qi = _queue.front ();
389                         _queue.pop_front ();
390                         if (qi.type == QueueItem::FULL && qi.encoded) {
391                                 --_queued_full_in_memory;
392                         }
393
394                         lock.unlock ();
395
396                         ReelWriter& reel = _reels[qi.reel];
397
398                         switch (qi.type) {
399                         case QueueItem::FULL:
400                                 LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
401                                 if (!qi.encoded) {
402                                         qi.encoded = Data (_film->j2c_path (qi.reel, qi.frame, qi.eyes, false));
403                                 }
404                                 reel.write (qi.encoded, qi.frame, qi.eyes);
405                                 ++_full_written;
406                                 break;
407                         case QueueItem::FAKE:
408                                 LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
409                                 reel.fake_write (qi.frame, qi.eyes, qi.size);
410                                 ++_fake_written;
411                                 break;
412                         case QueueItem::REPEAT:
413                                 LOG_DEBUG_ENCODE (N_("Writer REPEAT-writes %1"), qi.frame);
414                                 reel.repeat_write (qi.frame, qi.eyes);
415                                 ++_repeat_written;
416                                 break;
417                         }
418
419                         lock.lock ();
420                         _full_condition.notify_all ();
421                 }
422
423                 while (_queued_full_in_memory > _maximum_frames_in_memory) {
424                         /* Too many frames in memory which can't yet be written to the stream.
425                            Write some FULL frames to disk.
426                         */
427
428                         /* Find one from the back of the queue */
429                         _queue.sort ();
430                         list<QueueItem>::reverse_iterator i = _queue.rbegin ();
431                         while (i != _queue.rend() && (i->type != QueueItem::FULL || !i->encoded)) {
432                                 ++i;
433                         }
434
435                         DCPOMATIC_ASSERT (i != _queue.rend());
436                         ++_pushed_to_disk;
437                         /* For the log message below */
438                         int const awaiting = _reels[_queue.front().reel].last_written_video_frame() + 1;
439                         lock.unlock ();
440
441                         /* i is valid here, even though we don't hold a lock on the mutex,
442                            since list iterators are unaffected by insertion and only this
443                            thread could erase the last item in the list.
444                         */
445
446                         LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
447
448                         i->encoded->write_via_temp (
449                                 _film->j2c_path (i->reel, i->frame, i->eyes, true),
450                                 _film->j2c_path (i->reel, i->frame, i->eyes, false)
451                                 );
452
453                         lock.lock ();
454                         i->encoded.reset ();
455                         --_queued_full_in_memory;
456                         _full_condition.notify_all ();
457                 }
458         }
459 }
460 catch (...)
461 {
462         store_current ();
463 }
464
465 void
466 Writer::terminate_thread (bool can_throw)
467 {
468         boost::mutex::scoped_lock lock (_state_mutex);
469         if (_thread == 0) {
470                 return;
471         }
472
473         _finish = true;
474         _empty_condition.notify_all ();
475         _full_condition.notify_all ();
476         lock.unlock ();
477
478         if (_thread->joinable ()) {
479                 _thread->join ();
480         }
481
482         if (can_throw) {
483                 rethrow ();
484         }
485
486         delete _thread;
487         _thread = 0;
488 }
489
490 void
491 Writer::finish ()
492 {
493         if (!_thread) {
494                 return;
495         }
496
497         LOG_GENERAL_NC ("Terminating writer thread");
498
499         terminate_thread (true);
500
501         LOG_GENERAL_NC ("Finishing ReelWriters");
502
503         BOOST_FOREACH (ReelWriter& i, _reels) {
504                 i.finish ();
505         }
506
507         LOG_GENERAL_NC ("Writing XML");
508
509         dcp::DCP dcp (_film->dir (_film->dcp_name()));
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         dcp.add (cpl);
519
520         /* Calculate digests for each reel in parallel */
521
522         shared_ptr<Job> job = _job.lock ();
523         job->sub (_("Computing digests"));
524
525         boost::asio::io_service service;
526         boost::thread_group pool;
527
528         shared_ptr<boost::asio::io_service::work> work (new boost::asio::io_service::work (service));
529
530         int const threads = max (1, Config::instance()->master_encoding_threads ());
531
532         for (int i = 0; i < threads; ++i) {
533                 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
534         }
535
536         BOOST_FOREACH (ReelWriter& i, _reels) {
537                 boost::function<void (float)> set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
538                 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
539         }
540
541         work.reset ();
542         pool.join_all ();
543         service.stop ();
544
545         /* Add reels to CPL */
546
547         BOOST_FOREACH (ReelWriter& i, _reels) {
548                 cpl->add (i.create_reel (_reel_assets, _fonts));
549         }
550
551         dcp::XMLMetadata meta;
552         meta.annotation_text = cpl->annotation_text ();
553         meta.creator = Config::instance()->dcp_creator ();
554         if (meta.creator.empty ()) {
555                 meta.creator = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
556         }
557         meta.issuer = Config::instance()->dcp_issuer ();
558         if (meta.issuer.empty ()) {
559                 meta.issuer = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
560         }
561         meta.set_issue_date_now ();
562
563         cpl->set_metadata (meta);
564
565         shared_ptr<const dcp::CertificateChain> signer;
566         if (_film->is_signed ()) {
567                 signer = Config::instance()->signer_chain ();
568                 /* We did check earlier, but check again here to be on the safe side */
569                 string reason;
570                 if (!signer->valid (&reason)) {
571                         throw InvalidSignerError (reason);
572                 }
573         }
574
575         dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer, Config::instance()->dcp_metadata_filename_format());
576
577         LOG_GENERAL (
578                 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
579                 );
580
581         write_cover_sheet ();
582 }
583
584 void
585 Writer::write_cover_sheet ()
586 {
587         boost::filesystem::path const cover = _film->file ("COVER_SHEET.txt");
588         FILE* f = fopen_boost (cover, "w");
589         if (!f) {
590                 throw OpenFileError (cover, errno, OpenFileError::WRITE);
591         }
592
593         string text = Config::instance()->cover_sheet ();
594         boost::algorithm::replace_all (text, "$CPL_NAME", _film->name());
595         boost::algorithm::replace_all (text, "$TYPE", _film->dcp_content_type()->pretty_name());
596         boost::algorithm::replace_all (text, "$CONTAINER", _film->container()->container_nickname());
597         boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _film->isdcf_metadata().audio_language);
598
599         optional<string> subtitle_language;
600         BOOST_FOREACH (shared_ptr<Content> i, _film->content()) {
601                 BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
602                         if (j->type() == TEXT_OPEN_SUBTITLE && j->use()) {
603                                 subtitle_language = j->language ();
604                         }
605                 }
606         }
607         boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_language.get_value_or("None"));
608
609         boost::uintmax_t size = 0;
610         for (
611                 boost::filesystem::recursive_directory_iterator i = boost::filesystem::recursive_directory_iterator(_film->dir(_film->dcp_name()));
612                 i != boost::filesystem::recursive_directory_iterator();
613                 ++i) {
614                 if (boost::filesystem::is_regular_file (i->path ())) {
615                         size += boost::filesystem::file_size (i->path ());
616                 }
617         }
618
619         if (size > (1000000000L)) {
620                 boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1GB", dcp::locale_convert<string> (size / 1000000000.0, 1, true)));
621         } else {
622                 boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1MB", dcp::locale_convert<string> (size / 1000000.0, 1, true)));
623         }
624
625         pair<int, int> ch = audio_channel_types (_film->mapped_audio_channels(), _film->audio_channels());
626         string description = String::compose("%1.%2", ch.first, ch.second);
627
628         if (description == "0.0") {
629                 description = _("None");
630         } else if (description == "1.0") {
631                 description = _("Mono");
632         } else if (description == "2.0") {
633                 description = _("Stereo");
634         }
635         boost::algorithm::replace_all (text, "$AUDIO", description);
636
637         int h, m, s, fr;
638         _film->length().split (_film->video_frame_rate(), h, m, s, fr);
639         string length;
640         if (h == 0 && m == 0) {
641                 length = String::compose("%1s", s);
642         } else if (h == 0 && m > 0) {
643                 length = String::compose("%1m%2s", m, s);
644         } else if (h > 0 && m > 0) {
645                 length = String::compose("%1h%2m%3s", h, m, s);
646         }
647
648         boost::algorithm::replace_all (text, "$LENGTH", length);
649
650         checked_fwrite (text.c_str(), text.length(), f, cover);
651         fclose (f);
652 }
653
654 /** @param frame Frame index within the whole DCP.
655  *  @return true if we can fake-write this frame.
656  */
657 bool
658 Writer::can_fake_write (Frame frame) const
659 {
660         if (_film->encrypted()) {
661                 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
662                 return false;
663         }
664
665         /* We have to do a proper write of the first frame so that we can set up the JPEG2000
666            parameters in the asset writer.
667         */
668
669         ReelWriter const & reel = _reels[video_reel(frame)];
670
671         /* Make frame relative to the start of the reel */
672         frame -= reel.start ();
673         return (frame != 0 && frame < reel.first_nonexistant_frame());
674 }
675
676 /** @param track Closed caption track if type == TEXT_CLOSED_CAPTION */
677 void
678 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
679 {
680         vector<ReelWriter>::iterator* reel = 0;
681
682         switch (type) {
683         case TEXT_OPEN_SUBTITLE:
684                 reel = &_subtitle_reel;
685                 break;
686         case TEXT_CLOSED_CAPTION:
687                 DCPOMATIC_ASSERT (track);
688                 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
689                 reel = &_caption_reels[*track];
690                 break;
691         default:
692                 DCPOMATIC_ASSERT (false);
693         }
694
695         DCPOMATIC_ASSERT (*reel != _reels.end());
696         while ((*reel)->period().to <= period.from) {
697                 ++(*reel);
698                 DCPOMATIC_ASSERT (*reel != _reels.end());
699         }
700
701         (*reel)->write (text, type, track, period);
702 }
703
704 void
705 Writer::write (list<shared_ptr<Font> > fonts)
706 {
707         /* Just keep a list of unique fonts and we'll deal with them in ::finish */
708
709         BOOST_FOREACH (shared_ptr<Font> i, fonts) {
710                 bool got = false;
711                 BOOST_FOREACH (shared_ptr<Font> j, _fonts) {
712                         if (*i == *j) {
713                                 got = true;
714                         }
715                 }
716
717                 if (!got) {
718                         _fonts.push_back (i);
719                 }
720         }
721 }
722
723 bool
724 operator< (QueueItem const & a, QueueItem const & b)
725 {
726         if (a.reel != b.reel) {
727                 return a.reel < b.reel;
728         }
729
730         if (a.frame != b.frame) {
731                 return a.frame < b.frame;
732         }
733
734         return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
735 }
736
737 bool
738 operator== (QueueItem const & a, QueueItem const & b)
739 {
740         return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
741 }
742
743 void
744 Writer::set_encoder_threads (int threads)
745 {
746         boost::mutex::scoped_lock lm (_state_mutex);
747         _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
748         _maximum_queue_size = threads * 16;
749 }
750
751 void
752 Writer::write (ReferencedReelAsset asset)
753 {
754         _reel_assets.push_back (asset);
755 }
756
757 size_t
758 Writer::video_reel (int frame) const
759 {
760         DCPTime t = DCPTime::from_frames (frame, _film->video_frame_rate ());
761         size_t i = 0;
762         while (i < _reels.size() && !_reels[i].period().contains (t)) {
763                 ++i;
764         }
765
766         DCPOMATIC_ASSERT (i < _reels.size ());
767         return i;
768 }
769
770 void
771 Writer::set_digest_progress (Job* job, float progress)
772 {
773         /* I believe this is thread-safe */
774         _digest_progresses[boost::this_thread::get_id()] = progress;
775
776         boost::mutex::scoped_lock lm (_digest_progresses_mutex);
777         float min_progress = FLT_MAX;
778         for (map<boost::thread::id, float>::const_iterator i = _digest_progresses.begin(); i != _digest_progresses.end(); ++i) {
779                 min_progress = min (min_progress, i->second);
780         }
781
782         job->set_progress (min_progress);
783 }