Reword again: Text -> Caption and Plain -> Text.
[dcpomatic.git] / src / lib / writer.cc
index 17bd21daf6cb61b0bef42cd6c0167a5166f33aad..ea4a6d29beab4616dde6f4ff423b7b29d47ce050 100644 (file)
@@ -1,19 +1,20 @@
 /*
-    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2017 Carl Hetherington <cth@carlh.net>
 
-    This program is free software; you can redistribute it and/or modify
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
     the Free Software Foundation; either version 2 of the License, or
     (at your option) any later version.
 
-    This program is distributed in the hope that it will be useful,
+    DCP-o-matic is distributed in the hope that it will be useful,
     but WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     GNU General Public License for more details.
 
     You should have received a copy of the GNU General Public License
-    along with this program; if not, write to the Free Software
-    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
 
 */
 
 #include "job.h"
 #include "cross.h"
 #include "audio_buffers.h"
-#include "md5_digester.h"
-#include "data.h"
 #include "version.h"
 #include "font.h"
 #include "util.h"
 #include "reel_writer.h"
 #include <dcp/cpl.h>
+#include <dcp/locale_convert.h>
 #include <boost/foreach.hpp>
 #include <fstream>
 #include <cerrno>
 #include <iostream>
+#include <cfloat>
 
 #include "i18n.h"
 
@@ -59,9 +60,13 @@ using std::pair;
 using std::string;
 using std::list;
 using std::cout;
+using std::map;
+using std::min;
+using std::max;
 using boost::shared_ptr;
 using boost::weak_ptr;
 using boost::dynamic_pointer_cast;
+using dcp::Data;
 
 Writer::Writer (shared_ptr<const Film> film, weak_ptr<Job> j)
        : _film (film)
@@ -69,40 +74,45 @@ Writer::Writer (shared_ptr<const Film> film, weak_ptr<Job> j)
        , _thread (0)
        , _finish (false)
        , _queued_full_in_memory (0)
