Subtitle language handling tweaks; write multiple subtitle languages
[dcpomatic.git] / src / lib / writer.cc
index 8e2079aebfdad8c17382ac1953ebf49985e72bd2..a9700f4f5edbbb7272e2ed2735c56f74cd2535b3 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2018 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2019 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
@@ -23,6 +23,7 @@
 #include "film.h"
 #include "ratio.h"
 #include "log.h"
+#include "dcpomatic_log.h"
 #include "dcp_video.h"
 #include "dcp_content_type.h"
 #include "audio_mapping.h"
@@ -34,6 +35,7 @@
 #include "font.h"
 #include "util.h"
 #include "reel_writer.h"
+#include "text_content.h"
 #include <dcp/cpl.h>
 #include <dcp/locale_convert.h>
 #include <boost/foreach.hpp>
 
 #include "i18n.h"
 
-#define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL);
-#define LOG_GENERAL_NC(...) _film->log()->log (__VA_ARGS__, LogEntry::TYPE_GENERAL);
-#define LOG_DEBUG_ENCODE(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_DEBUG_ENCODE);
-#define LOG_TIMING(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_TIMING);
-#define LOG_WARNING_NC(...) _film->log()->log (__VA_ARGS__, LogEntry::TYPE_WARNING);
-#define LOG_WARNING(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_WARNING);
-#define LOG_ERROR(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_ERROR);
-
 /* OS X strikes again */
 #undef set_key
 
@@ -68,12 +62,16 @@ using boost::shared_ptr;
 using boost::weak_ptr;
 using boost::dynamic_pointer_cast;
 using boost::optional;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
 using dcp::Data;
+using dcp::ArrayData;
+using namespace dcpomatic;
 
 Writer::Writer (shared_ptr<const Film> film, weak_ptr<Job> j)
        : _film (film)
        , _job (j)
-       , _thread (0)
        , _finish (false)
        , _queued_full_in_memory (0)
        /* These will be reset to sensible values when J2KEncoder is created */
@@ -93,6 +91,8 @@ Writer::Writer (shared_ptr<const Film> film, weak_ptr<Job> j)
                _reels.push_back (ReelWriter (film, p, job, reel_index++, reels.size(), _film->content_summary(p)));
        }
 
+       _last_written.resize (reels.size());
+
        /* 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.
        */
@@ -101,10 +101,11 @@ Writer::Writer (shared_ptr<const Film> film, weak_ptr<Job> j)
        BOOST_FOREACH (DCPTextTrack i, _film->closed_caption_tracks()) {
                _caption_reels[i] = _reels.begin ();
        }
+       _atmos_reel = _reels.begin ();
 
-       /* Check that the signer is OK if we need one */
+       /* Check that the signer is OK */
        string reason;
-       if (_film->is_signed() && !Config::instance()->signer_chain()->valid(&reason)) {
+       if (!Config::instance()->signer_chain()->valid(&reason)) {
                throw InvalidSignerError (reason);
        }
 }
@@ -112,9 +113,9 @@ Writer::Writer (shared_ptr<const Film> film, weak_ptr<Job> j)
 void
 Writer::start ()
 {
-       _thread = new boost::thread (boost::bind (&Writer::thread, this));
+       _thread = boost::thread (boost::bind(&Writer::thread, this));
 #ifdef DCPOMATIC_LINUX
-       pthread_setname_np (_thread->native_handle(), "writer");
+       pthread_setname_np (_thread.native_handle(), "writer");
 #endif
 }
 
@@ -130,7 +131,7 @@ Writer::~Writer ()
  *  @param eyes Eyes that this frame image is for.
  */
 void
-Writer::write (Data encoded, Frame frame, Eyes eyes)
+Writer::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
 {
        boost::mutex::scoped_lock lock (_state_mutex);
 
@@ -220,20 +221,18 @@ Writer::fake_write (Frame frame, Eyes eyes)
        }
 
        size_t const reel = video_reel (frame);
