X-Git-Url: https://main.carlh.net/gitweb/?p=dcpomatic.git;a=blobdiff_plain;f=src%2Flib%2Ffilm.cc;h=9ffe09f6b811953e36e3fb6ea6b7a4e1fca38968;hp=1b0f140959cb5b5b09372a78f63b4cd234bd592c;hb=f06c5136e7d3cd0a8e1814763c7774859998efe4;hpb=9b98aa5abe15ae2499a11236798d02e29fd094d7 diff --git a/src/lib/film.cc b/src/lib/film.cc index 1b0f14095..ed2c5a372 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -1,24 +1,25 @@ /* - Copyright (C) 2012-2015 Carl Hetherington + Copyright (C) 2012-2020 Carl Hetherington - This program is free software; you can redistribute it and/or modify + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, + DCP-o-matic is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + along with DCP-o-matic. If not, see . */ /** @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. */ @@ -26,10 +27,12 @@ #include "job.h" #include "util.h" #include "job_manager.h" +#include "dcp_encoder.h" #include "transcode_job.h" #include "upload_job.h" #include "null_log.h" #include "file_log.h" +#include "dcpomatic_log.h" #include "exceptions.h" #include "examine_content_job.h" #include "config.h" @@ -37,29 +40,34 @@ #include "dcp_content_type.h" #include "ratio.h" #include "cross.h" -#include "safe_stringstream.h" #include "environment_info.h" -#include "raw_convert.h" #include "audio_processor.h" -#include "md5_digester.h" +#include "digester.h" #include "compose.hpp" #include "screen.h" #include "audio_content.h" #include "video_content.h" -#include "subtitle_content.h" +#include "text_content.h" #include "ffmpeg_content.h" #include "dcp_content.h" #include "screen_kdm.h" +#include "cinema.h" +#include "change_signaller.h" +#include "check_content_change_job.h" #include #include #include #include #include #include +#include +#include +#include #include #include #include #include +#include #include #include #include @@ -81,14 +89,20 @@ using std::cout; using std::list; using std::set; using std::runtime_error; +using std::copy; +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 boost::optional; using boost::is_any_of; +using dcp::raw_convert; +using namespace dcpomatic; -#define LOG_GENERAL(...) log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL); -#define LOG_GENERAL_NC(...) log()->log (__VA_ARGS__, LogEntry::TYPE_GENERAL); +string const Film::metadata_file = "metadata.xml"; /* 5 -> 6 * AudioMapping XML changed. @@ -106,15 +120,25 @@ using boost::is_any_of; * * 32 -> 33 * Changed to in FFmpegSubtitleStream + * 33 -> 34 + * Content only contains audio/subtitle-related tags if those things + * are present. + * 34 -> 35 + * 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. */ -int const Film::current_state_version = 33; +int const Film::current_state_version = 37; /** Construct a Film object in a given directory. * * @param dir Film directory. */ -Film::Film (boost::filesystem::path dir, bool log) +Film::Film (optional dir) : _playlist (new Playlist) , _use_isdcf_name (true) , _dcp_content_type (Config::instance()->default_dcp_content_type ()) @@ -122,47 +146,56 @@ Film::Film (boost::filesystem::path dir, bool log) , _resolution (RESOLUTION_2K) , _signed (true) , _encrypted (false) + , _context_id (dcp::make_uuid ()) , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ()) , _isdcf_metadata (Config::instance()->default_isdcf_metadata ()) , _video_frame_rate (24) - , _audio_channels (6) + , _audio_channels (Config::instance()->default_dcp_audio_channels ()) , _three_d (false) , _sequence (true) , _interop (Config::instance()->default_interop ()) , _audio_processor (0) , _reel_type (REELTYPE_SINGLE) , _reel_length (2000000000) - , _upload_after_make_dcp (false) + , _upload_after_make_dcp (Config::instance()->default_upload_after_make_dcp()) + , _reencode_j2k (false) + , _user_explicit_video_frame_rate (false) , _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)); + _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)); - /* 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? - */ + 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? + */ - boost::filesystem::path p (boost::filesystem::system_complete (dir)); - 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() == "..") { + 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 == "..") { + 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 { - result = result.parent_path (); } - } else if (*i != ".") { - result /= *i; } + + set_directory (result.make_preferred ()); } - set_directory (result.make_preferred ()); - if (log) { + if (_directory) { _log.reset (new FileLog (file ("log"))); } else { _log.reset (new NullLog); @@ -187,32 +220,30 @@ Film::video_identifier () const { DCPOMATIC_ASSERT (container ()); - SafeStringStream s; - s.imbue (std::locale::classic ()); - - s << container()->id() - << "_" << resolution_to_string (_resolution) - << "_" << _playlist->video_identifier() - << "_" << _video_frame_rate - << "_" << j2k_bandwidth(); + string s = container()->id() + + "_" + resolution_to_string (_resolution) + + "_" + _playlist->video_identifier() + + "_" + raw_convert(_video_frame_rate) + + "_" + raw_convert(j2k_bandwidth()); if (encrypted ()) { - s << "_E"; + /* This is insecure but hey, the key is in plaintext in metadata.xml */ + s += "_E" + _key.hex(); } else { - s << "_P"; + s += "_P"; } if (_interop) { - s << "_I"; + s += "_I"; } else { - s << "_S"; + s += "_S"; } if (_three_d) { - s << "_3D"; + s += "_3D"; } - return s.str (); + return s; } /** @return The file to write video frame info to */ @@ -242,15 +273,14 @@ Film::audio_analysis_path (shared_ptr playlist) const { boost::filesystem::path p = dir ("analysis"); - MD5Digester digester; + Digester digester; BOOST_FOREACH (shared_ptr i, playlist->content ()) { - shared_ptr ac = dynamic_pointer_cast (i); - if (!ac) { + if (!i->audio) { continue; } - digester.add (ac->digest ()); - digester.add (ac->audio_mapping().digest ()); + digester.add (i->digest ()); + digester.add (i->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 @@ -258,7 +288,7 @@ Film::audio_analysis_path (shared_ptr playlist) const analysis at the plotting stage rather than having to recompute it. */ - digester.add (ac->audio_gain ()); + digester.add (i->audio->gain ()); } } @@ -266,16 +296,54 @@ 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 */ +/** 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. + */ void -Film::make_dcp () +Film::make_dcp (bool gui, bool check) { if (dcp_name().find ("/") != string::npos) { - throw BadSettingError (_("name"), _("cannot contain slashes")); + throw BadSettingError (_("name"), _("Cannot contain slashes")); + } + + if (container() == 0) { + throw MissingSettingError (_("container")); + } + + if (content().empty()) { + throw runtime_error (_("You must add some content to the DCP before creating it")); + } + + if (length() == DCPTime()) { + throw runtime_error (_("The DCP is empty, perhaps because all the content has zero length.")); + } + + if (dcp_content_type() == 0) { + throw MissingSettingError (_("content type")); + } + + if (name().empty()) { + set_name ("DCP"); + } + + 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")); + } + if (dcp && dcp->needs_assets()) { + throw runtime_error (_("Some of your content needs an OV")); + } } set_isdcf_date_today (); @@ -291,27 +359,18 @@ Film::make_dcp () if (Config::instance()->only_servers_encode ()) { LOG_GENERAL_NC ("0 threads: ONLY SERVERS SET TO ENCODE"); } else { - LOG_GENERAL ("%1 threads", Config::instance()->num_local_encoding_threads()); + LOG_GENERAL ("%1 threads", Config::instance()->master_encoding_threads()); } LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth()); - if (container() == 0) { - throw MissingSettingError (_("container")); - } - - if (content().empty()) { - throw runtime_error (_("You must add some content to the DCP before creating it")); - } - - if (dcp_content_type() == 0) { - throw MissingSettingError (_("content type")); - } - - if (name().empty()) { - throw MissingSettingError (_("name")); + shared_ptr tj (new TranscodeJob (shared_from_this())); + tj->set_encoder (shared_ptr (new DCPEncoder (shared_from_this(), tj))); + if (check) { + shared_ptr cc (new CheckContentChangeJob(shared_from_this(), tj, gui)); + JobManager::instance()->add (cc); + } else { + JobManager::instance()->add (tj); } - - JobManager::instance()->add (shared_ptr (new TranscodeJob (shared_from_this()))); } /** Start a job to send our DCP to the configured TMS */ @@ -323,7 +382,7 @@ Film::send_dcp_to_tms () } shared_ptr -Film::metadata () const +Film::metadata (bool with_content_paths) const { shared_ptr doc (new xmlpp::Document); xmlpp::Element* root = doc->create_root_node ("Metadata"); @@ -352,43 +411,90 @@ Film::metadata () const root->add_child("Signed")->add_child_text (_signed ? "1" : "0"); root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0"); root->add_child("Key")->add_child_text (_key.hex ()); + root->add_child("ContextID")->add_child_text (_context_id); if (_audio_processor) { root->add_child("AudioProcessor")->add_child_text (_audio_processor->id ()); } - root->add_child("ReelType")->add_child_text (raw_convert (_reel_type)); + 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")); + 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"); + 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) { + i.as_xml (root->add_child("Rating")); + } + root->add_child("ContentVersion")->add_child_text(_content_version); + _playlist->as_xml (root->add_child ("Playlist"), with_content_paths); return doc; } +void +Film::write_metadata (boost::filesystem::path path) const +{ + shared_ptr doc = metadata (); + doc->write_to_file_formatted (path.string()); +} + /** Write state to our `metadata' file */ void Film::write_metadata () const { - boost::filesystem::create_directories (directory ()); + DCPOMATIC_ASSERT (directory()); + boost::filesystem::create_directories (directory().get()); shared_ptr doc = metadata (); - doc->write_to_file_formatted (file("metadata.xml").string ()); + doc->write_to_file_formatted (file(metadata_file).string ()); _dirty = false; } +/** Write a template from this film */ +void +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 ()); +} + /** Read state from our metadata file. * @return Notes about things that the user should know about, or empty. */ list -Film::read_metadata () +Film::read_metadata (optional 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 (!path) { + if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file (metadata_file))) { + throw runtime_error (_("This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version. You will need to create a new Film, re-add your content and set it up again. Sorry!")); + } + + path = file (metadata_file); + } + + if (!boost::filesystem::exists(*path)) { + throw FileNotFoundError(*path); } cxml::Document f ("Metadata"); - f.read_file (file ("metadata.xml")); + f.read_file (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!")); + } 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); + if (!boost::filesystem::is_regular_file(older)) { + try { + boost::filesystem::copy_file(*path, older); + } catch (...) { + /* Never mind; at least we tried */ + } + } } _name = f.string_child ("Name"); @@ -402,6 +508,7 @@ Film::read_metadata () _isdcf_date = boost::gregorian::from_undelimited_string (f.string_child ("DCIDate")); } + { optional c = f.optional_string_child ("DCPContentType"); if (c) { @@ -440,6 +547,7 @@ Film::read_metadata () _three_d = f.bool_child ("ThreeD"); _interop = f.bool_child ("Interop"); _key = dcp::Key (f.string_child ("Key")); + _context_id = f.optional_string_child("ContextID").get_value_or (dcp::make_uuid ()); if (f.optional_string_child ("AudioProcessor")) { _audio_processor = AudioProcessor::from_id (f.string_child ("AudioProcessor")); @@ -447,32 +555,57 @@ Film::read_metadata () _audio_processor = 0; } + if (_audio_processor && !Config::instance()->show_experimental_audio_processors()) { + list 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); + _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")) { + _markers[dcp::marker_from_string(i->string_attribute("Type"))] = DCPTime(dcp::raw_convert(i->content())); + } + + BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children("Rating")) { + _ratings.push_back (dcp::Rating(i)); + } + + _content_version = f.optional_string_child("ContentVersion").get_value_or(""); 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 */ - set_backtrace_file (file ("backtrace.txt")); + if (_directory) { + set_backtrace_file (file ("backtrace.txt")); + } _dirty = false; return notes; } /** Given a directory name, return its full path within the Film's directory. - * The directory (and its parents) will be created if they do not exist. + * @param d directory name within the Film's directory. + * @param create true to create the directory (and its parents) if they do not exist. */ boost::filesystem::path -Film::dir (boost::filesystem::path d) const +Film::dir (boost::filesystem::path d, bool create) const { + DCPOMATIC_ASSERT (_directory); + boost::filesystem::path p; - p /= _directory; + p /= _directory.get(); p /= d; - boost::filesystem::create_directories (p); + if (create) { + boost::filesystem::create_directories (p); + } return p; } @@ -483,8 +616,10 @@ Film::dir (boost::filesystem::path d) const boost::filesystem::path Film::file (boost::filesystem::path f) const { + DCPOMATIC_ASSERT (_directory); + boost::filesystem::path p; - p /= _directory; + p /= _directory.get(); p /= f; boost::filesystem::create_directories (p.parent_path ()); @@ -492,11 +627,36 @@ Film::file (boost::filesystem::path f) const return p; } +list +Film::mapped_audio_channels () const +{ + list mapped; + + if (audio_processor ()) { + /* Processors are mapped 1:1 to DCP outputs so we can work out mappings from there */ + for (int i = 0; i < audio_processor()->out_channels(); ++i) { + mapped.push_back (i); + } + } else { + BOOST_FOREACH (shared_ptr i, content ()) { + if (i->audio) { + list c = i->audio->mapping().mapped_output_channels (); + copy (c.begin(), c.end(), back_inserter (mapped)); + } + } + + mapped.sort (); + mapped.unique (); + } + + return mapped; +} + /** @return a ISDCF-compliant name for a DCP of this film */ string Film::isdcf_name (bool if_created_now) const { - SafeStringStream d; + string d; string raw_name = name (); @@ -539,202 +699,201 @@ Film::isdcf_name (bool if_created_now) const fixed_name = fixed_name.substr (0, 14); } - d << fixed_name; + d += fixed_name; if (dcp_content_type()) { - d << "_" << dcp_content_type()->isdcf_name(); - d << "-" << isdcf_metadata().content_version; + d += "_" + dcp_content_type()->isdcf_name(); + d += "-" + raw_convert(isdcf_metadata().content_version); } ISDCFMetadata const dm = isdcf_metadata (); if (dm.temp_version) { - d << "-Temp"; + d += "-Temp"; } if (dm.pre_release) { - d << "-Pre"; + d += "-Pre"; } if (dm.red_band) { - d << "-RedBand"; + d += "-RedBand"; } if (!dm.chain.empty ()) { - d << "-" << dm.chain; + d += "-" + dm.chain; } if (three_d ()) { - d << "-3D"; + d += "-3D"; } if (dm.two_d_version_of_three_d) { - d << "-2D"; + d += "-2D"; } if (!dm.mastered_luminance.empty ()) { - d << "-" << dm.mastered_luminance; + d += "-" + dm.mastered_luminance; } if (video_frame_rate() != 24) { - d << "-" << video_frame_rate(); + d += "-" + raw_convert(video_frame_rate()); } if (container()) { - d << "_" << container()->isdcf_name(); + d += "_" + container()->isdcf_name(); } /* XXX: this uses the first bit of content only */ - /* The standard says we don't do this for trailers, for some strange reason */ + /* Interior aspect ratio. The standard says we don't do this for trailers, for some strange reason */ if (dcp_content_type() && dcp_content_type()->libdcp_kind() != dcp::TRAILER) { Ratio const * content_ratio = 0; BOOST_FOREACH (shared_ptr i, content ()) { - shared_ptr vc = dynamic_pointer_cast (i); - if (vc) { + if (i->video) { /* Here's the first piece of video content */ - if (vc->scale().ratio ()) { - content_ratio = vc->scale().ratio (); + if (i->video->scale().ratio ()) { + content_ratio = i->video->scale().ratio (); } else { - content_ratio = Ratio::from_ratio (vc->video_size().ratio ()); + content_ratio = Ratio::from_ratio (i->video->size().ratio ()); } break; } } if (content_ratio && content_ratio != container()) { - d << "-" << content_ratio->isdcf_name(); + /* This needs to be the numeric version of the ratio, and ::id() is close enough */ + d += "-" + content_ratio->id(); } } if (!dm.audio_language.empty ()) { - d << "_" << dm.audio_language; - if (!dm.subtitle_language.empty()) { - - bool burnt_in = false; - BOOST_FOREACH (shared_ptr i, content ()) { - shared_ptr sc = dynamic_pointer_cast (i); - if (!sc) { - continue; - } + d += "_" + dm.audio_language; + + /* I'm not clear on the precise details of the convention for CCAP labelling; + for now I'm just appending -CCAP if we have any closed captions. + */ - if (sc->use_subtitles() && sc->burn_subtitles()) { - burnt_in = true; + 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; + } + } else if (j->type() == TEXT_CLOSED_CAPTION && j->use()) { + ccap = true; } } + } - string language = dm.subtitle_language; - if (burnt_in) { - transform (language.begin(), language.end(), language.begin(), ::tolower); + 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); } else { - transform (language.begin(), language.end(), language.begin(), ::toupper); + transform (subtitle_language->begin(), subtitle_language->end(), subtitle_language->begin(), ::toupper); } - d << "-" << language; + d += "-" + *subtitle_language; + if (ccap) { + d += "-CCAP"; + } } else { - d << "-XX"; + /* No subtitles */ + d += "-XX"; } } if (!dm.territory.empty ()) { - d << "_" << dm.territory; + d += "_" + dm.territory; if (dm.rating.empty ()) { - d << "-NR"; + d += "-NR"; } else { - d << "-" << dm.rating; + d += "-" + dm.rating; } } - /* Find all mapped channels */ - - int non_lfe = 0; - int lfe = 0; - - if (audio_processor ()) { - /* Processors are mapped 1:1 to DCP outputs so we can guess the number of LFE/ - non-LFE from the channel counts. - */ - non_lfe = audio_processor()->out_channels (); - if (non_lfe >= 4) { - --non_lfe; - ++lfe; - } - } else { - list mapped; - BOOST_FOREACH (shared_ptr i, content ()) { - shared_ptr ac = dynamic_pointer_cast (i); - if (ac) { - list c = ac->audio_mapping().mapped_output_channels (); - copy (c.begin(), c.end(), back_inserter (mapped)); - } - } - - mapped.sort (); - mapped.unique (); + /* Count mapped audio channels */ - /* Count them */ - - for (list::const_iterator i = mapped.begin(); i != mapped.end(); ++i) { - if (*i >= audio_channels()) { - /* This channel is mapped but is not included in the DCP */ - continue; - } + list mapped = mapped_audio_channels (); - if (static_cast (*i) == dcp::LFE) { - ++lfe; - } else { - ++non_lfe; - } - } + pair ch = audio_channel_types (mapped, audio_channels()); + if (!ch.first && !ch.second) { + d += "_MOS"; + } else if (ch.first) { + d += String::compose("_%1%2", ch.first, ch.second); } - if (non_lfe) { - d << "_" << non_lfe << lfe; + if (audio_channels() > static_cast(dcp::HI) && find(mapped.begin(), mapped.end(), dcp::HI) != mapped.end()) { + d += "-HI"; + } + if (audio_channels() > static_cast(dcp::VI) && find(mapped.begin(), mapped.end(), dcp::VI) != mapped.end()) { + d += "-VI"; } - /* XXX: HI/VI */ - - d << "_" << resolution_to_string (_resolution); + d += "_" + resolution_to_string (_resolution); if (!dm.studio.empty ()) { - d << "_" << dm.studio; + d += "_" + dm.studio; } if (if_created_now) { - d << "_" << boost::gregorian::to_iso_string (boost::gregorian::day_clock::local_day ()); + d += "_" + boost::gregorian::to_iso_string (boost::gregorian::day_clock::local_day ()); } else { - d << "_" << boost::gregorian::to_iso_string (_isdcf_date); + d += "_" + boost::gregorian::to_iso_string (_isdcf_date); } if (!dm.facility.empty ()) { - d << "_" << dm.facility; + d += "_" + dm.facility; } if (_interop) { - d << "_IOP"; + d += "_IOP"; } else { - d << "_SMPTE"; + d += "_SMPTE"; } if (three_d ()) { - d << "-3D"; + d += "-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())) { + if (!dc) { + continue; + } + + bool any_text = false; + for (int i = 0; i < TEXT_COUNT; ++i) { + if (dc->reference_text(static_cast(i))) { + any_text = true; + } + } + if (dc->reference_video() || dc->reference_audio() || any_text) { vf = true; } } if (vf) { - d << "_VF"; + d += "_VF"; } else { - d << "_OV"; + d += "_OV"; } - return d.str (); + return d; } /** @return name to give the DCP */ @@ -743,24 +902,10 @@ Film::dcp_name (bool if_created_now) const { string unfiltered; if (use_isdcf_name()) { - unfiltered = isdcf_name (if_created_now); - } else { - unfiltered = name (); - } - - /* Filter out `bad' characters which cause problems with some systems. - There's no apparent list of what really is allowed, so this is a guess. - */ - - string filtered; - string const allowed = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"; - for (size_t i = 0; i < unfiltered.size(); ++i) { - if (allowed.find (unfiltered[i]) != string::npos) { - filtered += unfiltered[i]; - } + return careful_string_filter (isdcf_name (if_created_now)); } - return filtered; + return careful_string_filter (name ()); } void @@ -773,133 +918,158 @@ Film::set_directory (boost::filesystem::path d) void Film::set_name (string n) { + ChangeSignaller ch (this, NAME); _name = n; - signal_changed (NAME); } void Film::set_use_isdcf_name (bool u) { + ChangeSignaller ch (this, USE_ISDCF_NAME); _use_isdcf_name = u; - signal_changed (USE_ISDCF_NAME); } void Film::set_dcp_content_type (DCPContentType const * t) { + ChangeSignaller ch (this, DCP_CONTENT_TYPE); _dcp_content_type = t; - signal_changed (DCP_CONTENT_TYPE); } void Film::set_container (Ratio const * c) { + ChangeSignaller ch (this, CONTAINER); _container = c; - signal_changed (CONTAINER); } void Film::set_resolution (Resolution r) { + ChangeSignaller ch (this, RESOLUTION); _resolution = r; - signal_changed (RESOLUTION); } void Film::set_j2k_bandwidth (int b) { + ChangeSignaller ch (this, J2K_BANDWIDTH); _j2k_bandwidth = b; - signal_changed (J2K_BANDWIDTH); } void Film::set_isdcf_metadata (ISDCFMetadata m) { + ChangeSignaller ch (this, ISDCF_METADATA); _isdcf_metadata = m; - signal_changed (ISDCF_METADATA); } +/** @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) { + ChangeSignaller ch (this, 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) { + ChangeSignaller ch (this, AUDIO_CHANNELS); _audio_channels = c; - signal_changed (AUDIO_CHANNELS); } void Film::set_three_d (bool t) { + ChangeSignaller ch (this, THREE_D); _three_d = t; - signal_changed (THREE_D); if (_three_d && _isdcf_metadata.two_d_version_of_three_d) { + ChangeSignaller ch (this, ISDCF_METADATA); _isdcf_metadata.two_d_version_of_three_d = false; - signal_changed (ISDCF_METADATA); } } void Film::set_interop (bool i) { + ChangeSignaller ch (this, INTEROP); _interop = i; - signal_changed (INTEROP); } void Film::set_audio_processor (AudioProcessor const * processor) { + ChangeSignaller ch1 (this, AUDIO_PROCESSOR); + ChangeSignaller ch2 (this, AUDIO_CHANNELS); _audio_processor = processor; - signal_changed (AUDIO_PROCESSOR); - signal_changed (AUDIO_CHANNELS); } void Film::set_reel_type (ReelType t) { + ChangeSignaller ch (this, REEL_TYPE); _reel_type = t; - signal_changed (REEL_TYPE); } /** @param r Desired reel length in bytes */ void Film::set_reel_length (int64_t r) { + ChangeSignaller ch (this, REEL_LENGTH); _reel_length = r; - signal_changed (REEL_LENGTH); } void Film::set_upload_after_make_dcp (bool u) { + ChangeSignaller ch (this, UPLOAD_AFTER_MAKE_DCP); _upload_after_make_dcp = u; - signal_changed (UPLOAD_AFTER_MAKE_DCP); } void -Film::signal_changed (Property p) +Film::set_reencode_j2k (bool r) { - _dirty = true; + ChangeSignaller ch (this, REENCODE_J2K); + _reencode_j2k = r; +} - switch (p) { - case Film::CONTENT: - set_video_frame_rate (_playlist->best_dcp_frame_rate ()); - break; - case Film::VIDEO_FRAME_RATE: - case Film::SEQUENCE: - _playlist->maybe_sequence (); - break; - default: - break; - } +void +Film::signal_change (ChangeType type, int p) +{ + signal_change (type, static_cast(p)); +} + +void +Film::signal_change (ChangeType type, Property p) +{ + if (type == CHANGE_TYPE_DONE) { + _dirty = true; - emit (boost::bind (boost::ref (Changed), p)); + if (p == Film::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 == Film::VIDEO_FRAME_RATE || p == Film::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 @@ -915,33 +1085,46 @@ Film::j2c_path (int reel, Frame frame, Eyes eyes, bool tmp) const p /= "j2c"; p /= video_identifier (); - SafeStringStream s; - s.width (8); - s << setfill('0') << reel << "_" << frame; + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%08d_%08" PRId64, reel, frame); + string s (buffer); if (eyes == EYES_LEFT) { - s << ".L"; + s += ".L"; } else if (eyes == EYES_RIGHT) { - s << ".R"; + s += ".R"; } - s << ".j2c"; + s += ".j2c"; if (tmp) { - s << ".tmp"; + s += ".tmp"; } - p /= s.str(); + p /= s; return file (p); } -/** Find all the DCPs in our directory that can be dcp::DCP::read() and return details of their CPLs */ +static +bool +cpl_summary_compare (CPLSummary const & a, CPLSummary const & b) +{ + return a.last_write_time > b.last_write_time; +} + +/** 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 (); + } + vector out; - boost::filesystem::path const dir = directory (); + boost::filesystem::path const dir = directory().get(); for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator(dir); i != boost::filesystem::directory_iterator(); ++i) { if ( boost::filesystem::is_directory (*i) && @@ -949,44 +1132,37 @@ Film::cpls () const ) { try { - dcp::DCP dcp (*i); - dcp.read (); - out.push_back ( - CPLSummary ( - i->path().leaf().string(), - dcp.cpls().front()->id(), - dcp.cpls().front()->annotation_text(), - dcp.cpls().front()->file() - ) - ); + out.push_back (CPLSummary(*i)); } catch (...) { } } } + sort (out.begin(), out.end(), cpl_summary_compare); + return out; } void Film::set_signed (bool s) { + ChangeSignaller ch (this, SIGNED); _signed = s; - signal_changed (SIGNED); } void Film::set_encrypted (bool e) { + ChangeSignaller ch (this, ENCRYPTED); _encrypted = e; - signal_changed (ENCRYPTED); } void Film::set_key (dcp::Key key) { + ChangeSignaller ch (this, KEY); _key = key; - signal_changed (KEY); } ContentList @@ -995,31 +1171,27 @@ Film::content () const return _playlist->content (); } +/** @param content Content to add. + * @param disable_audio_analysis true to never do automatic audio analysis, even if it is enabled in configuration. + */ void -Film::examine_content (shared_ptr c) -{ - shared_ptr j (new ExamineContentJob (shared_from_this(), c)); - JobManager::instance()->add (j); -} - -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.empty ()) { - 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)); + shared_ptr j (new ExamineContentJob (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 (); if (!job || !job->finished_ok ()) { @@ -1032,12 +1204,13 @@ Film::maybe_add_content (weak_ptr j, weak_ptr c) } add_content (content); - if (Config::instance()->automatic_audio_analysis() && dynamic_pointer_cast (content)) { + + if (Config::instance()->automatic_audio_analysis() && content->audio && !disable_audio_analysis) { shared_ptr playlist (new Playlist); - playlist->add (content); + 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); } @@ -1047,13 +1220,20 @@ void Film::add_content (shared_ptr c) { /* Add {video,subtitle} content after any existing {video,subtitle} content */ - if (dynamic_pointer_cast (c)) { - c->set_position (_playlist->video_end ()); - } else if (dynamic_pointer_cast (c)) { - c->set_position (_playlist->subtitle_end ()); + if (c->video) { + 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()) { + c->take_settings_from (i); + } } - _playlist->add (c); + _playlist->add (shared_from_this(), c); } void @@ -1065,26 +1245,33 @@ Film::remove_content (shared_ptr c) 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_dcp_frame_rate (); + /* Don't default to anything above 30fps (make the user select that explicitly) */ + int best = _playlist->best_video_frame_rate (); + if (best > 30) { + best /= 2; + } + return best; } FrameRateChange @@ -1094,51 +1281,96 @@ 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) { + signal_change (type, Film::CONTENT); + } else if (p == AudioContentProperty::STREAMS) { + signal_change (type, Film::NAME); + } - if (p == VideoContentProperty::VIDEO_FRAME_RATE) { - set_video_frame_rate (_playlist->best_dcp_frame_rate ()); - } else if (p == AudioContentProperty::AUDIO_STREAMS) { - signal_changed (NAME); + 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); } +} - emit (boost::bind (boost::ref (ContentChanged), c, p, frequent)); +void +Film::playlist_length_change () +{ + LengthChange (); } void -Film::playlist_changed () +Film::playlist_change (ChangeType type) { - signal_changed (CONTENT); - signal_changed (NAME); + signal_change (type, CONTENT); + signal_change (type, NAME); + + if (type == CHANGE_TYPE_DONE) { + check_settings_consistency (); + } +} + +/** 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 () +{ + bool change_made = false; + BOOST_FOREACH (shared_ptr i, content()) { + shared_ptr 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 (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.")); + } } void Film::playlist_order_changed () { - signal_changed (CONTENT_ORDER); + /* XXX: missing PENDING */ + signal_change (CHANGE_TYPE_DONE, CONTENT_ORDER); } int Film::audio_frame_rate () const { - BOOST_FOREACH (shared_ptr i, content ()) { - shared_ptr a = dynamic_pointer_cast (i); - if (a && a->has_rate_above_48k ()) { - return 96000; - } - } - + /* It seems that nobody makes 96kHz DCPs at the moment, so let's avoid them. + See #1436. + */ return 48000; } void Film::set_sequence (bool s) { + if (s == _sequence) { + return; + } + + ChangeSignaller cc (this, SEQUENCE); _sequence = s; _playlist->set_sequence (s); - signal_changed (SEQUENCE); } /** @return Size of the largest possible image in whatever resolution we are using */ @@ -1163,41 +1395,114 @@ Film::frame_size () const return fit_ratio_within (container()->ratio(), full_frame ()); } +/** @param recipient KDM recipient certificate. + * @param trusted_devices Certificate thumbprints of other trusted devices (can be empty). + * @param cpl_file CPL filename. + * @param from KDM from time expressed as a local time with an offset from UTC. + * @param until KDM to time expressed as a local time with an offset from UTC. + * @param formulation KDM formulation to use. + * @param disable_forensic_marking_picture true to disable forensic marking of picture. + * @param disable_forensic_marking_audio if not set, don't disable forensic marking of audio. If set to 0, + * disable all forensic marking; if set above 0, disable forensic marking above that channel. + */ dcp::EncryptedKDM Film::make_kdm ( dcp::Certificate recipient, - vector trusted_devices, + vector trusted_devices, boost::filesystem::path cpl_file, dcp::LocalTime from, dcp::LocalTime until, - dcp::Formulation formulation + dcp::Formulation formulation, + bool disable_forensic_marking_picture, + optional disable_forensic_marking_audio ) const { + if (!_encrypted) { + 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 (); if (!signer->valid ()) { throw InvalidSignerError (); } - return dcp::DecryptedKDM ( - cpl, key(), from, until, "DCP-o-matic", cpl->content_title_text(), dcp::LocalTime().as_string() - ).encrypt (signer, recipient, trusted_devices, formulation); -} + /* Find keys that have been added to imported, encrypted DCP content */ + list imported_keys; + BOOST_FOREACH (shared_ptr i, content()) { + shared_ptr 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 (); + copy (keys.begin(), keys.end(), back_inserter (imported_keys)); + } + } + + map, dcp::Key> keys; + + BOOST_FOREACH(shared_ptr i, cpl->reel_mxfs()) { + if (!i->key_id()) { + continue; + } -list + /* Get any imported key for this ID */ + bool done = false; + BOOST_FOREACH (dcp::DecryptedKDMKey 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(); + done = true; + } + } + + if (!done) { + /* No imported key; it must be an asset that we encrypted */ + LOG_GENERAL ("Using our own key for %1", i->key_id().get()); + keys[i] = key(); + } + } + + return dcp::DecryptedKDM ( + cpl->id(), keys, from, until, cpl->content_title_text(), cpl->content_title_text(), dcp::LocalTime().as_string() + ).encrypt (signer, recipient, trusted_devices, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio); +} + +/** @param screens Screens to make KDMs for. + * @param cpl_file Path to CPL to make KDMs for. + * @param from KDM from time expressed as a local time in the time zone of the Screen's Cinema. + * @param until KDM to time expressed as a local time in the time zone of the Screen's Cinema. + * @param formulation KDM formulation to use. + * @param disable_forensic_marking_picture true to disable forensic marking of picture. + * @param disable_forensic_marking_audio if not set, don't disable forensic marking of audio. If set to 0, + * disable all forensic marking; if set above 0, disable forensic marking above that channel. + */ +list > Film::make_kdms ( list > screens, - boost::filesystem::path dcp, - dcp::LocalTime from, - dcp::LocalTime until, - dcp::Formulation formulation + 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; + list > kdms; BOOST_FOREACH (shared_ptr i, screens) { if (i->recipient) { - kdms.push_back (ScreenKDM (i, make_kdm (i->recipient.get(), i->trusted_devices, dcp, from, until, formulation))); + 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))); } } @@ -1210,13 +1515,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(), j2k_bandwidth(), 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. */ @@ -1253,11 +1558,9 @@ Film::subtitle_language () const { set languages; - ContentList cl = content (); - BOOST_FOREACH (shared_ptr& c, cl) { - shared_ptr sc = dynamic_pointer_cast (c); - if (sc) { - languages.insert (sc->subtitle_language ()); + BOOST_FOREACH (shared_ptr i, content()) { + BOOST_FOREACH (shared_ptr j, i->text) { + languages.insert (j->language ()); } } @@ -1273,29 +1576,6 @@ Film::subtitle_language () const 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 and the number of input channels. - */ -void -Film::make_audio_mapping_default (AudioMapping& mapping) const -{ - if (audio_processor ()) { - audio_processor()->make_audio_mapping_default (mapping); - } else { - mapping.make_zero (); - if (mapping.input_channels() == 1) { - /* Mono -> 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. */ @@ -1309,30 +1589,18 @@ Film::audio_output_names () const DCPOMATIC_ASSERT (MAX_DCP_AUDIO_CHANNELS == 16); vector n; - n.push_back (_("L")); - n.push_back (_("R")); - n.push_back (_("C")); - n.push_back (_("Lfe")); - n.push_back (_("Ls")); - n.push_back (_("Rs")); - n.push_back (_("HI")); - n.push_back (_("VI")); - n.push_back (_("Lc")); - n.push_back (_("Rc")); - n.push_back (_("BsL")); - n.push_back (_("BsR")); - n.push_back (_("DBP")); - n.push_back (_("DBS")); - n.push_back (""); - n.push_back (""); - - return vector (n.begin(), n.begin() + audio_channels ()); + + for (int i = 0; i < audio_channels(); ++i) { + n.push_back (short_audio_channel_name (i)); + } + + return n; } void Film::repeat_content (ContentList c, int n) { - _playlist->repeat (c, n); + _playlist->repeat (shared_from_this(), c, n); } void @@ -1351,7 +1619,7 @@ list Film::reels () const { list p; - DCPTime const len = length().round_up (video_frame_rate ()); + DCPTime const len = length(); switch (reel_type ()) { case REELTYPE_SINGLE: @@ -1359,39 +1627,43 @@ Film::reels () const break; case REELTYPE_BY_VIDEO_CONTENT: { - optional last_split; - shared_ptr last_video; - ContentList cl = content (); - BOOST_FOREACH (shared_ptr c, content ()) { - shared_ptr v = dynamic_pointer_cast (c); - if (v) { - BOOST_FOREACH (DCPTime t, v->reel_split_points()) { - if (last_split) { - p.push_back (DCPTimePeriod (last_split.get(), t)); - } - last_split = t; + /* Collect all reel boundaries */ + list split_points; + split_points.push_back (DCPTime()); + split_points.push_back (len); + BOOST_FOREACH (shared_ptr c, content()) { + if (c->video) { + BOOST_FOREACH (DCPTime t, c->reel_split_points(shared_from_this())) { + split_points.push_back (t); } - last_video = v; + 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)); - } - - 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; + BOOST_FOREACH (DCPTime 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)); @@ -1403,3 +1675,163 @@ Film::reels () const return p; } + +/** @param period A period within the DCP + * @return Name of the content which most contributes to the given period. + */ +string +Film::content_summary (DCPTimePeriod period) const +{ + return _playlist->content_summary (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)); + _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; + _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; + _three_d = _template_film->_three_d; + _interop = _template_film->_interop; + _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; +} + +pair +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 +{ + BOOST_FOREACH (shared_ptr i, _playlist->content()) { + shared_ptr d = dynamic_pointer_cast(i); + if (d && d->reference_video()) { + return true; + } + } + + return false; +} + +bool +Film::references_dcp_audio () const +{ + BOOST_FOREACH (shared_ptr i, _playlist->content()) { + shared_ptr d = dynamic_pointer_cast(i); + if (d && d->reference_audio()) { + 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) { + /* 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()); + if (j->type() == TEXT_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) +{ + ChangeSignaller ch (this, MARKERS); + _markers[type] = time; +} + +void +Film::unset_marker (dcp::Marker type) +{ + ChangeSignaller ch (this, MARKERS); + _markers.erase (type); +} + +void +Film::set_ratings (vector r) +{ + ChangeSignaller ch (this, RATINGS); + _ratings = r; +} + +void +Film::set_content_version (string v) +{ + ChangeSignaller ch (this, CONTENT_VERSION); + _content_version = v; +} + +optional +Film::marker (dcp::Marker type) const +{ + map::const_iterator i = _markers.find (type); + if (i == _markers.end()) { + return optional(); + } + return i->second; +} + +shared_ptr +Film::info_file_handle (DCPTimePeriod period, bool read) const +{ + return shared_ptr (new InfoFileHandle(_info_file_mutex, info_file(period), read)); +} + +InfoFileHandle::InfoFileHandle (boost::mutex& mutex, boost::filesystem::path file, bool read) + : _lock (mutex) + , _file (file) +{ + if (read) { + _handle = fopen_boost (file, "rb"); + if (!_handle) { + throw OpenFileError (file, errno, OpenFileError::READ); + } + } else { + bool const exists = boost::filesystem::exists (file); + if (exists) { + _handle = fopen_boost (file, "r+b"); + } else { + _handle = fopen_boost (file, "wb"); + } + + if (!_handle) { + throw OpenFileError (file, errno, exists ? OpenFileError::READ_WRITE : OpenFileError::WRITE); + } + } +} + +InfoFileHandle::~InfoFileHandle () +{ + fclose (_handle); +}