X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Flib%2Ffilm.cc;h=a0f25d84bbeaffda6b64f2fd6711dec15b66cddc;hb=9b946fc5250eff5a5dd4a661896916fcd5d9bd4b;hp=988b7e2fe639f6ef40b6f91d09e23a283a753fce;hpb=114fad7b8c36a0aa1c6f924870e186ab60e32009;p=dcpomatic.git diff --git a/src/lib/film.cc b/src/lib/film.cc index 988b7e2fe..a0f25d84b 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2019 Carl Hetherington + Copyright (C) 2012-2021 Carl Hetherington This file is part of DCP-o-matic. @@ -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" @@ -50,10 +51,12 @@ #include "text_content.h" #include "ffmpeg_content.h" #include "dcp_content.h" -#include "screen_kdm.h" +#include "kdm_with_metadata.h" #include "cinema.h" #include "change_signaller.h" #include "check_content_change_job.h" +#include "ffmpeg_subtitle_stream.h" +#include "font.h" #include #include #include @@ -66,7 +69,6 @@ #include #include #include -#include #include #include #include @@ -94,11 +96,15 @@ using std::back_inserter; using std::map; using std::exception; using std::find; -using boost::shared_ptr; -using boost::weak_ptr; -using boost::dynamic_pointer_cast; +using std::shared_ptr; +using std::weak_ptr; +using std::make_shared; +using std::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; @@ -130,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. * @@ -144,7 +152,6 @@ Film::Film (optional 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 ()) @@ -157,9 +164,16 @@ Film::Film (optional 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) @@ -167,8 +181,9 @@ Film::Film (optional dir) set_isdcf_date_today (); _playlist_change_connection = _playlist->Change.connect (bind (&Film::playlist_change, this, _1)); - _playlist_order_changed_connection = _playlist->OrderChanged.connect (bind (&Film::playlist_order_changed, this)); + _playlist_order_changed_connection = _playlist->OrderChange.connect (bind (&Film::playlist_order_changed, this)); _playlist_content_change_connection = _playlist->ContentChange.connect (bind (&Film::playlist_content_change, this, _1, _2, _3, _4)); + _playlist_length_change_connection = _playlist->LengthChange.connect (bind(&Film::playlist_length_change, this)); if (dir) { /* Make state.directory a complete path without ..s (where possible) @@ -178,15 +193,16 @@ Film::Film (optional dir) boost::filesystem::path p (boost::filesystem::system_complete (dir.get())); boost::filesystem::path result; - for (boost::filesystem::path::iterator i = p.begin(); i != p.end(); ++i) { - if (*i == "..") { - if (boost::filesystem::is_symlink (result) || result.filename() == "..") { - result /= *i; + for (auto i: p) { + if (i == "..") { + boost::system::error_code ec; + if (boost::filesystem::is_symlink(result, ec) || result.filename() == "..") { + result /= i; } else { result = result.parent_path (); } - } else if (*i != ".") { - result /= *i; + } else if (i != ".") { + result /= i; } } @@ -194,9 +210,9 @@ Film::Film (optional dir) } if (_directory) { - _log.reset (new FileLog (file ("log"))); + _log = make_shared(file("log")); } else { - _log.reset (new NullLog); + _log = make_shared(); } _playlist->set_sequence (_sequence); @@ -204,11 +220,11 @@ Film::Film (optional dir) Film::~Film () { - BOOST_FOREACH (boost::signals2::connection& i, _job_connections) { + for (auto& i: _job_connections) { i.disconnect (); } - BOOST_FOREACH (boost::signals2::connection& i, _audio_analysis_connections) { + for (auto& i: _audio_analysis_connections) { i.disconnect (); } } @@ -269,10 +285,10 @@ Film::internal_video_asset_filename (DCPTimePeriod p) const boost::filesystem::path Film::audio_analysis_path (shared_ptr playlist) const { - boost::filesystem::path p = dir ("analysis"); + auto p = dir ("analysis"); Digester digester; - BOOST_FOREACH (shared_ptr i, playlist->content ()) { + for (auto i: playlist->content ()) { if (!i->audio) { continue; } @@ -300,6 +316,39 @@ Film::audio_analysis_path (shared_ptr playlist) const return p; } + +boost::filesystem::path +Film::subtitle_analysis_path (shared_ptr content) const +{ + auto p = dir ("analysis"); + + Digester digester; + digester.add (content->digest()); + + if (!content->text.empty()) { + auto tc = content->text.front(); + digester.add (tc->x_scale()); + digester.add (tc->y_scale()); + for (auto i: tc->fonts()) { + digester.add (i->id()); + } + if (tc->effect()) { + digester.add (tc->effect().get()); + } + digester.add (tc->line_spacing()); + digester.add (tc->outline_width()); + } + + auto fc = dynamic_pointer_cast(content); + if (fc) { + digester.add (fc->subtitle_stream()->identifier()); + } + + p /= digester.get (); + return p; +} + + /** Add suitable Jobs to the JobManager to create a DCP for this Film. * @param gui true if this is being called from a GUI tool. * @param check true to check the content in the project for changes before making the DCP. @@ -311,7 +360,7 @@ Film::make_dcp (bool gui, bool check) throw BadSettingError (_("name"), _("Cannot contain slashes")); } - if (container() == 0) { + if (container() == nullptr) { throw MissingSettingError (_("container")); } @@ -319,7 +368,11 @@ Film::make_dcp (bool gui, bool check) throw runtime_error (_("You must add some content to the DCP before creating it")); } - if (dcp_content_type() == 0) { + if (length() == DCPTime()) { + throw runtime_error (_("The DCP is empty, perhaps because all the content has zero length.")); + } + + if (dcp_content_type() == nullptr) { throw MissingSettingError (_("content type")); } @@ -327,11 +380,11 @@ Film::make_dcp (bool gui, bool check) set_name ("DCP"); } - BOOST_FOREACH (shared_ptr i, content ()) { + for (auto i: content ()) { if (!i->paths_valid()) { throw runtime_error (_("some of your content is missing")); } - shared_ptr dcp = dynamic_pointer_cast (i); + auto dcp = dynamic_pointer_cast(i); if (dcp && dcp->needs_kdm()) { throw runtime_error (_("Some of your content needs a KDM")); } @@ -342,11 +395,11 @@ Film::make_dcp (bool gui, bool check) set_isdcf_date_today (); - BOOST_FOREACH (string i, environment_info ()) { + for (auto i: environment_info ()) { LOG_GENERAL_NC (i); } - BOOST_FOREACH (shared_ptr i, content ()) { + for (auto i: content ()) { LOG_GENERAL ("Content: %1", i->technical_summary()); } LOG_GENERAL ("DCP video rate %1 fps", video_frame_rate()); @@ -357,10 +410,10 @@ Film::make_dcp (bool gui, bool check) } LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth()); - shared_ptr tj (new TranscodeJob (shared_from_this())); - tj->set_encoder (shared_ptr (new DCPEncoder (shared_from_this(), tj))); + auto tj = make_shared(shared_from_this()); + tj->set_encoder (make_shared(shared_from_this(), tj)); if (check) { - shared_ptr cc (new CheckContentChangeJob(shared_from_this(), tj, gui)); + auto cc = make_shared(shared_from_this(), tj, gui); JobManager::instance()->add (cc); } else { JobManager::instance()->add (tj); @@ -371,15 +424,14 @@ Film::make_dcp (bool gui, bool check) void Film::send_dcp_to_tms () { - shared_ptr j (new UploadJob (shared_from_this())); - JobManager::instance()->add (j); + JobManager::instance()->add(make_shared(shared_from_this())); } shared_ptr Film::metadata (bool with_content_paths) const { - shared_ptr doc (new xmlpp::Document); - xmlpp::Element* root = doc->create_root_node ("Metadata"); + auto doc = make_shared(); + auto root = doc->create_root_node ("Metadata"); root->add_child("Version")->add_child_text (raw_convert (current_state_version)); root->add_child("Name")->add_child_text (_name); @@ -402,7 +454,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); @@ -411,17 +462,34 @@ Film::metadata (bool with_content_paths) const } root->add_child("ReelType")->add_child_text (raw_convert (static_cast (_reel_type))); root->add_child("ReelLength")->add_child_text (raw_convert (_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::const_iterator i = _markers.begin(); i != _markers.end(); ++i) { - xmlpp::Element* m = root->add_child("Marker"); + auto m = root->add_child("Marker"); m->set_attribute("Type", dcp::marker_to_string(i->first)); m->add_child_text(raw_convert(i->second.get())); } - BOOST_FOREACH (dcp::Rating i, _ratings) { + for (auto i: _ratings) { i.as_xml (root->add_child("Rating")); } + for (auto 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(_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(_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"); + for (auto 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; @@ -430,8 +498,7 @@ Film::metadata (bool with_content_paths) const void Film::write_metadata (boost::filesystem::path path) const { - shared_ptr doc = metadata (); - doc->write_to_file_formatted (path.string()); + metadata()->write_to_file_formatted(path.string()); } /** Write state to our `metadata' file */ @@ -440,8 +507,7 @@ Film::write_metadata () const { DCPOMATIC_ASSERT (directory()); boost::filesystem::create_directories (directory().get()); - shared_ptr doc = metadata (); - doc->write_to_file_formatted (file(metadata_file).string ()); + metadata()->write_to_file_formatted(file(metadata_file).string()); _dirty = false; } @@ -451,7 +517,7 @@ Film::write_template (boost::filesystem::path path) const { boost::filesystem::create_directories (path.parent_path()); shared_ptr doc = metadata (false); - doc->write_to_file_formatted (path.string ()); + metadata(false)->write_to_file_formatted(path.string()); } /** Read state from our metadata file. @@ -468,6 +534,10 @@ Film::read_metadata (optional path) path = file (metadata_file); } + if (!boost::filesystem::exists(*path)) { + throw FileNotFoundError(*path); + } + cxml::Document f ("Metadata"); f.read_file (path.get ()); @@ -476,7 +546,7 @@ Film::read_metadata (optional path) throw runtime_error (_("This film was created with a newer version of DCP-o-matic, and it cannot be loaded into this version. Sorry!")); } else if (_state_version < current_state_version) { /* This is an older version; save a copy (if we haven't already) */ - boost::filesystem::path const older = path->parent_path() / String::compose("metadata.%1.xml", _state_version); + auto const older = path->parent_path() / String::compose("metadata.%1.xml", _state_version); if (!boost::filesystem::is_regular_file(older)) { try { boost::filesystem::copy_file(*path, older); @@ -499,14 +569,14 @@ Film::read_metadata (optional path) { - optional c = f.optional_string_child ("DCPContentType"); + auto c = f.optional_string_child("DCPContentType"); if (c) { _dcp_content_type = DCPContentType::from_isdcf_name (c.get ()); } } { - optional c = f.optional_string_child ("Container"); + auto c = f.optional_string_child("Container"); if (c) { _container = Ratio::from_id (c.get ()); } @@ -515,7 +585,6 @@ Film::read_metadata (optional path) _resolution = string_to_resolution (f.string_child ("Resolution")); _j2k_bandwidth = f.number_child ("J2KBandwidth"); _video_frame_rate = f.number_child ("VideoFrameRate"); - _signed = f.optional_bool_child("Signed").get_value_or (true); _encrypted = f.bool_child ("Encrypted"); _audio_channels = f.number_child ("AudioChannels"); /* We used to allow odd numbers (and zero) channels, but it's just not worth @@ -545,7 +614,7 @@ Film::read_metadata (optional path) } if (_audio_processor && !Config::instance()->show_experimental_audio_processors()) { - list ap = AudioProcessor::visible(); + auto ap = AudioProcessor::visible(); if (find(ap.begin(), ap.end(), _audio_processor) == ap.end()) { Config::instance()->set_show_experimental_audio_processors(true); } @@ -553,18 +622,59 @@ Film::read_metadata (optional path) _reel_type = static_cast (f.optional_number_child("ReelType").get_value_or (static_cast(REELTYPE_SINGLE))); _reel_length = f.optional_number_child("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); - BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children("Marker")) { + for (auto i: f.node_children("Marker")) { _markers[dcp::marker_from_string(i->string_attribute("Type"))] = DCPTime(dcp::raw_convert(i->content())); } - BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children("Rating")) { + for (auto i: f.node_children("Rating")) { _ratings.push_back (dcp::Rating(i)); } + for (auto i: f.node_children("ContentVersion")) { + _content_versions.push_back (i->content()); + } + + auto name_language = f.optional_string_child("NameLanguage"); + if (name_language) { + _name_language = dcp::LanguageTag (*name_language); + } + auto audio_language = f.optional_string_child("AudioLanguage"); + if (audio_language) { + _audio_language = dcp::LanguageTag (*audio_language); + } + auto release_territory = f.optional_string_child("ReleaseTerritory"); + if (release_territory) { + _release_territory = dcp::LanguageTag::RegionSubtag (*release_territory); + } + + _version_number = f.optional_number_child("VersionNumber").get_value_or(0); + + auto 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(""); + + auto value = f.optional_number_child("LuminanceValue").get_value_or(4.5); + auto 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); + + for (auto i: f.node_children("SubtitleLanguage")) { + _subtitle_languages.push_back (dcp::LanguageTag(i->content())); + } + list notes; _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version, notes); @@ -573,6 +683,41 @@ Film::read_metadata (optional path) set_backtrace_file (file ("backtrace.txt")); } + /* Around 2.15.108 we removed subtitle language state from the text content and the ISDCF + * metadata and put it into the Film instead. If we've loaded an old Film let's try and fish + * out the settings from where they were so that they don't get lost. + */ + + optional found_language; + + for (auto i: f.node_child("Playlist")->node_children("Content")) { + auto text = i->optional_node_child("Text"); + if (text && text->optional_string_child("Language") && !found_language) { + try { + found_language = dcp::LanguageTag(text->string_child("Language")); + } catch (...) {} + } + } + + if (_state_version >= 9) { + auto isdcf_language = f.node_child("ISDCFMetadata")->optional_string_child("SubtitleLanguage"); + if (isdcf_language && !found_language) { + try { + found_language = dcp::LanguageTag(*isdcf_language); + } catch (...) { + try { + found_language = dcp::LanguageTag(boost::algorithm::to_lower_copy(*isdcf_language)); + } catch (...) { + + } + } + } + } + + if (found_language) { + _subtitle_languages.push_back (*found_language); + } + _dirty = false; return notes; } @@ -625,9 +770,9 @@ Film::mapped_audio_channels () const mapped.push_back (i); } } else { - BOOST_FOREACH (shared_ptr i, content ()) { + for (auto i: content ()) { if (i->audio) { - list c = i->audio->mapping().mapped_output_channels (); + auto c = i->audio->mapping().mapped_output_channels (); copy (c.begin(), c.end(), back_inserter (mapped)); } } @@ -645,7 +790,7 @@ Film::isdcf_name (bool if_created_now) const { string d; - string raw_name = name (); + auto raw_name = name (); /* Split the raw name up into words */ vector words; @@ -654,16 +799,16 @@ Film::isdcf_name (bool if_created_now) const string fixed_name; /* Add each word to fixed_name */ - for (vector::const_iterator i = words.begin(); i != words.end(); ++i) { - string w = *i; + for (auto i: words) { + string w = i; /* First letter is always capitalised */ w[0] = toupper (w[0]); /* Count caps in w */ size_t caps = 0; - for (size_t i = 0; i < w.size(); ++i) { - if (isupper (w[i])) { + for (size_t j = 0; j < w.size(); ++j) { + if (isupper (w[j])) { ++caps; } } @@ -672,13 +817,13 @@ Film::isdcf_name (bool if_created_now) const leave it alone. */ if (caps == w.size ()) { - for (size_t i = 1; i < w.size(); ++i) { - w[i] = tolower (w[i]); + for (size_t j = 1; j < w.size(); ++j) { + w[j] = tolower (w[j]); } } - for (size_t i = 0; i < w.size(); ++i) { - fixed_name += w[i]; + for (size_t j = 0; j < w.size(); ++j) { + fixed_name += w[j]; } } @@ -693,7 +838,7 @@ Film::isdcf_name (bool if_created_now) const d += "-" + raw_convert(isdcf_metadata().content_version); } - ISDCFMetadata const dm = isdcf_metadata (); + auto const dm = isdcf_metadata (); if (dm.temp_version) { d += "-Temp"; @@ -735,15 +880,11 @@ Film::isdcf_name (bool if_created_now) const /* 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 i, content ()) { + Ratio const* content_ratio = nullptr; + for (auto 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; } } @@ -761,39 +902,27 @@ Film::isdcf_name (bool if_created_now) const for now I'm just appending -CCAP if we have any closed captions. */ - optional subtitle_language; - bool burnt_in = true; - bool ccap = false; - BOOST_FOREACH (shared_ptr i, content()) { - BOOST_FOREACH (shared_ptr j, i->text) { - if (j->type() == TEXT_OPEN_SUBTITLE && j->use()) { - subtitle_language = j->language (); - if (!j->burn()) { - burnt_in = false; - } + auto burnt_in = true; + auto ccap = false; + for (auto i: content()) { + for (auto j: i->text) { + 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()) { + auto 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"; } @@ -814,9 +943,9 @@ Film::isdcf_name (bool if_created_now) const /* Count mapped audio channels */ - list mapped = mapped_audio_channels (); + auto mapped = mapped_audio_channels (); - pair ch = audio_channel_types (mapped, audio_channels()); + auto ch = audio_channel_types (mapped, audio_channels()); if (!ch.first && !ch.second) { d += "_MOS"; } else if (ch.first) { @@ -856,9 +985,9 @@ Film::isdcf_name (bool if_created_now) const d += "-3D"; } - bool vf = false; - BOOST_FOREACH (shared_ptr i, content ()) { - shared_ptr dc = dynamic_pointer_cast (i); + auto vf = false; + for (auto i: content()) { + auto dc = dynamic_pointer_cast(i); if (!dc) { continue; } @@ -923,20 +1052,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 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 ch (this, RESOLUTION); _resolution = r; + + if (explicit_user) { + _user_explicit_resolution = true; + } } + void Film::set_j2k_bandwidth (int b) { @@ -1014,13 +1162,6 @@ Film::set_reel_length (int64_t r) _reel_length = r; } -void -Film::set_upload_after_make_dcp (bool u) -{ - ChangeSignaller ch (this, UPLOAD_AFTER_MAKE_DCP); - _upload_after_make_dcp = u; -} - void Film::set_reencode_j2k (bool r) { @@ -1111,15 +1252,15 @@ Film::cpls () const vector out; - boost::filesystem::path const dir = directory().get(); - for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator(dir); i != boost::filesystem::directory_iterator(); ++i) { + auto const dir = directory().get(); + for (auto i: boost::filesystem::directory_iterator(dir)) { if ( - boost::filesystem::is_directory (*i) && - i->path().leaf() != "j2c" && i->path().leaf() != "video" && i->path().leaf() != "info" && i->path().leaf() != "analysis" + boost::filesystem::is_directory (i) && + i.path().leaf() != "j2c" && i.path().leaf() != "video" && i.path().leaf() != "info" && i.path().leaf() != "analysis" ) { try { - out.push_back (CPLSummary(*i)); + out.push_back (CPLSummary(i)); } catch (...) { } @@ -1131,13 +1272,6 @@ Film::cpls () const return out; } -void -Film::set_signed (bool s) -{ - ChangeSignaller ch (this, SIGNED); - _signed = s; -} - void Film::set_encrypted (bool e) { @@ -1145,13 +1279,6 @@ Film::set_encrypted (bool e) _encrypted = e; } -void -Film::set_key (dcp::Key key) -{ - ChangeSignaller ch (this, KEY); - _key = key; -} - ContentList Film::content () const { @@ -1168,7 +1295,7 @@ Film::examine_and_add_content (shared_ptr content, bool disable_audio_a run_ffprobe (content->path(0), file("ffprobe.log")); } - shared_ptr j (new ExamineContentJob (shared_from_this(), content)); + auto j = make_shared(shared_from_this(), content); _job_connections.push_back ( j->Finished.connect (bind (&Film::maybe_add_content, this, weak_ptr(j), weak_ptr(content), disable_audio_analysis)) @@ -1180,12 +1307,12 @@ Film::examine_and_add_content (shared_ptr content, bool disable_audio_a void Film::maybe_add_content (weak_ptr j, weak_ptr c, bool disable_audio_analysis) { - shared_ptr job = j.lock (); + auto job = j.lock (); if (!job || !job->finished_ok ()) { return; } - shared_ptr content = c.lock (); + auto content = c.lock (); if (!content) { return; } @@ -1193,7 +1320,7 @@ Film::maybe_add_content (weak_ptr j, weak_ptr c, bool disable_audi add_content (content); if (Config::instance()->automatic_audio_analysis() && content->audio && !disable_audio_analysis) { - shared_ptr playlist (new Playlist); + auto playlist = make_shared(); playlist->add (shared_from_this(), content); boost::signals2::connection c; JobManager::instance()->analyse_audio ( @@ -1215,18 +1342,64 @@ Film::add_content (shared_ptr c) if (_template_film) { /* Take settings from the first piece of content of c's type in _template */ - BOOST_FOREACH (shared_ptr i, _template_film->content()) { + for (auto i: _template_film->content()) { c->take_settings_from (i); } } _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 video; + for (auto 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 c) { _playlist->remove (c); + maybe_set_container_and_resolution (); } void @@ -1241,18 +1414,20 @@ Film::move_content_later (shared_ptr c) _playlist->move_later (shared_from_this(), c); } -/** @return length of the film from time 0 to the last thing on the playlist */ +/** @return length of the film from time 0 to the last thing on the playlist, + * with a minimum length of 1 second. + */ DCPTime Film::length () const { - return _playlist->length(shared_from_this()).ceil(video_frame_rate()); + return max(DCPTime::from_seconds(1), _playlist->length(shared_from_this()).ceil(video_frame_rate())); } int Film::best_video_frame_rate () const { /* Don't default to anything above 30fps (make the user select that explicitly) */ - int best = _playlist->best_video_frame_rate (); + auto best = _playlist->best_video_frame_rate (); if (best > 30) { best /= 2; } @@ -1276,9 +1451,20 @@ Film::playlist_content_change (ChangeType type, weak_ptr c, int p, bool if (type == CHANGE_TYPE_DONE) { emit (boost::bind (boost::ref (ContentChange), type, c, p, frequent)); + if (!frequent) { + check_settings_consistency (); + } } else { ContentChange (type, c, p, frequent); } + + _dirty = true; +} + +void +Film::playlist_length_change () +{ + LengthChange (); } void @@ -1288,28 +1474,61 @@ Film::playlist_change (ChangeType type) signal_change (type, NAME); if (type == CHANGE_TYPE_DONE) { - /* Check that this change hasn't made our settings inconsistent */ - bool change_made = false; - BOOST_FOREACH (shared_ptr i, content()) { - shared_ptr d = dynamic_pointer_cast(i); - if (!d) { - continue; - } + check_settings_consistency (); + } - string why_not; - if (d->reference_video() && !d->can_reference_video(shared_from_this(), why_not)) { - d->set_reference_video(false); - change_made = true; - } - if (d->reference_audio() && !d->can_reference_audio(shared_from_this(), why_not)) { - d->set_reference_audio(false); - change_made = true; + _dirty = true; +} + +/** Check for (and if necessary fix) impossible settings combinations, like + * video set to being referenced when it can't be. + */ +void +Film::check_settings_consistency () +{ + optional atmos_rate; + for (auto 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; + for (auto i: content()) { + auto d = dynamic_pointer_cast(i); + if (!d) { + continue; + } - if (change_made) { - Message (_("DCP-o-matic had to change your settings for referring to DCPs as OV. Please review those settings to make sure they are what you want.")); + string why_not; + if (d->reference_video() && !d->can_reference_video(shared_from_this(), why_not)) { + d->set_reference_video(false); + change_made = true; + } + if (d->reference_audio() && !d->can_reference_audio(shared_from_this(), why_not)) { + d->set_reference_audio(false); + change_made = true; } + if (d->reference_text(TEXT_OPEN_SUBTITLE) && !d->can_reference_text(shared_from_this(), TEXT_OPEN_SUBTITLE, why_not)) { + d->set_reference_text(TEXT_OPEN_SUBTITLE, false); + change_made = true; + } + if (d->reference_text(TEXT_CLOSED_CAPTION) && !d->can_reference_text(shared_from_this(), TEXT_CLOSED_CAPTION, why_not)) { + d->set_reference_text(TEXT_CLOSED_CAPTION, false); + change_made = true; + } + } + + if (change_made) { + Message (_("DCP-o-matic had to change your settings for referring to DCPs as OV. Please review those settings to make sure they are what you want.")); } } @@ -1363,6 +1582,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 +{ + auto const frame = frame_size (); + dcp::Size active; + + for (auto 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. @@ -1389,33 +1628,33 @@ Film::make_kdm ( throw runtime_error (_("Cannot make a KDM as this project is not encrypted.")); } - shared_ptr cpl (new dcp::CPL (cpl_file)); - shared_ptr signer = Config::instance()->signer_chain (); + auto cpl = make_shared(cpl_file); + auto signer = Config::instance()->signer_chain(); if (!signer->valid ()) { throw InvalidSignerError (); } /* Find keys that have been added to imported, encrypted DCP content */ list imported_keys; - BOOST_FOREACH (shared_ptr i, content()) { - shared_ptr d = dynamic_pointer_cast (i); + for (auto i: content()) { + auto d = dynamic_pointer_cast (i); if (d && d->kdm()) { dcp::DecryptedKDM kdm (d->kdm().get(), Config::instance()->decryption_chain()->key().get()); - list keys = kdm.keys (); + auto keys = kdm.keys (); copy (keys.begin(), keys.end(), back_inserter (imported_keys)); } } map, dcp::Key> keys; - BOOST_FOREACH(shared_ptr i, cpl->reel_mxfs()) { + for (auto i: cpl->reel_mxfs()) { if (!i->key_id()) { continue; } /* Get any imported key for this ID */ bool done = false; - BOOST_FOREACH (dcp::DecryptedKDMKey j, imported_keys) { + for (auto j: imported_keys) { if (j.id() == i->key_id().get()) { LOG_GENERAL ("Using imported key for %1", i->key_id().get()); keys[i] = j.key(); @@ -1435,47 +1674,6 @@ Film::make_kdm ( ).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 > -Film::make_kdms ( - list > screens, - boost::filesystem::path cpl_file, - boost::posix_time::ptime from, - boost::posix_time::ptime until, - dcp::Formulation formulation, - bool disable_forensic_marking_picture, - optional disable_forensic_marking_audio - ) const -{ - list > kdms; - - BOOST_FOREACH (shared_ptr i, screens) { - if (i->recipient) { - dcp::EncryptedKDM const kdm = make_kdm ( - i->recipient.get(), - i->trusted_device_thumbprints(), - cpl_file, - dcp::LocalTime (from, i->cinema ? i->cinema->utc_offset_hour() : 0, i->cinema ? i->cinema->utc_offset_minute() : 0), - dcp::LocalTime (until, i->cinema ? i->cinema->utc_offset_hour() : 0, i->cinema ? i->cinema->utc_offset_minute() : 0), - formulation, - disable_forensic_marking_picture, - disable_forensic_marking_audio - ); - - kdms.push_back (shared_ptr(new DCPScreenKDM(i, kdm))); - } - } - - return kdms; -} /** @return The approximate disk space required to encode a DCP of this film with the * current settings, in bytes. @@ -1500,7 +1698,7 @@ Film::should_be_enough_disk_space (double& required, double& available, bool& ca boost::filesystem::path test = internal_video_asset_dir() / "test"; boost::filesystem::path test2 = internal_video_asset_dir() / "test2"; can_hard_link = true; - FILE* f = fopen_boost (test, "w"); + auto f = fopen_boost (test, "w"); if (f) { fclose (f); boost::system::error_code ec; @@ -1512,7 +1710,7 @@ Film::should_be_enough_disk_space (double& required, double& available, bool& ca boost::filesystem::remove (test2); } - boost::filesystem::space_info s = boost::filesystem::space (internal_video_asset_dir ()); + auto s = boost::filesystem::space (internal_video_asset_dir ()); required = double (required_disk_space ()) / 1073741824.0f; if (!can_hard_link) { required *= 2; @@ -1521,33 +1719,10 @@ Film::should_be_enough_disk_space (double& required, double& available, bool& ca return (available - required) > 1; } -string -Film::subtitle_language () const -{ - set languages; - - BOOST_FOREACH (shared_ptr i, content()) { - BOOST_FOREACH (shared_ptr 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 +vector Film::audio_output_names () const { if (audio_processor ()) { @@ -1556,10 +1731,12 @@ Film::audio_output_names () const DCPOMATIC_ASSERT (MAX_DCP_AUDIO_CHANNELS == 16); - vector n; + vector 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; @@ -1587,7 +1764,7 @@ list Film::reels () const { list p; - DCPTime const len = length(); + auto const len = length(); switch (reel_type ()) { case REELTYPE_SINGLE: @@ -1595,37 +1772,43 @@ Film::reels () const break; case REELTYPE_BY_VIDEO_CONTENT: { - optional last_split; - shared_ptr last_video; - BOOST_FOREACH (shared_ptr c, content ()) { + /* Collect all reel boundaries */ + list split_points; + split_points.push_back (DCPTime()); + split_points.push_back (len); + for (auto c: content()) { if (c->video) { - BOOST_FOREACH (DCPTime t, c->reel_split_points(shared_from_this())) { - if (last_split) { - p.push_back (DCPTimePeriod (last_split.get(), t)); - } - last_split = t; + for (auto t: c->reel_split_points(shared_from_this())) { + split_points.push_back (t); } - last_video = c; + split_points.push_back (c->end(shared_from_this())); } } - DCPTime video_end = last_video ? last_video->end(shared_from_this()) : DCPTime(0); - if (last_split) { - /* Definitely go from the last split to the end of the video content */ - p.push_back (DCPTimePeriod (last_split.get(), video_end)); - } - - if (video_end < len) { - /* And maybe go after that as well if there is any non-video hanging over the end */ - p.push_back (DCPTimePeriod (video_end, len)); + split_points.sort (); + split_points.unique (); + + /* Make them into periods, coalescing any that are less than 1 second long */ + optional last; + for (auto t: split_points) { + if (last && (t - *last) >= DCPTime::from_seconds(1)) { + /* Period from *last to t is long enough; use it and start a new one */ + p.push_back (DCPTimePeriod(*last, t)); + last = t; + } else if (!last) { + /* That was the first time, so start a new period */ + last = t; + } } break; } case REELTYPE_BY_LENGTH: { DCPTime current; - /* Integer-divide reel length by the size of one frame to give the number of frames per reel */ - Frame const reel_in_frames = _reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8); + /* Integer-divide reel length by the size of one frame to give the number of frames per reel, + * making sure we don't go less than 1s long. + */ + Frame const reel_in_frames = max(_reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8), static_cast(video_frame_rate())); while (current < len) { DCPTime end = min (len, current + DCPTime::from_frames (reel_in_frames, video_frame_rate ())); p.push_back (DCPTimePeriod (current, end)); @@ -1658,7 +1841,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; @@ -1667,7 +1849,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; } @@ -1686,8 +1867,8 @@ Film::copy_from (shared_ptr film) bool Film::references_dcp_video () const { - BOOST_FOREACH (shared_ptr i, _playlist->content()) { - shared_ptr d = dynamic_pointer_cast(i); + for (auto i: _playlist->content()) { + auto d = dynamic_pointer_cast(i); if (d && d->reference_video()) { return true; } @@ -1699,8 +1880,8 @@ Film::references_dcp_video () const bool Film::references_dcp_audio () const { - BOOST_FOREACH (shared_ptr i, _playlist->content()) { - shared_ptr d = dynamic_pointer_cast(i); + for (auto i: _playlist->content()) { + auto d = dynamic_pointer_cast(i); if (d && d->reference_audio()) { return true; } @@ -1709,14 +1890,28 @@ Film::references_dcp_audio () const return false; } + +bool +Film::contains_atmos_content () const +{ + for (auto i: _playlist->content()) { + if (i->atmos) { + return true; + } + } + + return false; +} + + list Film::closed_caption_tracks () const { list tt; - BOOST_FOREACH (shared_ptr i, content()) { - BOOST_FOREACH (shared_ptr j, i->text) { + for (auto i: content()) { + for (auto j: i->text) { /* XXX: Empty DCPTextTrack ends up being a magic value here - the "unknown" or "not specified" track */ - DCPTextTrack dtt = j->dcp_track().get_value_or(DCPTextTrack()); + auto dtt = j->dcp_track().get_value_or(DCPTextTrack()); if (j->type() == TEXT_CLOSED_CAPTION && find(tt.begin(), tt.end(), dtt) == tt.end()) { tt.push_back (dtt); } @@ -1740,6 +1935,15 @@ Film::unset_marker (dcp::Marker type) _markers.erase (type); } + +void +Film::clear_markers () +{ + ChangeSignaller ch (this, MARKERS); + _markers.clear (); +} + + void Film::set_ratings (vector r) { @@ -1747,12 +1951,115 @@ Film::set_ratings (vector r) _ratings = r; } +void +Film::set_content_versions (vector v) +{ + ChangeSignaller ch (this, CONTENT_VERSIONS); + _content_versions = v; +} + + +void +Film::set_name_language (dcp::LanguageTag lang) +{ + ChangeSignaller ch (this, NAME_LANGUAGE); + _name_language = lang; +} + + +void +Film::set_audio_language (dcp::LanguageTag lang) +{ + ChangeSignaller ch (this, AUDIO_LANGUAGE); + _audio_language = lang; +} + + +void +Film::set_release_territory (dcp::LanguageTag::RegionSubtag region) +{ + ChangeSignaller ch (this, RELEASE_TERRITORY); + _release_territory = region; +} + + +void +Film::set_status (dcp::Status s) +{ + ChangeSignaller ch (this, STATUS); + _status = s; +} + + +void +Film::set_version_number (int v) +{ + ChangeSignaller ch (this, VERSION_NUMBER); + _version_number = v; +} + + +void +Film::set_chain (string c) +{ + ChangeSignaller ch (this, CHAIN); + _chain = c; +} + + +void +Film::set_distributor (string d) +{ + ChangeSignaller ch (this, DISTRIBUTOR); + _distributor = d; +} + + +void +Film::set_luminance (dcp::Luminance l) +{ + ChangeSignaller ch (this, LUMINANCE); + _luminance = l; +} + + +void +Film::set_subtitle_language (dcp::LanguageTag language) +{ + set_subtitle_languages ({language}); +} + + +void +Film::unset_subtitle_language () +{ + ChangeSignaller ch (this, SUBTITLE_LANGUAGES); + _subtitle_languages.clear(); +} + + +void +Film::set_subtitle_languages (vector languages) +{ + ChangeSignaller ch (this, SUBTITLE_LANGUAGES); + _subtitle_languages = languages; +} + + +void +Film::set_facility (string f) +{ + ChangeSignaller ch (this, FACILITY); + _facility = f; +} + + optional Film::marker (dcp::Marker type) const { - map::const_iterator i = _markers.find (type); + auto i = _markers.find (type); if (i == _markers.end()) { - return optional(); + return {}; } return i->second; } @@ -1760,7 +2067,7 @@ Film::marker (dcp::Marker type) const shared_ptr Film::info_file_handle (DCPTimePeriod period, bool read) const { - return shared_ptr (new InfoFileHandle(_info_file_mutex, info_file(period), read)); + return std::make_shared(_info_file_mutex, info_file(period), read); } InfoFileHandle::InfoFileHandle (boost::mutex& mutex, boost::filesystem::path file, bool read) @@ -1773,7 +2080,7 @@ InfoFileHandle::InfoFileHandle (boost::mutex& mutex, boost::filesystem::path fil throw OpenFileError (file, errno, OpenFileError::READ); } } else { - bool const exists = boost::filesystem::exists (file); + auto const exists = boost::filesystem::exists (file); if (exists) { _handle = fopen_boost (file, "r+b"); } else { @@ -1790,3 +2097,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()); + } +}