+
+ _last_write_times.clear ();
+ for (auto i: _paths) {
+ boost::system::error_code ec;
+ auto last_write = boost::filesystem::last_write_time(i, ec);
+ _last_write_times.push_back (ec ? 0 : last_write);
+ }
+}
+
+
+void
+Content::signal_change (ChangeType c, int p)
+{
+ try {
+ if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
+ Change (c, shared_from_this(), p, _change_signals_frequent);
+ } else {
+ emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
+ }
+ } catch (std::bad_weak_ptr &) {
+ /* This must be during construction; never mind */
+ }
+}
+
+
+void
+Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
+{
+ /* video and audio content can modify its position */
+
+ if (video) {
+ video->modify_position (film, p);
+ }
+
+ /* Only allow the audio to modify if we have no video;
+ sometimes p can't be on an integer video AND audio frame,
+ and in these cases we want the video constraint to be
+ satisfied since (I think) the audio code is better able to
+ cope.
+ */
+ if (!video && audio) {
+ audio->modify_position (film, p);
+ }
+
+ ContentChangeSignaller cc (this, ContentProperty::POSITION);
+
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ if (p == _position && !force_emit) {
+ cc.abort ();
+ return;
+ }
+
+ _position = p;
+ }
+}
+
+
+void
+Content::set_trim_start (ContentTime t)
+{
+ /* video and audio content can modify its start trim */
+
+ if (video) {
+ video->modify_trim_start (t);
+ }
+
+ /* See note in ::set_position */
+ if (!video && audio) {
+ audio->modify_trim_start (t);
+ }
+
+ ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
+
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ _trim_start = t;
+ }
+}
+
+
+void
+Content::set_trim_end (ContentTime t)
+{
+ ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
+
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ _trim_end = t;
+ }
+}
+
+
+shared_ptr<Content>
+Content::clone () const
+{
+ /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
+ xmlpp::Document doc;
+ auto node = doc.create_root_node ("Content");
+ as_xml (node, true);
+
+ /* notes is unused here (we assume) */
+ list<string> notes;
+ return content_factory (make_shared<cxml::Node>(node), Film::current_state_version, notes);
+}
+
+
+string
+Content::technical_summary () const
+{
+ auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
+ if (_video_frame_rate) {
+ s += String::compose(" %1", *_video_frame_rate);
+ }
+ return s;
+}
+
+
+DCPTime
+Content::length_after_trim (shared_ptr<const Film> film) const
+{
+ auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
+ if (video) {
+ length = length.round(film->video_frame_rate());
+ }
+ return length;
+}
+
+
+/** @return string which changes when something about this content changes which affects
+ * the appearance of its video.
+ */
+string
+Content::identifier () const
+{
+ char buffer[256];
+ snprintf (
+ buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
+ Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
+ );
+ return buffer;
+}
+
+
+bool
+Content::paths_valid () const
+{
+ for (auto i: _paths) {
+ if (!boost::filesystem::exists (i)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+void
+Content::set_paths (vector<boost::filesystem::path> paths)
+{
+ ContentChangeSignaller cc (this, ContentProperty::PATH);
+
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ _paths = paths;
+ _last_write_times.clear ();
+ for (auto i: _paths) {
+ boost::system::error_code ec;
+ auto last_write = boost::filesystem::last_write_time(i, ec);
+ _last_write_times.push_back (ec ? 0 : last_write);
+ }
+ }
+}
+
+
+string
+Content::path_summary () const
+{
+ /* XXX: should handle multiple paths more gracefully */
+
+ DCPOMATIC_ASSERT (number_of_paths ());
+
+ auto s = path(0).filename().string();
+ if (number_of_paths() > 1) {
+ s += " ...";
+ }
+
+ return s;
+}
+
+
+/** @return a list of properties that might be interesting to the user */
+list<UserProperty>
+Content::user_properties (shared_ptr<const Film> film) const
+{
+ list<UserProperty> p;
+ add_properties (film, p);
+ return p;
+}
+
+
+/** @return DCP times of points within this content where a reel split could occur */
+list<DCPTime>
+Content::reel_split_points (shared_ptr<const Film>) const
+{
+ list<DCPTime> t;
+ /* This is only called for video content and such content has its position forced
+ to start on a frame boundary.
+ */
+ t.push_back (position());
+ return t;
+}
+
+
+void
+Content::set_video_frame_rate (double r)
+{
+ ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
+
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
+ cc.abort();
+ }
+ _video_frame_rate = r;
+ }
+
+ /* Make sure trim is still on a frame boundary */
+ if (video) {
+ set_trim_start (trim_start());
+ }
+}
+
+
+void
+Content::unset_video_frame_rate ()
+{
+ ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
+
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ _video_frame_rate = optional<double>();
+ }
+}
+
+
+double
+Content::active_video_frame_rate (shared_ptr<const Film> film) const
+{
+ {
+ boost::mutex::scoped_lock lm (_mutex);
+ if (_video_frame_rate) {
+ return _video_frame_rate.get ();
+ }
+ }
+
+ /* No frame rate specified, so assume this content has been
+ prepared for any concurrent video content or perhaps
+ just the DCP rate.
+ */
+ return film->active_frame_rate_change(position()).source;