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