Rename Subtitle -> Text
[dcpomatic.git] / src / lib / film.cc
index 13a03d929602a1e05c5a1a558b1127e8e999aeec..0be1ddd7b71a19161c44f6957f63c5a3aae23213 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2018 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
@@ -19,7 +19,7 @@
 */
 
 /** @file  src/film.cc
- *  @brief A representation of some audio and video content, and details of
+ *  @brief A representation of some audio, video and subtitle content, and details of
  *  how they should be presented in a DCP.
  */
 
@@ -27,6 +27,7 @@
 #include "job.h"
 #include "util.h"
 #include "job_manager.h"
+#include "dcp_encoder.h"
 #include "transcode_job.h"
 #include "upload_job.h"
 #include "null_log.h"
@@ -45,7 +46,7 @@
 #include "screen.h"
 #include "audio_content.h"
 #include "video_content.h"
-#include "subtitle_content.h"
+#include "text_content.h"
 #include "ffmpeg_content.h"
 #include "dcp_content.h"
 #include "screen_kdm.h"
@@ -57,6 +58,8 @@
 #include <dcp/local_time.h>
 #include <dcp/decrypted_kdm.h>
 #include <dcp/raw_convert.h>
+#include <dcp/reel_mxf.h>
+#include <dcp/reel_asset.h>
 #include <libxml++/libxml++.h>
 #include <boost/filesystem.hpp>
 #include <boost/algorithm/string.hpp>
@@ -83,6 +86,9 @@ using std::cout;
 using std::list;
 using std::set;
 using std::runtime_error;
+using std::copy;
+using std::back_inserter;
+using std::map;
 using boost::shared_ptr;
 using boost::weak_ptr;
 using boost::dynamic_pointer_cast;
@@ -93,6 +99,8 @@ using dcp::raw_convert;
 #define LOG_GENERAL(...) log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL);
 #define LOG_GENERAL_NC(...) log()->log (__VA_ARGS__, LogEntry::TYPE_GENERAL);
 
+string const Film::metadata_file = "metadata.xml";
+
 /* 5 -> 6
  * AudioMapping XML changed.
  * 6 -> 7
@@ -132,6 +140,7 @@ Film::Film (optional<boost::filesystem::path> dir)
        , _resolution (RESOLUTION_2K)
        , _signed (true)
        , _encrypted (false)
+       , _context_id (dcp::make_uuid ())
        , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ())
        , _isdcf_metadata (Config::instance()->default_isdcf_metadata ())
        , _video_frame_rate (24)
@@ -142,7 +151,7 @@ Film::Film (optional<boost::filesystem::path> dir)
        , _audio_processor (0)
        , _reel_type (REELTYPE_SINGLE)
        , _reel_length (2000000000)
-       , _upload_after_make_dcp (false)
+       , _upload_after_make_dcp (Config::instance()->default_upload_after_make_dcp())
        , _state_version (current_state_version)
        , _dirty (false)
 {
@@ -284,7 +293,36 @@ void
 Film::make_dcp ()
 {
        if (dcp_name().find ("/") != string::npos) {
-               throw BadSettingError (_("name"), _("cannot contain slashes"));
+               throw BadSettingError (_("name"), _("Cannot contain slashes"));
+       }
+
+       if (container() == 0) {
+               throw MissingSettingError (_("container"));
+       }
+
+       if (content().empty()) {
+               throw runtime_error (_("You must add some content to the DCP before creating it"));
+       }
+
+       if (dcp_content_type() == 0) {
+               throw MissingSettingError (_("content type"));
+       }
+
+       if (name().empty()) {
+               set_name ("DCP");
+       }
+
+       BOOST_FOREACH (shared_ptr<const Content> i, content ()) {
+               if (!i->paths_valid()) {
+                       throw runtime_error (_("some of your content is missing"));
+               }
+               shared_ptr<const DCPContent> dcp = dynamic_pointer_cast<const DCPContent> (i);
+               if (dcp && dcp->needs_kdm()) {
+                       throw runtime_error (_("Some of your content needs a KDM"));
+               }
+               if (dcp && dcp->needs_assets()) {
+                       throw runtime_error (_("Some of your content needs an OV"));
+               }
        }
 
        set_isdcf_date_today ();
@@ -300,27 +338,13 @@ Film::make_dcp ()
        if (Config::instance()->only_servers_encode ()) {
                LOG_GENERAL_NC ("0 threads: ONLY SERVERS SET TO ENCODE");
        } else {
-               LOG_GENERAL ("%1 threads", Config::instance()->num_local_encoding_threads());
+               LOG_GENERAL ("%1 threads", Config::instance()->master_encoding_threads());
        }
        LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth());
 
-       if (container() == 0) {
-               throw MissingSettingError (_("container"));
-       }
-
-       if (content().empty()) {
-               throw runtime_error (_("You must add some content to the DCP before creating it"));
-       }
-
-       if (dcp_content_type() == 0) {
-               throw MissingSettingError (_("content type"));
-       }
-
-       if (name().empty()) {
-               throw MissingSettingError (_("name"));
-       }
-
-       JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this())));
+       shared_ptr<TranscodeJob> tj (new TranscodeJob (shared_from_this()));
+       tj->set_encoder (shared_ptr<Encoder> (new DCPEncoder (shared_from_this(), tj)));
+       JobManager::instance()->add (tj);
 }
 
 /** Start a job to send our DCP to the configured TMS */