-       Frame const reel_frame = frame - _reels[reel].start ();
-
-       FILE* file = fopen_boost (_film->info_file(_reels[reel].period()), "rb");
-       if (!file) {
-               throw ReadFileError (_film->info_file(_reels[reel].period()));
-       }
-       dcp::FrameInfo info = _reels[reel].read_frame_info (file, reel_frame, eyes);
-       fclose (file);
+       Frame const frame_in_reel = frame - _reels[reel].start ();
 
        QueueItem qi;
        qi.type = QueueItem::FAKE;
-       qi.size = info.size;
+
+       {
+               shared_ptr<InfoFileHandle> info_file = _film->info_file_handle(_reels[reel].period(), true);
+               qi.size = _reels[reel].read_frame_info(info_file, frame_in_reel, eyes).size;
+       }
+
        qi.reel = reel;
-       qi.frame = reel_frame;
+       qi.frame = frame_in_reel;
        if (_film->three_d() && eyes == EYES_BOTH) {
                qi.eyes = EYES_LEFT;
                _queue.push_back (qi);
@@ -276,8 +275,11 @@ Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
                        /* Easy case: we can write all the audio to this reel */
                        _audio_reel->write (audio);
                        t = end;
+               } else if (_audio_reel->period().to <= t) {
+                       /* This reel is entirely before the start of our audio; just skip the reel */
+                       ++_audio_reel;
                } else {
-                       /* Split the audio into two and write the first part */
+                       /* This audio is over a reel boundary; split the audio into two and write the first part */
                        DCPTime part_lengths[2] = {
                                _audio_reel->period().to - t,
                                end - _audio_reel->period().to
@@ -289,15 +291,12 @@ Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
                        };
 
                        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);
+                               shared_ptr<AudioBuffers> part (new AudioBuffers(audio, part_frames[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;
+                               audio.reset (new AudioBuffers(audio, part_frames[1], part_frames[0]));
                        } else {
                                audio.reset ();
                        }
@@ -308,7 +307,21 @@ Writer::write (shared_ptr<const AudioBuffers> audio, DCPTime const time)
        }
 }
 
-/** This must be called from Writer::thread() with an appropriate lock held */
+
+void
+Writer::write (shared_ptr<const dcp::AtmosFrame> atmos, DCPTime time, AtmosMetadata metadata)
+{
+       if (_atmos_reel->period().to == time) {
+               ++_atmos_reel;
+               DCPOMATIC_ASSERT (_atmos_reel != _reels.end());
+       }
+
+       /* We assume that we get a video frame's worth of data here */
+       _atmos_reel->write (atmos, metadata);
+}
+
+
+/** Caller must hold a lock on _state_mutex */
 bool
 Writer::have_sequenced_image_at_queue_head ()
 {
@@ -317,30 +330,41 @@ Writer::have_sequenced_image_at_queue_head ()
        }
 
        _queue.sort ();
-
        QueueItem const & f = _queue.front();
-       ReelWriter const & reel = _reels[f.reel];
+       return _last_written[f.reel].next(f);
+}
 
-       /* The queue should contain only EYES_LEFT/EYES_RIGHT pairs or EYES_BOTH */
 
-       if (f.eyes == EYES_BOTH) {
+bool
+Writer::LastWritten::next (QueueItem qi) const
+{
+       if (qi.eyes == EYES_BOTH) {
                /* 2D */
-               return f.frame == (reel.last_written_video_frame() + 1);
+               return qi.frame == (_frame + 1);
        }
 
        /* 3D */
 
-       if (reel.last_written_eyes() == EYES_LEFT && f.frame == reel.last_written_video_frame() && f.eyes == EYES_RIGHT) {
+       if (_eyes == EYES_LEFT && qi.frame == _frame && qi.eyes == EYES_RIGHT) {
                return true;
        }
 
-       if (reel.last_written_eyes() == EYES_RIGHT && f.frame == (reel.last_written_video_frame() + 1) && f.eyes == EYES_LEFT) {
+       if (_eyes == EYES_RIGHT && qi.frame == (_frame + 1) && qi.eyes == EYES_LEFT) {
                return true;
        }
 
        return false;
 }
 
+
+void
+Writer::LastWritten::update (QueueItem qi)
+{
+       _frame = qi.frame;
+       _eyes = qi.eyes;
+}
+
+
 void
 Writer::thread ()
 try
@@ -375,11 +399,11 @@ try
                        /* (Hopefully temporarily) log anything that was not written */
                        if (!_queue.empty() && !have_sequenced_image_at_queue_head()) {
                                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, (int) i->eyes);
+                               BOOST_FOREACH (QueueItem const& i, _queue) {
+                                       if (i.type == QueueItem::FULL) {
+                                               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, (int) i->eyes);
+                                               LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i.size, i.frame, (int) i.eyes);
                                        }
                                }
                        }
