/*
- Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2012-2017 Carl Hetherington <cth@carlh.net>
This file is part of DCP-o-matic.
*/
/** @file src/film.cc
- * @brief A representation of some audio and video content, and details of
+ * @brief A representation of some audio, video and subtitle content, and details of
* how they should be presented in a DCP.
*/
#include "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 <dcp/local_time.h>
#include <dcp/decrypted_kdm.h>
#include <dcp/raw_convert.h>
+#include <dcp/reel_mxf.h>
+#include <dcp/reel_asset.h>
#include <libxml++/libxml++.h>
#include <boost/filesystem.hpp>
#include <boost/algorithm/string.hpp>
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;
#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.
* 6 -> 7
* @param dir Film directory.
*/
-Film::Film (boost::filesystem::path dir, bool log)
+Film::Film (optional<boost::filesystem::path> dir)
: _playlist (new Playlist)
, _use_isdcf_name (true)
, _dcp_content_type (Config::instance()->default_dcp_content_type ())
, _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)
_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)
- 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 == "..") {
+ 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);
throw BadSettingError (_("name"), _("cannot contain slashes"));
}
+ if (container() == 0) {
+ throw MissingSettingError (_("container"));
+ }
+
+ if (content().empty()) {
+ throw runtime_error (_("you must add some content to the DCP before creating it"));
+ }
+
+ if (dcp_content_type() == 0) {
+ throw MissingSettingError (_("content type"));
+ }
+
+ if (name().empty()) {
+ throw MissingSettingError (_("name"));
+ }
+
+ BOOST_FOREACH (shared_ptr<const Content> i, content ()) {
+ if (!i->paths_valid()) {
+ throw runtime_error (_("some of your content is missing"));
+ }
+ shared_ptr<const DCPContent> dcp = dynamic_pointer_cast<const DCPContent> (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 ()) {
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"));
- }
-
- JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this())));
+ shared_ptr<TranscodeJob> tj (new TranscodeJob (shared_from_this()));
+ tj->set_encoder (shared_ptr<Encoder> (new DCPEncoder (shared_from_this(), tj)));
+ JobManager::instance()->add (tj);
}
/** Start a job to send our DCP to the configured TMS */
}
shared_ptr<xmlpp::Document>
-Film::metadata () const
+Film::metadata (bool with_content_paths) const
{
shared_ptr<xmlpp::Document> doc (new xmlpp::Document);
xmlpp::Element* root = doc->create_root_node ("Metadata");
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<string> (static_cast<int> (_reel_type)));
root->add_child("ReelLength")->add_child_text (raw_convert<string> (_reel_length));
root->add_child("UploadAfterMakeDCP")->add_child_text (_upload_after_make_dcp ? "1" : "0");
- _playlist->as_xml (root->add_child ("Playlist"));
+ _playlist->as_xml (root->add_child ("Playlist"), with_content_paths);
return doc;
}
void
Film::write_metadata () const
{
- boost::filesystem::create_directories (directory ());
+ DCPOMATIC_ASSERT (directory());
+ boost::filesystem::create_directories (directory().get());
shared_ptr<xmlpp::Document> 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<xmlpp::Document> 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<string>
-Film::read_metadata ()
+Film::read_metadata (optional<boost::filesystem::path> 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);
}
cxml::Document f ("Metadata");
- f.read_file (file ("metadata.xml"));
+ f.read_file (path.get ());
_state_version = f.number_child<int> ("Version");
if (_state_version > current_state_version) {
_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"));
_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 Filn'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;
}
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 ());
/* Count mapped audio channels */
- int non_lfe = 0;
- int lfe = 0;
-
- BOOST_FOREACH (int i, mapped_audio_channels ()) {
- if (i >= audio_channels()) {
- /* This channel is mapped but is not included in the DCP */
- continue;
- }
-
- if (static_cast<dcp::Channel> (i) == dcp::LFE) {
- ++lfe;
- } else {
- ++non_lfe;
- }
- }
-
- if (non_lfe) {
- d += String::compose("_%1%2", non_lfe, lfe);
+ pair<int, int> ch = audio_channel_types (mapped_audio_channels(), audio_channels());
+ if (ch.first) {
+ d += String::compose("_%1%2", ch.first, ch.second);
}
/* XXX: HI/VI */
{
string unfiltered;
if (use_isdcf_name()) {
- unfiltered = isdcf_name (if_created_now);
- } else {
- unfiltered = name ();
+ return careful_string_filter (isdcf_name (if_created_now));
}
- /* 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 filtered;
+ return careful_string_filter (name ());
}
void
vector<CPLSummary>
Film::cpls () const
{
+ if (!directory ()) {
+ return vector<CPLSummary> ();
+ }
+
vector<CPLSummary> 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) &&
return _playlist->content ();
}
-void
-Film::examine_content (shared_ptr<Content> c)
-{
- shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
- JobManager::instance()->add (j);
-}
-
void
Film::examine_and_add_content (shared_ptr<Content> c)
{
- if (dynamic_pointer_cast<FFmpegContent> (c) && !_directory.empty ()) {
+ if (dynamic_pointer_cast<FFmpegContent> (c) && _directory) {
run_ffprobe (c->path(0), file ("ffprobe.log"), _log);
}
}
add_content (content);
+
if (Config::instance()->automatic_audio_analysis() && content->audio) {
shared_ptr<Playlist> playlist (new Playlist);
playlist->add (content);
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<Content> i, _template_film->content()) {
+ if (typeid(i.get()) == typeid(c.get())) {
+ c->use_template (i);
+ }
+ }
+ }
+
_playlist->add (c);
}
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
+/** @param recipient KDM recipient certificate.
+ * @param trusted_devices Certificates 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.
*/
dcp::EncryptedKDM
Film::make_kdm (
throw InvalidSignerError ();
}
+ /* Find keys that have been added to imported, encrypted DCP content */
+ list<dcp::DecryptedKDMKey> imported_keys;
+ BOOST_FOREACH (shared_ptr<Content> i, content()) {
+ shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent> (i);
+ if (d && d->kdm()) {
+ dcp::DecryptedKDM kdm (d->kdm().get(), Config::instance()->decryption_chain()->key().get());
+ list<dcp::DecryptedKDMKey> keys = kdm.keys ();
+ copy (keys.begin(), keys.end(), back_inserter (imported_keys));
+ }
+ }
+
+ map<shared_ptr<const dcp::ReelMXF>, dcp::Key> keys;
+
+ BOOST_FOREACH(shared_ptr<const dcp::ReelAsset> i, cpl->reel_assets ()) {
+ shared_ptr<const dcp::ReelMXF> mxf = boost::dynamic_pointer_cast<const dcp::ReelMXF> (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()) {
+ LOG_GENERAL ("Using imported key for %1", mxf->key_id().get());
+ keys[mxf] = 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", mxf->key_id().get());
+ keys[mxf] = key();
+ }
+ }
+
return dcp::DecryptedKDM (
- cpl, key(), from, until, cpl->content_title_text(), cpl->content_title_text(), dcp::LocalTime().as_string()
+ cpl->id(), keys, from, until, cpl->content_title_text(), cpl->content_title_text(), dcp::LocalTime().as_string()
).encrypt (signer, recipient, trusted_devices, formulation);
}
-/** @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.
+/** @param screens Screens to make KDMs for.
+ * @param cpl_file Path to CPL to make KDMs for.
+ * @param from KDM from time expressed as a local time in the time zone of the Screen's Cinema.
+ * @param until KDM to time expressed as a local time in the time zone of the Screen's Cinema.
+ * @param formulation KDM formulation to use.
*/
list<ScreenKDM>
Film::make_kdms (
list<shared_ptr<Screen> > screens,
- boost::filesystem::path dcp,
+ boost::filesystem::path cpl_file,
boost::posix_time::ptime from,
boost::posix_time::ptime until,
dcp::Formulation formulation
dcp::EncryptedKDM const kdm = make_kdm (
i->recipient.get(),
i->trusted_devices,
- dcp,
+ cpl_file,
dcp::LocalTime (from, i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()),
dcp::LocalTime (until, i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()),
formulation
DCPOMATIC_ASSERT (MAX_DCP_AUDIO_CHANNELS == 16);
vector<string> 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<string> (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::reels () const
{
list<DCPTimePeriod> p;
- DCPTime const len = length().round_up (video_frame_rate ());
+ DCPTime const len = length().ceil (video_frame_rate ());
switch (reel_type ()) {
case REELTYPE_SINGLE:
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 notes;
}
+
+void
+Film::use_template (string name)
+{
+ _template_film.reset (new Film (optional<boost::filesystem::path>()));
+ _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;
+}
+
+pair<double, double>
+Film::speed_up_range (int dcp_frame_rate) const
+{
+ return _playlist->speed_up_range (dcp_frame_rate);
+}
+
+void
+Film::copy_from (shared_ptr<const Film> film)
+{
+ read_metadata (film->file (metadata_file));
+}