-       , _maximum_frames_in_memory (0)
+       /* These will be reset to sensible values when J2KEncoder is created */
+       , _maximum_frames_in_memory (8)
+       , _maximum_queue_size (8)
        , _full_written (0)
        , _fake_written (0)
        , _repeat_written (0)
        , _pushed_to_disk (0)
 {
-       /* Remove any old DCP */
-       boost::filesystem::remove_all (_film->dir (_film->dcp_name ()));
-
        shared_ptr<Job> job = _job.lock ();
        DCPOMATIC_ASSERT (job);
 
-       BOOST_FOREACH (DCPTimePeriod p, _film->reels ()) {
-               _reels.push_back (ReelWriter (film, p, job));
+       int reel_index = 0;
+       list<DCPTimePeriod> const reels = _film->reels ();
+       BOOST_FOREACH (DCPTimePeriod p, reels) {
+               _reels.push_back (ReelWriter (film, p, job, reel_index++, reels.size(), _film->content_summary(p)));
        }
 
-       /* We can keep track of the current audio and subtitle reels easily because audio
-          and subs arrive to the Writer in sequence.  This is not so for video.
+       /* We can keep track of the current audio, subtitle and closed caption reels easily because audio
+          and captions arrive to the Writer in sequence.  This is not so for video.
        */
        _audio_reel = _reels.begin ();
-       _subtitle_reel = _reels.begin ();
+       for (int i = 0; i < TEXT_COUNT; ++i) {
+               _text_reel[i] = _reels.begin ();
+       }
 
        /* Check that the signer is OK if we need one */
-       if (_film->is_signed() && !Config::instance()->signer_chain()->valid ()) {
-               throw InvalidSignerError ();
+       string reason;
+       if (_film->is_signed() && !Config::instance()->signer_chain()->valid(&reason)) {
+               throw InvalidSignerError (reason);
        }
-
-       job->sub (_("Encoding image data"));
 }
 
 void
 Writer::start ()
 {
        _thread = new boost::thread (boost::bind (&Writer::thread, this));
+#ifdef DCPOMATIC_LINUX
+       pthread_setname_np (_thread->native_handle(), "writer");
+#endif
 }
 
 Writer::~Writer ()
@@ -122,7 +132,9 @@ Writer::write (Data encoded, Frame frame, Eyes eyes)
        boost::mutex::scoped_lock lock (_state_mutex);
 
        while (_queued_full_in_memory > _maximum_frames_in_memory) {
-               /* The queue is too big; wait until that is sorted out */
+               /* There are too many full frames in memory; wake the main writer thread and
+                  wait until it sorts everything out */
+               _empty_condition.notify_all ();
                _full_condition.wait (lock);
        }
 
@@ -165,8 +177,11 @@ Writer::repeat (Frame frame, Eyes eyes)
 {
        boost::mutex::scoped_lock lock (_state_mutex);
 
-       while (_queued_full_in_memory > _maximum_frames_in_memory) {
-               /* The queue is too big; wait until that is sorted out */
+       while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
+               /* The queue is too big, and the main writer thread can run and fix it, so
+                  wake it and wait until it has done.
+               */
+               _empty_condition.notify_all ();
                _full_condition.wait (lock);
        }
 
@@ -193,8 +208,11 @@ Writer::fake_write (Frame frame, Eyes eyes)
 {
        boost::mutex::scoped_lock lock (_state_mutex);
 
-       while (_queued_full_in_memory > _maximum_frames_in_memory) {
-               /* The queue is too big; wait until that is sorted out */
+       while (_queue.size() > _maximum_queue_size && have_sequenced_image_at_queue_head()) {
+               /* The queue is too big, and the main writer thread can run and fix it, so
+                  wake it and wait until it has done.
+               */
+               _empty_condition.notify_all ();
                _full_condition.wait (lock);
        }
 
@@ -227,23 +245,63 @@ Writer::fake_write (Frame frame, Eyes eyes)
        _empty_condition.notify_all ();
 }
 
-/** Write one video frame's worth of audio frames to the DCP.
- *  @param audio Audio data or 0 if there is no audio to be written here (i.e. it is referenced).
+/** Write some audio frames to the DCP.
+ *  @param audio Audio data.
+ *  @param time Time of this data within the DCP.
  *  This method is not thread safe.
  */
 void
-Writer::write (shared_ptr<const AudioBuffers> audio)
+Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
 {
-       if (_audio_reel == _reels.end ()) {
-               /* This audio is off the end of the last reel; ignore it */
-               return;
-       }
+       DCPOMATIC_ASSERT (audio);
 
-       _audio_reel->write (audio);
+       int const afr = _film->audio_frame_rate();
+
+       DCPTime const end = time + DCPTime::from_frames(audio->frames(), afr);
+
+       /* The audio we get might span a reel boundary, and if so we have to write it in bits */
+
+       DCPTime t = time;
+       while (t < end) {
+
+               if (_audio_reel == _reels.end ()) {
+                       /* This audio is off the end of the last reel; ignore it */
+                       return;
+               }
+
+               if (end <= _audio_reel->period().to) {
+                       /* Easy case: we can write all the audio to this reel */
+                       _audio_reel->write (audio);
+                       t = end;
+               } else {
+                       /* Split the audio into two and write the first part */
+                       DCPTime part_lengths[2] = {
+                               _audio_reel->period().to - t,
+                               end - _audio_reel->period().to
+                       };
+
+                       Frame part_frames[2] = {
+                               part_lengths[0].frames_ceil(afr),
+                               part_lengths[1].frames_ceil(afr)
+                       };
+
+                       if (part_frames[0]) {
+                               shared_ptr<AudioBuffers> part (new AudioBuffers (audio->channels(), part_frames[0]));
+                               part->copy_from (audio.get(), part_frames[0], 0, 0);
+                               _audio_reel->write (part);
+                       }
+
+                       if (part_frames[1]) {
+                               shared_ptr<AudioBuffers> part (new AudioBuffers (audio->channels(), part_frames[1]));
+                               part->copy_from (audio.get(), part_frames[1], part_frames[0], 0);
+                               audio = part;
+                       } else {
+                               audio.reset ();
+                       }
 
-       /* written is in video frames, not audio frames */
-       if (_audio_reel->total_written_audio_frames() >= _audio_reel->period().duration().frames_floor (_film->video_frame_rate())) {
-               ++_audio_reel;
+                       ++_audio_reel;
+                       t += part_lengths[0];
+               }
        }
 }
 
@@ -316,9 +374,9 @@ try
                                LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
                                for (list<QueueItem>::const_iterator i = _queue.begin(); i != _queue.end(); ++i) {
                                        if (i->type == QueueItem::FULL) {
-                                               LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i->frame, i->eyes);
+                                               LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i->frame, (int) i->eyes);
                                        } else {
-                                               LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i->size, i->frame, i->eyes);
+                                               LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i->size, i->frame, (int) i->eyes);
                                        }
                                }
                        }
