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