@@ -389,6 +413,7 @@ try
                /* Write any frames that we can write; i.e. those that are in sequence. */
                while (have_sequenced_image_at_queue_head ()) {
                        QueueItem qi = _queue.front ();
+                       _last_written[qi.reel].update (qi);
                        _queue.pop_front ();
                        if (qi.type == QueueItem::FULL && qi.encoded) {
                                --_queued_full_in_memory;
@@ -402,14 +427,14 @@ try
                        case QueueItem::FULL:
                                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));
+                                       qi.encoded.reset (new ArrayData(_film->j2c_path(qi.reel, qi.frame, qi.eyes, false)));
                                }
                                reel.write (qi.encoded, qi.frame, qi.eyes);
                                ++_full_written;
                                break;
                        case QueueItem::FAKE:
                                LOG_DEBUG_ENCODE (N_("Writer FAKE-writes %1"), qi.frame);
-                               reel.fake_write (qi.frame, qi.eyes, qi.size);
+                               reel.fake_write (qi.size);
                                ++_fake_written;
                                break;
                        case QueueItem::REPEAT:
@@ -438,7 +463,7 @@ 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() + 1;
+                       int const awaiting = _last_written[_queue.front().reel].frame() + 1;
                        lock.unlock ();
 
                        /* i is valid here, even though we don't hold a lock on the mutex,
@@ -468,32 +493,28 @@ catch (...)
 void
 Writer::terminate_thread (bool can_throw)
 {
+       boost::this_thread::disable_interruption dis;
+
        boost::mutex::scoped_lock lock (_state_mutex);
-       if (_thread == 0) {
-               return;
-       }
 
        _finish = true;
        _empty_condition.notify_all ();
        _full_condition.notify_all ();
        lock.unlock ();
 
-       if (_thread->joinable ()) {
-               _thread->join ();
-       }
+       try {
+               _thread.join ();
+       } catch (...) {}
 
        if (can_throw) {
                rethrow ();
        }
-
-       delete _thread;
-       _thread = 0;
 }
 
 void
 Writer::finish ()
 {
-       if (!_thread) {
+       if (!_thread.joinable()) {
                return;
        }
 
@@ -545,37 +566,82 @@ Writer::finish ()
        pool.join_all ();
        service.stop ();
 
-       /* Add reels to CPL */
+       /* Add reels */
 
        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);
+       /* Add metadata */
+
+       string creator = Config::instance()->dcp_creator();
+       if (creator.empty()) {
+               creator = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
        }
