X-Git-Url: https://main.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Flib%2Ffilm.cc;h=79eb313ce44de451e37184094e92454e2b492085;hb=df29125d2f63b5c2e36b373462eac9684d8046e4;hp=5310ef71fcf8d37e0b27c6771f3747d6187178bf;hpb=92b6430402753a572c33d594ba0745a4e461edf4;p=dcpomatic.git diff --git a/src/lib/film.cc b/src/lib/film.cc index 5310ef71f..79eb313ce 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -1,19 +1,20 @@ /* - Copyright (C) 2012-2015 Carl Hetherington + Copyright (C) 2012-2017 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 . */ @@ -27,38 +28,46 @@ #include "util.h" #include "job_manager.h" #include "transcode_job.h" -#include "scp_dcp_job.h" -#include "log.h" +#include "upload_job.h" +#include "null_log.h" +#include "file_log.h" #include "exceptions.h" #include "examine_content_job.h" #include "config.h" #include "playlist.h" -#include "player.h" #include "dcp_content_type.h" #include "ratio.h" #include "cross.h" -#include "cinema.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 "ffmpeg_content.h" +#include "dcp_content.h" +#include "screen_kdm.h" +#include "cinema.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 @@ -66,31 +75,28 @@ #include "i18n.h" using std::string; -using std::multimap; using std::pair; -using std::map; using std::vector; using std::setfill; using std::min; using std::max; using std::make_pair; -using std::endl; using std::cout; using std::list; using std::set; +using std::runtime_error; +using std::copy; +using std::back_inserter; +using std::map; using boost::shared_ptr; using boost::weak_ptr; using boost::dynamic_pointer_cast; -using boost::to_upper_copy; -using boost::ends_with; -using boost::starts_with; using boost::optional; using boost::is_any_of; -using dcp::Size; -using dcp::Signer; +using dcp::raw_convert; -#define LOG_GENERAL(...) log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL); -#define LOG_GENERAL_NC(...) log()->log (__VA_ARGS__, Log::TYPE_GENERAL); +#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. @@ -105,15 +111,25 @@ using dcp::Signer; * * Bumped to 32 for 2.0 branch; some times are expressed in Times rather * than frames now. + * + * 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. */ -int const Film::current_state_version = 32; +int const Film::current_state_version = 36; /** 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 ()) @@ -121,54 +137,67 @@ 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_video (true) - , _interop (false) + , _sequence (true) + , _interop (Config::instance()->default_interop ()) , _audio_processor (0) + , _reel_type (REELTYPE_SINGLE) + , _reel_length (2000000000) + , _upload_after_make_dcp (false) , _state_version (current_state_version) , _dirty (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)); - /* Make state.directory a complete path without ..s (where possible) - (Code swiped from Adam Bowen on stackoverflow) - */ + 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 == "..") { + if (boost::filesystem::is_symlink (result) || 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); } - _playlist->set_sequence_video (_sequence_video); + _playlist->set_sequence (_sequence); } Film::~Film () { - for (list::const_iterator i = _job_connections.begin(); i != _job_connections.end(); ++i) { - i->disconnect (); + BOOST_FOREACH (boost::signals2::connection& i, _job_connections) { + i.disconnect (); + } + + BOOST_FOREACH (boost::signals2::connection& i, _audio_analysis_connections) { + i.disconnect (); } } @@ -177,41 +206,38 @@ 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"; + s += "_E"; } 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 */ boost::filesystem::path -Film::info_file () const +Film::info_file (DCPTimePeriod period) const { boost::filesystem::path p; p /= "info"; - p /= video_identifier (); + p /= video_identifier () + "_" + raw_convert (period.from.get()) + "_" + raw_convert (period.to.get()); return file (p); } @@ -222,9 +248,9 @@ Film::internal_video_asset_dir () const } boost::filesystem::path -Film::internal_video_asset_filename () const +Film::internal_video_asset_filename (DCPTimePeriod p) const { - return video_identifier() + ".mxf"; + return video_identifier() + "_" + raw_convert (p.from.get()) + "_" + raw_convert (p.to.get()) + ".mxf"; } boost::filesystem::path @@ -232,16 +258,23 @@ 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 (ac->audio_gain ()); + 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 + is because we can fake any gain change in a single-content + analysis at the plotting stage rather than having to + recompute it. + */ + digester.add (i->audio->gain ()); + } } if (audio_processor ()) { @@ -260,23 +293,12 @@ Film::make_dcp () throw BadSettingError (_("name"), _("cannot contain slashes")); } - set_isdcf_date_today (); - - environment_info (log ()); - - BOOST_FOREACH (shared_ptr i, content ()) { - LOG_GENERAL ("Content: %1", i->technical_summary()); - } - LOG_GENERAL ("DCP video rate %1 fps", video_frame_rate()); - LOG_GENERAL ("%1 threads", Config::instance()->num_local_encoding_threads()); - LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth()); - if (container() == 0) { throw MissingSettingError (_("container")); } if (content().empty()) { - throw StringError (_("You must add some content to the DCP before creating it")); + throw runtime_error (_("you must add some content to the DCP before creating it")); } if (dcp_content_type() == 0) { @@ -287,6 +309,36 @@ Film::make_dcp () throw MissingSettingError (_("name")); } + 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 (); + + 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()->num_local_encoding_threads()); + } + LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth()); + JobManager::instance()->add (shared_ptr (new TranscodeJob (shared_from_this()))); } @@ -294,12 +346,12 @@ Film::make_dcp () void Film::send_dcp_to_tms () { - shared_ptr j (new SCPDCPJob (shared_from_this())); + shared_ptr j (new UploadJob (shared_from_this())); JobManager::instance()->add (j); } 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"); @@ -323,15 +375,19 @@ Film::metadata () const 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("SequenceVideo")->add_child_text (_sequence_video ? "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); if (_audio_processor) { root->add_child("AudioProcessor")->add_child_text (_audio_processor->id ()); } - _playlist->as_xml (root->add_child ("Playlist")); + 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); return doc; } @@ -340,28 +396,42 @@ Film::metadata () const 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 ()); _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 StringError (_("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.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!")); + } + + path = file ("metadata.xml"); } 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 StringError (_("This film was created with a newer version of DCP-o-matic, and it cannot be loaded into this version. Sorry!")); + throw runtime_error (_("This film was created with a newer version of DCP-o-matic, and it cannot be loaded into this version. Sorry!")); } _name = f.string_child ("Name"); @@ -403,10 +473,17 @@ Film::read_metadata () } else if ((_audio_channels % 2) == 1) { _audio_channels++; } - _sequence_video = f.bool_child ("SequenceVideo"); + + if (f.optional_bool_child("SequenceVideo")) { + _sequence = f.bool_child("SequenceVideo"); + } else { + _sequence = f.bool_child("Sequence"); + } + _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")); @@ -414,12 +491,18 @@ Film::read_metadata () _audio_processor = 0; } + _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); + 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; @@ -431,8 +514,10 @@ Film::read_metadata () boost::filesystem::path Film::dir (boost::filesystem::path d) const { + DCPOMATIC_ASSERT (_directory); + boost::filesystem::path p; - p /= _directory; + p /= _directory.get(); p /= d; boost::filesystem::create_directories (p); @@ -446,8 +531,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 ()); @@ -455,17 +542,42 @@ 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 (); /* Split the raw name up into words */ vector words; - split (words, raw_name, is_any_of (" ")); + split (words, raw_name, is_any_of (" _-")); string fixed_name; @@ -502,172 +614,175 @@ 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(); } - ContentList cl = 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; - for (ContentList::iterator i = cl.begin(); i != cl.end(); ++i) { - shared_ptr vc = dynamic_pointer_cast (*i); - if (vc) { + BOOST_FOREACH (shared_ptr i, content ()) { + 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(); + d += "-" + content_ratio->isdcf_name(); } } if (!dm.audio_language.empty ()) { - d << "_" << dm.audio_language; + d += "_" + dm.audio_language; if (!dm.subtitle_language.empty()) { - d << "-" << dm.subtitle_language; + + bool burnt_in = true; + BOOST_FOREACH (shared_ptr i, content ()) { + if (!i->subtitle) { + continue; + } + + if (i->subtitle->use() && !i->subtitle->burn()) { + burnt_in = false; + } + } + + 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); + } + + d += "-" + language; } else { - d << "-XX"; + d += "-XX"; } } if (!dm.territory.empty ()) { - d << "_" << dm.territory; - if (!dm.rating.empty ()) { - d << "-" << dm.rating; + d += "_" + dm.territory; + if (dm.rating.empty ()) { + d += "-NR"; + } else { + d += "-" + dm.rating; } } - /* Find all mapped channels */ + /* Count mapped audio 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; - for (ContentList::const_iterator i = cl.begin(); i != cl.end(); ++i) { - 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)); - } + BOOST_FOREACH (int i, mapped_audio_channels ()) { + if (i >= audio_channels()) { + /* This channel is mapped but is not included in the DCP */ + continue; } - mapped.sort (); - mapped.unique (); - - /* 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; - } - - if (static_cast (*i) == dcp::LFE) { - ++lfe; - } else { - ++non_lfe; - } + if (static_cast (i) == dcp::LFE) { + ++lfe; + } else { + ++non_lfe; } } if (non_lfe) { - d << "_" << non_lfe << lfe; + d += String::compose("_%1%2", non_lfe, lfe); } /* 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())) { + vf = true; + } } - if (!dm.package_type.empty ()) { - d << "_" << dm.package_type; + if (vf) { + d += "_VF"; + } else { + d += "_OV"; } - return d.str (); + return d; } /** @return name to give the DCP */ @@ -771,6 +886,11 @@ Film::set_three_d (bool t) { _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); + } } void @@ -788,6 +908,28 @@ Film::set_audio_processor (AudioProcessor const * processor) signal_changed (AUDIO_CHANNELS); } +void +Film::set_reel_type (ReelType t) +{ + _reel_type = t; + signal_changed (REEL_TYPE); +} + +/** @param r Desired reel length in bytes */ +void +Film::set_reel_length (int64_t r) +{ + _reel_length = r; + signal_changed (REEL_LENGTH); +} + +void +Film::set_upload_after_make_dcp (bool u) +{ + _upload_after_make_dcp = u; + signal_changed (UPLOAD_AFTER_MAKE_DCP); +} + void Film::signal_changed (Property p) { @@ -795,11 +937,11 @@ Film::signal_changed (Property p) switch (p) { case Film::CONTENT: - set_video_frame_rate (_playlist->best_dcp_frame_rate ()); + set_video_frame_rate (_playlist->best_video_frame_rate ()); break; case Film::VIDEO_FRAME_RATE: - case Film::SEQUENCE_VIDEO: - _playlist->maybe_sequence_video (); + case Film::SEQUENCE: + _playlist->maybe_sequence (); break; default: break; @@ -815,29 +957,29 @@ Film::set_isdcf_date_today () } boost::filesystem::path -Film::j2c_path (int f, Eyes e, bool t) const +Film::j2c_path (int reel, Frame frame, Eyes eyes, bool tmp) const { boost::filesystem::path p; p /= "j2c"; p /= video_identifier (); - SafeStringStream s; - s.width (8); - s << setfill('0') << f; + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%08d_%08" PRId64, reel, frame); + string s (buffer); - if (e == EYES_LEFT) { - s << ".L"; - } else if (e == EYES_RIGHT) { - s << ".R"; + if (eyes == EYES_LEFT) { + s += ".L"; + } else if (eyes == EYES_RIGHT) { + s += ".R"; } - s << ".j2c"; + s += ".j2c"; - if (t) { - s << ".tmp"; + if (tmp) { + s += ".tmp"; } - p /= s.str(); + p /= s; return file (p); } @@ -845,9 +987,13 @@ Film::j2c_path (int f, Eyes e, bool t) const 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) && @@ -857,12 +1003,13 @@ Film::cpls () const 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() + dcp.cpls().front()->file().get() ) ); } catch (...) { @@ -901,24 +1048,17 @@ Film::content () const return _playlist->content (); } -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) { - if (dynamic_pointer_cast (c)) { + if (dynamic_pointer_cast (c) && _directory) { run_ffprobe (c->path(0), file ("ffprobe.log"), _log); } shared_ptr j (new ExamineContentJob (shared_from_this(), c)); _job_connections.push_back ( - j->Finished.connect (bind (&Film::maybe_add_content, this, boost::weak_ptr (j), boost::weak_ptr (c))) + j->Finished.connect (bind (&Film::maybe_add_content, this, weak_ptr (j), weak_ptr (c))) ); JobManager::instance()->add (j); @@ -933,17 +1073,40 @@ Film::maybe_add_content (weak_ptr j, weak_ptr c) } shared_ptr content = c.lock (); - if (content) { - add_content (content); + if (!content) { + return; + } + + add_content (content); + + if (Config::instance()->automatic_audio_analysis() && content->audio) { + shared_ptr playlist (new Playlist); + playlist->add (content); + boost::signals2::connection c; + JobManager::instance()->analyse_audio ( + shared_from_this (), playlist, c, bind (&Film::audio_analysis_finished, this) + ); + _audio_analysis_connections.push_back (c); } } void Film::add_content (shared_ptr c) { - /* Add video content after any existing content */ - if (dynamic_pointer_cast (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 ()); + } + + 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); + } + } } _playlist->add (c); @@ -967,6 +1130,7 @@ Film::move_content_later (shared_ptr c) _playlist->move_later (c); } +/** @return length of the film from time 0 to the last thing on the playlist */ DCPTime Film::length () const { @@ -976,7 +1140,7 @@ Film::length () const int Film::best_video_frame_rate () const { - return _playlist->best_dcp_frame_rate (); + return _playlist->best_video_frame_rate (); } FrameRateChange @@ -986,11 +1150,13 @@ Film::active_frame_rate_change (DCPTime t) const } void -Film::playlist_content_changed (boost::weak_ptr c, int p, bool frequent) +Film::playlist_content_changed (weak_ptr c, int p, bool frequent) { - if (p == VideoContentProperty::VIDEO_FRAME_RATE) { - set_video_frame_rate (_playlist->best_dcp_frame_rate ()); - } else if (p == AudioContentProperty::AUDIO_STREAMS) { + _dirty = true; + + if (p == ContentProperty::VIDEO_FRAME_RATE) { + set_video_frame_rate (_playlist->best_video_frame_rate ()); + } else if (p == AudioContentProperty::STREAMS) { signal_changed (NAME); } @@ -1004,19 +1170,30 @@ Film::playlist_changed () signal_changed (NAME); } +void +Film::playlist_order_changed () +{ + signal_changed (CONTENT_ORDER); +} + int Film::audio_frame_rate () const { - /* XXX */ + BOOST_FOREACH (shared_ptr i, content ()) { + if (i->audio && i->audio->has_rate_above_48k ()) { + return 96000; + } + } + return 48000; } void -Film::set_sequence_video (bool s) +Film::set_sequence (bool s) { - _sequence_video = s; - _playlist->set_sequence_video (s); - signal_changed (SEQUENCE_VIDEO); + _sequence = s; + _playlist->set_sequence (s); + signal_changed (SEQUENCE); } /** @return Size of the largest possible image in whatever resolution we are using */ @@ -1041,9 +1218,13 @@ Film::frame_size () const return fit_ratio_within (container()->ratio(), full_frame ()); } +/** @param from KDM from time expressed as a local time with an offset from UTC + * @param to KDM to time expressed as a local time with an offset from UTC + */ dcp::EncryptedKDM Film::make_kdm ( - dcp::Certificate target, + dcp::Certificate recipient, + vector trusted_devices, boost::filesystem::path cpl_file, dcp::LocalTime from, dcp::LocalTime until, @@ -1051,30 +1232,76 @@ Film::make_kdm ( ) const { shared_ptr cpl (new dcp::CPL (cpl_file)); - shared_ptr signer = Config::instance()->signer(); + shared_ptr signer = Config::instance()->signer_chain (); if (!signer->valid ()) { throw InvalidSignerError (); } + /* Find keys that have been added to imported, encrypted DCP content */ + list imported_keys; + BOOST_FOREACH (shared_ptr i, content()) { + shared_ptr d = dynamic_pointer_cast (i); + 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_assets ()) { + shared_ptr mxf = boost::dynamic_pointer_cast (i); + if (!mxf || !mxf->key_id()) { + continue; + } + + /* Get any imported key for this ID */ + bool done = false; + BOOST_FOREACH (dcp::DecryptedKDMKey j, imported_keys) { + if (j.id() == mxf->key_id().get()) { + keys[mxf] = j.key(); + done = true; + } + } + + if (!done) { + /* No imported key; it must be an asset that we encrypted */ + keys[mxf] = key(); + } + } + return dcp::DecryptedKDM ( - cpl, key(), from, until, "DCP-o-matic", cpl->content_title_text(), dcp::LocalTime().as_string() - ).encrypt (signer, target, formulation); + cpl->id(), keys, from, until, cpl->content_title_text(), cpl->content_title_text(), dcp::LocalTime().as_string() + ).encrypt (signer, recipient, trusted_devices, formulation); } -list +/** @param from KDM from time expressed as a local time in the time zone of the Screen's Cinema. + * @param to KDM to time expressed as a local time in the time zone of the Screen's Cinema. + */ +list Film::make_kdms ( list > screens, boost::filesystem::path dcp, - dcp::LocalTime from, - dcp::LocalTime until, + boost::posix_time::ptime from, + boost::posix_time::ptime until, dcp::Formulation formulation ) const { - list kdms; - - for (list >::iterator i = screens.begin(); i != screens.end(); ++i) { - if ((*i)->certificate) { - kdms.push_back (make_kdm ((*i)->certificate.get(), dcp, from, until, formulation)); + list kdms; + + BOOST_FOREACH (shared_ptr i, screens) { + if (i->recipient) { + dcp::EncryptedKDM const kdm = make_kdm ( + i->recipient.get(), + i->trusted_devices, + dcp, + 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)); } } @@ -1087,12 +1314,12 @@ Film::make_kdms ( uint64_t Film::required_disk_space () const { - return uint64_t (j2k_bandwidth() / 8) * length().seconds(); + return _playlist->required_disk_space (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 availabe are filled in with the amount of disk space + * false is returned and required and available are filled in with the amount of disk space * required and available respectively (in Gb). * * Note: the decision made by this method isn't, of course, 100% reliable. @@ -1132,9 +1359,8 @@ Film::subtitle_language () const ContentList cl = content (); BOOST_FOREACH (shared_ptr& c, cl) { - shared_ptr sc = dynamic_pointer_cast (c); - if (sc) { - languages.insert (sc->subtitle_language ()); + if (c->subtitle) { + languages.insert (c->subtitle->language ()); } } @@ -1152,18 +1378,44 @@ Film::subtitle_language () const /** 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. + * is in use, the number of input channels and any filename supplied. */ void -Film::make_audio_mapping_default (AudioMapping& mapping) const +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) { - /* Mono -> Centre */ - mapping.set (0, static_cast (dcp::CENTRE), 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) { @@ -1183,21 +1435,15 @@ Film::audio_output_names () const return audio_processor()->input_names (); } + 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")); - - 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 @@ -1211,3 +1457,129 @@ Film::remove_content (ContentList c) { _playlist->remove (c); } + +void +Film::audio_analysis_finished () +{ + /* XXX */ +} + +list +Film::reels () const +{ + list p; + DCPTime const len = length().ceil (video_frame_rate ()); + + switch (reel_type ()) { + case REELTYPE_SINGLE: + p.push_back (DCPTimePeriod (DCPTime (), len)); + break; + case REELTYPE_BY_VIDEO_CONTENT: + { + optional last_split; + shared_ptr last_video; + BOOST_FOREACH (shared_ptr 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; + } + last_video = c; + } + } + + 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)); + } + 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); + while (current < len) { + DCPTime end = min (len, current + DCPTime::from_frames (reel_in_frames, video_frame_rate ())); + p.push_back (DCPTimePeriod (current, end)); + current = end; + } + break; + } + } + + return p; +} + +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; +} + +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; +}