d85db56894ccb59525ab18ef00303810a08d7ee6
[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         /* 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 = boost::thread (boost::bind(&Writer::thread, this));
110 #ifdef DCPOMATIC_LINUX
111         pthread_setname_np (_thread.native_handle(), "writer");
112 #endif
113 }
114
115 Writer::~Writer ()
116 {
117         terminate_thread (false);
118 }
119
120 /** Pass a video frame to the writer for writing to disk at some point.
121  *  This method can be called with frames out of order.
122  *  @param encoded JPEG2000-encoded data.
123  *  @param frame Frame index within the DCP.
124  *  @param eyes Eyes that this frame image is for.
125  */
126 void
127 Writer::write (Data encoded, Frame frame, Eyes eyes)
128 {
129         boost::mutex::scoped_lock lock (_state_mutex);
130
131         while (_queued_full_in_memory > _maximum_frames_in_memory) {
132                 /* There are too many full frames in memory; wake the main writer thread and
133                    wait until it sorts everything out */
134                 _empty_condition.notify_all ();
135                 _full_condition.wait (lock);
136         }
137
138         QueueItem qi;
139         qi.type = QueueItem::FULL;
140         qi.encoded = encoded;
141         qi.reel = video_reel (frame);
142         qi.frame = frame - _reels[qi.reel].start ();
143
144         if (_film->three_d() && eyes == EYES_BOTH) {
145                 /* 2D material in a 3D DCP; fake the 3D */
146                 qi.eyes = EYES_LEFT;
147                 _queue.push_back (qi);
148                 ++_queued_full_in_memory;
149                 qi.eyes = EYES_RIGHT;
150                 _queue.push_back (qi);
151                 ++_queued_full_in_memory;
152         } else {
153                 qi.eyes = eyes;
154                 _queue.push_back (qi);
155                 ++_queued_full_in_memory;
156         }
157
158         /* Now there's something to do: wake anything wait()ing on _empty_condition */
159         _empty_condition.notify_all ();
160 }
161
162 bool
163 Writer::can_repeat (Frame frame) const
164 {
165         return frame > _reels[video_reel(frame)].start();
166 }
167
168 /** Repeat the last frame that was written to a reel as a new frame.
169  *  @param frame Frame index within the DCP of the new (repeated) frame.
170  *  @param eyes Eyes that this repeated frame image is for.
171  */
172 void
173 Writer::repeat (Frame frame, Eyes eyes)
174 {
175         boost::mutex::scoped_lock lock (_state_mutex);
176
177         while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
178                 /* The queue is too big, and the main writer thread can run and fix it, so
179                    wake it and wait until it has done.
180                 */
181                 _empty_condition.notify_all ();
182                 _full_condition.wait (lock);
183         }
184
185         QueueItem qi;
186         qi.type = QueueItem::REPEAT;
187         qi.reel = video_reel (frame);
188         qi.frame = frame - _reels[qi.reel].start ();
189         if (_film->three_d() && eyes == EYES_BOTH) {
190                 qi.eyes = EYES_LEFT;
191                 _queue.push_back (qi);
192                 qi.eyes = EYES_RIGHT;
193                 _queue.push_back (qi);
194         } else {
195                 qi.eyes = eyes;
196                 _queue.push_back (qi);
197         }
198
199         /* Now there's something to do: wake anything wait()ing on _empty_condition */
200         _empty_condition.notify_all ();
201 }
202
203 void
204 Writer::fake_write (Frame frame, Eyes eyes)
205 {
206         boost::mutex::scoped_lock lock (_state_mutex);
207
208         while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
209                 /* The queue is too big, and the main writer thread can run and fix it, so
210                    wake it and wait until it has done.
211                 */
212                 _empty_condition.notify_all ();
213                 _full_condition.wait (lock);
214         }
215
216         size_t const reel = video_reel (frame);
217         Frame const reel_frame = frame - _reels[reel].start ();
218
219         QueueItem qi;
220         qi.type = QueueItem::FAKE;
221
222         {
223                 shared_ptr<InfoFileHandle> info_file = _film->info_file_handle(_reels[reel].period(), true);
224                 qi.size = _reels[reel].read_frame_info(info_file, reel_frame, eyes).size;
225         }
226
227         qi.reel = reel;
228         qi.frame = reel_frame;
229         if (_film->three_d() && eyes == EYES_BOTH) {
230                 qi.eyes = EYES_LEFT;
231                 _queue.push_back (qi);
232                 qi.eyes = EYES_RIGHT;
233                 _queue.push_back (qi);
234         } else {
235                 qi.eyes = eyes;
236                 _queue.push_back (qi);
237         }
238
239         /* Now there's something to do: wake anything wait()ing on _empty_condition */
240         _empty_condition.notify_all ();
241 }
242
243 /** Write some audio frames to the DCP.
244  *  @param audio Audio data.
245  *  @param time Time of this data within the DCP.
246  *  This method is not thread safe.
247  */
248 void
249 Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
250 {
251         DCPOMATIC_ASSERT (audio);
252
253         int const afr = _film->audio_frame_rate();
254
255         DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
256
257         /* The audio we get might span a reel boundary, and if so we have to write it in bits */
258
259         DCPTime t = time;
260         while (t < end) {
261
262                 if (_audio_reel == _reels.end ()) {
263                         /* This audio is off the end of the last reel; ignore it */
264                         return;
265                 }
266
267                 if (end <= _audio_reel->period().to) {
268                         /* Easy case: we can write all the audio to this reel */
269                         _audio_reel->write (audio);
270                         t = end;
271                 } else if (_audio_reel->period().to <= t) {
272                         /* This reel is entirely before the start of our audio; just skip the reel */
273                         ++_audio_reel;
274                 } else {
275                         /* This audio is over a reel boundary; split the audio into two and write the first part */
276                         DCPTime part_lengths[2] = {
277                                 _audio_reel->period().to - t,
278                                 end - _audio_reel->period().to
279                         };
280
281                         Frame part_frames[2] = {
282                                 part_lengths[0].frames_ceil(afr),
283                                 part_lengths[1].frames_ceil(afr)
284                         };
285
286                         if (part_frames[0]) {
287                                 shared_ptr<AudioBuffers> part (new AudioBuffers (audio->channels(), part_frames[0]));
288                                 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.joinable()) {
468                 return;
469         }
470
471         _finish = true;
472         _empty_condition.notify_all ();
473         _full_condition.notify_all ();
474         lock.unlock ();
475
476         _thread.join ();
477
478         if (can_throw) {
479                 rethrow ();
480         }
481 }
482
483 void
484 Writer::finish ()
485 {
486         if (!_thread.joinable()) {
487                 return;
488         }
489
490         LOG_GENERAL_NC ("Terminating writer thread");
491
492         terminate_thread (true);
493
494         LOG_GENERAL_NC ("Finishing ReelWriters");
495
496         BOOST_FOREACH (ReelWriter& i, _reels) {
497                 i.finish ();
498         }
499
500         LOG_GENERAL_NC ("Writing XML");
501
502         dcp::DCP dcp (_film->dir (_film->dcp_name()));
503
504         shared_ptr<dcp::CPL> cpl (
505                 new dcp::CPL (
506                         _film->dcp_name(),
507                         _film->dcp_content_type()->libdcp_kind ()
508                         )
509                 );
510
511         dcp.add (cpl);
512
513         /* Calculate digests for each reel in parallel */
514
515         shared_ptr<Job> job = _job.lock ();
516         job->sub (_("Computing digests"));
517
518         boost::asio::io_service service;
519         boost::thread_group pool;
520
521         shared_ptr<boost::asio::io_service::work> work (new boost::asio::io_service::work (service));
522
523         int const threads = max (1, Config::instance()->master_encoding_threads ());
524
525         for (int i = 0; i < threads; ++i) {
526                 pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
527         }
528
529         BOOST_FOREACH (ReelWriter& i, _reels) {
530                 boost::function<void (float)> set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
531                 service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
532         }
533
534         work.reset ();
535         pool.join_all ();
536         service.stop ();
537
538         /* Add reels to CPL */
539
540         BOOST_FOREACH (ReelWriter& i, _reels) {
541                 cpl->add (i.create_reel (_reel_assets, _fonts));
542         }
543
544         dcp::XMLMetadata meta;
545         meta.annotation_text = cpl->annotation_text ();
546         meta.creator = Config::instance()->dcp_creator ();
547         if (meta.creator.empty ()) {
548                 meta.creator = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
549         }
550         meta.issuer = Config::instance()->dcp_issuer ();
551         if (meta.issuer.empty ()) {
552                 meta.issuer = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
553         }
554         meta.set_issue_date_now ();
555
556         cpl->set_metadata (meta);
557         cpl->set_ratings (vector_to_list(_film->ratings()));
558         cpl->set_content_version_label_text (_film->content_version());
559
560         shared_ptr<const dcp::CertificateChain> signer;
561         if (_film->is_signed ()) {
562                 signer = Config::instance()->signer_chain ();
563                 /* We did check earlier, but check again here to be on the safe side */
564                 string reason;
565                 if (!signer->valid (&reason)) {
566                         throw InvalidSignerError (reason);
567                 }
568         }
569
570         dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer, Config::instance()->dcp_metadata_filename_format());
571
572         LOG_GENERAL (
573                 N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
574                 );
575
576         write_cover_sheet ();
577 }
578
579 void
580 Writer::write_cover_sheet ()
581 {
582         boost::filesystem::path const cover = _film->file ("COVER_SHEET.txt");
583         FILE* f = fopen_boost (cover, "w");
584         if (!f) {
585                 throw OpenFileError (cover, errno, OpenFileError::WRITE);
586         }
587
588         string text = Config::instance()->cover_sheet ();
589         boost::algorithm::replace_all (text, "$CPL_NAME", _film->name());
590         boost::algorithm::replace_all (text, "$TYPE", _film->dcp_content_type()->pretty_name());
591         boost::algorithm::replace_all (text, "$CONTAINER", _film->container()->container_nickname());
592         boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _film->isdcf_metadata().audio_language);
593
594         optional<string> subtitle_language;
595         BOOST_FOREACH (shared_ptr<Content> i, _film->content()) {
596                 BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
597                         if (j->type() == TEXT_OPEN_SUBTITLE && j->use()) {
598                                 subtitle_language = j->language ();
599                         }
600                 }
601         }
602         boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_language.get_value_or("None"));
603
604         boost::uintmax_t size = 0;
605         for (
606                 boost::filesystem::recursive_directory_iterator i = boost::filesystem::recursive_directory_iterator(_film->dir(_film->dcp_name()));
607                 i != boost::filesystem::recursive_directory_iterator();
608                 ++i) {
609                 if (boost::filesystem::is_regular_file (i->path ())) {
610                         size += boost::filesystem::file_size (i->path ());
611                 }
612         }
613
614         if (size > (1000000000L)) {
615                 boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1GB", dcp::locale_convert<string> (size / 1000000000.0, 1, true)));
616         } else {
617                 boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1MB", dcp::locale_convert<string> (size / 1000000.0, 1, true)));
618         }
619
620         pair<int, int> ch = audio_channel_types (_film->mapped_audio_channels(), _film->audio_channels());
621         string description = String::compose("%1.%2", ch.first, ch.second);
622
623         if (description == "0.0") {
624                 description = _("None");
625         } else if (description == "1.0") {
626                 description = _("Mono");
627         } else if (description == "2.0") {
628                 description = _("Stereo");
629         }
630         boost::algorithm::replace_all (text, "$AUDIO", description);
631
632         int h, m, s, fr;
633         _film->length().split (_film->video_frame_rate(), h, m, s, fr);
634         string length;
635         if (h == 0 && m == 0) {
636                 length = String::compose("%1s", s);
637         } else if (h == 0 && m > 0) {
638                 length = String::compose("%1m%2s", m, s);
639         } else if (h > 0 && m > 0) {
640                 length = String::compose("%1h%2m%3s", h, m, s);
641         }
642
643         boost::algorithm::replace_all (text, "$LENGTH", length);
644
645         checked_fwrite (text.c_str(), text.length(), f, cover);
646         fclose (f);
647 }
648
649 /** @param frame Frame index within the whole DCP.
650  *  @return true if we can fake-write this frame.
651  */
652 bool
653 Writer::can_fake_write (Frame frame) const
654 {
655         if (_film->encrypted()) {
656                 /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
657                 return false;
658         }
659
660         /* We have to do a proper write of the first frame so that we can set up the JPEG2000
661            parameters in the asset writer.
662         */
663
664         ReelWriter const & reel = _reels[video_reel(frame)];
665
666         /* Make frame relative to the start of the reel */
667         frame -= reel.start ();
668         return (frame != 0 && frame < reel.first_nonexistant_frame());
669 }
670
671 /** @param track Closed caption track if type == TEXT_CLOSED_CAPTION */
672 void
673 Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
674 {
675         vector<ReelWriter>::iterator* reel = 0;
676
677         switch (type) {
678         case TEXT_OPEN_SUBTITLE:
679                 reel = &_subtitle_reel;
680                 break;
681         case TEXT_CLOSED_CAPTION:
682                 DCPOMATIC_ASSERT (track);
683                 DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
684                 reel = &_caption_reels[*track];
685                 break;
686         default:
687                 DCPOMATIC_ASSERT (false);
688         }
689
690         DCPOMATIC_ASSERT (*reel != _reels.end());
691         while ((*reel)->period().to <= period.from) {
692                 ++(*reel);
693                 DCPOMATIC_ASSERT (*reel != _reels.end());
694         }
695
696         (*reel)->write (text, type, track, period);
697 }
698
699 void
700 Writer::write (list<shared_ptr<Font> > fonts)
701 {
702         /* Just keep a list of unique fonts and we'll deal with them in ::finish */
703
704         BOOST_FOREACH (shared_ptr<Font> i, fonts) {
705                 bool got = false;
706                 BOOST_FOREACH (shared_ptr<Font> j, _fonts) {
707                         if (*i == *j) {
708                                 got = true;
709                         }
710                 }
711
712                 if (!got) {
713                         _fonts.push_back (i);
714                 }
715         }
716 }
717
718 bool
719 operator< (QueueItem const & a, QueueItem const & b)
720 {
721         if (a.reel != b.reel) {
722                 return a.reel < b.reel;
723         }
724
725         if (a.frame != b.frame) {
726                 return a.frame < b.frame;
727         }
728
729         return static_cast<int> (a.eyes) < static_cast<int> (b.eyes);
730 }
731
732 bool
733 operator== (QueueItem const & a, QueueItem const & b)
734 {
735         return a.reel == b.reel && a.frame == b.frame && a.eyes == b.eyes;
736 }
737
738 void
739 Writer::set_encoder_threads (int threads)
740 {
741         boost::mutex::scoped_lock lm (_state_mutex);
742         _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
743         _maximum_queue_size = threads * 16;
744 }
745
746 void
747 Writer::write (ReferencedReelAsset asset)
748 {
749         _reel_assets.push_back (asset);
750 }
751
752 size_t
753 Writer::video_reel (int frame) const
754 {
755         DCPTime t = DCPTime::from_frames (frame, _film->video_frame_rate ());
756         size_t i = 0;
757         while (i < _reels.size() && !_reels[i].period().contains (t)) {
758                 ++i;
759         }
760
761         DCPOMATIC_ASSERT (i < _reels.size ());
762         return i;
763 }
764
765 void
766 Writer::set_digest_progress (Job* job, float progress)
767 {
768         /* I believe this is thread-safe */
769         _digest_progresses[boost::this_thread::get_id()] = progress;
770
771         boost::mutex::scoped_lock lm (_digest_progresses_mutex);
772         float min_progress = FLT_MAX;
773         for (map<boost::thread::id, float>::const_iterator i = _digest_progresses.begin(); i != _digest_progresses.end(); ++i) {
774                 min_progress = min (min_progress, i->second);
775         }
776
777         job->set_progress (min_progress);
778
779         Waker waker;
780         waker.nudge ();
781 }