2 Copyright (C) 2013-2018 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 DCP-o-matic is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
21 /** @file src/lib/content.cc
22 * @brief Content class.
26 #include "change_signaller.h"
28 #include "content_factory.h"
29 #include "video_content.h"
30 #include "audio_content.h"
31 #include "text_content.h"
32 #include "exceptions.h"
35 #include "compose.hpp"
36 #include <dcp/locale_convert.h>
37 #include <dcp/raw_convert.h>
38 #include <libcxml/cxml.h>
39 #include <libxml++/libxml++.h>
40 #include <boost/thread/mutex.hpp>
51 using boost::shared_ptr;
52 using boost::optional;
53 using dcp::raw_convert;
54 using dcp::locale_convert;
56 int const ContentProperty::PATH = 400;
57 int const ContentProperty::POSITION = 401;
58 int const ContentProperty::LENGTH = 402;
59 int const ContentProperty::TRIM_START = 403;
60 int const ContentProperty::TRIM_END = 404;
61 int const ContentProperty::VIDEO_FRAME_RATE = 405;
67 , _change_signals_frequent (false)
72 Content::Content (DCPTime p)
76 , _change_signals_frequent (false)
81 Content::Content (boost::filesystem::path p)
85 , _change_signals_frequent (false)
90 Content::Content (cxml::ConstNodePtr node)
91 : _change_signals_frequent (false)
93 list<cxml::NodePtr> path_children = node->node_children ("Path");
94 BOOST_FOREACH (cxml::NodePtr i, path_children) {
95 _paths.push_back (i->content());
96 optional<time_t> const mod = i->optional_number_attribute<time_t>("mtime");
98 _last_write_times.push_back (*mod);
99 } else if (boost::filesystem::exists(i->content())) {
100 _last_write_times.push_back (boost::filesystem::last_write_time(i->content()));
102 _last_write_times.push_back (0);
105 _digest = node->optional_string_child ("Digest").get_value_or ("X");
106 _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
107 _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
108 _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
109 _video_frame_rate = node->optional_number_child<double> ("VideoFrameRate");
112 Content::Content (vector<shared_ptr<Content> > c)
113 : _position (c.front()->position ())
114 , _trim_start (c.front()->trim_start ())
115 , _trim_end (c.back()->trim_end ())
116 , _video_frame_rate (c.front()->video_frame_rate())
117 , _change_signals_frequent (false)
119 for (size_t i = 0; i < c.size(); ++i) {
120 if (i > 0 && c[i]->trim_start() > ContentTime ()) {
121 throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
124 if (i < (c.size() - 1) && c[i]->trim_end () > ContentTime ()) {
125 throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
129 (_video_frame_rate && !c[i]->_video_frame_rate) ||
130 (!_video_frame_rate && c[i]->_video_frame_rate)
132 throw JoinError (_("Content to be joined must have the same video frame rate"));
135 if (_video_frame_rate && fabs (_video_frame_rate.get() - c[i]->_video_frame_rate.get()) > VIDEO_FRAME_RATE_EPSILON) {
136 throw JoinError (_("Content to be joined must have the same video frame rate"));
139 for (size_t j = 0; j < c[i]->number_of_paths(); ++j) {
140 _paths.push_back (c[i]->path(j));
141 _last_write_times.push_back (c[i]->_last_write_times[j]);
147 Content::as_xml (xmlpp::Node* node, bool with_paths) const
149 boost::mutex::scoped_lock lm (_mutex);
152 for (size_t i = 0; i < _paths.size(); ++i) {
153 xmlpp::Element* p = node->add_child("Path");
154 p->add_child_text (_paths[i].string());
155 p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
158 node->add_child("Digest")->add_child_text (_digest);
159 node->add_child("Position")->add_child_text (raw_convert<string> (_position.get ()));
160 node->add_child("TrimStart")->add_child_text (raw_convert<string> (_trim_start.get ()));
161 node->add_child("TrimEnd")->add_child_text (raw_convert<string> (_trim_end.get ()));
162 if (_video_frame_rate) {
163 node->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate.get()));
168 Content::calculate_digest () const
170 boost::mutex::scoped_lock lm (_mutex);
171 vector<boost::filesystem::path> p = _paths;
174 /* Some content files are very big, so we use a poor man's
175 digest here: a digest of the first and last 1e6 bytes with the
176 size of the first file tacked on the end as a string.
178 return digest_head_tail(p, 1000000) + raw_convert<string>(boost::filesystem::file_size(p.front()));
182 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
185 job->sub (_("Computing digest"));
188 string const d = calculate_digest ();
190 boost::mutex::scoped_lock lm (_mutex);
193 _last_write_times.clear ();
194 BOOST_FOREACH (boost::filesystem::path i, _paths) {
195 _last_write_times.push_back (boost::filesystem::last_write_time(i));
200 Content::signal_change (ChangeType c, int p)
203 if (c == CHANGE_TYPE_PENDING || c == CHANGE_TYPE_CANCELLED) {
204 Change (c, shared_from_this(), p, _change_signals_frequent);
206 emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
208 } catch (boost::bad_weak_ptr &) {
209 /* This must be during construction; never mind */
214 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
216 /* video and audio content can modify its position */
219 video->modify_position (film, p);
222 /* Only allow the audio to modify if we have no video;
223 sometimes p can't be on an integer video AND audio frame,
224 and in these cases we want the video constraint to be
225 satisfied since (I think) the audio code is better able to
228 if (!video && audio) {
229 audio->modify_position (film, p);
232 ChangeSignaller<Content> cc (this, ContentProperty::POSITION);
235 boost::mutex::scoped_lock lm (_mutex);
236 if (p == _position && !force_emit) {
246 Content::set_trim_start (ContentTime t)
248 /* video and audio content can modify its start trim */
251 video->modify_trim_start (t);
254 /* See note in ::set_position */
255 if (!video && audio) {
256 audio->modify_trim_start (t);
259 ChangeSignaller<Content> cc (this, ContentProperty::TRIM_START);
262 boost::mutex::scoped_lock lm (_mutex);
268 Content::set_trim_end (ContentTime t)
270 ChangeSignaller<Content> cc (this, ContentProperty::TRIM_END);
273 boost::mutex::scoped_lock lm (_mutex);
280 Content::clone () const
282 /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
284 xmlpp::Node* node = doc.create_root_node ("Content");
287 /* notes is unused here (we assume) */
289 return content_factory (cxml::NodePtr(new cxml::Node(node)), Film::current_state_version, notes);
293 Content::technical_summary () const
295 string s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
296 if (_video_frame_rate) {
297 s += String::compose(" %1", *_video_frame_rate);
303 Content::length_after_trim (shared_ptr<const Film> film) const
305 return max (DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
308 /** @return string which changes when something about this content changes which affects
309 * the appearance of its video.
312 Content::identifier () const
316 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
317 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
323 Content::paths_valid () const
325 BOOST_FOREACH (boost::filesystem::path i, _paths) {
326 if (!boost::filesystem::exists (i)) {
335 Content::set_paths (vector<boost::filesystem::path> paths)
337 ChangeSignaller<Content> cc (this, ContentProperty::PATH);
340 boost::mutex::scoped_lock lm (_mutex);
342 _last_write_times.clear ();
343 BOOST_FOREACH (boost::filesystem::path i, _paths) {
344 _last_write_times.push_back (boost::filesystem::last_write_time(i));
350 Content::path_summary () const
352 /* XXX: should handle multiple paths more gracefully */
354 DCPOMATIC_ASSERT (number_of_paths ());
356 string s = path(0).filename().string ();
357 if (number_of_paths() > 1) {
364 /** @return a list of properties that might be interesting to the user */
366 Content::user_properties (shared_ptr<const Film> film) const
368 list<UserProperty> p;
369 add_properties (film, p);
373 /** @return DCP times of points within this content where a reel split could occur */
375 Content::reel_split_points (shared_ptr<const Film>) const
378 /* This is only called for video content and such content has its position forced
379 to start on a frame boundary.
381 t.push_back (position());
386 Content::set_video_frame_rate (double r)
388 ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
391 boost::mutex::scoped_lock lm (_mutex);
392 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
395 _video_frame_rate = r;
398 /* Make sure trim is still on a frame boundary */
400 set_trim_start (trim_start());
405 Content::unset_video_frame_rate ()
407 ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
410 boost::mutex::scoped_lock lm (_mutex);
411 _video_frame_rate = optional<double>();
416 Content::active_video_frame_rate (shared_ptr<const Film> film) const
419 boost::mutex::scoped_lock lm (_mutex);
420 if (_video_frame_rate) {
421 return _video_frame_rate.get ();
425 /* No frame rate specified, so assume this content has been
426 prepared for any concurrent video content or perhaps
429 return film->active_frame_rate_change(position()).source;
433 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
435 p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
437 if (_video_frame_rate) {
443 locale_convert<string> (_video_frame_rate.get(), 5),
444 _("frames per second")
450 UserProperty::GENERAL,
451 _("Prepared for video frame rate"),
452 locale_convert<string> (_video_frame_rate.get(), 5),
453 _("frames per second")
460 /** Take settings from the given content if it is of the correct type */
462 Content::take_settings_from (shared_ptr<const Content> c)
464 if (video && c->video) {
465 video->take_settings_from (c->video);
467 if (audio && c->audio) {
468 audio->take_settings_from (c->audio);
471 list<shared_ptr<TextContent> >::iterator i = text.begin ();
472 list<shared_ptr<TextContent> >::const_iterator j = c->text.begin ();
473 while (i != text.end() && j != c->text.end()) {
474 (*i)->take_settings_from (*j);
480 shared_ptr<TextContent>
481 Content::only_text () const
483 DCPOMATIC_ASSERT (text.size() < 2);
485 return shared_ptr<TextContent> ();
487 return text.front ();
490 shared_ptr<TextContent>
491 Content::text_of_original_type (TextType type) const
493 BOOST_FOREACH (shared_ptr<TextContent> i, text) {
494 if (i->original_type() == type) {
499 return shared_ptr<TextContent> ();
503 Content::add_path (boost::filesystem::path p)
505 boost::mutex::scoped_lock lm (_mutex);
506 _paths.push_back (p);
507 _last_write_times.push_back (boost::filesystem::last_write_time(p));