X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Flib%2Ffilm.cc;h=c91a8047133dc0e14a1ff0cc47f0a946ada72e9e;hb=987a92d9906b306fb74ab65b578aa5bb510bea43;hp=3a8b29f8603a8ddf30938a0bc06f6f005b2c37cd;hpb=6bec4750da08011e944ab53bda1ff9a51065c795;p=dcpomatic.git diff --git a/src/lib/film.cc b/src/lib/film.cc index 3a8b29f86..13c1558e9 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington + Copyright (C) 2012-2013 Carl Hetherington This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,52 +28,91 @@ #include #include #include +#include +#include +#include #include "film.h" -#include "format.h" -#include "imagemagick_encoder.h" #include "job.h" -#include "filter.h" -#include "transcoder.h" #include "util.h" #include "job_manager.h" -#include "ab_transcode_job.h" #include "transcode_job.h" #include "scp_dcp_job.h" -#include "copy_from_dvd_job.h" -#include "make_dcp_job.h" -#include "film_state.h" #include "log.h" -#include "options.h" #include "exceptions.h" #include "examine_content_job.h" #include "scaler.h" -#include "decoder_factory.h" #include "config.h" -#include "check_hashes_job.h" #include "version.h" - -using namespace std; -using namespace boost; - -/** Construct a Film object in a given directory, reading any metadata - * file that exists in that directory. An exception will be thrown if - * must_exist is true, and the specified directory does not exist. +#include "ui_signaller.h" +#include "playlist.h" +#include "player.h" +#include "dcp_content_type.h" +#include "ratio.h" +#include "cross.h" + +#include "i18n.h" + +using std::string; +using std::stringstream; +using std::multimap; +using std::pair; +using std::map; +using std::vector; +using std::ifstream; +using std::ofstream; +using std::setfill; +using std::min; +using std::make_pair; +using std::endl; +using std::cout; +using std::list; +using boost::shared_ptr; +using boost::weak_ptr; +using boost::lexical_cast; +using boost::dynamic_pointer_cast; +using boost::to_upper_copy; +using boost::ends_with; +using boost::starts_with; +using boost::optional; +using libdcp::Size; + +int const Film::state_version = 4; + +/** Construct a Film object in a given directory. * * @param d Film directory. - * @param must_exist true to throw an exception if does not exist. */ -Film::Film (string d, bool must_exist) +Film::Film (string d) + : _playlist (new Playlist) + , _use_dci_name (true) + , _dcp_content_type (Config::instance()->default_dcp_content_type ()) + , _container (Config::instance()->default_container ()) + , _resolution (RESOLUTION_2K) + , _scaler (Scaler::from_id ("bicubic")) + , _with_subtitles (false) + , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ()) + , _dci_metadata (Config::instance()->default_dci_metadata ()) + , _video_frame_rate (24) + , _audio_channels (MAX_AUDIO_CHANNELS) + , _three_d (false) + , _sequence_video (true) + , _dirty (false) { + set_dci_date_today (); + + _playlist->Changed.connect (bind (&Film::playlist_changed, this)); + _playlist->ContentChanged.connect (bind (&Film::playlist_content_changed, this, _1, _2)); + /* Make state.directory a complete path without ..s (where possible) (Code swiped from Adam Bowen on stackoverflow) */ - filesystem::path p (filesystem::system_complete (d)); - filesystem::path result; - for (filesystem::path::iterator i = p.begin(); i != p.end(); ++i) { + boost::filesystem::path p (boost::filesystem::system_complete (d)); + boost::filesystem::path result; + for (boost::filesystem::path::iterator i = p.begin(); i != p.end(); ++i) { if (*i == "..") { - if (filesystem::is_symlink (result) || result.filename() == "..") { + if (boost::filesystem::is_symlink (result) || result.filename() == "..") { result /= *i; } else { result = result.parent_path (); @@ -84,284 +123,790 @@ Film::Film (string d, bool must_exist) } set_directory (result.string ()); - - if (!filesystem::exists (directory())) { - if (must_exist) { - throw OpenFileError (directory()); - } else { - filesystem::create_directory (directory()); - } - } - - read_metadata (); + _log.reset (new FileLog (file ("log"))); - _log = new FileLog (file ("log")); + _playlist->set_sequence_video (_sequence_video); } -Film::~Film () +string +Film::video_identifier () const { - delete _log; + assert (container ()); + LocaleGuard lg; + + stringstream s; + s << container()->id() + << "_" << resolution_to_string (_resolution) + << "_" << _playlist->video_identifier() + << "_" << _video_frame_rate + << "_" << scaler()->id() + << "_" << j2k_bandwidth(); + + if (_three_d) { + s << "_3D"; + } + + return s.str (); } -/** @return The path to the directory to write JPEG2000 files to */ +/** @return The path to the directory to write video frame info files to */ string -Film::j2k_dir () const +Film::info_dir () const { - assert (format()); - - filesystem::path p; + boost::filesystem::path p; + p /= "info"; + p /= video_identifier (); + return dir (p.string()); +} - /* Start with j2c */ - p /= "j2c"; +string +Film::internal_video_mxf_dir () const +{ + return dir ("video"); +} - pair f = Filter::ffmpeg_strings (filters()); +string +Film::internal_video_mxf_filename () const +{ + return video_identifier() + ".mxf"; +} - /* Write stuff to specify the filter / post-processing settings that are in use, - so that we don't get confused about J2K files generated using different - settings. - */ - stringstream s; - s << format()->id() - << "_" << content_digest() - << "_" << crop().left << "_" << crop().right << "_" << crop().top << "_" << crop().bottom - << "_" << f.first << "_" << f.second - << "_" << scaler()->id(); +string +Film::video_mxf_filename () const +{ + return filename_safe_name() + "_video.mxf"; +} - p /= s.str (); +string +Film::audio_mxf_filename () const +{ + return filename_safe_name() + "_audio.mxf"; +} - /* Similarly for the A/B case */ - if (dcp_ab()) { - stringstream s; - pair fa = Filter::ffmpeg_strings (Config::instance()->reference_filters()); - s << "ab_" << Config::instance()->reference_scaler()->id() << "_" << fa.first << "_" << fa.second; - p /= s.str (); +string +Film::filename_safe_name () const +{ + string const n = name (); + string o; + for (size_t i = 0; i < n.length(); ++i) { + if (isalnum (n[i])) { + o += n[i]; + } else { + o += "_"; + } } - - return dir (p.string()); + + return o; } -/** Add suitable Jobs to the JobManager to create a DCP for this Film. - * @param true to transcode, false to use the WAV and J2K files that are already there. - */ +boost::filesystem::path +Film::audio_analysis_path (shared_ptr c) const +{ + boost::filesystem::path p = dir ("analysis"); + p /= c->digest(); + return p; +} + +/** Add suitable Jobs to the JobManager to create a DCP for this Film */ void -Film::make_dcp (bool transcode) +Film::make_dcp () { + set_dci_date_today (); + if (dcp_name().find ("/") != string::npos) { - throw BadSettingError ("name", "cannot contain slashes"); + throw BadSettingError (_("name"), _("cannot contain slashes")); } - log()->log (String::compose ("DVD-o-matic %1 git %2 using %3", dvdomatic_version, dvdomatic_git_commit, dependency_version_summary())); + log()->log (String::compose ("DCP-o-matic %1 git %2 using %3", dcpomatic_version, dcpomatic_git_commit, dependency_version_summary())); { char buffer[128]; gethostname (buffer, sizeof (buffer)); log()->log (String::compose ("Starting to make DCP on %1", buffer)); } - - if (format() == 0) { - throw MissingSettingError ("format"); + + ContentList cl = content (); + for (ContentList::const_iterator i = cl.begin(); i != cl.end(); ++i) { + log()->log (String::compose ("Content: %1", (*i)->technical_summary())); + } + log()->log (String::compose ("DCP video rate %1 fps", video_frame_rate())); + log()->log (String::compose ("%1 threads", Config::instance()->num_local_encoding_threads())); + log()->log (String::compose ("J2K bandwidth %1", j2k_bandwidth())); +#ifdef DCPOMATIC_DEBUG + log()->log ("DCP-o-matic built in debug mode."); +#else + log()->log ("DCP-o-matic built in optimised mode."); +#endif +#ifdef LIBDCP_DEBUG + log()->log ("libdcp built in debug mode."); +#else + log()->log ("libdcp built in optimised mode."); +#endif + log()->log (String::compose ("CPU: %1, %2 processors", cpu_info(), boost::thread::hardware_concurrency ())); + list > const m = mount_info (); + for (list >::const_iterator i = m.begin(); i != m.end(); ++i) { + log()->log (String::compose ("Mount: %1 %2", i->first, i->second)); + } + + if (container() == 0) { + throw MissingSettingError (_("container")); } - if (content().empty ()) { - throw MissingSettingError ("content"); + if (content().empty()) { + throw StringError (_("You must add some content to the DCP before creating it")); } if (dcp_content_type() == 0) { - throw MissingSettingError ("content type"); + throw MissingSettingError (_("content type")); } if (name().empty()) { - throw MissingSettingError ("name"); + throw MissingSettingError (_("name")); } - shared_ptr fs = state_copy (); - shared_ptr o (new Options (j2k_dir(), ".j2c", dir ("wavs"))); - o->out_size = format()->dcp_size (); - if (dcp_frames() == 0) { - /* Decode the whole film, no blacking */ - o->black_after = 0; - } else { - switch (dcp_trim_action()) { - case CUT: - /* Decode only part of the film, no blacking */ - o->black_after = 0; - break; - case BLACK_OUT: - /* Decode the whole film, but black some frames out */ - o->black_after = dcp_frames (); + JobManager::instance()->add (shared_ptr (new TranscodeJob (shared_from_this()))); +} + +/** Start a job to send our DCP to the configured TMS */ +void +Film::send_dcp_to_tms () +{ + shared_ptr j (new SCPDCPJob (shared_from_this())); + JobManager::instance()->add (j); +} + +/** Count the number of frames that have been encoded for this film. + * @return frame count. + */ +int +Film::encoded_frames () const +{ + if (container() == 0) { + return 0; + } + + int N = 0; + for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (info_dir ()); i != boost::filesystem::directory_iterator(); ++i) { + ++N; + boost::this_thread::interruption_point (); + } + + return N; +} + +/** Write state to our `metadata' file */ +void +Film::write_metadata () const +{ + if (!boost::filesystem::exists (directory())) { + boost::filesystem::create_directory (directory()); + } + + boost::mutex::scoped_lock lm (_state_mutex); + LocaleGuard lg; + + boost::filesystem::create_directories (directory()); + + xmlpp::Document doc; + xmlpp::Element* root = doc.create_root_node ("Metadata"); + + root->add_child("Version")->add_child_text (lexical_cast (state_version)); + root->add_child("Name")->add_child_text (_name); + root->add_child("UseDCIName")->add_child_text (_use_dci_name ? "1" : "0"); + + if (_dcp_content_type) { + root->add_child("DCPContentType")->add_child_text (_dcp_content_type->dci_name ()); + } + + if (_container) { + root->add_child("Container")->add_child_text (_container->id ()); + } + + root->add_child("Resolution")->add_child_text (resolution_to_string (_resolution)); + root->add_child("Scaler")->add_child_text (_scaler->id ()); + root->add_child("WithSubtitles")->add_child_text (_with_subtitles ? "1" : "0"); + root->add_child("J2KBandwidth")->add_child_text (lexical_cast (_j2k_bandwidth)); + _dci_metadata.as_xml (root->add_child ("DCIMetadata")); + root->add_child("VideoFrameRate")->add_child_text (lexical_cast (_video_frame_rate)); + root->add_child("DCIDate")->add_child_text (boost::gregorian::to_iso_string (_dci_date)); + root->add_child("AudioChannels")->add_child_text (lexical_cast (_audio_channels)); + root->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0"); + root->add_child("SequenceVideo")->add_child_text (_sequence_video ? "1" : "0"); + _playlist->as_xml (root->add_child ("Playlist")); + + doc.write_to_file_formatted (file ("metadata.xml")); + + _dirty = false; +} + +/** Read state from our metadata file */ +void +Film::read_metadata () +{ + boost::mutex::scoped_lock lm (_state_mutex); + LocaleGuard lg; + + 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!")); + } + + cxml::File f (file ("metadata.xml"), "Metadata"); + + _name = f.string_child ("Name"); + _use_dci_name = f.bool_child ("UseDCIName"); + + { + optional c = f.optional_string_child ("DCPContentType"); + if (c) { + _dcp_content_type = DCPContentType::from_dci_name (c.get ()); } } + + { + optional c = f.optional_string_child ("Container"); + if (c) { + _container = Ratio::from_id (c.get ()); + } + } + + _resolution = string_to_resolution (f.string_child ("Resolution")); + _scaler = Scaler::from_id (f.string_child ("Scaler")); + _with_subtitles = f.bool_child ("WithSubtitles"); + _j2k_bandwidth = f.number_child ("J2KBandwidth"); + _dci_metadata = DCIMetadata (f.node_child ("DCIMetadata")); + _video_frame_rate = f.number_child ("VideoFrameRate"); + _dci_date = boost::gregorian::from_undelimited_string (f.string_child ("DCIDate")); + _audio_channels = f.number_child ("AudioChannels"); + _sequence_video = f.bool_child ("SequenceVideo"); + _three_d = f.bool_child ("ThreeD"); + + _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist")); + + _dirty = false; +} + +/** 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. + */ +string +Film::dir (string d) const +{ + boost::mutex::scoped_lock lm (_directory_mutex); + + boost::filesystem::path p; + p /= _directory; + p /= d; - o->padding = format()->dcp_padding (this); - o->ratio = format()->ratio_as_float (this); - o->decode_subtitles = with_subtitles (); + boost::filesystem::create_directories (p); + + return p.string (); +} + +/** Given a file or directory name, return its full path within the Film's directory. + * _directory_mutex must not be locked on entry. + * Any required parent directories will be created. + */ +string +Film::file (string f) const +{ + boost::mutex::scoped_lock lm (_directory_mutex); + + boost::filesystem::path p; + p /= _directory; + p /= f; + + boost::filesystem::create_directories (p.parent_path ()); + + return p.string (); +} - shared_ptr r; +/** @return a DCI-compliant name for a DCP of this film */ +string +Film::dci_name (bool if_created_now) const +{ + stringstream d; + + string fixed_name = to_upper_copy (name()); + for (size_t i = 0; i < fixed_name.length(); ++i) { + if (fixed_name[i] == ' ') { + fixed_name[i] = '-'; + } + } - if (transcode) { - if (dcp_ab()) { - r = JobManager::instance()->add (shared_ptr (new ABTranscodeJob (fs, o, log(), shared_ptr ()))); + /* Spec is that the name part should be maximum 14 characters, as I understand it */ + if (fixed_name.length() > 14) { + fixed_name = fixed_name.substr (0, 14); + } + + d << fixed_name; + + if (dcp_content_type()) { + d << "_" << dcp_content_type()->dci_name(); + } + + if (three_d ()) { + d << "-3D"; + } + + if (video_frame_rate() != 24) { + d << "-" << video_frame_rate(); + } + + if (container()) { + d << "_" << container()->dci_name(); + } + + DCIMetadata const dm = dci_metadata (); + + if (!dm.audio_language.empty ()) { + d << "_" << dm.audio_language; + if (!dm.subtitle_language.empty() && with_subtitles()) { + d << "-" << dm.subtitle_language; } else { - r = JobManager::instance()->add (shared_ptr (new TranscodeJob (fs, o, log(), shared_ptr ()))); + d << "-XX"; } } - r = JobManager::instance()->add (shared_ptr (new CheckHashesJob (fs, o, log(), r))); - JobManager::instance()->add (shared_ptr (new MakeDCPJob (fs, o, log(), r))); + if (!dm.territory.empty ()) { + d << "_" << dm.territory; + if (!dm.rating.empty ()) { + d << "-" << dm.rating; + } + } + + switch (audio_channels ()) { + case 1: + d << "_10"; + break; + case 2: + d << "_20"; + break; + case 3: + d << "_30"; + break; + case 4: + d << "_40"; + break; + case 5: + d << "_50"; + break; + case 6: + d << "_51"; + break; + } + + d << "_" << resolution_to_string (_resolution); + + if (!dm.studio.empty ()) { + d << "_" << dm.studio; + } + + if (if_created_now) { + d << "_" << boost::gregorian::to_iso_string (boost::gregorian::day_clock::local_day ()); + } else { + d << "_" << boost::gregorian::to_iso_string (_dci_date); + } + + if (!dm.facility.empty ()) { + d << "_" << dm.facility; + } + + if (!dm.package_type.empty ()) { + d << "_" << dm.package_type; + } + + return d.str (); +} + +/** @return name to give the DCP */ +string +Film::dcp_name (bool if_created_now) const +{ + if (use_dci_name()) { + return dci_name (if_created_now); + } + + return name(); } + void -Film::copy_from_dvd_post_gui () +Film::set_directory (string d) { - const string dvd_dir = dir ("dvd"); + boost::mutex::scoped_lock lm (_state_mutex); + _directory = d; + _dirty = true; +} - string largest_file; - uintmax_t largest_size = 0; - for (filesystem::directory_iterator i = filesystem::directory_iterator (dvd_dir); i != filesystem::directory_iterator(); ++i) { - uintmax_t const s = filesystem::file_size (*i); - if (s > largest_size) { +void +Film::set_name (string n) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _name = n; + } + signal_changed (NAME); +} -#if BOOST_FILESYSTEM_VERSION == 3 - largest_file = filesystem::path(*i).generic_string(); -#else - largest_file = i->string (); -#endif - largest_size = s; - } +void +Film::set_use_dci_name (bool u) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _use_dci_name = u; } + signal_changed (USE_DCI_NAME); +} - set_content (largest_file); +void +Film::set_dcp_content_type (DCPContentType const * t) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _dcp_content_type = t; + } + signal_changed (DCP_CONTENT_TYPE); } -/** Start a job to examine our content file */ void -Film::examine_content () +Film::set_container (Ratio const * c) { - if (_examine_content_job) { - return; + { + boost::mutex::scoped_lock lm (_state_mutex); + _container = c; } + signal_changed (CONTAINER); +} - set_thumbs (vector ()); - filesystem::remove_all (dir ("thumbs")); +void +Film::set_resolution (Resolution r) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _resolution = r; + } + signal_changed (RESOLUTION); +} - /* This call will recreate the directory */ - dir ("thumbs"); - - _examine_content_job.reset (new ExamineContentJob (state_copy (), log(), shared_ptr ())); - _examine_content_job->Finished.connect (sigc::mem_fun (*this, &Film::examine_content_post_gui)); - JobManager::instance()->add (_examine_content_job); +void +Film::set_scaler (Scaler const * s) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _scaler = s; + } + signal_changed (SCALER); } void -Film::examine_content_post_gui () +Film::set_with_subtitles (bool w) { - set_length (_examine_content_job->last_video_frame ()); - set_audio_to_discard (_examine_content_job->audio_to_discard ()); - _examine_content_job.reset (); + { + boost::mutex::scoped_lock lm (_state_mutex); + _with_subtitles = w; + } + signal_changed (WITH_SUBTITLES); +} - string const tdir = dir ("thumbs"); - vector thumbs; +void +Film::set_j2k_bandwidth (int b) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _j2k_bandwidth = b; + } + signal_changed (J2K_BANDWIDTH); +} - for (filesystem::directory_iterator i = filesystem::directory_iterator (tdir); i != filesystem::directory_iterator(); ++i) { +void +Film::set_dci_metadata (DCIMetadata m) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _dci_metadata = m; + } + signal_changed (DCI_METADATA); +} - /* Aah, the sweet smell of progress */ -#if BOOST_FILESYSTEM_VERSION == 3 - string const l = filesystem::path(*i).leaf().generic_string(); -#else - string const l = i->leaf (); -#endif - - size_t const d = l.find (".png"); - size_t const t = l.find (".tmp"); - if (d != string::npos && t == string::npos) { - thumbs.push_back (atoi (l.substr (0, d).c_str())); - } +void +Film::set_video_frame_rate (int f) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _video_frame_rate = f; } + signal_changed (VIDEO_FRAME_RATE); +} - sort (thumbs.begin(), thumbs.end()); - set_thumbs (thumbs); +void +Film::set_audio_channels (int c) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _audio_channels = c; + } + signal_changed (AUDIO_CHANNELS); } +void +Film::set_three_d (bool t) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _three_d = t; + } + signal_changed (THREE_D); +} -/** @return full paths to any audio files that this Film has */ -vector -Film::audio_files () const +void +Film::signal_changed (Property p) { - vector f; - for (filesystem::directory_iterator i = filesystem::directory_iterator (dir("wavs")); i != filesystem::directory_iterator(); ++i) { - f.push_back (i->path().string ()); + { + boost::mutex::scoped_lock lm (_state_mutex); + _dirty = true; + } + + switch (p) { + case Film::CONTENT: + set_video_frame_rate (_playlist->best_dcp_frame_rate ()); + break; + case Film::VIDEO_FRAME_RATE: + case Film::SEQUENCE_VIDEO: + _playlist->maybe_sequence_video (); + break; + default: + break; } - return f; + if (ui_signaller) { + ui_signaller->emit (boost::bind (boost::ref (Changed), p)); + } } -/** Start a job to send our DCP to the configured TMS */ void -Film::send_dcp_to_tms () +Film::set_dci_date_today () { - shared_ptr j (new SCPDCPJob (state_copy (), log(), shared_ptr ())); - JobManager::instance()->add (j); + _dci_date = boost::gregorian::day_clock::local_day (); } -void -Film::copy_from_dvd () +string +Film::info_path (int f, Eyes e) const { - shared_ptr j (new CopyFromDVDJob (state_copy (), log(), shared_ptr ())); - j->Finished.connect (sigc::mem_fun (*this, &Film::copy_from_dvd_post_gui)); - JobManager::instance()->add (j); + boost::filesystem::path p; + p /= info_dir (); + + stringstream s; + s.width (8); + s << setfill('0') << f; + + if (e == EYES_LEFT) { + s << ".L"; + } else if (e == EYES_RIGHT) { + s << ".R"; + } + + s << ".md5"; + + p /= s.str(); + + /* info_dir() will already have added any initial bit of the path, + so don't call file() on this. + */ + return p.string (); } -/** Count the number of frames that have been encoded for this film. - * @return frame count. - */ -int -Film::encoded_frames () const +string +Film::j2c_path (int f, Eyes e, bool t) const { - if (format() == 0) { - return 0; + boost::filesystem::path p; + p /= "j2c"; + p /= video_identifier (); + + stringstream s; + s.width (8); + s << setfill('0') << f; + + if (e == EYES_LEFT) { + s << ".L"; + } else if (e == EYES_RIGHT) { + s << ".R"; } + + s << ".j2c"; - int N = 0; - for (filesystem::directory_iterator i = filesystem::directory_iterator (j2k_dir ()); i != filesystem::directory_iterator(); ++i) { - ++N; - this_thread::interruption_point (); + if (t) { + s << ".tmp"; } - return N; + p /= s.str(); + return file (p.string ()); } -/** Return the filename of a subtitle image if one exists for a given thumb index. - * @param Thumbnail index. - * @return Position of the image within the source frame, and the image filename, if one exists. - * Otherwise the filename will be empty. +/** Make an educated guess as to whether we have a complete DCP + * or not. + * @return true if we do. */ -pair -Film::thumb_subtitle (int n) const + +bool +Film::have_dcp () const { - string sub_file = thumb_base(n) + ".sub"; - if (!filesystem::exists (sub_file)) { - return pair (); + try { + libdcp::DCP dcp (dir (dcp_name())); + dcp.read (); + } catch (...) { + return false; } - pair sub; - - ifstream f (sub_file.c_str ()); - multimap kv = read_key_value (f); - for (map::const_iterator i = kv.begin(); i != kv.end(); ++i) { - if (i->first == "x") { - sub.first.x = lexical_cast (i->second); - } else if (i->first == "y") { - sub.first.y = lexical_cast (i->second); - sub.second = String::compose ("%1.sub.png", thumb_base(n)); - } + return true; +} + +shared_ptr +Film::make_player () const +{ + return shared_ptr (new Player (shared_from_this (), _playlist)); +} + +shared_ptr +Film::playlist () const +{ + boost::mutex::scoped_lock lm (_state_mutex); + return _playlist; +} + +ContentList +Film::content () const +{ + return _playlist->content (); +} + +void +Film::examine_and_add_content (shared_ptr c) +{ + shared_ptr j (new ExamineContentJob (shared_from_this(), c)); + j->Finished.connect (bind (&Film::maybe_add_content, this, boost::weak_ptr (j), boost::weak_ptr (c))); + JobManager::instance()->add (j); +} + +void +Film::maybe_add_content (weak_ptr j, weak_ptr c) +{ + shared_ptr job = j.lock (); + if (!job || !job->finished_ok ()) { + return; } - return sub; + shared_ptr content = c.lock (); + if (content) { + add_content (content); + } +} + +void +Film::add_content (shared_ptr c) +{ + /* Add video content after any existing content */ + if (dynamic_pointer_cast (c)) { + c->set_start (_playlist->video_end ()); + } + + _playlist->add (c); +} + +void +Film::remove_content (shared_ptr c) +{ + _playlist->remove (c); +} + +Time +Film::length () const +{ + return _playlist->length (); +} + +bool +Film::has_subtitles () const +{ + return _playlist->has_subtitles (); +} + +OutputVideoFrame +Film::best_video_frame_rate () const +{ + return _playlist->best_dcp_frame_rate (); } void -Film::set_content (string c) +Film::playlist_content_changed (boost::weak_ptr c, int p) { - FilmState::set_content (c); - examine_content (); + if (p == VideoContentProperty::VIDEO_FRAME_RATE) { + set_video_frame_rate (_playlist->best_dcp_frame_rate ()); + } + + if (ui_signaller) { + ui_signaller->emit (boost::bind (boost::ref (ContentChanged), c, p)); + } +} + +void +Film::playlist_changed () +{ + signal_changed (CONTENT); +} + +OutputAudioFrame +Film::time_to_audio_frames (Time t) const +{ + return t * audio_frame_rate () / TIME_HZ; +} + +OutputVideoFrame +Film::time_to_video_frames (Time t) const +{ + return t * video_frame_rate () / TIME_HZ; +} + +Time +Film::audio_frames_to_time (OutputAudioFrame f) const +{ + return f * TIME_HZ / audio_frame_rate (); +} + +Time +Film::video_frames_to_time (OutputVideoFrame f) const +{ + return f * TIME_HZ / video_frame_rate (); +} + +OutputAudioFrame +Film::audio_frame_rate () const +{ + /* XXX */ + return 48000; +} + +void +Film::set_sequence_video (bool s) +{ + { + boost::mutex::scoped_lock lm (_state_mutex); + _sequence_video = s; + _playlist->set_sequence_video (s); + } + + signal_changed (SEQUENCE_VIDEO); +} + +libdcp::Size +Film::full_frame () const +{ + switch (_resolution) { + case RESOLUTION_2K: + return libdcp::Size (2048, 1080); + case RESOLUTION_4K: + return libdcp::Size (4096, 2160); + } + + assert (false); + return libdcp::Size (); }