2 Copyright (C) 2013-2021 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 std::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 for (auto i: node->node_children("Path")) {
95 _paths.push_back (i->content());
96 auto 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);
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 auto const d = calculate_digest ();
190 boost::mutex::scoped_lock lm (_mutex);
193 _last_write_times.clear ();
194 for (auto 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 == ChangeType::PENDING || c == ChangeType::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 (std::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 auto 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 auto 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 auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
307 length = length.round(film->video_frame_rate());
312 /** @return string which changes when something about this content changes which affects
313 * the appearance of its video.
316 Content::identifier () const
320 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
321 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
327 Content::paths_valid () const
329 for (auto i: _paths) {
330 if (!boost::filesystem::exists (i)) {
339 Content::set_paths (vector<boost::filesystem::path> paths)
341 ChangeSignaller<Content> cc (this, ContentProperty::PATH);
344 boost::mutex::scoped_lock lm (_mutex);
346 _last_write_times.clear ();
347 for (auto i: _paths) {
348 _last_write_times.push_back (boost::filesystem::last_write_time(i));
354 Content::path_summary () const
356 /* XXX: should handle multiple paths more gracefully */
358 DCPOMATIC_ASSERT (number_of_paths ());
360 auto s = path(0).filename().string();
361 if (number_of_paths() > 1) {
368 /** @return a list of properties that might be interesting to the user */
370 Content::user_properties (shared_ptr<const Film> film) const
372 list<UserProperty> p;
373 add_properties (film, p);
377 /** @return DCP times of points within this content where a reel split could occur */
379 Content::reel_split_points (shared_ptr<const Film>) const
382 /* This is only called for video content and such content has its position forced
383 to start on a frame boundary.
385 t.push_back (position());
390 Content::set_video_frame_rate (double r)
392 ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
395 boost::mutex::scoped_lock lm (_mutex);
396 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
399 _video_frame_rate = r;
402 /* Make sure trim is still on a frame boundary */
404 set_trim_start (trim_start());
409 Content::unset_video_frame_rate ()
411 ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
414 boost::mutex::scoped_lock lm (_mutex);
415 _video_frame_rate = optional<double>();
420 Content::active_video_frame_rate (shared_ptr<const Film> film) const
423 boost::mutex::scoped_lock lm (_mutex);
424 if (_video_frame_rate) {
425 return _video_frame_rate.get ();
429 /* No frame rate specified, so assume this content has been
430 prepared for any concurrent video content or perhaps
433 return film->active_frame_rate_change(position()).source;
437 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
439 p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
441 if (_video_frame_rate) {
447 locale_convert<string> (_video_frame_rate.get(), 5),
448 _("frames per second")
454 UserProperty::GENERAL,
455 _("Prepared for video frame rate"),
456 locale_convert<string> (_video_frame_rate.get(), 5),
457 _("frames per second")
464 /** Take settings from the given content if it is of the correct type */
466 Content::take_settings_from (shared_ptr<const Content> c)
468 if (video && c->video) {
469 video->take_settings_from (c->video);
471 if (audio && c->audio) {
472 audio->take_settings_from (c->audio);
475 auto i = text.begin ();
476 auto j = c->text.begin ();
477 while (i != text.end() && j != c->text.end()) {
478 (*i)->take_settings_from (*j);
484 shared_ptr<TextContent>
485 Content::only_text () const
487 DCPOMATIC_ASSERT (text.size() < 2);
489 return shared_ptr<TextContent> ();
491 return text.front ();
494 shared_ptr<TextContent>
495 Content::text_of_original_type (TextType type) const
498 if (i->original_type() == type) {
503 return shared_ptr<TextContent> ();
507 Content::add_path (boost::filesystem::path p)
509 boost::mutex::scoped_lock lm (_mutex);
510 _paths.push_back (p);
511 _last_write_times.push_back (boost::filesystem::last_write_time(p));