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