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 "dcpomatic_log.h"
33 #include "exceptions.h"
36 #include "text_content.h"
38 #include "video_content.h"
39 #include <dcp/locale_convert.h>
40 #include <dcp/raw_convert.h>
41 #include <libcxml/cxml.h>
42 #include <libxml++/libxml++.h>
43 #include <boost/thread/mutex.hpp>
51 using std::make_shared;
52 using std::shared_ptr;
55 using boost::optional;
56 using dcp::locale_convert;
57 using dcp::raw_convert;
58 using namespace dcpomatic;
61 int const ContentProperty::PATH = 400;
62 int const ContentProperty::POSITION = 401;
63 int const ContentProperty::LENGTH = 402;
64 int const ContentProperty::TRIM_START = 403;
65 int const ContentProperty::TRIM_END = 404;
66 int const ContentProperty::VIDEO_FRAME_RATE = 405;
75 Content::Content (DCPTime p)
82 Content::Content (boost::filesystem::path p)
88 Content::Content (cxml::ConstNodePtr node)
90 for (auto i: node->node_children("Path")) {
91 _paths.push_back (i->content());
92 auto const mod = i->optional_number_attribute<time_t>("mtime");
94 _last_write_times.push_back (*mod);
96 boost::system::error_code ec;
97 auto last_write = boost::filesystem::last_write_time(i->content(), ec);
98 _last_write_times.push_back (ec ? 0 : last_write);
101 _digest = node->optional_string_child ("Digest").get_value_or ("X");
102 _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
103 _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
104 _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
105 _video_frame_rate = node->optional_number_child<double> ("VideoFrameRate");
109 Content::Content (vector<shared_ptr<Content>> c)
110 : _position (c.front()->position())
111 , _trim_start (c.front()->trim_start())
112 , _trim_end (c.back()->trim_end())
113 , _video_frame_rate (c.front()->video_frame_rate())
115 for (size_t i = 0; i < c.size(); ++i) {
116 if (i > 0 && c[i]->trim_start() > ContentTime ()) {
117 throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
120 if (i < (c.size() - 1) && c[i]->trim_end () > ContentTime ()) {
121 throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
125 (_video_frame_rate && !c[i]->_video_frame_rate) ||
126 (!_video_frame_rate && c[i]->_video_frame_rate)
128 throw JoinError (_("Content to be joined must have the same video frame rate"));
131 if (_video_frame_rate && fabs (_video_frame_rate.get() - c[i]->_video_frame_rate.get()) > VIDEO_FRAME_RATE_EPSILON) {
132 throw JoinError (_("Content to be joined must have the same video frame rate"));
135 for (size_t j = 0; j < c[i]->number_of_paths(); ++j) {
136 _paths.push_back (c[i]->path(j));
137 _last_write_times.push_back (c[i]->_last_write_times[j]);
144 Content::as_xml (xmlpp::Node* node, bool with_paths) const
146 boost::mutex::scoped_lock lm (_mutex);
149 for (size_t i = 0; i < _paths.size(); ++i) {
150 auto p = node->add_child("Path");
151 p->add_child_text (_paths[i].string());
152 p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
155 node->add_child("Digest")->add_child_text(_digest);
156 node->add_child("Position")->add_child_text(raw_convert<string>(_position.get()));
157 node->add_child("TrimStart")->add_child_text(raw_convert<string>(_trim_start.get()));
158 node->add_child("TrimEnd")->add_child_text(raw_convert<string>(_trim_end.get()));
159 if (_video_frame_rate) {
160 node->add_child("VideoFrameRate")->add_child_text(raw_convert<string>(_video_frame_rate.get()));
166 Content::calculate_digest () const
168 /* Some content files are very big, so we use a poor man's
169 digest here: a digest of the first and last 1e6 bytes with the
170 size of the first file tacked on the end as a string.
172 return simple_digest (paths());
177 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
180 job->sub (_("Computing digest"));
183 auto const d = calculate_digest ();
185 boost::mutex::scoped_lock lm (_mutex);
188 _last_write_times.clear ();
189 for (auto i: _paths) {
190 boost::system::error_code ec;
191 auto last_write = boost::filesystem::last_write_time(i, ec);
192 _last_write_times.push_back (ec ? 0 : last_write);
198 Content::signal_change (ChangeType c, int p)
201 if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
202 Change (c, shared_from_this(), p, _change_signals_frequent);
204 emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
206 } catch (std::bad_weak_ptr &) {
207 /* This must be during construction; never mind */
213 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
215 /* video and audio content can modify its position */
218 video->modify_position (film, p);
221 /* Only allow the audio to modify if we have no video;
222 sometimes p can't be on an integer video AND audio frame,
223 and in these cases we want the video constraint to be
224 satisfied since (I think) the audio code is better able to
227 if (!video && audio) {
228 audio->modify_position (film, p);
231 ContentChangeSignaller cc (this, ContentProperty::POSITION);
234 boost::mutex::scoped_lock lm (_mutex);
235 if (p == _position && !force_emit) {
246 Content::set_trim_start(shared_ptr<const Film> film, ContentTime t)
248 DCPOMATIC_ASSERT (t.get() >= 0);
250 /* video and audio content can modify its start trim */
253 video->modify_trim_start (t);
256 /* See note in ::set_position */
257 if (!video && audio) {
258 audio->modify_trim_start(film, t);
261 ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
264 boost::mutex::scoped_lock lm (_mutex);
265 if (_trim_start == t) {
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(shared_ptr<const Film> film, 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(film, 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 auto const current_last_write_time = boost::filesystem::last_write_time(_paths[i]);
567 if (current_last_write_time != last_write_time(i)) {
568 LOG_GENERAL("Content %1 write time changed from %2 to %3", _paths[i], last_write_time(i), current_last_write_time);
569 write_time_changed = true;
574 /* Only check digest if the write time is the same */
575 auto const digest_changed = !write_time_changed && (calculate_digest() != digest());
576 if (digest_changed) {
577 LOG_GENERAL("Content %1 digest changed", _paths[0]);
580 return (write_time_changed || digest_changed);