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.
28 #include "change_signaller.h"
30 #include "content_factory.h"
31 #include "video_content.h"
32 #include "audio_content.h"
33 #include "text_content.h"
34 #include "exceptions.h"
37 #include "compose.hpp"
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 boost::mutex::scoped_lock lm (_mutex);
176 /* Some content files are very big, so we use a poor man's
177 digest here: a digest of the first and last 1e6 bytes with the
178 size of the first file tacked on the end as a string.
180 return digest_head_tail(p, 1000000) + raw_convert<string>(boost::filesystem::file_size(p.front()));
185 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
188 job->sub (_("Computing digest"));
191 auto const d = calculate_digest ();
193 boost::mutex::scoped_lock lm (_mutex);
196 _last_write_times.clear ();
197 for (auto i: _paths) {
198 boost::system::error_code ec;
199 auto last_write = boost::filesystem::last_write_time(i, ec);
200 _last_write_times.push_back (ec ? 0 : last_write);
206 Content::signal_change (ChangeType c, int p)
209 if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
210 Change (c, shared_from_this(), p, _change_signals_frequent);
212 emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
214 } catch (std::bad_weak_ptr &) {
215 /* This must be during construction; never mind */
221 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
223 /* video and audio content can modify its position */
226 video->modify_position (film, p);
229 /* Only allow the audio to modify if we have no video;
230 sometimes p can't be on an integer video AND audio frame,
231 and in these cases we want the video constraint to be
232 satisfied since (I think) the audio code is better able to
235 if (!video && audio) {
236 audio->modify_position (film, p);
239 ContentChangeSignaller cc (this, ContentProperty::POSITION);
242 boost::mutex::scoped_lock lm (_mutex);
243 if (p == _position && !force_emit) {
254 Content::set_trim_start (ContentTime t)
256 DCPOMATIC_ASSERT (t.get() >= 0);
258 /* video and audio content can modify its start trim */
261 video->modify_trim_start (t);
264 /* See note in ::set_position */
265 if (!video && audio) {
266 audio->modify_trim_start (t);
269 ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
272 boost::mutex::scoped_lock lm (_mutex);
279 Content::set_trim_end (ContentTime t)
281 DCPOMATIC_ASSERT (t.get() >= 0);
283 ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
286 boost::mutex::scoped_lock lm (_mutex);
293 Content::clone () const
295 /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
297 auto node = doc.create_root_node ("Content");
300 /* notes is unused here (we assume) */
302 return content_factory (make_shared<cxml::Node>(node), Film::current_state_version, notes);
307 Content::technical_summary () const
309 auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
310 if (_video_frame_rate) {
311 s += String::compose(" %1", *_video_frame_rate);
318 Content::length_after_trim (shared_ptr<const Film> film) const
320 auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
322 length = length.round(film->video_frame_rate());
328 /** @return string which changes when something about this content changes which affects
329 * the appearance of its video.
332 Content::identifier () const
336 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
337 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
344 Content::paths_valid () const
346 for (auto i: _paths) {
347 if (!boost::filesystem::exists (i)) {
357 Content::set_paths (vector<boost::filesystem::path> paths)
359 ContentChangeSignaller cc (this, ContentProperty::PATH);
362 boost::mutex::scoped_lock lm (_mutex);
364 _last_write_times.clear ();
365 for (auto i: _paths) {
366 boost::system::error_code ec;
367 auto last_write = boost::filesystem::last_write_time(i, ec);
368 _last_write_times.push_back (ec ? 0 : last_write);
375 Content::path_summary () const
377 /* XXX: should handle multiple paths more gracefully */
379 DCPOMATIC_ASSERT (number_of_paths ());
381 auto s = path(0).filename().string();
382 if (number_of_paths() > 1) {
390 /** @return a list of properties that might be interesting to the user */
392 Content::user_properties (shared_ptr<const Film> film) const
394 list<UserProperty> p;
395 add_properties (film, p);
400 /** @return DCP times of points within this content where a reel split could occur */
402 Content::reel_split_points (shared_ptr<const Film>) const
405 /* This is only called for video content and such content has its position forced
406 to start on a frame boundary.
408 t.push_back (position());
414 Content::set_video_frame_rate (double r)
416 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
419 boost::mutex::scoped_lock lm (_mutex);
420 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
423 _video_frame_rate = r;
426 /* Make sure trim is still on a frame boundary */
428 set_trim_start (trim_start());
434 Content::unset_video_frame_rate ()
436 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
439 boost::mutex::scoped_lock lm (_mutex);
440 _video_frame_rate = optional<double>();
446 Content::active_video_frame_rate (shared_ptr<const Film> film) const
449 boost::mutex::scoped_lock lm (_mutex);
450 if (_video_frame_rate) {
451 return _video_frame_rate.get ();
455 /* No frame rate specified, so assume this content has been
456 prepared for any concurrent video content or perhaps
459 return film->active_frame_rate_change(position()).source;
464 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
466 auto paths_to_show = std::min(number_of_paths(), size_t{8});
468 for (auto i = size_t{0}; i < paths_to_show; ++i) {
469 paths += path(i).string();
470 if (i < (paths_to_show - 1)) {
474 if (paths_to_show < number_of_paths()) {
475 paths += String::compose("... and %1 more", number_of_paths() - paths_to_show);
479 UserProperty::GENERAL,
480 paths_to_show > 1 ? _("Filenames") : _("Filename"),
485 if (_video_frame_rate) {
491 locale_convert<string> (_video_frame_rate.get(), 5),
492 _("frames per second")
498 UserProperty::GENERAL,
499 _("Prepared for video frame rate"),
500 locale_convert<string> (_video_frame_rate.get(), 5),
501 _("frames per second")
509 /** Take settings from the given content if it is of the correct type */
511 Content::take_settings_from (shared_ptr<const Content> c)
513 if (video && c->video) {
514 video->take_settings_from (c->video);
516 if (audio && c->audio) {
517 audio->take_settings_from (c->audio);
520 auto i = text.begin ();
521 auto j = c->text.begin ();
522 while (i != text.end() && j != c->text.end()) {
523 (*i)->take_settings_from (*j);
530 shared_ptr<TextContent>
531 Content::only_text () const
533 DCPOMATIC_ASSERT (text.size() < 2);
537 return text.front ();
541 shared_ptr<TextContent>
542 Content::text_of_original_type (TextType type) const
545 if (i->original_type() == type) {
555 Content::add_path (boost::filesystem::path p)
557 boost::mutex::scoped_lock lm (_mutex);
558 _paths.push_back (p);
559 boost::system::error_code ec;
560 auto last_write = boost::filesystem::last_write_time(p, ec);
561 _last_write_times.push_back (ec ? 0 : last_write);
566 Content::changed () const
568 bool write_time_changed = false;
569 for (auto i = 0U; i < _paths.size(); ++i) {
570 if (boost::filesystem::last_write_time(_paths[i]) != last_write_time(i)) {
571 write_time_changed = true;
576 return (write_time_changed || calculate_digest() != digest());