Store subtitle language(s) in Film, and allow setup of those
[dcpomatic.git] / src / lib / film.cc
index b233e5ee690a5ccf984aac7f730dd04ad8c13f94..9943711f55cfdc45b3f8a6536a83ffe32447addb 100644 (file)
@@ -23,6 +23,7 @@
  *  how they should be presented in a DCP.
  */
 
+#include "atmos_content.h"
 #include "film.h"
 #include "job.h"
 #include "util.h"
@@ -101,6 +102,9 @@ using boost::weak_ptr;
 using boost::dynamic_pointer_cast;
 using boost::optional;
 using boost::is_any_of;
+#if BOOST_VERSION >= 106100
+using namespace boost::placeholders;
+#endif
 using dcp::raw_convert;
 using namespace dcpomatic;
 
@@ -132,8 +136,10 @@ string const Film::metadata_file = "metadata.xml";
  * 36 -> 37
  * TextContent can be in a Caption tag, and some of the tag names
  * have had Subtitle prefixes or suffixes removed.
+ * 37 -> 38
+ * VideoContent scale expressed just as "guess" or "custom"
  */
-int const Film::current_state_version = 37;
+int const Film::current_state_version = 38;
 
 /** Construct a Film object in a given directory.
  *
@@ -146,7 +152,6 @@ Film::Film (optional<boost::filesystem::path> dir)
        , _dcp_content_type (Config::instance()->default_dcp_content_type ())
        , _container (Config::instance()->default_container ())
        , _resolution (RESOLUTION_2K)
-       , _signed (true)
        , _encrypted (false)
        , _context_id (dcp::make_uuid ())
        , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ())
@@ -159,9 +164,16 @@ Film::Film (optional<boost::filesystem::path> dir)
        , _audio_processor (0)
        , _reel_type (REELTYPE_SINGLE)
        , _reel_length (2000000000)
-       , _upload_after_make_dcp (Config::instance()->default_upload_after_make_dcp())
        , _reencode_j2k (false)
        , _user_explicit_video_frame_rate (false)
+       , _user_explicit_container (false)
+       , _user_explicit_resolution (false)
+       , _name_language (dcp::LanguageTag("en-US"))
+       , _audio_language (dcp::LanguageTag("en-US"))
+       , _release_territory (dcp::LanguageTag::RegionSubtag("US"))
+       , _version_number (1)
+       , _status (dcp::FINAL)
+       , _luminance (dcp::Luminance(4.5, dcp::Luminance::FOOT_LAMBERT))
        , _state_version (current_state_version)
        , _dirty (false)
        , _tolerant (false)
@@ -443,7 +455,6 @@ Film::metadata (bool with_content_paths) const
        root->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0");
        root->add_child("Sequence")->add_child_text (_sequence ? "1" : "0");
        root->add_child("Interop")->add_child_text (_interop ? "1" : "0");
-       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);
@@ -452,7 +463,6 @@ Film::metadata (bool with_content_paths) const
        }
        root->add_child("ReelType")->add_child_text (raw_convert<string> (static_cast<int> (_reel_type)));
        root->add_child("ReelLength")->add_child_text (raw_convert<string> (_reel_length));
-       root->add_child("UploadAfterMakeDCP")->add_child_text (_upload_after_make_dcp ? "1" : "0");
        root->add_child("ReencodeJ2K")->add_child_text (_reencode_j2k ? "1" : "0");
        root->add_child("UserExplicitVideoFrameRate")->add_child_text(_user_explicit_video_frame_rate ? "1" : "0");
        for (map<dcp::Marker, DCPTime>::const_iterator i = _markers.begin(); i != _markers.end(); ++i) {
@@ -463,7 +473,24 @@ Film::metadata (bool with_content_paths) const
        BOOST_FOREACH (dcp::Rating i, _ratings) {
                i.as_xml (root->add_child("Rating"));
        }
-       root->add_child("ContentVersion")->add_child_text(_content_version);
+       BOOST_FOREACH (string i, _content_versions) {
+               root->add_child("ContentVersion")->add_child_text(i);
+       }
+       root->add_child("NameLanguage")->add_child_text(_name_language.to_string());
+       root->add_child("AudioLanguage")->add_child_text(_audio_language.to_string());
+       root->add_child("ReleaseTerritory")->add_child_text(_release_territory.subtag());
+       root->add_child("VersionNumber")->add_child_text(raw_convert<string>(_version_number));
+       root->add_child("Status")->add_child_text(dcp::status_to_string(_status));
+       root->add_child("Chain")->add_child_text(_chain);
+       root->add_child("Distributor")->add_child_text(_distributor);
+       root->add_child("Facility")->add_child_text(_facility);
+       root->add_child("LuminanceValue")->add_child_text(raw_convert<string>(_luminance.value()));
+       root->add_child("LuminanceUnit")->add_child_text(dcp::Luminance::unit_to_string(_luminance.unit()));
+       root->add_child("UserExplicitContainer")->add_child_text(_user_explicit_container ? "1" : "0");
+       root->add_child("UserExplicitResolution")->add_child_text(_user_explicit_resolution ? "1" : "0");
+       BOOST_FOREACH (dcp::LanguageTag i, _subtitle_languages) {
+               root->add_child("SubtitleLanguage")->add_child_text(i.to_string());
+       }
        _playlist->as_xml (root->add_child ("Playlist"), with_content_paths);
 
        return doc;
@@ -561,7 +588,6 @@ Film::read_metadata (optional<boost::filesystem::path> path)
        _resolution = string_to_resolution (f.string_child ("Resolution"));
        _j2k_bandwidth = f.number_child<int> ("J2KBandwidth");
        _video_frame_rate = f.number_child<int> ("VideoFrameRate");
-       _signed = f.optional_bool_child("Signed").get_value_or (true);
        _encrypted = f.bool_child ("Encrypted");
        _audio_channels = f.number_child<int> ("AudioChannels");
        /* We used to allow odd numbers (and zero) channels, but it's just not worth
@@ -599,7 +625,6 @@ Film::read_metadata (optional<boost::filesystem::path> path)
 
        _reel_type = static_cast<ReelType> (f.optional_number_child<int>("ReelType").get_value_or (static_cast<int>(REELTYPE_SINGLE)));
        _reel_length = f.optional_number_child<int64_t>("ReelLength").get_value_or (2000000000);
-       _upload_after_make_dcp = f.optional_bool_child("UploadAfterMakeDCP").get_value_or (false);
        _reencode_j2k = f.optional_bool_child("ReencodeJ2K").get_value_or(false);
        _user_explicit_video_frame_rate = f.optional_bool_child("UserExplicitVideoFrameRate").get_value_or(false);
 
@@ -611,7 +636,47 @@ Film::read_metadata (optional<boost::filesystem::path> path)
                _ratings.push_back (dcp::Rating(i));
        }
 
-       _content_version = f.optional_string_child("ContentVersion").get_value_or("");
+       BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children("ContentVersion")) {
+               _content_versions.push_back (i->content());
+       }
+
+       optional<string> name_language = f.optional_string_child("NameLanguage");
+       if (name_language) {
+               _name_language = dcp::LanguageTag (*name_language);
+       }
+       optional<string> audio_language = f.optional_string_child("AudioLanguage");
+       if (audio_language) {
+               _audio_language = dcp::LanguageTag (*audio_language);
+       }
+       optional<string> release_territory = f.optional_string_child("ReleaseTerritory");
+       if (release_territory) {
+               _release_territory = dcp::LanguageTag::RegionSubtag (*release_territory);
+       }
+
+       _version_number = f.optional_number_child<int>("VersionNumber").get_value_or(0);
+
+       optional<string> status = f.optional_string_child("Status");
+       if (status) {
+               _status = dcp::string_to_status (*status);
+       }
+
+       _chain = f.optional_string_child("Chain").get_value_or("");
+       _distributor = f.optional_string_child("Distributor").get_value_or("");
+       _facility = f.optional_string_child("Facility").get_value_or("");
+
+       float value = f.optional_number_child<float>("LuminanceValue").get_value_or(4.5);
+       optional<string> unit = f.optional_string_child("LuminanceUnit");
+       if (unit) {
+               _luminance = dcp::Luminance (value, dcp::Luminance::string_to_unit(*unit));
+       }
+
+       /* Disable guessing for files made in previous DCP-o-matic versions */
+       _user_explicit_container = f.optional_bool_child("UserExplicitContainer").get_value_or(true);
+       _user_explicit_resolution = f.optional_bool_child("UserExplicitResolution").get_value_or(true);
+
+       BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children("SubtitleLanguage")) {
+               _subtitle_languages.push_back (dcp::LanguageTag(i->content()));
+       }
 
        list<string> notes;
        _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version, notes);
