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