@@ -339,7 +397,7 @@ try
 
                        switch (qi.type) {
                        case QueueItem::FULL:
-                               LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, qi.eyes);
+                               LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
                                if (!qi.encoded) {
                                        qi.encoded = Data (_film->j2c_path (qi.reel, qi.frame, qi.eyes, false));
                                }
@@ -359,19 +417,7 @@ try
                        }
 
                        lock.lock ();
-
-                       shared_ptr<Job> job = _job.lock ();
-                       DCPOMATIC_ASSERT (job);
-                       int64_t total = _film->length().frames_round (_film->video_frame_rate ());
-                       if (_film->three_d ()) {
-                               /* _full_written and so on are incremented for each eye, so we need to double the total
-                                  frames to get the correct progress.
-                               */
-                               total *= 2;
-                       }
-                       if (total) {
-                               job->set_progress (float (_full_written + _fake_written + _repeat_written) / total);
-                       }
+                       _full_condition.notify_all ();
                }
 
                while (_queued_full_in_memory > _maximum_frames_in_memory) {
@@ -388,6 +434,8 @@ try
 
                        DCPOMATIC_ASSERT (i != _queue.rend());
                        ++_pushed_to_disk;
+                       /* For the log message below */
+                       int const awaiting = _reels[_queue.front().reel].last_written_video_frame();
                        lock.unlock ();
 
                        /* i is valid here, even though we don't hold a lock on the mutex,
@@ -395,7 +443,7 @@ try
                           thread could erase the last item in the list.
                        */
 
-                       LOG_GENERAL ("Writer full; pushes %1 to disk", i->frame);
+                       LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
 
                        i->encoded->write_via_temp (
                                _film->j2c_path (i->reel, i->frame, i->eyes, true),
@@ -405,10 +453,8 @@ try
                        lock.lock ();
                        i->encoded.reset ();
                        --_queued_full_in_memory;
+                       _full_condition.notify_all ();
                }
-
-               /* The queue has probably just gone down a bit; notify anything wait()ing on _full_condition */
-               _full_condition.notify_all ();
        }
 }
 catch (...)
@@ -429,8 +475,10 @@ Writer::terminate_thread (bool can_throw)
        _full_condition.notify_all ();
        lock.unlock ();
 
-       DCPOMATIC_ASSERT (_thread->joinable ());
-       _thread->join ();
+       if (_thread->joinable ()) {
+               _thread->join ();
+       }
+
        if (can_throw) {
                rethrow ();
        }
@@ -446,12 +494,18 @@ Writer::finish ()
                return;
        }
 
+       LOG_GENERAL_NC ("Terminating writer thread");
+
        terminate_thread (true);
 
+       LOG_GENERAL_NC ("Finishing ReelWriters");
+
        BOOST_FOREACH (ReelWriter& i, _reels) {
                i.finish ();
        }
 
