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