@@ -787,11 +852,7 @@ Film::isdcf_name (bool if_created_now) const
                BOOST_FOREACH (shared_ptr<Content> i, content ()) {
                        if (i->video) {
                                /* Here's the first piece of video content */
-                               if (i->video->scale().ratio ()) {
-                                       content_ratio = i->video->scale().ratio ();
-                               } else {
-                                       content_ratio = Ratio::from_ratio (i->video->size().ratio ());
-                               }
+                               content_ratio = Ratio::nearest_from_ratio(i->video->scaled_size(frame_size()).ratio());
                                break;
                        }
                }
@@ -809,39 +870,27 @@ Film::isdcf_name (bool if_created_now) const
                   for now I'm just appending -CCAP if we have any closed captions.
                */
 
-               optional<string> subtitle_language;
                bool burnt_in = true;
                bool ccap = false;
                BOOST_FOREACH (shared_ptr<Content> i, content()) {
                        BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
-                               if (j->type() == TEXT_OPEN_SUBTITLE && j->use()) {
-                                       subtitle_language = j->language ();
-                                       if (!j->burn()) {
-                                               burnt_in = false;
-                                       }
+                               if (j->type() == TEXT_OPEN_SUBTITLE && j->use() && !j->burn()) {
+                                       burnt_in = false;
                                } else if (j->type() == TEXT_CLOSED_CAPTION && j->use()) {
                                        ccap = true;
                                }
                        }
                }
 