+       LOG_GENERAL_NC ("Writing XML");
+
        dcp::DCP dcp (_film->dir (_film->dcp_name()));
 
        shared_ptr<dcp::CPL> cpl (
@@ -463,16 +517,39 @@ Writer::finish ()
 
        dcp.add (cpl);
 
+       /* Calculate digests for each reel in parallel */
+
+       shared_ptr<Job> job = _job.lock ();
+       job->sub (_("Computing digests"));
+
+       boost::asio::io_service service;
+       boost::thread_group pool;
+
+       shared_ptr<boost::asio::io_service::work> work (new boost::asio::io_service::work (service));
+
+       int const threads = max (1, Config::instance()->master_encoding_threads ());
+
+       for (int i = 0; i < threads; ++i) {
+               pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
+       }
+
        BOOST_FOREACH (ReelWriter& i, _reels) {
+               boost::function<void (float)> set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
+               service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
+       }
 
-               cpl->add (i.create_reel (_reel_assets, _fonts));
+       work.reset ();
+       pool.join_all ();
+       service.stop ();
 
-               shared_ptr<Job> job = _job.lock ();
-               DCPOMATIC_ASSERT (job);
-               i.calculate_digests (job);
+       /* Add reels to CPL */
+
+       BOOST_FOREACH (ReelWriter& i, _reels) {
+               cpl->add (i.create_reel (_reel_assets, _fonts));
        }
 
        dcp::XMLMetadata meta;
+       meta.annotation_text = cpl->annotation_text ();
        meta.creator = Config::instance()->dcp_creator ();
        if (meta.creator.empty ()) {
                meta.creator = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
@@ -489,16 +566,80 @@ Writer::finish ()
        if (_film->is_signed ()) {
                signer = Config::instance()->signer_chain ();
                /* We did check earlier, but check again here to be on the safe side */
-               if (!signer->valid ()) {
-                       throw InvalidSignerError ();
+               string reason;
+               if (!signer->valid (&reason)) {
+                       throw InvalidSignerError (reason);
                }
        }
 
-       dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer);
+       dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer, Config::instance()->dcp_metadata_filename_format());
 
        LOG_GENERAL (
                N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
                );
+
+       write_cover_sheet ();
+}
+
+void
+Writer::write_cover_sheet ()
+{
+       boost::filesystem::path const cover = _film->file ("COVER_SHEET.txt");
+       FILE* f = fopen_boost (cover, "w");
+       if (!f) {
+               throw OpenFileError (cover, errno, false);
+       }
+
+       string text = Config::instance()->cover_sheet ();
+       boost::algorithm::replace_all (text, "$CPL_NAME", _film->name());
+       boost::algorithm::replace_all (text, "$TYPE", _film->dcp_content_type()->pretty_name());
+       boost::algorithm::replace_all (text, "$CONTAINER", _film->container()->container_nickname());
+       boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _film->isdcf_metadata().audio_language);
+       boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _film->isdcf_metadata().subtitle_language);
+
+       boost::uintmax_t size = 0;
+       for (
+               boost::filesystem::recursive_directory_iterator i = boost::filesystem::recursive_directory_iterator(_film->dir(_film->dcp_name()));
+               i != boost::filesystem::recursive_directory_iterator();
+               ++i) {
+               if (boost::filesystem::is_regular_file (i->path ())) {
+                       size += boost::filesystem::file_size (i->path ());
+               }
+       }
+
+       if (size > (1000000000L)) {
+               boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1GB", dcp::locale_convert<string> (size / 1000000000.0, 1, true)));
+       } else {
+               boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1MB", dcp::locale_convert<string> (size / 1000000.0, 1, true)));
+       }
+
+       pair<int, int> ch = audio_channel_types (_film->mapped_audio_channels(), _film->audio_channels());
+       string description = String::compose("%1.%2", ch.first, ch.second);
+
+       if (description == "0.0") {
+               description = _("None");
+       } else if (description == "1.0") {
+               description = _("Mono");
+       } else if (description == "2.0") {
+               description = _("Stereo");
+       }
+       boost::algorithm::replace_all (text, "$AUDIO", description);
+
+       int h, m, s, fr;
+       _film->length().split (_film->video_frame_rate(), h, m, s, fr);
+       string length;
+       if (h == 0 && m == 0) {
+               length = String::compose("%1s", s);
+       } else if (h == 0 && m > 0) {
+               length = String::compose("%1m%2s", m, s);
+       } else if (h > 0 && m > 0) {
+               length = String::compose("%1h%2m%3s", h, m, s);
+       }
+
+       boost::algorithm::replace_all (text, "$LENGTH", length);
+
+       fwrite (text.c_str(), 1, text.length(), f);
+       fclose (f);
 }
 
 /** @param frame Frame index within the whole DCP.
@@ -507,6 +648,11 @@ Writer::finish ()
 bool
 Writer::can_fake_write (Frame frame) const
 {
+       if (_film->encrypted()) {
+               /* We need to re-write the frame because the asset ID is embedded in the HMAC... I think... */
+               return false;
+       }
+
        /* We have to do a proper write of the first frame so that we can set up the JPEG2000
           parameters in the asset writer.
        */
