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 return max (DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
309 /** @return string which changes when something about this content changes which affects
310 * the appearance of its video.
313 Content::identifier () const
317 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
318 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
324 Content::paths_valid () const
326 BOOST_FOREACH (boost::filesystem::path i, _paths) {
327 if (!boost::filesystem::exists (i)) {
336 Content::set_paths (vector<boost::filesystem::path> paths)
338 ChangeSignaller<Content> cc (this, ContentProperty::PATH);
341 boost::mutex::scoped_lock lm (_mutex);
343 _last_write_times.clear ();
344 BOOST_FOREACH (boost::filesystem::path i, _paths) {
345 _last_write_times.push_back (boost::filesystem::last_write_time(i));
351 Content::path_summary () const
353 /* XXX: should handle multiple paths more gracefully */
355 DCPOMATIC_ASSERT (number_of_paths ());
357 string s = path(0).filename().string ();
358 if (number_of_paths() > 1) {
365 /** @return a list of properties that might be interesting to the user */
367 Content::user_properties (shared_ptr<const Film> film) const
369 list<UserProperty> p;
370 add_properties (film, p);
374 /** @return DCP times of points within this content where a reel split could occur */
376 Content::reel_split_points (shared_ptr<const Film>) const
379 /* This is only called for video content and such content has its position forced
380 to start on a frame boundary.
382 t.push_back (position());
387 Content::set_video_frame_rate (double r)
389 ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
392 boost::mutex::scoped_lock lm (_mutex);
393 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
396 _video_frame_rate = r;
399 /* Make sure trim is still on a frame boundary */
401 set_trim_start (trim_start());
406 Content::unset_video_frame_rate ()
408 ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
411 boost::mutex::scoped_lock lm (_mutex);
412 _video_frame_rate = optional<double>();
417 Content::active_video_frame_rate (shared_ptr<const Film> film) const
420 boost::mutex::scoped_lock lm (_mutex);
421 if (_video_frame_rate) {
422 return _video_frame_rate.get ();
426 /* No frame rate specified, so assume this content has been
427 prepared for any concurrent video content or perhaps
430 return film->active_frame_rate_change(position()).source;
434 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
436 p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
438 if (_video_frame_rate) {
444 locale_convert<string> (_video_frame_rate.get(), 5),
445 _("frames per second")
451 UserProperty::GENERAL,
452 _("Prepared for video frame rate"),
453 locale_convert<string> (_video_frame_rate.get(), 5),
454 _("frames per second")
461 /** Take settings from the given content if it is of the correct type */
463 Content::take_settings_from (shared_ptr<const Content> c)
465 if (video && c->video) {
466 video->take_settings_from (c->video);
468 if (audio && c->audio) {
469 audio->take_settings_from (c->audio);
472 list<shared_ptr<TextContent> >::iterator i = text.begin ();
473 list<shared_ptr<TextContent> >::const_iterator j = c->text.begin ();
474 while (i != text.end() && j != c->text.end()) {
475 (*i)->take_settings_from (*j);
481 shared_ptr<TextContent>
482 Content::only_text () const
484 DCPOMATIC_ASSERT (text.size() < 2);
486 return shared_ptr<TextContent> ();
488 return text.front ();
491 shared_ptr<TextContent>
492 Content::text_of_original_type (TextType type) const
494 BOOST_FOREACH (shared_ptr<TextContent> i, text) {
495 if (i->original_type() == type) {
500 return shared_ptr<TextContent> ();
504 Content::add_path (boost::filesystem::path p)
506 boost::mutex::scoped_lock lm (_mutex);
507 _paths.push_back (p);
508 _last_write_times.push_back (boost::filesystem::last_write_time(p));