-               if (dm.subtitle_language) {
-                       /* Subtitle language is overridden in ISDCF metadata, primarily to handle
-                          content with pre-burnt subtitles.
-                       */
-                       d += "-" + *dm.subtitle_language;
-                       if (ccap) {
-                               d += "-CCAP";
-                       }
-               } else if (subtitle_language) {
-                       /* Language is worked out from the content */
-                       if (burnt_in && *subtitle_language != "XX") {
-                               transform (subtitle_language->begin(), subtitle_language->end(), subtitle_language->begin(), ::tolower);
+               if (!_subtitle_languages.empty()) {
+                       string lang = _subtitle_languages.front().language().get_value_or("en").subtag();
+                       if (burnt_in) {
+                               transform (lang.begin(), lang.end(), lang.begin(), ::tolower);
                        } else {
-                               transform (subtitle_language->begin(), subtitle_language->end(), subtitle_language->begin(), ::toupper);
+                               transform (lang.begin(), lang.end(), lang.begin(), ::toupper);
                        }
 
-                       d += "-" + *subtitle_language;
+                       d += "-" + lang;
                        if (ccap) {
                                d += "-CCAP";
                        }
@@ -971,20 +1020,39 @@ Film::set_dcp_content_type (DCPContentType const * t)
        _dcp_content_type = t;
 }
 
+
+/** @param explicit_user true if this is being set because of
+ *  a direct user request, false if it is being done because
+ *  DCP-o-matic is guessing the best container to use.
+ */
 void
-Film::set_container (Ratio const * c)
+Film::set_container (Ratio const * c, bool explicit_user)
 {
        ChangeSignaller<Film> ch (this, CONTAINER);
        _container = c;
+
+       if (explicit_user) {
+               _user_explicit_container = true;
+       }
 }
 
+
+/** @param explicit_user true if this is being set because of
+ *  a direct user request, false if it is being done because
+ *  DCP-o-matic is guessing the best resolution to use.
+ */
 void
-Film::set_resolution (Resolution r)
+Film::set_resolution (Resolution r, bool explicit_user)
 {
        ChangeSignaller<Film> ch (this, RESOLUTION);
        _resolution = r;
+
+       if (explicit_user) {
+               _user_explicit_resolution = true;
+       }
 }
 
+
 void
 Film::set_j2k_bandwidth (int b)
 {
@@ -1062,13 +1130,6 @@ Film::set_reel_length (int64_t r)
        _reel_length = r;
 }
 
-void
-Film::set_upload_after_make_dcp (bool u)
-{
-       ChangeSignaller<Film> ch (this, UPLOAD_AFTER_MAKE_DCP);
-       _upload_after_make_dcp = u;
-}
-
 void
 Film::set_reencode_j2k (bool r)
 {
@@ -1179,13 +1240,6 @@ Film::cpls () const
        return out;
 }
 
-void
-Film::set_signed (bool s)
-{
-       ChangeSignaller<Film> ch (this, SIGNED);
-       _signed = s;
-}
-
 void
 Film::set_encrypted (bool e)
 {
@@ -1193,13 +1247,6 @@ Film::set_encrypted (bool e)
        _encrypted = e;
 }
 
-void
-Film::set_key (dcp::Key key)
-{
-       ChangeSignaller<Film> ch (this, KEY);
-       _key = key;
-}
-
 ContentList
 Film::content () const
 {
@@ -1269,12 +1316,58 @@ Film::add_content (shared_ptr<Content> c)
        }
 
        _playlist->add (shared_from_this(), c);
+
+       maybe_set_container_and_resolution ();
+       if (c->atmos) {
+               set_audio_channels (14);
+               set_interop (false);
+       }
+}
+
+
+void
+Film::maybe_set_container_and_resolution ()
+{
+       /* Get the only piece of video content, if there is only one */
+       shared_ptr<VideoContent> video;
+       BOOST_FOREACH (shared_ptr<const Content> i, _playlist->content()) {
+               if (i->video) {
+                       if (!video) {
+                               video = i->video;
+                       } else {
+                               video.reset ();
+                       }
+               }
+       }
+
+       if (video) {
+               /* This is the only piece of video content in this Film.  Use it to make a guess for
+                * DCP container size and resolution, unless the user has already explicitly set these
+                * things.
+                */
+               if (!_user_explicit_container) {
+                       if (video->size().ratio() > 2.3) {
+                               set_container (Ratio::from_id("239"), false);
+                       } else {
+                               set_container (Ratio::from_id("185"), false);
+                       }
+               }
+
+               if (!_user_explicit_resolution) {
+                       if (video->size_after_crop().width > 2048 || video->size_after_crop().height > 1080) {
+                               set_resolution (RESOLUTION_4K, false);
+                       } else {
+                               set_resolution (RESOLUTION_2K, false);
+                       }
+               }
+       }
 }
 
 void
 Film::remove_content (shared_ptr<Content> c)
 {
        _playlist->remove (c);
+       maybe_set_container_and_resolution ();
 }
 
 void
@@ -1332,6 +1425,8 @@ Film::playlist_content_change (ChangeType type, weak_ptr<Content> c, int p, bool
        } else {
                ContentChange (type, c, p, frequent);
        }
+
+       _dirty = true;
 }
 
 void