@@ -519,24 +665,35 @@ Writer::can_fake_write (Frame frame) const
 }
 
 void
-Writer::write (PlayerSubtitles subs)
+Writer::write (PlayerCaption text, TextType type, DCPTimePeriod period)
 {
-       if (subs.text.empty ()) {
-               return;
+       while (_text_reel[type]->period().to <= period.from) {
+               ++_text_reel[type];
+               DCPOMATIC_ASSERT (_text_reel[type] != _reels.end());
        }
 
-       if (_subtitle_reel->period().to <= subs.from) {
-               ++_subtitle_reel;
-       }
+       DCPOMATIC_ASSERT (_text_reel[type] != _reels.end());
 
-       _subtitle_reel->write (subs);
+       _text_reel[type]->write (text, type, period);
 }
 
 void
 Writer::write (list<shared_ptr<Font> > fonts)
 {
-       /* Just keep a list of fonts and we'll deal with them in ::finish */
-       copy (fonts.begin (), fonts.end (), back_inserter (_fonts));
+       /* Just keep a list of unique fonts and we'll deal with them in ::finish */
+
+       BOOST_FOREACH (shared_ptr<Font> i, fonts) {
+               bool got = false;
+               BOOST_FOREACH (shared_ptr<Font> j, _fonts) {
+                       if (*i == *j) {
+                               got = true;
+                       }
+               }
+
+               if (!got) {
+                       _fonts.push_back (i);
+               }
+       }
 }
 
 bool
@@ -562,7 +719,9 @@ operator== (QueueItem const & a, QueueItem const & b)
 void
 Writer::set_encoder_threads (int threads)
 {
-       _maximum_frames_in_memory = lrint (threads * 1.1);
+       boost::mutex::scoped_lock lm (_state_mutex);
+       _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
+       _maximum_queue_size = threads * 16;
 }
 
 void
@@ -583,3 +742,18 @@ Writer::video_reel (int frame) const
        DCPOMATIC_ASSERT (i < _reels.size ());
        return i;
 }
+
+void
+Writer::set_digest_progress (Job* job, float progress)
+{
+       /* I believe this is thread-safe */
+       _digest_progresses[boost::this_thread::get_id()] = progress;
+
+       boost::mutex::scoped_lock lm (_digest_progresses_mutex);
+       float min_progress = FLT_MAX;
+       for (map<boost::thread::id, float>::const_iterator i = _digest_progresses.begin(); i != _digest_progresses.end(); ++i) {
+               min_progress = min (min_progress, i->second);
+       }
+
+       job->set_progress (min_progress);
+}