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;
55 using namespace dcpomatic;
57 int const ContentProperty::PATH = 400;
58 int const ContentProperty::POSITION = 401;
59 int const ContentProperty::LENGTH = 402;
60 int const ContentProperty::TRIM_START = 403;
61 int const ContentProperty::TRIM_END = 404;
62 int const ContentProperty::VIDEO_FRAME_RATE = 405;
68 , _change_signals_frequent (false)
73 Content::Content (DCPTime p)
77 , _change_signals_frequent (false)
82 Content::Content (boost::filesystem::path p)
86 , _change_signals_frequent (false)
91 Content::Content (cxml::ConstNodePtr node)
92 : _change_signals_frequent (false)
94 list<cxml::NodePtr> path_children = node->node_children ("Path");
95 BOOST_FOREACH (cxml::NodePtr i, path_children) {
96 _paths.push_back (i->content());
97 optional<time_t> const mod = i->optional_number_attribute<time_t>("mtime");
99 _last_write_times.push_back (*mod);
100 } else if (boost::filesystem::exists(i->content())) {
101 _last_write_times.push_back (boost::filesystem::last_write_time(i->content()));
103 _last_write_times.push_back (0);
106 _digest = node->optional_string_child ("Digest").get_value_or ("X");
107 _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
108 _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
109 _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
110 _video_frame_rate = node->optional_number_child<double> ("VideoFrameRate");
113 Content::Content (vector<shared_ptr<Content> > c)
114 : _position (c.front()->position ())
115 , _trim_start (c.front()->trim_start ())
116 , _trim_end (c.back()->trim_end ())
117 , _video_frame_rate (c.front()->video_frame_rate())
118 , _change_signals_frequent (false)
120 for (size_t i = 0; i < c.size(); ++i) {
121 if (i > 0 && c[i]->trim_start() > ContentTime ()) {
122 throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
125 if (i < (c.size() - 1) && c[i]->trim_end () > ContentTime ()) {
126 throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
130 (_video_frame_rate && !c[i]->_video_frame_rate) ||
131 (!_video_frame_rate && c[i]->_video_frame_rate)
133 throw JoinError (_("Content to be joined must have the same video frame rate"));
136 if (_video_frame_rate && fabs (_video_frame_rate.get() - c[i]->_video_frame_rate.get()) > VIDEO_FRAME_RATE_EPSILON) {
137 throw JoinError (_("Content to be joined must have the same video frame rate"));
140 for (size_t j = 0; j < c[i]->number_of_paths(); ++j) {
141 _paths.push_back (c[i]->path(j));
142 _last_write_times.push_back (c[i]->_last_write_times[j]);
148 Content::as_xml (xmlpp::Node* node, bool with_paths) const
150 boost::mutex::scoped_lock lm (_mutex);
153 for (size_t i = 0; i < _paths.size(); ++i) {
154 xmlpp::Element* p = node->add_child("Path");
155 p->add_child_text (_paths[i].string());
156 p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
159 node->add_child("Digest")->add_child_text (_digest);
160 node->add_child("Position")->add_child_text (raw_convert<string> (_position.get ()));
161 node->add_child("TrimStart")->add_child_text (raw_convert<string> (_trim_start.get ()));
162 node->add_child("TrimEnd")->add_child_text (raw_convert<string> (_trim_end.get ()));
163 if (_video_frame_rate) {
164 node->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate.get()));
169 Content::calculate_digest () const
171 boost::mutex::scoped_lock lm (_mutex);
172 vector<boost::filesystem::path> p = _paths;
175 /* Some content files are very big, so we use a poor man's
176 digest here: a digest of the first and last 1e6 bytes with the
177 size of the first file tacked on the end as a string.
179 return digest_head_tail(p, 1000000) + raw_convert<string>(boost::filesystem::file_size(p.front()));
183 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
186 job->sub (_("Computing digest"));
189 string const d = calculate_digest ();
191 boost::mutex::scoped_lock lm (_mutex);
194 _last_write_times.clear ();
195 BOOST_FOREACH (boost::filesystem::path i, _paths) {
196 _last_write_times.push_back (boost::filesystem::last_write_time(i));
201 Content::signal_change (ChangeType c, int p)
204 if (c == CHANGE_TYPE_PENDING || c == CHANGE_TYPE_CANCELLED) {
205 Change (c, shared_from_this(), p, _change_signals_frequent);
207 emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
209 } catch (boost::bad_weak_ptr &) {
210 /* This must be during construction; never mind */
215 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
217 /* video and audio content can modify its position */
220 video->modify_position (film, p);
223 /* Only allow the audio to modify if we have no video;
224 sometimes p can't be on an integer video AND audio frame,
225 and in these cases we want the video constraint to be
226 satisfied since (I think) the audio code is better able to
229 if (!video && audio) {
230 audio->modify_position (film, p);
233 ChangeSignaller<Content> cc (this, ContentProperty::POSITION);
236 boost::mutex::scoped_lock lm (_mutex);
237 if (p == _position && !force_emit) {
247 Content::set_trim_start (ContentTime t)
249 /* video and audio content can modify its start trim */
252 video->modify_trim_start (t);
255 /* See note in ::set_position */
256 if (!video && audio) {
257 audio->modify_trim_start (t);
260 ChangeSignaller<Content> cc (this, ContentProperty::TRIM_START);
263 boost::mutex::scoped_lock lm (_mutex);
269 Content::set_trim_end (ContentTime t)
271 ChangeSignaller<Content> cc (this, ContentProperty::TRIM_END);
274 boost::mutex::scoped_lock lm (_mutex);
281 Content::clone () const
283 /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
285 xmlpp::Node* node = doc.create_root_node ("Content");
288 /* notes is unused here (we assume) */
290 return content_factory (cxml::NodePtr(new cxml::Node(node)), Film::current_state_version, notes);
294 Content::technical_summary () const
296 string s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
297 if (_video_frame_rate) {
298 s += String::compose(" %1", *_video_frame_rate);
304 Content::length_after_trim (shared_ptr<const Film> film) const
306 DCPTime length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
308 length = length.round(film->video_frame_rate());
313 /** @return string which changes when something about this content changes which affects
314 * the appearance of its video.
317 Content::identifier () const
321 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
322 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
328 Content::paths_valid () const
330 BOOST_FOREACH (boost::filesystem::path i, _paths) {
331 if (!boost::filesystem::exists (i)) {
340 Content::set_paths (vector<boost::filesystem::path> paths)
342 ChangeSignaller<Content> cc (this, ContentProperty::PATH);
345 boost::mutex::scoped_lock lm (_mutex);
347 _last_write_times.clear ();
348 BOOST_FOREACH (boost::filesystem::path i, _paths) {
349 _last_write_times.push_back (boost::filesystem::last_write_time(i));
355 Content::path_summary () const
357 /* XXX: should handle multiple paths more gracefully */
359 DCPOMATIC_ASSERT (number_of_paths ());
361 string s = path(0).filename().string ();
362 if (number_of_paths() > 1) {
369 /** @return a list of properties that might be interesting to the user */
371 Content::user_properties (shared_ptr<const Film> film) const
373 list<UserProperty> p;
374 add_properties (film, p);
378 /** @return DCP times of points within this content where a reel split could occur */
380 Content::reel_split_points (shared_ptr<const Film>) const
383 /* This is only called for video content and such content has its position forced
384 to start on a frame boundary.
386 t.push_back (position());
391 Content::set_video_frame_rate (double r)
393 ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
396 boost::mutex::scoped_lock lm (_mutex);
397 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
400 _video_frame_rate = r;
403 /* Make sure trim is still on a frame boundary */
405 set_trim_start (trim_start());
410 Content::unset_video_frame_rate ()
412 ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
415 boost::mutex::scoped_lock lm (_mutex);
416 _video_frame_rate = optional<double>();
421 Content::active_video_frame_rate (shared_ptr<const Film> film) const
424 boost::mutex::scoped_lock lm (_mutex);
425 if (_video_frame_rate) {
426 return _video_frame_rate.get ();
430 /* No frame rate specified, so assume this content has been
431 prepared for any concurrent video content or perhaps
434 return film->active_frame_rate_change(position()).source;
438 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
440 p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
442 if (_video_frame_rate) {
448 locale_convert<string> (_video_frame_rate.get(), 5),
449 _("frames per second")
455 UserProperty::GENERAL,
456 _("Prepared for video frame rate"),
457 locale_convert<string> (_video_frame_rate.get(), 5),
458 _("frames per second")
465 /** Take settings from the given content if it is of the correct type */
467 Content::take_settings_from (shared_ptr<const Content> c)
469 if (video && c->video) {
470 video->take_settings_from (c->video);
472 if (audio && c->audio) {
473 audio->take_settings_from (c->audio);
476 list<shared_ptr<TextContent> >::iterator i = text.begin ();
477 list<shared_ptr<TextContent> >::const_iterator j = c->text.begin ();
478 while (i != text.end() && j != c->text.end()) {
479 (*i)->take_settings_from (*j);
485 shared_ptr<TextContent>
486 Content::only_text () const
488 DCPOMATIC_ASSERT (text.size() < 2);
490 return shared_ptr<TextContent> ();
492 return text.front ();
495 shared_ptr<TextContent>
496 Content::text_of_original_type (TextType type) const
498 BOOST_FOREACH (shared_ptr<TextContent> i, text) {
499 if (i->original_type() == type) {
504 return shared_ptr<TextContent> ();
508 Content::add_path (boost::filesystem::path p)
510 boost::mutex::scoped_lock lm (_mutex);
511 _paths.push_back (p);
512 _last_write_times.push_back (boost::filesystem::last_write_time(p));