@@ -1349,6 +1444,8 @@ Film::playlist_change (ChangeType type)
        if (type == CHANGE_TYPE_DONE) {
                check_settings_consistency ();
        }
+
+       _dirty = true;
 }
 
 /** Check for (and if necessary fix) impossible settings combinations, like
@@ -1357,6 +1454,21 @@ Film::playlist_change (ChangeType type)
 void
 Film::check_settings_consistency ()
 {
+       optional<int> atmos_rate;
+       BOOST_FOREACH (shared_ptr<Content> i, content()) {
+
+               if (i->atmos) {
+                       int rate = lrintf (i->atmos->edit_rate().as_float());
+                       if (atmos_rate && *atmos_rate != rate) {
+                               Message (_("You have more than one piece of Atmos content, and they do not have the same frame rate.  You must remove some Atmos content."));
+                       } else if (!atmos_rate && rate != video_frame_rate()) {
+                               atmos_rate = rate;
+                               set_video_frame_rate (rate, false);
+                               Message (_("DCP-o-matic had to change your settings so that the film's frame rate is the same as that of your Atmos content."));
+                       }
+               }
+       }
+
        bool change_made = false;
        BOOST_FOREACH (shared_ptr<Content> i, content()) {
                shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent>(i);
@@ -1430,6 +1542,26 @@ Film::frame_size () const
        return fit_ratio_within (container()->ratio(), full_frame ());
 }
 
+
+/** @return Area of Film::frame_size() that contains picture rather than pillar/letterboxing */
+dcp::Size
+Film::active_area () const
+{
+       dcp::Size const frame = frame_size ();
+       dcp::Size active;
+
+       BOOST_FOREACH (shared_ptr<Content> i, content()) {
+               if (i->video) {
+                       dcp::Size s = i->video->scaled_size (frame);
+                       active.width = max(active.width, s.width);
+                       active.height = max(active.height, s.height);
+               }
+       }
+
+       return active;
+}
+
+
 /** @param recipient KDM recipient certificate.
  *  @param trusted_devices Certificate thumbprints of other trusted devices (can be empty).
  *  @param cpl_file CPL filename.
@@ -1547,33 +1679,10 @@ Film::should_be_enough_disk_space (double& required, double& available, bool& ca
        return (available - required) > 1;
 }
 
-string
-Film::subtitle_language () const
-{
-       set<string> languages;
-
-       BOOST_FOREACH (shared_ptr<Content> i, content()) {
-               BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
-                       languages.insert (j->language ());
-               }
-       }
-
-       string all;
-       BOOST_FOREACH (string s, languages) {
-               if (!all.empty ()) {
-                       all += "/" + s;
-               } else {
-                       all += s;
-               }
-       }
-
-       return all;
-}
-
 /** @return The names of the channels that audio contents' outputs are passed into;
  *  this is either the DCP or a AudioProcessor.
  */
