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/>.
22 /** @file src/lib/content.cc
23 * @brief Content class.
27 #include "audio_content.h"
28 #include "change_signaller.h"
29 #include "compose.hpp"
31 #include "content_factory.h"
32 #include "exceptions.h"
35 #include "text_content.h"
37 #include "video_content.h"
38 #include <dcp/locale_convert.h>
39 #include <dcp/raw_convert.h>
40 #include <libcxml/cxml.h>
41 #include <libxml++/libxml++.h>
42 #include <boost/thread/mutex.hpp>
50 using std::make_shared;
51 using std::shared_ptr;
54 using boost::optional;
55 using dcp::locale_convert;
56 using dcp::raw_convert;
57 using namespace dcpomatic;
60 int const ContentProperty::PATH = 400;
61 int const ContentProperty::POSITION = 401;
62 int const ContentProperty::LENGTH = 402;
63 int const ContentProperty::TRIM_START = 403;
64 int const ContentProperty::TRIM_END = 404;
65 int const ContentProperty::VIDEO_FRAME_RATE = 405;
69 : _change_signals_frequent (false)
75 Content::Content (DCPTime p)
77 , _change_signals_frequent (false)
83 Content::Content (boost::filesystem::path p)
84 : _change_signals_frequent (false)
90 Content::Content (cxml::ConstNodePtr node)
91 : _change_signals_frequent (false)
93 for (auto i: node->node_children("Path")) {
94 _paths.push_back (i->content());
95 auto const mod = i->optional_number_attribute<time_t>("mtime");
97 _last_write_times.push_back (*mod);
99 boost::system::error_code ec;
100 auto last_write = boost::filesystem::last_write_time(i->content(), ec);
101 _last_write_times.push_back (ec ? 0 : last_write);
104 _digest = node->optional_string_child ("Digest").get_value_or ("X");
105 _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
106 _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
107 _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
108 _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]);
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 auto 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()));
170 Content::calculate_digest () const
172 /* Some content files are very big, so we use a poor man's
173 digest here: a digest of the first and last 1e6 bytes with the
174 size of the first file tacked on the end as a string.
176 return simple_digest (paths());
181 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
184 job->sub (_("Computing digest"));
187 auto const d = calculate_digest ();
189 boost::mutex::scoped_lock lm (_mutex);
192 _last_write_times.clear ();
193 for (auto i: _paths) {
194 boost::system::error_code ec;
195 auto last_write = boost::filesystem::last_write_time(i, ec);
196 _last_write_times.push_back (ec ? 0 : last_write);
202 Content::signal_change (ChangeType c, int p)
205 if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
206 Change (c, shared_from_this(), p, _change_signals_frequent);
208 emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
210 } catch (std::bad_weak_ptr &) {
211 /* This must be during construction; never mind */
217 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
219 /* video and audio content can modify its position */
222 video->modify_position (film, p);
225 /* Only allow the audio to modify if we have no video;
226 sometimes p can't be on an integer video AND audio frame,
227 and in these cases we want the video constraint to be
228 satisfied since (I think) the audio code is better able to
231 if (!video && audio) {
232 audio->modify_position (film, p);
235 ContentChangeSignaller cc (this, ContentProperty::POSITION);
238 boost::mutex::scoped_lock lm (_mutex);
239 if (p == _position && !force_emit) {
250 Content::set_trim_start (ContentTime t)
252 DCPOMATIC_ASSERT (t.get() >= 0);
254 /* video and audio content can modify its start trim */
257 video->modify_trim_start (t);
260 /* See note in ::set_position */
261 if (!video && audio) {
262 audio->modify_trim_start (t);
265 ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
268 boost::mutex::scoped_lock lm (_mutex);
275 Content::set_trim_end (ContentTime t)
277 DCPOMATIC_ASSERT (t.get() >= 0);
279 ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
282 boost::mutex::scoped_lock lm (_mutex);
289 Content::clone () const
291 /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
293 auto node = doc.create_root_node ("Content");
296 /* notes is unused here (we assume) */
298 return content_factory (make_shared<cxml::Node>(node), Film::current_state_version, notes);
303 Content::technical_summary () const
305 auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
306 if (_video_frame_rate) {
307 s += String::compose(" %1", *_video_frame_rate);
314 Content::length_after_trim (shared_ptr<const Film> film) const
316 auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
318 length = length.round(film->video_frame_rate());
324 /** @return string which changes when something about this content changes which affects
325 * the appearance of its video.
328 Content::identifier () const
332 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
333 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
340 Content::paths_valid () const
342 for (auto i: _paths) {
343 if (!boost::filesystem::exists (i)) {
353 Content::set_paths (vector<boost::filesystem::path> paths)
355 ContentChangeSignaller cc (this, ContentProperty::PATH);
358 boost::mutex::scoped_lock lm (_mutex);
360 _last_write_times.clear ();
361 for (auto i: _paths) {
362 boost::system::error_code ec;
363 auto last_write = boost::filesystem::last_write_time(i, ec);
364 _last_write_times.push_back (ec ? 0 : last_write);
371 Content::path_summary () const
373 /* XXX: should handle multiple paths more gracefully */
375 DCPOMATIC_ASSERT (number_of_paths ());
377 auto s = path(0).filename().string();
378 if (number_of_paths() > 1) {
386 /** @return a list of properties that might be interesting to the user */
388 Content::user_properties (shared_ptr<const Film> film) const
390 list<UserProperty> p;
391 add_properties (film, p);
396 /** @return DCP times of points within this content where a reel split could occur */
398 Content::reel_split_points (shared_ptr<const Film>) const
401 /* This is only called for video content and such content has its position forced
402 to start on a frame boundary.
404 t.push_back (position());
410 Content::set_video_frame_rate (double r)
412 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
415 boost::mutex::scoped_lock lm (_mutex);
416 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
419 _video_frame_rate = r;
422 /* Make sure trim is still on a frame boundary */
424 set_trim_start (trim_start());
430 Content::unset_video_frame_rate ()
432 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
435 boost::mutex::scoped_lock lm (_mutex);
436 _video_frame_rate = optional<double>();
442 Content::active_video_frame_rate (shared_ptr<const Film> film) const
445 boost::mutex::scoped_lock lm (_mutex);
446 if (_video_frame_rate) {
447 return _video_frame_rate.get ();
451 /* No frame rate specified, so assume this content has been
452 prepared for any concurrent video content or perhaps
455 return film->active_frame_rate_change(position()).source;
460 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
462 auto paths_to_show = std::min(number_of_paths(), size_t{8});
464 for (auto i = size_t{0}; i < paths_to_show; ++i) {
465 paths += path(i).string();
466 if (i < (paths_to_show - 1)) {
470 if (paths_to_show < number_of_paths()) {
471 paths += String::compose("... and %1 more", number_of_paths() - paths_to_show);
475 UserProperty::GENERAL,
476 paths_to_show > 1 ? _("Filenames") : _("Filename"),
481 if (_video_frame_rate) {
487 locale_convert<string> (_video_frame_rate.get(), 5),
488 _("frames per second")
494 UserProperty::GENERAL,
495 _("Prepared for video frame rate"),
496 locale_convert<string> (_video_frame_rate.get(), 5),
497 _("frames per second")
505 /** Take settings from the given content if it is of the correct type */
507 Content::take_settings_from (shared_ptr<const Content> c)
509 if (video && c->video) {
510 video->take_settings_from (c->video);
512 if (audio && c->audio) {
513 audio->take_settings_from (c->audio);
516 auto i = text.begin ();
517 auto j = c->text.begin ();
518 while (i != text.end() && j != c->text.end()) {
519 (*i)->take_settings_from (*j);
526 shared_ptr<TextContent>
527 Content::only_text () const
529 DCPOMATIC_ASSERT (text.size() < 2);
533 return text.front ();
537 shared_ptr<TextContent>
538 Content::text_of_original_type (TextType type) const
541 if (i->original_type() == type) {
551 Content::add_path (boost::filesystem::path p)
553 boost::mutex::scoped_lock lm (_mutex);
554 _paths.push_back (p);
555 boost::system::error_code ec;
556 auto last_write = boost::filesystem::last_write_time(p, ec);
557 _last_write_times.push_back (ec ? 0 : last_write);
562 Content::changed () const
564 bool write_time_changed = false;
565 for (auto i = 0U; i < _paths.size(); ++i) {
566 if (boost::filesystem::last_write_time(_paths[i]) != last_write_time(i)) {
567 write_time_changed = true;
572 return (write_time_changed || calculate_digest() != digest());