@@ -361,6 +385,7 @@ Film::metadata (bool with_content_paths) const
        root->add_child("Signed")->add_child_text (_signed ? "1" : "0");
        root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0");
        root->add_child("Key")->add_child_text (_key.hex ());
+       root->add_child("ContextID")->add_child_text (_context_id);
        if (_audio_processor) {
                root->add_child("AudioProcessor")->add_child_text (_audio_processor->id ());
        }
@@ -379,7 +404,7 @@ Film::write_metadata () const
        DCPOMATIC_ASSERT (directory());
        boost::filesystem::create_directories (directory().get());
        shared_ptr<xmlpp::Document> doc = metadata ();
-       doc->write_to_file_formatted (file("metadata.xml").string ());
+       doc->write_to_file_formatted (file(metadata_file).string ());
        _dirty = false;
 }
 
@@ -399,11 +424,11 @@ list<string>
 Film::read_metadata (optional<boost::filesystem::path> path)
 {
        if (!path) {
-               if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file ("metadata.xml"))) {
+               if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file (metadata_file))) {
                        throw runtime_error (_("This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version.  You will need to create a new Film, re-add your content and set it up again.  Sorry!"));
                }
 
-               path = file ("metadata.xml");
+               path = file (metadata_file);
        }
 
        cxml::Document f ("Metadata");
@@ -463,6 +488,7 @@ Film::read_metadata (optional<boost::filesystem::path> path)
        _three_d = f.bool_child ("ThreeD");
        _interop = f.bool_child ("Interop");
        _key = dcp::Key (f.string_child ("Key"));
+       _context_id = f.optional_string_child("ContextID").get_value_or (dcp::make_uuid ());
 
        if (f.optional_string_child ("AudioProcessor")) {
                _audio_processor = AudioProcessor::from_id (f.string_child ("AudioProcessor"));
@@ -488,10 +514,11 @@ Film::read_metadata (optional<boost::filesystem::path> path)
 }
 
 /** Given a directory name, return its full path within the Film's directory.
- *  The directory (and its parents) will be created if they do not exist.
+ *  @param d directory name within the Film's directory.
+ *  @param create true to create the directory (and its parents) if they do not exist.
  */
 boost::filesystem::path
-Film::dir (boost::filesystem::path d) const
+Film::dir (boost::filesystem::path d, bool create) const
 {
        DCPOMATIC_ASSERT (_directory);
 
@@ -499,7 +526,9 @@ Film::dir (boost::filesystem::path d) const
        p /= _directory.get();
        p /= d;
 
-       boost::filesystem::create_directories (p);
+       if (create) {
+               boost::filesystem::create_directories (p);
+       }
 
        return p;
 }
@@ -640,7 +669,7 @@ Film::isdcf_name (bool if_created_now) const
 
        /* XXX: this uses the first bit of content only */
 
-       /* The standard says we don't do this for trailers, for some strange reason */
+       /* Interior aspect ratio.  The standard says we don't do this for trailers, for some strange reason */
        if (dcp_content_type() && dcp_content_type()->libdcp_kind() != dcp::TRAILER) {
                Ratio const * content_ratio = 0;
                BOOST_FOREACH (shared_ptr<Content> i, content ()) {
@@ -656,7 +685,8 @@ Film::isdcf_name (bool if_created_now) const
                }
 
                if (content_ratio && content_ratio != container()) {
-                       d += "-" + content_ratio->isdcf_name();
+                       /* This needs to be the numeric version of the ratio, and ::id() is close enough */
+                       d += "-" + content_ratio->id();
                }
        }
 
@@ -699,24 +729,11 @@ Film::isdcf_name (bool if_created_now) const
 
        /* Count mapped audio channels */
 