-       meta.issuer = Config::instance()->dcp_issuer ();
-       if (meta.issuer.empty ()) {
-               meta.issuer = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
+
+       string issuer = Config::instance()->dcp_issuer();
+       if (issuer.empty()) {
+               issuer = String::compose("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
        }
-       meta.set_issue_date_now ();
 
-       cpl->set_metadata (meta);
+       cpl->set_ratings (_film->ratings());
 
-       shared_ptr<const dcp::CertificateChain> signer;
-       if (_film->is_signed ()) {
-               signer = Config::instance()->signer_chain ();
-               /* We did check earlier, but check again here to be on the safe side */
-               string reason;
-               if (!signer->valid (&reason)) {
-                       throw InvalidSignerError (reason);
+       vector<dcp::ContentVersion> cv;
+       BOOST_FOREACH (string i, _film->content_versions()) {
+               cv.push_back (dcp::ContentVersion(i));
+       }
+       cpl->set_content_versions (cv);
+
+       cpl->set_full_content_title_text (_film->name());
+       cpl->set_full_content_title_text_language (_film->name_language());
+       cpl->set_release_territory (_film->release_territory());
+       cpl->set_version_number (_film->version_number());
+       cpl->set_status (_film->status());
+       cpl->set_chain (_film->chain());
+       cpl->set_distributor (_film->distributor());
+       cpl->set_facility (_film->facility());
+       cpl->set_luminance (_film->luminance());
+
+       list<int> ac = _film->mapped_audio_channels ();
+       dcp::MCASoundField field = (
+               find(ac.begin(), ac.end(), static_cast<int>(dcp::BSL)) != ac.end() ||
+               find(ac.begin(), ac.end(), static_cast<int>(dcp::BSR)) != ac.end()
+               ) ? dcp::SEVEN_POINT_ONE : dcp::FIVE_POINT_ONE;
+
+       dcp::MainSoundConfiguration msc (field, _film->audio_channels());
+       BOOST_FOREACH (int i, ac) {
+               if (i < _film->audio_channels()) {
+                       msc.set_mapping (i, static_cast<dcp::Channel>(i));
                }
        }
 
-       dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer, Config::instance()->dcp_metadata_filename_format());
+       cpl->set_main_sound_configuration (msc.to_string());
+       cpl->set_main_sound_sample_rate (_film->audio_frame_rate());
+       cpl->set_main_picture_stored_area (_film->frame_size());
+       cpl->set_main_picture_active_area (_film->active_area());
+
+       vector<dcp::LanguageTag> sl = _film->subtitle_languages();
+       if (sl.size() > 1) {
+               cpl->set_additional_subtitle_languages(std::vector<dcp::LanguageTag>(sl.begin() + 1, sl.end()));
+       }
+
+       shared_ptr<const dcp::CertificateChain> signer;
+       signer = Config::instance()->signer_chain ();
+       /* We did check earlier, but check again here to be on the safe side */
+       string reason;
+       if (!signer->valid (&reason)) {
+               throw InvalidSignerError (reason);
+       }
+
+       dcp.write_xml (
+               _film->interop() ? dcp::INTEROP : dcp::SMPTE,
+               issuer,
+               creator,
+               dcp::LocalTime().as_string(),
+               String::compose("Created by libdcp %1", dcp::version),
+               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
@@ -590,7 +656,7 @@ 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);
+               throw OpenFileError (cover, errno, OpenFileError::WRITE);
        }
 
        string text = Config::instance()->cover_sheet ();
@@ -598,7 +664,13 @@ Writer::write_cover_sheet ()
        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);
+
+       vector<dcp::LanguageTag> subtitle_languages = _film->subtitle_languages();
+       if (subtitle_languages.empty()) {
+               boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", "None");
+       } else {
+               boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", subtitle_languages.front().description());
+       }
 
        boost::uintmax_t size = 0;
        for (
@@ -641,7 +713,7 @@ Writer::write_cover_sheet ()
 
        boost::algorithm::replace_all (text, "$LENGTH", length);
 
-       fwrite (text.c_str(), 1, text.length(), f);
+       checked_fwrite (text.c_str(), text.length(), f, cover);
        fclose (f);
 }
 
@@ -678,12 +750,15 @@ Writer::write (PlayerText text, TextType type, optional<DCPTextTrack> track, DCP
                reel = &_subtitle_reel;
                break;
        case TEXT_CLOSED_CAPTION:
+               DCPOMATIC_ASSERT (track);
+               DCPOMATIC_ASSERT (_caption_reels.find(*track) != _caption_reels.end());
                reel = &_caption_reels[*track];
                break;
        default:
                DCPOMATIC_ASSERT (false);
        }
 
+       DCPOMATIC_ASSERT (*reel != _reels.end());
        while ((*reel)->period().to <= period.from) {
                ++(*reel);
                DCPOMATIC_ASSERT (*reel != _reels.end());
@@ -761,14 +836,16 @@ Writer::video_reel (int frame) const
 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);
+
+       _digest_progresses[boost::this_thread::get_id()] = progress;
        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);
+
+       Waker waker;
+       waker.nudge ();
 }