-vector<string>
+vector<NamedChannel>
 Film::audio_output_names () const
 {
        if (audio_processor ()) {
@@ -1582,10 +1691,12 @@ Film::audio_output_names () const
 
        DCPOMATIC_ASSERT (MAX_DCP_AUDIO_CHANNELS == 16);
 
-       vector<string> n;
+       vector<NamedChannel> n;
 
        for (int i = 0; i < audio_channels(); ++i) {
-               n.push_back (short_audio_channel_name (i));
+               if (i != 8 && i != 9 && i != 15) {
+                       n.push_back (NamedChannel(short_audio_channel_name(i), i));
+               }
        }
 
        return n;
@@ -1690,7 +1801,6 @@ Film::use_template (string name)
        _resolution = _template_film->_resolution;
        _j2k_bandwidth = _template_film->_j2k_bandwidth;
        _video_frame_rate = _template_film->_video_frame_rate;
-       _signed = _template_film->_signed;
        _encrypted = _template_film->_encrypted;
        _audio_channels = _template_film->_audio_channels;
        _sequence = _template_film->_sequence;
@@ -1699,7 +1809,6 @@ Film::use_template (string name)
        _audio_processor = _template_film->_audio_processor;
        _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;
 }
 
@@ -1741,6 +1850,20 @@ Film::references_dcp_audio () const
        return false;
 }
 
