X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Flib%2Ffilm.cc;h=086d12e634188138590807aa9fab6c13f70f9b50;hb=21ebc2365dd7a66c05d130dc87861f7399ae834b;hp=04ff0025088300b6712659791e9c18fc7ee44934;hpb=89ae13638097f259f3e50b4b61068dd23451107d;p=dcpomatic.git diff --git a/src/lib/film.cc b/src/lib/film.cc index 04ff00250..a3e78e877 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2017 Carl Hetherington + Copyright (C) 2012-2021 Carl Hetherington This file is part of DCP-o-matic. @@ -18,85 +18,105 @@ */ + /** @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. */ + +#include "atmos_content.h" +#include "audio_content.h" +#include "audio_processor.h" +#include "change_signaller.h" +#include "cinema.h" +#include "compose.hpp" +#include "config.h" +#include "constants.h" +#include "cross.h" +#include "dcp_content.h" +#include "dcp_content_type.h" +#include "dcp_film_encoder.h" +#include "dcpomatic_log.h" +#include "digester.h" +#include "environment_info.h" +#include "examine_content_job.h" +#include "exceptions.h" +#include "ffmpeg_content.h" +#include "ffmpeg_subtitle_stream.h" +#include "file_log.h" #include "film.h" +#include "font.h" #include "job.h" -#include "util.h" #include "job_manager.h" -#include "transcode_job.h" -#include "upload_job.h" +#include "kdm_with_metadata.h" #include "null_log.h" -#include "file_log.h" -#include "exceptions.h" -#include "examine_content_job.h" -#include "config.h" #include "playlist.h" -#include "dcp_content_type.h" #include "ratio.h" -#include "cross.h" -#include "environment_info.h" -#include "audio_processor.h" -#include "digester.h" -#include "compose.hpp" #include "screen.h" -#include "audio_content.h" +#include "text_content.h" +#include "transcode_job.h" +#include "upload_job.h" +#include "variant.h" #include "video_content.h" -#include "subtitle_content.h" -#include "ffmpeg_content.h" -#include "dcp_content.h" -#include "screen_kdm.h" -#include "cinema.h" +#include "version.h" #include -#include #include -#include -#include +#include #include +#include +#include #include -#include #include +#include +#include #include -#include #include -#include +#include #include #include -#include -#include #include #include #include +#include #include +#include #include "i18n.h" -using std::string; -using std::pair; -using std::vector; -using std::setfill; -using std::min; -using std::max; -using std::make_pair; + +using std::back_inserter; +using std::copy; using std::cout; +using std::dynamic_pointer_cast; +using std::exception; +using std::find; using std::list; -using std::set; -using std::runtime_error; -using std::copy; -using std::back_inserter; +using std::make_pair; +using std::make_shared; using std::map; -using boost::shared_ptr; -using boost::weak_ptr; -using boost::dynamic_pointer_cast; +using std::max; +using std::min; +using std::pair; +using std::runtime_error; +using std::set; +using std::setfill; +using std::shared_ptr; +using std::string; +using std::vector; +using std::weak_ptr; using boost::optional; using boost::is_any_of; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif using dcp::raw_convert; +using namespace dcpomatic; + + +static constexpr char metadata_file[] = "metadata.xml"; +static constexpr char ui_state_file[] = "ui.xml"; -#define LOG_GENERAL(...) log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL); -#define LOG_GENERAL_NC(...) log()->log (__VA_ARGS__, LogEntry::TYPE_GENERAL); /* 5 -> 6 * AudioMapping XML changed. @@ -121,8 +141,14 @@ using dcp::raw_convert; * VideoFrameType in VideoContent is a string rather than an integer. * 35 -> 36 * EffectColour rather than OutlineColour in Subtitle. + * 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 = 36; +int const Film::current_state_version = 38; + /** Construct a Film object in a given directory. * @@ -131,60 +157,68 @@ int const Film::current_state_version = 36; Film::Film (optional dir) : _playlist (new Playlist) - , _use_isdcf_name (true) + , _use_isdcf_name (Config::instance()->use_isdcf_name_by_default()) , _dcp_content_type (Config::instance()->default_dcp_content_type ()) - , _container (Config::instance()->default_container ()) - , _resolution (RESOLUTION_2K) - , _signed (true) + , _container(Ratio::from_id("185")) + , _resolution (Resolution::TWO_K) , _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) , _audio_channels (Config::instance()->default_dcp_audio_channels ()) , _three_d (false) , _sequence (true) , _interop (Config::instance()->default_interop ()) + , _video_encoding(VideoEncoding::JPEG2000) + , _limit_to_smpte_bv20(false) , _audio_processor (0) - , _reel_type (REELTYPE_SINGLE) + , _reel_type (ReelType::SINGLE) , _reel_length (2000000000) - , _upload_after_make_dcp (false) + , _reencode_j2k (false) + , _user_explicit_video_frame_rate (false) + , _user_explicit_container (false) + , _user_explicit_resolution (false) + , _name_language (dcp::LanguageTag("en-US")) + , _release_territory(Config::instance()->default_territory()) + , _version_number (1) + , _status (dcp::Status::FINAL) + , _audio_language(Config::instance()->default_audio_language()) , _state_version (current_state_version) , _dirty (false) + , _tolerant (false) { set_isdcf_date_today (); - _playlist_changed_connection = _playlist->Changed.connect (bind (&Film::playlist_changed, this)); - _playlist_order_changed_connection = _playlist->OrderChanged.connect (bind (&Film::playlist_order_changed, this)); - _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Film::playlist_content_changed, this, _1, _2, _3)); + auto metadata = Config::instance()->default_metadata(); + if (metadata.find("chain") != metadata.end()) { + _chain = metadata["chain"]; + } + if (metadata.find("distributor") != metadata.end()) { + _distributor = metadata["distributor"]; + } + if (metadata.find("facility") != metadata.end()) { + _facility = metadata["facility"]; + } + if (metadata.find("studio") != metadata.end()) { + _studio = metadata["studio"]; + } - if (dir) { - /* Make state.directory a complete path without ..s (where possible) - (Code swiped from Adam Bowen on stackoverflow) - XXX: couldn't/shouldn't this just be boost::filesystem::canonical? - */ + for (auto encoding: {VideoEncoding::JPEG2000, VideoEncoding::MPEG2}) { + _video_bit_rate[encoding] = Config::instance()->default_video_bit_rate(encoding); + } - 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; - } else { - result = result.parent_path (); - } - } else if (*i != ".") { - result /= *i; - } - } + _playlist_change_connection = _playlist->Change.connect (bind (&Film::playlist_change, this, _1)); + _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)); - set_directory (result.make_preferred ()); + if (dir) { + set_directory(dcp::filesystem::weakly_canonical(*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); @@ -192,11 +226,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 (); } } @@ -210,24 +244,37 @@ Film::video_identifier () const + "_" + resolution_to_string (_resolution) + "_" + _playlist->video_identifier() + "_" + raw_convert(_video_frame_rate) - + "_" + raw_convert(j2k_bandwidth()); + + "_" + raw_convert(video_bit_rate(video_encoding())); if (encrypted ()) { - s += "_E"; + /* This is insecure but hey, the key is in plaintext in metadata.xml */ + s += "_E" + _key.hex(); } else { s += "_P"; } if (_interop) { s += "_I"; + if (_video_encoding == VideoEncoding::MPEG2) { + s += "_M"; + } } else { s += "_S"; + if (_limit_to_smpte_bv20) { + s += "_L20"; + } else { + s += "_L21"; + } } if (_three_d) { s += "_3D"; } + if (_reencode_j2k) { + s += "_R"; + } + return s; } @@ -256,16 +303,16 @@ 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 ()) { - if (!i->audio) { + for (auto content: playlist->content()) { + if (!content->audio) { continue; } - digester.add (i->digest ()); - digester.add (i->audio->mapping().digest ()); + digester.add(content->digest()); + digester.add(content->audio->mapping().digest()); if (playlist->content().size() != 1) { /* Analyses should be considered equal regardless of gain if they were made from just one piece of content. This @@ -273,7 +320,14 @@ Film::audio_analysis_path (shared_ptr playlist) const analysis at the plotting stage rather than having to recompute it. */ - digester.add (i->audio->gain ()); + digester.add(content->audio->gain()); + + /* Likewise we only care about position if we're looking at a + * whole-project view. + */ + digester.add(content->position().get()); + digester.add(content->trim_start().get()); + digester.add(content->trim_end().get()); } } @@ -281,135 +335,178 @@ Film::audio_analysis_path (shared_ptr playlist) const digester.add (audio_processor()->id ()); } + digester.add (audio_channels()); + p /= digester.get (); return p; } -/** Add suitable Jobs to the JobManager to create a DCP for this Film */ -void -Film::make_dcp () -{ - if (dcp_name().find ("/") != string::npos) { - 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()) { - throw MissingSettingError (_("name")); - } +boost::filesystem::path +Film::subtitle_analysis_path (shared_ptr content) const +{ + auto p = dir ("analysis"); - BOOST_FOREACH (shared_ptr i, content ()) { - if (!i->paths_valid()) { - throw runtime_error (_("some of your content is missing")); - } - shared_ptr dcp = dynamic_pointer_cast (i); - if (dcp && dcp->needs_kdm()) { - throw runtime_error (_("some of your content needs a KDM")); + Digester digester; + digester.add (content->digest()); + digester.add(_interop ? "1" : "0"); + + 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 (dcp && dcp->needs_assets()) { - throw runtime_error (_("some of your content needs an OV")); + if (tc->effect()) { + digester.add (tc->effect().get()); } + digester.add (tc->line_spacing()); + digester.add (tc->outline_width()); } - set_isdcf_date_today (); - - BOOST_FOREACH (string i, environment_info ()) { - LOG_GENERAL_NC (i); - } - - BOOST_FOREACH (shared_ptr i, content ()) { - LOG_GENERAL ("Content: %1", i->technical_summary()); - } - LOG_GENERAL ("DCP video rate %1 fps", video_frame_rate()); - if (Config::instance()->only_servers_encode ()) { - LOG_GENERAL_NC ("0 threads: ONLY SERVERS SET TO ENCODE"); - } else { - LOG_GENERAL ("%1 threads", Config::instance()->master_encoding_threads()); + auto fc = dynamic_pointer_cast(content); + if (fc) { + digester.add (fc->subtitle_stream()->identifier()); } - LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth()); - JobManager::instance()->add (shared_ptr (new TranscodeJob (shared_from_this()))); + p /= digester.get (); + return p; } + /** Start a job to send our DCP to the configured TMS */ 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); - root->add_child("UseISDCFName")->add_child_text (_use_isdcf_name ? "1" : "0"); + cxml::add_text_child(root, "Version", raw_convert(current_state_version)); + auto last_write = cxml::add_child(root, "LastWrittenBy"); + last_write->add_child_text (dcpomatic_version); + last_write->set_attribute("git", dcpomatic_git_commit); + cxml::add_text_child(root, "Name", _name); + cxml::add_text_child(root, "UseISDCFName", _use_isdcf_name ? "1" : "0"); if (_dcp_content_type) { - root->add_child("DCPContentType")->add_child_text (_dcp_content_type->isdcf_name ()); + cxml::add_text_child(root, "DCPContentType", _dcp_content_type->isdcf_name()); } if (_container) { - root->add_child("Container")->add_child_text (_container->id ()); - } - - root->add_child("Resolution")->add_child_text (resolution_to_string (_resolution)); - root->add_child("J2KBandwidth")->add_child_text (raw_convert (_j2k_bandwidth)); - _isdcf_metadata.as_xml (root->add_child ("ISDCFMetadata")); - root->add_child("VideoFrameRate")->add_child_text (raw_convert (_video_frame_rate)); - root->add_child("ISDCFDate")->add_child_text (boost::gregorian::to_iso_string (_isdcf_date)); - root->add_child("AudioChannels")->add_child_text (raw_convert (_audio_channels)); - 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); + cxml::add_text_child(root, "Container", _container->id()); + } + + cxml::add_text_child(root, "Resolution", resolution_to_string(_resolution)); + cxml::add_text_child(root, "J2KVideoBitRate", raw_convert(_video_bit_rate[VideoEncoding::JPEG2000])); + cxml::add_text_child(root, "MPEG2VideoBitRate", raw_convert(_video_bit_rate[VideoEncoding::MPEG2])); + cxml::add_text_child(root, "VideoFrameRate", raw_convert(_video_frame_rate)); + cxml::add_text_child(root, "AudioFrameRate", raw_convert(_audio_frame_rate)); + cxml::add_text_child(root, "ISDCFDate", boost::gregorian::to_iso_string(_isdcf_date)); + cxml::add_text_child(root, "AudioChannels", raw_convert(_audio_channels)); + cxml::add_text_child(root, "ThreeD", _three_d ? "1" : "0"); + cxml::add_text_child(root, "Sequence", _sequence ? "1" : "0"); + cxml::add_text_child(root, "Interop", _interop ? "1" : "0"); + cxml::add_text_child(root, "VideoEncoding", video_encoding_to_string(_video_encoding)); + cxml::add_text_child(root, "LimitToSMPTEBv20", _limit_to_smpte_bv20 ? "1" : "0"); + cxml::add_text_child(root, "Encrypted", _encrypted ? "1" : "0"); + cxml::add_text_child(root, "Key", _key.hex ()); + cxml::add_text_child(root, "ContextID", _context_id); if (_audio_processor) { - root->add_child("AudioProcessor")->add_child_text (_audio_processor->id ()); + cxml::add_text_child(root, "AudioProcessor", _audio_processor->id()); + } + cxml::add_text_child(root, "ReelType", raw_convert(static_cast (_reel_type))); + cxml::add_text_child(root, "ReelLength", raw_convert(_reel_length)); + for (auto boundary: _custom_reel_boundaries) { + cxml::add_text_child(root, "CustomReelBoundary", raw_convert(boundary.get())); + } + cxml::add_text_child(root, "ReencodeJ2K", _reencode_j2k ? "1" : "0"); + cxml::add_text_child(root, "UserExplicitVideoFrameRate", _user_explicit_video_frame_rate ? "1" : "0"); + for (auto const& marker: _markers) { + auto m = cxml::add_child(root, "Marker"); + m->set_attribute("type", dcp::marker_to_string(marker.first)); + m->add_child_text(raw_convert(marker.second.get())); + } + for (auto i: _ratings) { + i.as_xml(cxml::add_child(root, "Rating")); + } + for (auto i: _content_versions) { + cxml::add_text_child(root, "ContentVersion", i); + } + cxml::add_text_child(root, "NameLanguage", _name_language.to_string()); + cxml::add_text_child(root, "TerritoryType", territory_type_to_string(_territory_type)); + if (_release_territory) { + cxml::add_text_child(root, "ReleaseTerritory", _release_territory->subtag()); + } + if (_sign_language_video_language) { + cxml::add_text_child(root, "SignLanguageVideoLanguage", _sign_language_video_language->to_string()); + } + cxml::add_text_child(root, "VersionNumber", raw_convert(_version_number)); + cxml::add_text_child(root, "Status", dcp::status_to_string(_status)); + if (_chain) { + cxml::add_text_child(root, "Chain", *_chain); } - 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"); - _playlist->as_xml (root->add_child ("Playlist"), with_content_paths); + if (_distributor) { + cxml::add_text_child(root, "Distributor", *_distributor); + } + if (_facility) { + cxml::add_text_child(root, "Facility", *_facility); + } + if (_studio) { + cxml::add_text_child(root, "Studio", *_studio); + } + cxml::add_text_child(root, "TempVersion", _temp_version ? "1" : "0"); + cxml::add_text_child(root, "PreRelease", _pre_release ? "1" : "0"); + cxml::add_text_child(root, "RedBand", _red_band ? "1" : "0"); + cxml::add_text_child(root, "TwoDVersionOfThreeD", _two_d_version_of_three_d ? "1" : "0"); + if (_luminance) { + cxml::add_text_child(root, "LuminanceValue", raw_convert(_luminance->value())); + cxml::add_text_child(root, "LuminanceUnit", dcp::Luminance::unit_to_string(_luminance->unit())); + } + cxml::add_text_child(root, "UserExplicitContainer", _user_explicit_container ? "1" : "0"); + cxml::add_text_child(root, "UserExplicitResolution", _user_explicit_resolution ? "1" : "0"); + if (_audio_language) { + cxml::add_text_child(root, "AudioLanguage", _audio_language->to_string()); + } + _playlist->as_xml(cxml::add_child(root, "Playlist"), with_content_paths); return doc; } +void +Film::write_metadata (boost::filesystem::path path) const +{ + metadata()->write_to_file_formatted(path.string()); +} + /** Write state to our `metadata' file */ void -Film::write_metadata () const +Film::write_metadata () { DCPOMATIC_ASSERT (directory()); - boost::filesystem::create_directories (directory().get()); - shared_ptr doc = metadata (); - doc->write_to_file_formatted (file("metadata.xml").string ()); - _dirty = false; + dcp::filesystem::create_directories(directory().get()); + auto const filename = file(metadata_file); + try { + metadata()->write_to_file_formatted(filename.string()); + } catch (xmlpp::exception& e) { + throw FileError(String::compose("Could not write metadata file (%1)", e.what()), filename); + } + set_dirty (false); } /** Write a template from this film */ void Film::write_template (boost::filesystem::path path) const { - boost::filesystem::create_directories (path.parent_path()); + dcp::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. @@ -419,50 +516,76 @@ list Film::read_metadata (optional path) { if (!path) { - if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file ("metadata.xml"))) { - 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!")); + if (dcp::filesystem::exists(file("metadata")) && !dcp::filesystem::exists(file(metadata_file))) { + throw runtime_error( + variant::insert_dcpomatic( + _("This film was created with an older version of %1, 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); + } + + if (!dcp::filesystem::exists(*path)) { + throw FileNotFoundError(*path); } cxml::Document f ("Metadata"); - f.read_file (path.get ()); + f.read_file(dcp::filesystem::fix_long_path(path.get())); _state_version = f.number_child ("Version"); if (_state_version > current_state_version) { - throw runtime_error (_("This film was created with a newer version of DCP-o-matic, and it cannot be loaded into this version. Sorry!")); + throw runtime_error(variant::insert_dcpomatic(_("This film was created with a newer version of %1, 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) */ + auto const older = path->parent_path() / String::compose("metadata.%1.xml", _state_version); + if (!dcp::filesystem::is_regular_file(older)) { + try { + dcp::filesystem::copy_file(*path, older); + } catch (...) { + /* Never mind; at least we tried */ + } + } } + _last_written_by = f.optional_string_child("LastWrittenBy"); + _name = f.string_child ("Name"); if (_state_version >= 9) { _use_isdcf_name = f.bool_child ("UseISDCFName"); - _isdcf_metadata = ISDCFMetadata (f.node_child ("ISDCFMetadata")); _isdcf_date = boost::gregorian::from_undelimited_string (f.string_child ("ISDCFDate")); } else { _use_isdcf_name = f.bool_child ("UseDCIName"); - _isdcf_metadata = ISDCFMetadata (f.node_child ("DCIMetadata")); _isdcf_date = boost::gregorian::from_undelimited_string (f.string_child ("DCIDate")); } + { - 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 ()); } } _resolution = string_to_resolution (f.string_child ("Resolution")); - _j2k_bandwidth = f.number_child ("J2KBandwidth"); + if (auto j2k = f.optional_number_child("J2KBandwidth")) { + _video_bit_rate[VideoEncoding::JPEG2000] = *j2k; + } else { + _video_bit_rate[VideoEncoding::JPEG2000] = f.number_child("J2KVideoBitRate"); + } + _video_bit_rate[VideoEncoding::MPEG2] = f.optional_number_child("MPEG2VideoBitRate").get_value_or(Config::instance()->default_video_bit_rate(VideoEncoding::MPEG2)); _video_frame_rate = f.number_child ("VideoFrameRate"); - _signed = f.optional_bool_child("Signed").get_value_or (true); + _audio_frame_rate = f.optional_number_child("AudioFrameRate").get_value_or(48000); _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 @@ -482,6 +605,10 @@ Film::read_metadata (optional path) _three_d = f.bool_child ("ThreeD"); _interop = f.bool_child ("Interop"); + if (auto encoding = f.optional_string_child("VideoEncoding")) { + _video_encoding = video_encoding_from_string(*encoding); + } + _limit_to_smpte_bv20 = f.optional_bool_child("LimitToSMPTEBv20").get_value_or(false); _key = dcp::Key (f.string_child ("Key")); _context_id = f.optional_string_child("ContextID").get_value_or (dcp::make_uuid ()); @@ -491,12 +618,124 @@ Film::read_metadata (optional path) _audio_processor = 0; } - _reel_type = static_cast (f.optional_number_child("ReelType").get_value_or (static_cast(REELTYPE_SINGLE))); + if (_audio_processor && !Config::instance()->show_experimental_audio_processors()) { + auto ap = AudioProcessor::visible(); + if (find(ap.begin(), ap.end(), _audio_processor) == ap.end()) { + Config::instance()->set_show_experimental_audio_processors(true); + } + } + + _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); + for (auto boundary: f.node_children("CustomReelBoundary")) { + _custom_reel_boundaries.push_back(DCPTime(raw_convert(boundary->content()))); + } + _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); + + for (auto i: f.node_children("Marker")) { + auto type = i->optional_string_attribute("Type"); + if (!type) { + type = i->string_attribute("type"); + } + _markers[dcp::marker_from_string(*type)] = DCPTime(dcp::raw_convert(i->content())); + } + + 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 territory_type = f.optional_string_child("TerritoryType"); + if (territory_type) { + _territory_type = string_to_territory_type(*territory_type); + } + auto release_territory = f.optional_string_child("ReleaseTerritory"); + if (release_territory) { + _release_territory = dcp::LanguageTag::RegionSubtag (*release_territory); + } + + auto sign_language_video_language = f.optional_string_child("SignLanguageVideoLanguage"); + if (sign_language_video_language) { + _sign_language_video_language = dcp::LanguageTag(*sign_language_video_language); + } + + _version_number = f.optional_number_child("VersionNumber").get_value_or(1); + + auto status = f.optional_string_child("Status"); + if (status) { + _status = dcp::string_to_status (*status); + } + + _chain = f.optional_string_child("Chain"); + _distributor = f.optional_string_child("Distributor"); + _facility = f.optional_string_child("Facility"); + _studio = f.optional_string_child("Studio"); + _temp_version = f.optional_bool_child("TempVersion").get_value_or(false); + _pre_release = f.optional_bool_child("PreRelease").get_value_or(false); + _red_band = f.optional_bool_child("RedBand").get_value_or(false); + _two_d_version_of_three_d = f.optional_bool_child("TwoDVersionOfThreeD").get_value_or(false); + + auto value = f.optional_number_child("LuminanceValue"); + auto unit = f.optional_string_child("LuminanceUnit"); + if (value && 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); + + auto audio_language = f.optional_string_child("AudioLanguage"); + if (audio_language) { + _audio_language = dcp::LanguageTag(*audio_language); + } + + /* Read the old ISDCFMetadata tag from 2.14.x metadata */ + auto isdcf = f.optional_node_child("ISDCFMetadata"); + if (isdcf) { + if (auto territory = isdcf->optional_string_child("Territory")) { + try { + _release_territory = dcp::LanguageTag::RegionSubtag(*territory); + } catch (...) { + /* Invalid region subtag; just ignore it */ + } + } + if (auto audio_language = isdcf->optional_string_child("AudioLanguage")) { + try { + _audio_language = dcp::LanguageTag(*audio_language); + } catch (...) { + /* Invalid language tag; just ignore it */ + } + } + if (auto content_version = isdcf->optional_string_child("ContentVersion")) { + _content_versions.push_back (*content_version); + } + if (auto rating = isdcf->optional_string_child("Rating")) { + _ratings.push_back (dcp::Rating("", *rating)); + } + if (auto mastered_luminance = isdcf->optional_number_child("MasteredLuminance")) { + if (*mastered_luminance > 0) { + _luminance = dcp::Luminance(*mastered_luminance, dcp::Luminance::Unit::FOOT_LAMBERT); + } + } + _studio = isdcf->optional_string_child("Studio"); + _facility = isdcf->optional_string_child("Facility"); + _temp_version = isdcf->optional_bool_child("TempVersion").get_value_or(false); + _pre_release = isdcf->optional_bool_child("PreRelease").get_value_or(false); + _red_band = isdcf->optional_bool_child("RedBand").get_value_or(false); + _two_d_version_of_three_d = isdcf->optional_bool_child("TwoDVersionOfThreeD").get_value_or(false); + _chain = isdcf->optional_string_child("Chain"); + } list notes; - /* This method is the only one that can return notes (so far) */ _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version, notes); /* Write backtraces to this film's directory, until another film is loaded */ @@ -504,12 +743,12 @@ Film::read_metadata (optional path) set_backtrace_file (file ("backtrace.txt")); } - _dirty = false; + set_dirty (false); return notes; } /** Given a directory name, return its full path within the Film's directory. - * @param d directory name within the Filn's directory. + * @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 @@ -522,7 +761,7 @@ Film::dir (boost::filesystem::path d, bool create) const p /= d; if (create) { - boost::filesystem::create_directories (p); + dcp::filesystem::create_directories(p); } return p; @@ -540,7 +779,7 @@ Film::file (boost::filesystem::path f) const p /= _directory.get(); p /= f; - boost::filesystem::create_directories (p.parent_path ()); + dcp::filesystem::create_directories(p.parent_path()); return p; } @@ -556,10 +795,10 @@ 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 (); - copy (c.begin(), c.end(), back_inserter (mapped)); + auto c = i->audio->mapping().mapped_output_channels(); + copy (c.begin(), c.end(), back_inserter(mapped)); } } @@ -570,13 +809,72 @@ Film::mapped_audio_channels () const return mapped; } + +pair, vector> +Film::subtitle_languages(bool* burnt_in) const +{ + if (burnt_in) { + *burnt_in = true; + } + + pair, vector> result; + for (auto i: content()) { + auto dcp = dynamic_pointer_cast(i); + for (auto const& text: i->text) { + auto const use = text->use() || (dcp && dcp->reference_text(TextType::OPEN_SUBTITLE)); + if (use && text->type() == TextType::OPEN_SUBTITLE) { + if (!text->burn() && burnt_in) { + *burnt_in = false; + } + if (text->language()) { + if (text->language_is_additional()) { + result.second.push_back(text->language().get()); + } else { + result.first = text->language().get(); + } + } + } + } + if (i->video && i->video->burnt_subtitle_language()) { + result.first = i->video->burnt_subtitle_language(); + } + } + + std::sort (result.second.begin(), result.second.end()); + auto last = std::unique (result.second.begin(), result.second.end()); + result.second.erase (last, result.second.end()); + return result; +} + + +vector +Film::closed_caption_languages() const +{ + vector result; + for (auto i: content()) { + for (auto text: i->text) { + if (text->use() && text->type() == TextType::CLOSED_CAPTION && text->dcp_track() && text->dcp_track()->language) { + result.push_back(*text->dcp_track()->language); + } + } + } + + return result; +} + + /** @return a ISDCF-compliant name for a DCP of this film */ string Film::isdcf_name (bool if_created_now) const { - string d; + string isdcf_name; + + auto raw_name = name (); - string raw_name = name (); + auto to_upper = [](string s) { + transform(s.begin(), s.end(), s.begin(), ::toupper); + return s; + }; /* Split the raw name up into words */ vector words; @@ -585,16 +883,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; } } @@ -603,174 +901,215 @@ 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]; } } - if (fixed_name.length() > 14) { - fixed_name = fixed_name.substr (0, 14); - } + fixed_name = fixed_name.substr(0, Config::instance()->isdcf_name_part_length()); - d += fixed_name; + isdcf_name += fixed_name; if (dcp_content_type()) { - d += "_" + dcp_content_type()->isdcf_name(); - d += "-" + raw_convert(isdcf_metadata().content_version); + isdcf_name += "_" + dcp_content_type()->isdcf_name(); + string version = "1"; + if (_interop) { + if (!_content_versions.empty()) { + auto cv = _content_versions[0]; + if (!cv.empty() && std::all_of(cv.begin(), cv.end(), isdigit)) { + version = cv; + } + } + } else { + version = dcp::raw_convert(_version_number); + } + isdcf_name += "-" + version; } - ISDCFMetadata const dm = isdcf_metadata (); - - if (dm.temp_version) { - d += "-Temp"; + if (_temp_version) { + isdcf_name += "-Temp"; } - if (dm.pre_release) { - d += "-Pre"; + if (_pre_release) { + isdcf_name += "-Pre"; } - if (dm.red_band) { - d += "-RedBand"; + if (_red_band) { + isdcf_name += "-RedBand"; } - if (!dm.chain.empty ()) { - d += "-" + dm.chain; + if (_chain && !_chain->empty()) { + isdcf_name += "-" + *_chain; } if (three_d ()) { - d += "-3D"; + isdcf_name += "-3D"; } - if (dm.two_d_version_of_three_d) { - d += "-2D"; + if (_two_d_version_of_three_d) { + isdcf_name += "-2D"; } - if (!dm.mastered_luminance.empty ()) { - d += "-" + dm.mastered_luminance; + if (_luminance) { + auto fl = _luminance->value_in_foot_lamberts(); + char buffer[64]; + snprintf (buffer, sizeof(buffer), "%.1f", fl); + isdcf_name += String::compose("-%1fl", buffer); } if (video_frame_rate() != 24) { - d += "-" + raw_convert(video_frame_rate()); + isdcf_name += "-" + raw_convert(video_frame_rate()); } if (container()) { - d += "_" + container()->isdcf_name(); + isdcf_name += "_" + container()->isdcf_name(); } + auto content_list = content(); + /* XXX: this uses the first bit of content only */ - /* 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 ()) { - 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 ()); + /* 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::ContentKind::TRAILER) { + auto first_video = std::find_if(content_list.begin(), content_list.end(), [](shared_ptr c) { return static_cast(c->video); }); + if (first_video != content_list.end()) { + if (auto scaled_size = (*first_video)->video->scaled_size(frame_size())) { + auto first_ratio = lrintf(scaled_size->ratio() * 100); + auto container_ratio = lrintf(container()->ratio() * 100); + if (first_ratio != container_ratio) { + isdcf_name += "-" + dcp::raw_convert(first_ratio); } - break; } } - - if (content_ratio && content_ratio != container()) { - d += "-" + content_ratio->isdcf_name(); - } } - if (!dm.audio_language.empty ()) { - d += "_" + dm.audio_language; - if (!dm.subtitle_language.empty()) { - - bool burnt_in = true; - BOOST_FOREACH (shared_ptr i, content ()) { - if (!i->subtitle) { - continue; - } - - if (i->subtitle->use() && !i->subtitle->burn()) { - burnt_in = false; - } + auto entry_for_language = [](dcp::LanguageTag const& tag) { + /* Look up what we should be using for this tag in the DCNC name */ + for (auto const& dcnc: dcp::dcnc_tags()) { + if (tag.to_string() == dcnc.first) { + return dcnc.second; } + } + /* Fallback to the language subtag, if there is one */ + return tag.language() ? tag.language()->subtag() : "XX"; + }; - string language = dm.subtitle_language; - if (burnt_in && language != "XX") { - transform (language.begin(), language.end(), language.begin(), ::tolower); - } else { - transform (language.begin(), language.end(), language.begin(), ::toupper); - } + auto audio_language = _audio_language ? entry_for_language(*_audio_language) : "XX"; - d += "-" + language; + isdcf_name += "_" + to_upper (audio_language); + + bool burnt_in; + auto sub_langs = subtitle_languages(&burnt_in); + auto ccap_langs = closed_caption_languages(); + if (sub_langs.first && sub_langs.first->language()) { + auto lang = entry_for_language(*sub_langs.first); + if (burnt_in) { + transform (lang.begin(), lang.end(), lang.begin(), ::tolower); } else { - d += "-XX"; + lang = to_upper (lang); } - } - if (!dm.territory.empty ()) { - d += "_" + dm.territory; - if (dm.rating.empty ()) { - d += "-NR"; - } else { - d += "-" + dm.rating; + isdcf_name += "-" + lang; + } else if (!ccap_langs.empty()) { + isdcf_name += "-" + to_upper(entry_for_language(ccap_langs[0])) + "-CCAP"; + } else { + /* No subtitles */ + isdcf_name += "-XX"; + } + + if (_territory_type == TerritoryType::INTERNATIONAL_TEXTED) { + isdcf_name += "_INT-TD"; + } else if (_territory_type == TerritoryType::INTERNATIONAL_TEXTLESS) { + isdcf_name += "_INT-TL"; + } else if (_release_territory) { + auto territory = _release_territory->subtag(); + isdcf_name += "_" + to_upper (territory); + if (!_ratings.empty()) { + auto label = _ratings[0].label; + boost::erase_all(label, "+"); + boost::erase_all(label, "-"); + isdcf_name += "-" + label; } } /* Count mapped audio channels */ - pair ch = audio_channel_types (mapped_audio_channels(), audio_channels()); - if (ch.first) { - d += String::compose("_%1%2", ch.first, ch.second); + auto mapped = mapped_audio_channels (); + + auto ch = audio_channel_types (mapped, audio_channels()); + if (!ch.first && !ch.second) { + isdcf_name += "_MOS"; + } else if (ch.first) { + isdcf_name += String::compose("_%1%2", ch.first, ch.second); + } + + if (audio_channels() > static_cast(dcp::Channel::HI) && find(mapped.begin(), mapped.end(), static_cast(dcp::Channel::HI)) != mapped.end()) { + isdcf_name += "-HI"; + } + if (audio_channels() > static_cast(dcp::Channel::VI) && find(mapped.begin(), mapped.end(), static_cast(dcp::Channel::VI)) != mapped.end()) { + isdcf_name += "-VI"; } - /* XXX: HI/VI */ + if (find_if(content_list.begin(), content_list.end(), [](shared_ptr c) { return static_cast(c->atmos); }) != content_list.end()) { + isdcf_name += "-IAB"; + } - d += "_" + resolution_to_string (_resolution); + isdcf_name += "_" + resolution_to_string (_resolution); - if (!dm.studio.empty ()) { - d += "_" + dm.studio; + if (_studio && _studio->length() >= 2) { + isdcf_name += "_" + to_upper (_studio->substr(0, 4)); } if (if_created_now) { - d += "_" + boost::gregorian::to_iso_string (boost::gregorian::day_clock::local_day ()); + isdcf_name += "_" + boost::gregorian::to_iso_string (boost::gregorian::day_clock::local_day ()); } else { - d += "_" + boost::gregorian::to_iso_string (_isdcf_date); + isdcf_name += "_" + boost::gregorian::to_iso_string (_isdcf_date); } - if (!dm.facility.empty ()) { - d += "_" + dm.facility; + if (_facility && _facility->length() >= 3) { + isdcf_name += "_" + to_upper(_facility->substr(0, 3)); } if (_interop) { - d += "_IOP"; + isdcf_name += "_IOP"; } else { - d += "_SMPTE"; + isdcf_name += "_SMPTE"; } if (three_d ()) { - d += "-3D"; + isdcf_name += "-3D"; } - bool vf = false; - BOOST_FOREACH (shared_ptr i, content ()) { - shared_ptr dc = dynamic_pointer_cast (i); - if (dc && (dc->reference_video() || dc->reference_audio() || dc->reference_subtitle())) { + auto vf = false; + for (auto content: content_list) { + auto dcp = dynamic_pointer_cast(content); + if (!dcp) { + continue; + } + + bool any_text = false; + for (int i = 0; i < static_cast(TextType::COUNT); ++i) { + if (dcp->reference_text(static_cast(i))) { + any_text = true; + } + } + if (dcp->reference_video() || dcp->reference_audio() || any_text) { vf = true; } } if (vf) { - d += "_VF"; + isdcf_name += "_VF"; } else { - d += "_OV"; + isdcf_name += "_OV"; } - return d; + return isdcf_name; } /** @return name to give the DCP */ @@ -789,139 +1128,196 @@ void Film::set_directory (boost::filesystem::path d) { _directory = d; - _dirty = true; + set_dirty (true); } void Film::set_name (string n) { + FilmChangeSignaller ch(this, FilmProperty::NAME); _name = n; - signal_changed (NAME); } void Film::set_use_isdcf_name (bool u) { + FilmChangeSignaller ch(this, FilmProperty::USE_ISDCF_NAME); _use_isdcf_name = u; - signal_changed (USE_ISDCF_NAME); } void Film::set_dcp_content_type (DCPContentType const * t) { + FilmChangeSignaller ch(this, FilmProperty::DCP_CONTENT_TYPE); _dcp_content_type = t; - signal_changed (DCP_CONTENT_TYPE); } + +/** @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) { + FilmChangeSignaller ch(this, FilmProperty::CONTAINER); _container = c; - signal_changed (CONTAINER); + + 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) { + FilmChangeSignaller ch(this, FilmProperty::RESOLUTION); _resolution = r; - signal_changed (RESOLUTION); -} -void -Film::set_j2k_bandwidth (int b) -{ - _j2k_bandwidth = b; - signal_changed (J2K_BANDWIDTH); + if (explicit_user) { + _user_explicit_resolution = true; + } } + void -Film::set_isdcf_metadata (ISDCFMetadata m) +Film::set_video_bit_rate(VideoEncoding encoding, int64_t bit_rate) { - _isdcf_metadata = m; - signal_changed (ISDCF_METADATA); + FilmChangeSignaller ch(this, FilmProperty::VIDEO_BIT_RATE); + _video_bit_rate[encoding] = bit_rate; } +/** @param f New frame rate. + * @param user_explicit true if this comes from a direct user instruction, false if it is from + * DCP-o-matic being helpful. + */ void -Film::set_video_frame_rate (int f) +Film::set_video_frame_rate (int f, bool user_explicit) { + FilmChangeSignaller ch(this, FilmProperty::VIDEO_FRAME_RATE); _video_frame_rate = f; - signal_changed (VIDEO_FRAME_RATE); + if (user_explicit) { + _user_explicit_video_frame_rate = true; + } } void Film::set_audio_channels (int c) { + FilmChangeSignaller ch(this, FilmProperty::AUDIO_CHANNELS); _audio_channels = c; - signal_changed (AUDIO_CHANNELS); } void Film::set_three_d (bool t) { + FilmChangeSignaller ch(this, FilmProperty::THREE_D); _three_d = t; - signal_changed (THREE_D); - if (_three_d && _isdcf_metadata.two_d_version_of_three_d) { - _isdcf_metadata.two_d_version_of_three_d = false; - signal_changed (ISDCF_METADATA); + if (_three_d && _two_d_version_of_three_d) { + set_two_d_version_of_three_d (false); } } void Film::set_interop (bool i) { + FilmChangeSignaller ch(this, FilmProperty::INTEROP); _interop = i; - signal_changed (INTEROP); } + void -Film::set_audio_processor (AudioProcessor const * processor) +Film::set_video_encoding(VideoEncoding encoding) { - _audio_processor = processor; - signal_changed (AUDIO_PROCESSOR); - signal_changed (AUDIO_CHANNELS); + FilmChangeSignaller ch(this, FilmProperty::VIDEO_ENCODING); + _video_encoding = encoding; + check_settings_consistency(); } + void -Film::set_reel_type (ReelType t) +Film::set_limit_to_smpte_bv20(bool limit) { + FilmChangeSignaller ch(this, FilmProperty::LIMIT_TO_SMPTE_BV20); + _limit_to_smpte_bv20 = limit; +} + + +void +Film::set_audio_processor (AudioProcessor const * processor) +{ + FilmChangeSignaller ch1(this, FilmProperty::AUDIO_PROCESSOR); + FilmChangeSignaller ch2(this, FilmProperty::AUDIO_CHANNELS); + _audio_processor = processor; +} + +void +Film::set_reel_type (ReelType t) +{ + FilmChangeSignaller ch(this, FilmProperty::REEL_TYPE); _reel_type = t; - signal_changed (REEL_TYPE); } /** @param r Desired reel length in bytes */ void Film::set_reel_length (int64_t r) { + FilmChangeSignaller ch(this, FilmProperty::REEL_LENGTH); _reel_length = r; - signal_changed (REEL_LENGTH); } + +void +Film::set_custom_reel_boundaries(vector boundaries) +{ + FilmChangeSignaller ch(this, FilmProperty::CUSTOM_REEL_BOUNDARIES); + std::sort(boundaries.begin(), boundaries.end()); + _custom_reel_boundaries = std::move(boundaries); +} + + void -Film::set_upload_after_make_dcp (bool u) +Film::set_reencode_j2k (bool r) { - _upload_after_make_dcp = u; - signal_changed (UPLOAD_AFTER_MAKE_DCP); + FilmChangeSignaller ch(this, FilmProperty::REENCODE_J2K); + _reencode_j2k = r; } void -Film::signal_changed (Property p) +Film::signal_change (ChangeType type, int p) { - _dirty = true; + signal_change(type, static_cast(p)); +} - switch (p) { - case Film::CONTENT: - set_video_frame_rate (_playlist->best_video_frame_rate ()); - break; - case Film::VIDEO_FRAME_RATE: - case Film::SEQUENCE: - _playlist->maybe_sequence (); - break; - default: - break; - } +void +Film::signal_change(ChangeType type, FilmProperty p) +{ + if (type == ChangeType::DONE) { + set_dirty (true); - emit (boost::bind (boost::ref (Changed), p)); + if (p == FilmProperty::CONTENT) { + if (!_user_explicit_video_frame_rate) { + set_video_frame_rate (best_video_frame_rate()); + } + } + + emit (boost::bind (boost::ref (Change), type, p)); + + if (p == FilmProperty::VIDEO_FRAME_RATE || p == FilmProperty::SEQUENCE) { + /* We want to call Playlist::maybe_sequence but this must happen after the + main signal emission (since the butler will see that emission and un-suspend itself). + */ + emit (boost::bind(&Playlist::maybe_sequence, _playlist.get(), shared_from_this())); + } + } else { + Change (type, p); + } } void @@ -930,6 +1326,7 @@ Film::set_isdcf_date_today () _isdcf_date = boost::gregorian::day_clock::local_day (); } + boost::filesystem::path Film::j2c_path (int reel, Frame frame, Eyes eyes, bool tmp) const { @@ -941,9 +1338,9 @@ Film::j2c_path (int reel, Frame frame, Eyes eyes, bool tmp) const snprintf(buffer, sizeof(buffer), "%08d_%08" PRId64, reel, frame); string s (buffer); - if (eyes == EYES_LEFT) { + if (eyes == Eyes::LEFT) { s += ".L"; - } else if (eyes == EYES_RIGHT) { + } else if (eyes == Eyes::RIGHT) { s += ".R"; } @@ -957,63 +1354,46 @@ Film::j2c_path (int reel, Frame frame, Eyes eyes, bool tmp) const return file (p); } -/** Find all the DCPs in our directory that can be dcp::DCP::read() and return details of their CPLs */ + +/** Find all the DCPs in our directory that can be dcp::DCP::read() and return details of their CPLs. + * The list will be returned in reverse order of timestamp (i.e. most recent first). + */ vector Film::cpls () const { if (!directory ()) { - return vector (); + return {}; } 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 const& item: dcp::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" + dcp::filesystem::is_directory(item) && + item.path().filename() != "j2c" && item.path().filename() != "video" && item.path().filename() != "info" && item.path().filename() != "analysis" ) { try { - dcp::DCP dcp (*i); - dcp.read (); - DCPOMATIC_ASSERT (dcp.cpls().front()->file()); - out.push_back ( - CPLSummary ( - i->path().leaf().string(), - dcp.cpls().front()->id(), - dcp.cpls().front()->annotation_text(), - dcp.cpls().front()->file().get() - ) - ); + out.push_back(CPLSummary(item)); } catch (...) { } } } - return out; -} + sort(out.begin(), out.end(), [](CPLSummary const& a, CPLSummary const& b) { + return a.last_write_time > b.last_write_time; + }); -void -Film::set_signed (bool s) -{ - _signed = s; - signal_changed (SIGNED); + return out; } void Film::set_encrypted (bool e) { + FilmChangeSignaller ch(this, FilmProperty::ENCRYPTED); _encrypted = e; - signal_changed (ENCRYPTED); -} - -void -Film::set_key (dcp::Key key) -{ - _key = key; - signal_changed (KEY); } ContentList @@ -1022,43 +1402,46 @@ 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_and_add_content (shared_ptr c) +Film::examine_and_add_content (shared_ptr content, bool disable_audio_analysis) { - if (dynamic_pointer_cast (c) && _directory) { - run_ffprobe (c->path(0), file ("ffprobe.log"), _log); + if (dynamic_pointer_cast (content) && _directory) { + run_ffprobe (content->path(0), file("ffprobe.log")); } - shared_ptr j (new ExamineContentJob (shared_from_this(), c)); + 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 (c))) + j->Finished.connect (bind (&Film::maybe_add_content, this, weak_ptr(j), weak_ptr(content), disable_audio_analysis)) ); JobManager::instance()->add (j); } void -Film::maybe_add_content (weak_ptr j, weak_ptr c) +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; } add_content (content); - if (Config::instance()->automatic_audio_analysis() && content->audio) { - shared_ptr playlist (new Playlist); - playlist->add (content); + if (Config::instance()->automatic_audio_analysis() && content->audio && !disable_audio_analysis) { + auto playlist = make_shared(); + playlist->add (shared_from_this(), 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); } @@ -1069,52 +1452,105 @@ Film::add_content (shared_ptr c) { /* Add {video,subtitle} content after any existing {video,subtitle} content */ if (c->video) { - c->set_position (_playlist->video_end ()); - } else if (c->subtitle) { - c->set_position (_playlist->subtitle_end ()); + c->set_position (shared_from_this(), _playlist->video_end(shared_from_this())); + } else if (!c->text.empty()) { + c->set_position (shared_from_this(), _playlist->text_end(shared_from_this())); } 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()) { - if (typeid(i.get()) == typeid(c.get())) { - c->use_template (i); + 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) { + if (_audio_channels < 14) { + 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 content: _playlist->content()) { + if (content->video) { + if (!video) { + video = content->video; + } else { + video.reset (); } } } - _playlist->add (c); + if (video && video->size()) { + /* 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::FOUR_K, false); + } else { + set_resolution (Resolution::TWO_K, false); + } + } + } } void Film::remove_content (shared_ptr c) { _playlist->remove (c); + maybe_set_container_and_resolution (); } void Film::move_content_earlier (shared_ptr c) { - _playlist->move_earlier (c); + _playlist->move_earlier (shared_from_this(), c); } void Film::move_content_later (shared_ptr c) { - _playlist->move_later (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 (); + return max(DCPTime::from_seconds(1), _playlist->length(shared_from_this()).ceil(video_frame_rate())); } int Film::best_video_frame_rate () const { - return _playlist->best_video_frame_rate (); + /* Don't default to anything above 30fps (make the user select that explicitly) */ + auto best = _playlist->best_video_frame_rate (); + if (best > 30) { + best /= 2; + } + return best; } FrameRateChange @@ -1124,50 +1560,148 @@ Film::active_frame_rate_change (DCPTime t) const } void -Film::playlist_content_changed (weak_ptr c, int p, bool frequent) +Film::playlist_content_change (ChangeType type, weak_ptr c, int p, bool frequent) { - _dirty = true; - if (p == ContentProperty::VIDEO_FRAME_RATE) { - set_video_frame_rate (_playlist->best_video_frame_rate ()); + signal_change(type, FilmProperty::CONTENT); } else if (p == AudioContentProperty::STREAMS) { - signal_changed (NAME); + signal_change(type, FilmProperty::NAME); + } + + if (type == ChangeType::DONE) { + emit (boost::bind (boost::ref (ContentChange), type, c, p, frequent)); + if (!frequent) { + check_settings_consistency (); + } + } else { + ContentChange (type, c, p, frequent); } - emit (boost::bind (boost::ref (ContentChanged), c, p, frequent)); + set_dirty (true); } void -Film::playlist_changed () +Film::playlist_length_change () { - signal_changed (CONTENT); - signal_changed (NAME); + LengthChange (); } void -Film::playlist_order_changed () +Film::playlist_change (ChangeType type) { - signal_changed (CONTENT_ORDER); + signal_change(type, FilmProperty::CONTENT); + signal_change(type, FilmProperty::NAME); + + if (type == ChangeType::DONE) { + check_settings_consistency (); + } + + set_dirty (true); } -int -Film::audio_frame_rate () const -{ - BOOST_FOREACH (shared_ptr i, content ()) { - if (i->audio && i->audio->has_rate_above_48k ()) { - return 96000; +/** 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(variant::insert_dcpomatic(_("%1 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; + } + + 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(TextType::OPEN_SUBTITLE) && !d->can_reference_text(shared_from_this(), TextType::OPEN_SUBTITLE, why_not)) { + d->set_reference_text(TextType::OPEN_SUBTITLE, false); + change_made = true; + } + if (d->reference_text(TextType::CLOSED_CAPTION) && !d->can_reference_text(shared_from_this(), TextType::CLOSED_CAPTION, why_not)) { + d->set_reference_text(TextType::CLOSED_CAPTION, false); + change_made = true; + } + } + + if (change_made) { + Message(variant::insert_dcpomatic(_("%1 had to change your settings for referring to DCPs as OV. Please review those settings to make sure they are what you want."))); + } + + if (reel_type() == ReelType::CUSTOM) { + auto boundaries = custom_reel_boundaries(); + auto too_late = std::find_if(boundaries.begin(), boundaries.end(), [this](dcpomatic::DCPTime const& time) { + return time >= length(); + }); + + if (too_late != boundaries.end()) { + if (std::distance(too_late, boundaries.end()) > 1) { + Message(variant::insert_dcpomatic(_("%1 had to remove some of your custom reel boundaries as they no longer lie within the film."))); + } else { + Message(variant::insert_dcpomatic(_("%1 had to remove one of your custom reel boundaries as it no longer lies within the film."))); + } + boundaries.erase(too_late, boundaries.end()); + set_custom_reel_boundaries(boundaries); + } + } + + auto const hd = Ratio::from_id("178"); + + if (video_encoding() == VideoEncoding::MPEG2) { + if (container() != hd) { + set_container(hd); + Message(_("DCP-o-matic had to set your container to 1920x1080 as it's the only one that can be used with MPEG2 encoding.")); + } + if (three_d()) { + set_three_d(false); + Message(_("DCP-o-matic had to set your film to 2D as 3D is not yet supported with MPEG2 encoding.")); } + } else if (container() == hd && !Config::instance()->allow_any_container()) { + set_container(Ratio::from_id("185")); + Message(_("DCP-o-matic set your container to DCI Flat as it was previously 1920x1080 and that is not a standard ratio with JPEG2000 encoding.")); } +} - return 48000; +void +Film::playlist_order_changed () +{ + /* XXX: missing PENDING */ + signal_change(ChangeType::DONE, FilmProperty::CONTENT_ORDER); } + void Film::set_sequence (bool s) { + if (s == _sequence) { + return; + } + + FilmChangeSignaller cc(this, FilmProperty::SEQUENCE); _sequence = s; _playlist->set_sequence (s); - signal_changed (SEQUENCE); } /** @return Size of the largest possible image in whatever resolution we are using */ @@ -1175,9 +1709,9 @@ dcp::Size Film::full_frame () const { switch (_resolution) { - case RESOLUTION_2K: + case Resolution::TWO_K: return dcp::Size (2048, 1080); - case RESOLUTION_4K: + case Resolution::FOUR_K: return dcp::Size (4096, 2160); } @@ -1192,104 +1726,80 @@ Film::frame_size () const return fit_ratio_within (container()->ratio(), full_frame ()); } -/** @param recipient KDM recipient certificate. - * @param trusted_devices Certificates of other trusted devices (can be empty). - * @param cpl_file CPL filename. + +/** @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) { + if (auto s = i->video->scaled_size(frame)) { + active.width = max(active.width, s->width); + active.height = max(active.height, s->height); + } + } + } + + return active; +} + + +/* @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. */ -dcp::EncryptedKDM -Film::make_kdm ( - dcp::Certificate recipient, - vector trusted_devices, - boost::filesystem::path cpl_file, - dcp::LocalTime from, - dcp::LocalTime until, - dcp::Formulation formulation - ) const -{ - shared_ptr cpl (new dcp::CPL (cpl_file)); - shared_ptr signer = Config::instance()->signer_chain (); - if (!signer->valid ()) { - throw InvalidSignerError (); +dcp::DecryptedKDM +Film::make_kdm(boost::filesystem::path cpl_file, dcp::LocalTime from, dcp::LocalTime until) const +{ + if (!_encrypted) { + throw runtime_error (_("Cannot make a KDM as this project is not encrypted.")); } + auto cpl = make_shared(cpl_file); + /* 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; + map, dcp::Key> keys; - BOOST_FOREACH(shared_ptr i, cpl->reel_assets ()) { - shared_ptr mxf = boost::dynamic_pointer_cast (i); - if (!mxf || !mxf->key_id()) { + for (auto asset: cpl->reel_file_assets()) { + if (!asset->encrypted()) { 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(); + for (auto const& k: imported_keys) { + if (k.id() == asset->key_id().get()) { + LOG_GENERAL("Using imported key for %1", asset->key_id().get()); + keys[asset] = k.key(); done = true; } } 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(); + LOG_GENERAL("Using our own key for %1", asset->key_id().get()); + keys[asset] = 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); + ); } -/** @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. - */ -list -Film::make_kdms ( - list > screens, - boost::filesystem::path cpl_file, - boost::posix_time::ptime from, - boost::posix_time::ptime until, - dcp::Formulation formulation - ) const -{ - list kdms; - - BOOST_FOREACH (shared_ptr i, screens) { - if (i->recipient) { - dcp::EncryptedKDM const kdm = make_kdm ( - i->recipient.get(), - i->trusted_devices, - 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 - ); - - kdms.push_back (ScreenKDM (i, kdm)); - } - } - - return kdms; -} /** @return The approximate disk space required to encode a DCP of this film with the * current settings, in bytes. @@ -1297,13 +1807,13 @@ Film::make_kdms ( uint64_t Film::required_disk_space () const { - return _playlist->required_disk_space (j2k_bandwidth(), audio_channels(), audio_frame_rate()); + return _playlist->required_disk_space (shared_from_this(), video_bit_rate(video_encoding()), audio_channels(), audio_frame_rate()); } /** This method checks the disk that the Film is on and tries to decide whether or not * there will be enough space to make a DCP for it. If so, true is returned; if not, * false is returned and required and available are filled in with the amount of disk space - * required and available respectively (in Gb). + * required and available respectively (in GB). * * Note: the decision made by this method isn't, of course, 100% reliable. */ @@ -1314,19 +1824,19 @@ 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"); + dcp::File f(test, "w"); if (f) { - fclose (f); + f.close(); boost::system::error_code ec; - boost::filesystem::create_hard_link (test, test2, ec); + dcp::filesystem::create_hard_link(test, test2, ec); if (ec) { can_hard_link = false; } - boost::filesystem::remove (test); - boost::filesystem::remove (test2); + dcp::filesystem::remove(test); + dcp::filesystem::remove(test2); } - boost::filesystem::space_info s = boost::filesystem::space (internal_video_asset_dir ()); + auto s = dcp::filesystem::space(internal_video_asset_dir()); required = double (required_disk_space ()) / 1073741824.0f; if (!can_hard_link) { required *= 2; @@ -1335,83 +1845,10 @@ Film::should_be_enough_disk_space (double& required, double& available, bool& ca return (available - required) > 1; } -string -Film::subtitle_language () const -{ - set languages; - - ContentList cl = content (); - BOOST_FOREACH (shared_ptr& c, cl) { - if (c->subtitle) { - languages.insert (c->subtitle->language ()); - } - } - - string all; - BOOST_FOREACH (string s, languages) { - if (!all.empty ()) { - all += "/" + s; - } else { - all += s; - } - } - - return all; -} - -/** Change the gains of the supplied AudioMapping to make it a default - * for this film. The defaults are guessed based on what processor (if any) - * is in use, the number of input channels and any filename supplied. - */ -void -Film::make_audio_mapping_default (AudioMapping& mapping, optional filename) const -{ - static string const regex[] = { - ".*[\\._-]L[\\._-].*", - ".*[\\._-]R[\\._-].*", - ".*[\\._-]C[\\._-].*", - ".*[\\._-]Lfe[\\._-].*", - ".*[\\._-]Ls[\\._-].*", - ".*[\\._-]Rs[\\._-].*" - }; - - static int const regexes = sizeof(regex) / sizeof(*regex); - - if (audio_processor ()) { - audio_processor()->make_audio_mapping_default (mapping); - } else { - mapping.make_zero (); - if (mapping.input_channels() == 1) { - bool guessed = false; - - /* See if we can guess where this stream should go */ - if (filename) { - for (int i = 0; i < regexes; ++i) { - boost::regex e (regex[i], boost::regex::icase); - if (boost::regex_match (filename->string(), e) && i < mapping.output_channels()) { - mapping.set (0, i, 1); - guessed = true; - } - } - } - - if (!guessed) { - /* If we have no idea, just put it on centre */ - mapping.set (0, static_cast (dcp::CENTRE), 1); - } - } else { - /* 1:1 mapping */ - for (int i = 0; i < min (mapping.input_channels(), mapping.output_channels()); ++i) { - mapping.set (i, i, 1); - } - } - } -} - /** @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 ()) { @@ -1420,10 +1857,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 (Config::instance()->use_all_audio_channels() || (i != 8 && i != 9 && i != 15)) { + n.push_back (NamedChannel(short_audio_channel_name(i), i)); + } } return n; @@ -1432,7 +1871,7 @@ Film::audio_output_names () const void Film::repeat_content (ContentList c, int n) { - _playlist->repeat (c, n); + _playlist->repeat (shared_from_this(), c, n); } void @@ -1447,118 +1886,107 @@ Film::audio_analysis_finished () /* XXX */ } -list + +vector Film::reels () const { - list p; - DCPTime const len = length().ceil (video_frame_rate ()); + vector periods; + auto const len = length(); switch (reel_type ()) { - case REELTYPE_SINGLE: - p.push_back (DCPTimePeriod (DCPTime (), len)); + case ReelType::SINGLE: + periods.emplace_back(DCPTime(), len); break; - case REELTYPE_BY_VIDEO_CONTENT: + 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()) { - 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() : 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)); + 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 */ + periods.emplace_back(*last, t); + last = t; + } else if (!last) { + /* That was the first time, so start a new period */ + last = t; + } } - 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)); + if (!periods.empty()) { + periods.back().to = split_points.back(); } break; } - case REELTYPE_BY_LENGTH: + 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 / ((video_bit_rate(video_encoding()) / 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)); + periods.emplace_back(current, end); current = end; } break; } + case ReelType::CUSTOM: + { + DCPTimePeriod current; + for (auto boundary: _custom_reel_boundaries) { + current.to = boundary; + periods.push_back(current); + current.from = boundary; + } + current.to = len; + periods.push_back(current); + break; + } } - return p; + return periods; } + /** @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 -Film::fix_conflicting_settings () -{ - list notes; - - list was_referencing; - BOOST_FOREACH (shared_ptr i, content()) { - shared_ptr d = dynamic_pointer_cast (i); - if (d) { - list 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; + return _playlist->content_summary (shared_from_this(), period); } void Film::use_template (string name) { _template_film.reset (new Film (optional())); - _template_film->read_metadata (Config::instance()->template_path (name)); + _template_film->read_metadata (Config::instance()->template_read_path(name)); _use_isdcf_name = _template_film->_use_isdcf_name; _dcp_content_type = _template_film->_dcp_content_type; _container = _template_film->_container; _resolution = _template_film->_resolution; - _j2k_bandwidth = _template_film->_j2k_bandwidth; + for (auto encoding: { VideoEncoding::JPEG2000, VideoEncoding::MPEG2 }) { + _video_bit_rate[encoding] = _template_film->_video_bit_rate[encoding]; + } _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; @@ -1567,7 +1995,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; } pair @@ -1575,3 +2002,384 @@ Film::speed_up_range (int dcp_frame_rate) const { return _playlist->speed_up_range (dcp_frame_rate); } + +void +Film::copy_from (shared_ptr film) +{ + read_metadata (film->file (metadata_file)); +} + +bool +Film::references_dcp_video () const +{ + for (auto i: _playlist->content()) { + auto d = dynamic_pointer_cast(i); + if (d && d->reference_video()) { + return true; + } + } + + return false; +} + +bool +Film::references_dcp_audio () const +{ + for (auto i: _playlist->content()) { + auto d = dynamic_pointer_cast(i); + if (d && d->reference_audio()) { + return true; + } + } + + return false; +} + + +bool +Film::contains_atmos_content () const +{ + auto const content = _playlist->content(); + return std::find_if(content.begin(), content.end(), [](shared_ptr content) { + return static_cast(content->atmos); + }) != content.end(); +} + + +list +Film::closed_caption_tracks () const +{ + list tt; + for (auto i: content()) { + for (auto text: i->text) { + /* XXX: Empty DCPTextTrack ends up being a magic value here - the "unknown" or "not specified" track */ + auto dtt = text->dcp_track().get_value_or(DCPTextTrack()); + if (text->type() == TextType::CLOSED_CAPTION && find(tt.begin(), tt.end(), dtt) == tt.end()) { + tt.push_back (dtt); + } + } + } + + return tt; +} + +void +Film::set_marker (dcp::Marker type, DCPTime time) +{ + FilmChangeSignaller ch(this, FilmProperty::MARKERS); + _markers[type] = time; +} + + +void +Film::unset_marker (dcp::Marker type) +{ + FilmChangeSignaller ch(this, FilmProperty::MARKERS); + _markers.erase (type); +} + + +void +Film::clear_markers () +{ + FilmChangeSignaller ch(this, FilmProperty::MARKERS); + _markers.clear (); +} + + +void +Film::set_ratings (vector r) +{ + FilmChangeSignaller ch(this, FilmProperty::RATINGS); + _ratings = r; +} + +void +Film::set_content_versions (vector v) +{ + FilmChangeSignaller ch(this, FilmProperty::CONTENT_VERSIONS); + _content_versions = v; +} + + +void +Film::set_name_language (dcp::LanguageTag lang) +{ + FilmChangeSignaller ch(this, FilmProperty::NAME_LANGUAGE); + _name_language = lang; +} + + +void +Film::set_release_territory (optional region) +{ + FilmChangeSignaller ch(this, FilmProperty::RELEASE_TERRITORY); + _release_territory = region; +} + + +void +Film::set_status (dcp::Status s) +{ + FilmChangeSignaller ch(this, FilmProperty::STATUS); + _status = s; +} + + +void +Film::set_version_number (int v) +{ + FilmChangeSignaller ch(this, FilmProperty::VERSION_NUMBER); + _version_number = v; +} + + +void +Film::set_chain (optional c) +{ + FilmChangeSignaller ch(this, FilmProperty::CHAIN); + _chain = c; +} + + +void +Film::set_distributor (optional d) +{ + FilmChangeSignaller ch(this, FilmProperty::DISTRIBUTOR); + _distributor = d; +} + + +void +Film::set_luminance (optional l) +{ + FilmChangeSignaller ch(this, FilmProperty::LUMINANCE); + _luminance = l; +} + + +void +Film::set_facility (optional f) +{ + FilmChangeSignaller ch(this, FilmProperty::FACILITY); + _facility = f; +} + + +void +Film::set_studio (optional s) +{ + FilmChangeSignaller ch(this, FilmProperty::STUDIO); + _studio = s; +} + + +optional +Film::marker (dcp::Marker type) const +{ + auto i = _markers.find (type); + if (i == _markers.end()) { + return {}; + } + return i->second; +} + +shared_ptr +Film::info_file_handle (DCPTimePeriod period, bool read) const +{ + return std::make_shared(_info_file_mutex, info_file(period), read); +} + +InfoFileHandle::InfoFileHandle (boost::mutex& mutex, boost::filesystem::path path, bool read) + : _lock (mutex) + , _file(path, read ? "rb" : (dcp::filesystem::exists(path) ? "r+b" : "wb")) +{ + if (!_file) { + throw OpenFileError(path, errno, read ? OpenFileError::READ : (dcp::filesystem::exists(path) ? OpenFileError::READ_WRITE : OpenFileError::WRITE)); + } +} + + +/** 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::from_frames(1, video_frame_rate()); + } + + if (markers.find(dcp::Marker::LFOC) == markers.end()) { + markers[dcp::Marker::LFOC] = length() - DCPTime::from_frames(1, video_frame_rate()); + } +} + + +void +Film::set_temp_version (bool t) +{ + FilmChangeSignaller ch(this, FilmProperty::TEMP_VERSION); + _temp_version = t; +} + + +void +Film::set_pre_release (bool p) +{ + FilmChangeSignaller ch(this, FilmProperty::PRE_RELEASE); + _pre_release = p; +} + + +void +Film::set_red_band (bool r) +{ + FilmChangeSignaller ch(this, FilmProperty::RED_BAND); + _red_band = r; +} + + +void +Film::set_two_d_version_of_three_d (bool t) +{ + FilmChangeSignaller ch(this, FilmProperty::TWO_D_VERSION_OF_THREE_D); + _two_d_version_of_three_d = t; +} + + +void +Film::set_audio_language (optional language) +{ + FilmChangeSignaller ch(this, FilmProperty::AUDIO_LANGUAGE); + _audio_language = language; +} + + +void +Film::set_audio_frame_rate (int rate) +{ + FilmChangeSignaller ch(this, FilmProperty::AUDIO_FRAME_RATE); + _audio_frame_rate = rate; +} + + +bool +Film::has_sign_language_video_channel () const +{ + return _audio_channels >= static_cast(dcp::Channel::SIGN_LANGUAGE); +} + + +void +Film::set_sign_language_video_language (optional lang) +{ + FilmChangeSignaller ch(this, FilmProperty::SIGN_LANGUAGE_VIDEO_LANGUAGE); + _sign_language_video_language = lang; +} + + +void +Film::set_dirty (bool dirty) +{ + auto const changed = dirty != _dirty; + _dirty = dirty; + if (changed) { + emit (boost::bind(boost::ref(DirtyChange), _dirty)); + } +} + + +/** @return true if the metadata was (probably) last written by a version earlier + * than the given one; false if it definitely was not. + */ +bool +Film::last_written_by_earlier_than(int major, int minor, int micro) const +{ + if (!_last_written_by) { + return true; + } + + vector parts; + boost::split(parts, *_last_written_by, boost::is_any_of(".")); + + if (parts.size() != 3) { + /* Not sure what's going on, so let's say it was written by an old version */ + return true; + } + + if (boost::ends_with(parts[2], "pre")) { + parts[2] = parts[2].substr(0, parts[2].length() - 3); + } + + int our_major = dcp::raw_convert(parts[0]); + int our_minor = dcp::raw_convert(parts[1]); + int our_micro = dcp::raw_convert(parts[2]); + + if (our_major != major) { + return our_major < major; + } + + if (our_minor != minor) { + return our_minor < minor; + } + + return our_micro < micro; +} + + +void +Film::set_territory_type(TerritoryType type) +{ + FilmChangeSignaller ch(this, FilmProperty::TERRITORY_TYPE); + _territory_type = type; +} + + +void +Film::set_ui_state(string key, string value) +{ + _ui_state[key] = value; + write_ui_state(); +} + + +boost::optional +Film::ui_state(string key) const +{ + auto iter = _ui_state.find(key); + if (iter == _ui_state.end()) { + return {}; + } + + return iter->second; +} + + +void +Film::write_ui_state() const +{ + auto doc = make_shared(); + auto root = doc->create_root_node("UI"); + + for (auto state: _ui_state) { + cxml::add_text_child(root, state.first, state.second); + } + + try { + doc->write_to_file_formatted(dcp::filesystem::fix_long_path(file(ui_state_file)).string()); + } catch (...) {} +} + + +void +Film::read_ui_state() +{ + try { + cxml::Document xml("UI"); + xml.read_file(dcp::filesystem::fix_long_path(file(ui_state_file))); + for (auto node: xml.node_children()) { + if (!node->is_text()) { + _ui_state[node->name()] = node->content(); + } + } + } catch (...) {} +}