-       int non_lfe = 0;
-       int lfe = 0;
-
-       BOOST_FOREACH (int i, mapped_audio_channels ()) {
-               if (i >= audio_channels()) {
-                       /* This channel is mapped but is not included in the DCP */
-                       continue;
-               }
-
-               if (static_cast<dcp::Channel> (i) == dcp::LFE) {
-                       ++lfe;
-               } else {
-                       ++non_lfe;
-               }
-       }
-
-       if (non_lfe) {
-               d += String::compose("_%1%2", non_lfe, lfe);
+       pair<int, int> ch = audio_channel_types (mapped_audio_channels(), audio_channels());
+       if (!ch.first && !ch.second) {
+               d += "_MOS";
+       } else if (ch.first) {
+               d += String::compose("_%1%2", ch.first, ch.second);
        }
 
        /* XXX: HI/VI */
@@ -770,24 +787,10 @@ Film::dcp_name (bool if_created_now) const
 {
        string unfiltered;
        if (use_isdcf_name()) {
-               unfiltered = isdcf_name (if_created_now);
-       } else {
-               unfiltered = name ();
-       }
-
-       /* Filter out `bad' characters which cause problems with some systems.
-          There's no apparent list of what really is allowed, so this is a guess.
-       */
-
-       string filtered;
-       string const allowed = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
-       for (size_t i = 0; i < unfiltered.size(); ++i) {
-               if (allowed.find (unfiltered[i]) != string::npos) {
-                       filtered += unfiltered[i];
-               }
+               return careful_string_filter (isdcf_name (if_created_now));
        }
 
-       return filtered;
+       return careful_string_filter (name ());
 }
 
 void
@@ -1027,31 +1030,27 @@ Film::content () const
        return _playlist->content ();
 }
 
+/** @param content Content to add.
+ *  @param disable_audio_analysis true to never do automatic audio analysis, even if it is enabled in configuration.
+ */
 void
-Film::examine_content (shared_ptr<Content> c)
-{
-       shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
-       JobManager::instance()->add (j);
-}
-
-void
-Film::examine_and_add_content (shared_ptr<Content> c)
+Film::examine_and_add_content (shared_ptr<Content> content, bool disable_audio_analysis)
 {
-       if (dynamic_pointer_cast<FFmpegContent> (c) && _directory) {
-               run_ffprobe (c->path(0), file ("ffprobe.log"), _log);
+       if (dynamic_pointer_cast<FFmpegContent> (content) && _directory) {
+               run_ffprobe (content->path(0), file ("ffprobe.log"), _log);
        }
 
-       shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
+       shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), content));
 
        _job_connections.push_back (
-               j->Finished.connect (bind (&Film::maybe_add_content, this, weak_ptr<Job> (j), weak_ptr<Content> (c)))
+               j->Finished.connect (bind (&Film::maybe_add_content, this, weak_ptr<Job>(j), weak_ptr<Content>(content), disable_audio_analysis))
                );
 
        JobManager::instance()->add (j);
 }
 
 void
-Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c)
+Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c, bool disable_audio_analysis)
 {
        shared_ptr<Job> job = j.lock ();
        if (!job || !job->finished_ok ()) {
@@ -1065,12 +1064,12 @@ Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c)
 
        add_content (content);
 
-       if (Config::instance()->automatic_audio_analysis() && content->audio) {
+       if (Config::instance()->automatic_audio_analysis() && content->audio && !disable_audio_analysis) {
                shared_ptr<Playlist> playlist (new Playlist);
                playlist->add (content);
                boost::signals2::connection c;
                JobManager::instance()->analyse_audio (
-                       shared_from_this (), playlist, c, bind (&Film::audio_analysis_finished, this)
+                       shared_from_this(), playlist, false, c, bind (&Film::audio_analysis_finished, this)
                        );
                _audio_analysis_connections.push_back (c);
        }
@@ -1089,9 +1088,7 @@ Film::add_content (shared_ptr<Content> c)
        if (_template_film) {
                /* Take settings from the first piece of content of c's type in _template */
                BOOST_FOREACH (shared_ptr<Content> i, _template_film->content()) {
-                       if (typeid(i.get()) == typeid(c.get())) {
-                               c->use_template (i);
-                       }
+                       c->take_settings_from (i);
                }
        }
 
@@ -1120,7 +1117,7 @@ Film::move_content_later (shared_ptr<Content> c)
 DCPTime
 Film::length () const
 {
-       return _playlist->length ();
+       return _playlist->length().ceil(video_frame_rate());
 }
 
 int
@@ -1177,6 +1174,10 @@ Film::audio_frame_rate () const
 void
 Film::set_sequence (bool s)
 {
+       if (s == _sequence) {
+               return;
+       }
+
        _sequence = s;
        _playlist->set_sequence (s);
        signal_changed (SEQUENCE);
@@ -1204,8 +1205,15 @@ Film::frame_size () const
        return fit_ratio_within (container()->ratio(), full_frame ());
 }
 
-/** @param from KDM from time expressed as a local time with an offset from UTC
- *  @param to KDM to time expressed as a local time with an offset from UTC
+/** @param recipient KDM recipient certificate.
+ *  @param trusted_devices Certificates of other trusted devices (can be empty).
+ *  @param cpl_file CPL filename.
+ *  @param from KDM from time expressed as a local time with an offset from UTC.
+ *  @param until KDM to time expressed as a local time with an offset from UTC.
+ *  @param formulation KDM formulation to use.
+ *  @param disable_forensic_marking_picture true to disable forensic marking of picture.
+ *  @param disable_forensic_marking_audio if not set, don't disable forensic marking of audio.  If set to 0,
+ *  disable all forensic marking; if set above 0, disable forensic marking above that channel.
  */
 dcp::EncryptedKDM
 Film::make_kdm (
@@ -1214,30 +1222,80 @@ Film::make_kdm (
        boost::filesystem::path cpl_file,
        dcp::LocalTime from,
        dcp::LocalTime until,
-       dcp::Formulation formulation
+       dcp::Formulation formulation,
+       bool disable_forensic_marking_picture,
+       optional<int> disable_forensic_marking_audio
        ) const
 {
+       if (!_encrypted) {
+               throw runtime_error (_("Cannot make a KDM as this project is not encrypted."));
+       }
+
        shared_ptr<const dcp::CPL> cpl (new dcp::CPL (cpl_file));
        shared_ptr<const dcp::CertificateChain> signer = Config::instance()->signer_chain ();
        if (!signer->valid ()) {
                throw InvalidSignerError ();
        }
 
-       return dcp::DecryptedKDM (
-               cpl, key(), from, until, cpl->content_title_text(), cpl->content_title_text(), dcp::LocalTime().as_string()
-               ).encrypt (signer, recipient, trusted_devices, formulation);
-}
+       /* Find keys that have been added to imported, encrypted DCP content */
+       list<dcp::DecryptedKDMKey> imported_keys;
+       BOOST_FOREACH (shared_ptr<Content> i, content()) {
+               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent> (i);
+               if (d && d->kdm()) {
+                       dcp::DecryptedKDM kdm (d->kdm().get(), Config::instance()->decryption_chain()->key().get());
+                       list<dcp::DecryptedKDMKey> keys = kdm.keys ();
+                       copy (keys.begin(), keys.end(), back_inserter (imported_keys));
+               }
+       }
+
+       map<shared_ptr<const dcp::ReelMXF>, dcp::Key> keys;
+
+       BOOST_FOREACH(shared_ptr<const dcp::ReelAsset> i, cpl->reel_assets ()) {
+               shared_ptr<const dcp::ReelMXF> mxf = boost::dynamic_pointer_cast<const dcp::ReelMXF> (i);
+               if (!mxf || !mxf->key_id()) {
+                       continue;
+               }
+
+               /* Get any imported key for this ID */
+               bool done = false;
+               BOOST_FOREACH (dcp::DecryptedKDMKey j, imported_keys) {
+                       if (j.id() == mxf->key_id().get()) {
+                               LOG_GENERAL ("Using imported key for %1", mxf->key_id().get());
+                               keys[mxf] = j.key();
+                               done = true;
+                       }
+               }
 
-/** @param from KDM from time expressed as a local time in the time zone of the Screen's Cinema.
- *  @param to KDM to time expressed as a local time in the time zone of the Screen's Cinema.
+               if (!done) {
+                       /* No imported key; it must be an asset that we encrypted */
+                       LOG_GENERAL ("Using our own key for %1", mxf->key_id().get());
+                       keys[mxf] = key();
+               }
+       }
+
+       return dcp::DecryptedKDM (
+               cpl->id(), keys, from, until, cpl->content_title_text(), cpl->content_title_text(), dcp::LocalTime().as_string()
+               ).encrypt (signer, recipient, trusted_devices, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio);
+}
+
+/** @param screens Screens to make KDMs for.
+ *  @param cpl_file Path to CPL to make KDMs for.
+ *  @param from KDM from time expressed as a local time in the time zone of the Screen's Cinema.
+ *  @param until KDM to time expressed as a local time in the time zone of the Screen's Cinema.
+ *  @param formulation KDM formulation to use.
+ *  @param disable_forensic_marking_picture true to disable forensic marking of picture.
+ *  @param disable_forensic_marking_audio if not set, don't disable forensic marking of audio.  If set to 0,
+ *  disable all forensic marking; if set above 0, disable forensic marking above that channel.
  */
 list<ScreenKDM>
 Film::make_kdms (
        list<shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl_file,
        boost::posix_time::ptime from,
        boost::posix_time::ptime until,
-       dcp::Formulation formulation
+       dcp::Formulation formulation,
+       bool disable_forensic_marking_picture,
+       optional<int> disable_forensic_marking_audio
        ) const
 {
        list<ScreenKDM> kdms;
@@ -1247,10 +1305,12 @@ Film::make_kdms (
                        dcp::EncryptedKDM const kdm = make_kdm (
                                i->recipient.get(),
                                i->trusted_devices,
-                               dcp,
+                               cpl_file,
                                dcp::LocalTime (from, i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()),
                                dcp::LocalTime (until, i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()),
-                               formulation
+                               formulation,
+                               disable_forensic_marking_picture,
+                               disable_forensic_marking_audio
                                );
 
                        kdms.push_back (ScreenKDM (i, kdm));
@@ -1420,7 +1480,7 @@ list<DCPTimePeriod>
 Film::reels () const
 {
        list<DCPTimePeriod> p;
-       DCPTime const len = length().round_up (video_frame_rate ());
+       DCPTime const len = length();
 
        switch (reel_type ()) {
        case REELTYPE_SINGLE:
@@ -1471,48 +1531,15 @@ Film::reels () const
        return p;
 }
 
+/** @param period A period within the DCP
+ *  @return Name of the content which most contributes to the given period.
+ */
 string
 Film::content_summary (DCPTimePeriod period) const
 {
        return _playlist->content_summary (period);
 }
 
-list<string>
-Film::fix_conflicting_settings ()
-{
-       list<string> notes;
-
-       list<boost::filesystem::path> was_referencing;
-       BOOST_FOREACH (shared_ptr<Content> i, content()) {
-               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent> (i);
-               if (d) {
-                       list<string> reasons;
-                       bool was = false;
-                       if (!d->can_reference_video(reasons) && d->reference_video()) {
-                               d->set_reference_video (false);
-                               was = true;
-                       }
-                       if (!d->can_reference_audio(reasons) && d->reference_audio()) {
-                               d->set_reference_audio (false);
-                               was = true;
-                       }
-                       if (!d->can_reference_subtitle(reasons) && d->reference_subtitle()) {
-                               d->set_reference_subtitle (false);
-                               was = true;
-                       }
-                       if (was) {
-                               was_referencing.push_back (d->path(0).parent_path().filename());
-                       }
-               }
-       }
-
-       BOOST_FOREACH (boost::filesystem::path d, was_referencing) {
-               notes.push_back (String::compose (_("The DCP %1 was being referred to by this film.  This not now possible because the reel sizes in the film no longer agree with those in the imported DCP.\n\nSetting the 'Reel type' to 'split by video content' will probably help.\n\nAfter doing that you would need to re-tick the appropriate 'refer to existing DCP' checkboxes."), d.string()));
-       }
-
-       return notes;
-}
-
 void
 Film::use_template (string name)
 {
@@ -1534,4 +1561,43 @@ Film::use_template (string name)
        _reel_type = _template_film->_reel_type;
        _reel_length = _template_film->_reel_length;
        _upload_after_make_dcp = _template_film->_upload_after_make_dcp;
+       _isdcf_metadata = _template_film->_isdcf_metadata;
+}
+
+pair<double, double>
+Film::speed_up_range (int dcp_frame_rate) const
+{
+       return _playlist->speed_up_range (dcp_frame_rate);
+}
+
+void
+Film::copy_from (shared_ptr<const Film> film)
+{
+       read_metadata (film->file (metadata_file));
+}
+
+bool
+Film::references_dcp_video () const
+{
+       BOOST_FOREACH (shared_ptr<Content> i, _playlist->content()) {
+               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent>(i);
+               if (d && d->reference_video()) {
+                       return true;
+               }
+       }
+
+       return false;
+}
+
+bool
+Film::references_dcp_audio () const
+{
+       BOOST_FOREACH (shared_ptr<Content> i, _playlist->content()) {
+               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent>(i);
+               if (d && d->reference_audio()) {
+                       return true;
+               }
+       }
+
+       return false;
 }