+
+bool
+Film::contains_atmos_content () const
+{
+       BOOST_FOREACH (shared_ptr<Content> i, _playlist->content()) {
+               if (i->atmos) {
+                       return true;
+               }
+       }
+
+       return false;
+}
+
+
 list<DCPTextTrack>
 Film::closed_caption_tracks () const
 {
@@ -1789,12 +1912,110 @@ Film::set_ratings (vector<dcp::Rating> r)
 }
 
 void
-Film::set_content_version (string v)
+Film::set_content_versions (vector<string> v)
+{
+       ChangeSignaller<Film> ch (this, CONTENT_VERSIONS);
+       _content_versions = v;
+}
+
+
+void
+Film::set_name_language (dcp::LanguageTag lang)
+{
+       ChangeSignaller<Film> ch (this, NAME_LANGUAGE);
+       _name_language = lang;
+}
+
+
+void
+Film::set_audio_language (dcp::LanguageTag lang)
+{
+       ChangeSignaller<Film> ch (this, AUDIO_LANGUAGE);
+       _audio_language = lang;
+}
+
+
+void
+Film::set_release_territory (dcp::LanguageTag::RegionSubtag region)
+{
+       ChangeSignaller<Film> ch (this, RELEASE_TERRITORY);
+       _release_territory = region;
+}
+
+
+void
+Film::set_status (dcp::Status s)
+{
+       ChangeSignaller<Film> ch (this, STATUS);
+       _status = s;
+}
+
+
+void
+Film::set_version_number (int v)
 {
-       ChangeSignaller<Film> ch (this, CONTENT_VERSION);
-       _content_version = v;
+       ChangeSignaller<Film> ch (this, VERSION_NUMBER);
+       _version_number = v;
 }
 
+
+void
+Film::set_chain (string c)
+{
+       ChangeSignaller<Film> ch (this, CHAIN);
+       _chain = c;
+}
+
+
+void
+Film::set_distributor (string d)
+{
+       ChangeSignaller<Film> ch (this, DISTRIBUTOR);
+       _distributor = d;
+}
+
+
+void
+Film::set_luminance (dcp::Luminance l)
+{
+       ChangeSignaller<Film> ch (this, LUMINANCE);
+       _luminance = l;
+}
+
+
+void
+Film::set_subtitle_language (dcp::LanguageTag language)
+{
+       vector<dcp::LanguageTag> lang;
+       lang.push_back (language);
+       set_subtitle_languages (lang);
+}
+
+
+void
+Film::unset_subtitle_language ()
+{
+       ChangeSignaller<Film> ch (this, SUBTITLE_LANGUAGES);
+       _subtitle_languages.clear();
+}
+
+
+void
+Film::set_subtitle_languages (vector<dcp::LanguageTag> languages)
+{
+       ChangeSignaller<Film> ch (this, SUBTITLE_LANGUAGES);
+       _subtitle_languages = languages;
+}
+
+
+void
+Film::set_facility (string f)
+{
+       ChangeSignaller<Film> ch (this, FACILITY);
+       _facility = f;
+}
+
+
 optional<DCPTime>
 Film::marker (dcp::Marker type) const
 {
@@ -1838,3 +2059,17 @@ InfoFileHandle::~InfoFileHandle ()
 {
        fclose (_handle);
 }
+
+
+/** Add FFOC and LFOC markers to a list if they are not already there */
+void
+Film::add_ffoc_lfoc (Markers& markers) const
+{
+       if (markers.find(dcp::Marker::FFOC) == markers.end()) {
+               markers[dcp::Marker::FFOC] = dcpomatic::DCPTime(0);
+       }
+
+       if (markers.find(dcp::Marker::LFOC) == markers.end()) {
+               markers[dcp::Marker::LFOC] = length() - DCPTime::from_frames(1, video_frame_rate());
+       }
+}