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;
74 Content::Content (DCPTime p)
81 Content::Content (boost::filesystem::path p)
87 Content::Content (cxml::ConstNodePtr node)
89 for (auto i: node->node_children("Path")) {
90 _paths.push_back (i->content());
91 auto const mod = i->optional_number_attribute<time_t>("mtime");
93 _last_write_times.push_back (*mod);
95 boost::system::error_code ec;
96 auto last_write = dcp::filesystem::last_write_time(i->content(), ec);
97 _last_write_times.push_back (ec ? 0 : last_write);
100 _digest = node->optional_string_child ("Digest").get_value_or ("X");
101 _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
102 _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
103 _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
104 _video_frame_rate = node->optional_number_child<double> ("VideoFrameRate");
108 Content::Content (vector<shared_ptr<Content>> c)
109 : _position (c.front()->position())
110 , _trim_start (c.front()->trim_start())
111 , _trim_end (c.back()->trim_end())
112 , _video_frame_rate (c.front()->video_frame_rate())
114 for (size_t i = 0; i < c.size(); ++i) {
115 if (i > 0 && c[i]->trim_start() > ContentTime ()) {
116 throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
119 if (i < (c.size() - 1) && c[i]->trim_end () > ContentTime ()) {
120 throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
124 (_video_frame_rate && !c[i]->_video_frame_rate) ||
125 (!_video_frame_rate && c[i]->_video_frame_rate)
127 throw JoinError (_("Content to be joined must have the same video frame rate"));
130 if (_video_frame_rate && fabs (_video_frame_rate.get() - c[i]->_video_frame_rate.get()) > VIDEO_FRAME_RATE_EPSILON) {
131 throw JoinError (_("Content to be joined must have the same video frame rate"));
134 for (size_t j = 0; j < c[i]->number_of_paths(); ++j) {
135 _paths.push_back (c[i]->path(j));
136 _last_write_times.push_back (c[i]->_last_write_times[j]);
143 Content::as_xml (xmlpp::Node* node, bool with_paths) const
145 boost::mutex::scoped_lock lm (_mutex);
148 for (size_t i = 0; i < _paths.size(); ++i) {
149 auto p = node->add_child("Path");
150 p->add_child_text (_paths[i].string());
151 p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
154 node->add_child("Digest")->add_child_text(_digest);
155 node->add_child("Position")->add_child_text(raw_convert<string>(_position.get()));
156 node->add_child("TrimStart")->add_child_text(raw_convert<string>(_trim_start.get()));
157 node->add_child("TrimEnd")->add_child_text(raw_convert<string>(_trim_end.get()));
158 if (_video_frame_rate) {
159 node->add_child("VideoFrameRate")->add_child_text(raw_convert<string>(_video_frame_rate.get()));
165 Content::calculate_digest () const
167 /* Some content files are very big, so we use a poor man's
168 digest here: a digest of the first and last 1e6 bytes with the
169 size of the first file tacked on the end as a string.
171 return simple_digest (paths());
176 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
179 job->sub (_("Computing digest"));
182 auto const d = calculate_digest ();
184 boost::mutex::scoped_lock lm (_mutex);
187 _last_write_times.clear ();
188 for (auto i: _paths) {
189 boost::system::error_code ec;
190 auto last_write = dcp::filesystem::last_write_time(i, ec);
191 _last_write_times.push_back (ec ? 0 : last_write);
197 Content::signal_change (ChangeType c, int p)
200 if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
201 Change (c, shared_from_this(), p, _change_signals_frequent);
203 emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
205 } catch (std::bad_weak_ptr &) {
206 /* This must be during construction; never mind */
212 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
214 /* video and audio content can modify its position */
217 video->modify_position (film, p);
220 /* Only allow the audio to modify if we have no video;
221 sometimes p can't be on an integer video AND audio frame,
222 and in these cases we want the video constraint to be
223 satisfied since (I think) the audio code is better able to
226 if (!video && audio) {
227 audio->modify_position (film, p);
230 ContentChangeSignaller cc (this, ContentProperty::POSITION);
233 boost::mutex::scoped_lock lm (_mutex);
234 if (p == _position && !force_emit) {
245 Content::set_trim_start(shared_ptr<const Film> film, ContentTime t)
247 DCPOMATIC_ASSERT (t.get() >= 0);
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(film, t);
260 ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
263 boost::mutex::scoped_lock lm (_mutex);
264 if (_trim_start == t) {
274 Content::set_trim_end (ContentTime t)
276 DCPOMATIC_ASSERT (t.get() >= 0);
278 ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
281 boost::mutex::scoped_lock lm (_mutex);
288 Content::clone () const
290 /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
292 auto node = doc.create_root_node ("Content");
295 /* notes is unused here (we assume) */
297 return content_factory (make_shared<cxml::Node>(node), Film::current_state_version, notes);
302 Content::technical_summary () const
304 auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
305 if (_video_frame_rate) {
306 s += String::compose(" %1", *_video_frame_rate);
313 Content::length_after_trim (shared_ptr<const Film> film) const
315 auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
317 length = length.round(film->video_frame_rate());
323 /** @return string which changes when something about this content changes which affects
324 * the appearance of its video.
327 Content::identifier () const
331 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
332 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
339 Content::paths_valid () const
341 for (auto i: _paths) {
342 if (!dcp::filesystem::exists(i)) {
352 Content::set_paths (vector<boost::filesystem::path> paths)
354 ContentChangeSignaller cc (this, ContentProperty::PATH);
357 boost::mutex::scoped_lock lm (_mutex);
359 _last_write_times.clear ();
360 for (auto i: _paths) {
361 boost::system::error_code ec;
362 auto last_write = dcp::filesystem::last_write_time(i, ec);
363 _last_write_times.push_back (ec ? 0 : last_write);
370 Content::path_summary () const
372 /* XXX: should handle multiple paths more gracefully */
374 DCPOMATIC_ASSERT (number_of_paths ());
376 auto s = path(0).filename().string();
377 if (number_of_paths() > 1) {
385 /** @return a list of properties that might be interesting to the user */
387 Content::user_properties (shared_ptr<const Film> film) const
389 list<UserProperty> p;
390 add_properties (film, p);
395 /** @return DCP times of points within this content where a reel split could occur */
397 Content::reel_split_points (shared_ptr<const Film>) const
400 /* This is only called for video content and such content has its position forced
401 to start on a frame boundary.
403 t.push_back (position());
409 Content::set_video_frame_rate(shared_ptr<const Film> film, double r)
411 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
414 boost::mutex::scoped_lock lm (_mutex);
415 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
418 _video_frame_rate = r;
421 /* Make sure trim is still on a frame boundary */
423 set_trim_start(film, trim_start());
429 Content::unset_video_frame_rate ()
431 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
434 boost::mutex::scoped_lock lm (_mutex);
435 _video_frame_rate = optional<double>();
441 Content::active_video_frame_rate (shared_ptr<const Film> film) const
444 boost::mutex::scoped_lock lm (_mutex);
445 if (_video_frame_rate) {
446 return _video_frame_rate.get ();
450 /* No frame rate specified, so assume this content has been
451 prepared for any concurrent video content or perhaps
454 return film->active_frame_rate_change(position()).source;
459 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
461 auto paths_to_show = std::min(number_of_paths(), size_t{8});
463 for (auto i = size_t{0}; i < paths_to_show; ++i) {
464 paths += path(i).string();
465 if (i < (paths_to_show - 1)) {
469 if (paths_to_show < number_of_paths()) {
470 paths += String::compose("... and %1 more", number_of_paths() - paths_to_show);
474 UserProperty::GENERAL,
475 paths_to_show > 1 ? _("Filenames") : _("Filename"),
480 if (_video_frame_rate) {
486 locale_convert<string> (_video_frame_rate.get(), 5),
487 _("frames per second")
493 UserProperty::GENERAL,
494 _("Prepared for video frame rate"),
495 locale_convert<string> (_video_frame_rate.get(), 5),
496 _("frames per second")
504 /** Take settings from the given content if it is of the correct type */
506 Content::take_settings_from (shared_ptr<const Content> c)
508 if (video && c->video) {
509 video->take_settings_from (c->video);
511 if (audio && c->audio) {
512 audio->take_settings_from (c->audio);
515 auto i = text.begin ();
516 auto j = c->text.begin ();
517 while (i != text.end() && j != c->text.end()) {
518 (*i)->take_settings_from (*j);
525 shared_ptr<TextContent>
526 Content::only_text () const
528 DCPOMATIC_ASSERT (text.size() < 2);
532 return text.front ();
536 shared_ptr<TextContent>
537 Content::text_of_original_type (TextType type) const
540 if (i->original_type() == type) {
550 Content::add_path (boost::filesystem::path p)
552 boost::mutex::scoped_lock lm (_mutex);
553 _paths.push_back (p);
554 boost::system::error_code ec;
555 auto last_write = dcp::filesystem::last_write_time(p, ec);
556 _last_write_times.push_back (ec ? 0 : last_write);
561 Content::changed () const
563 bool write_time_changed = false;
564 for (auto i = 0U; i < _paths.size(); ++i) {
565 if (dcp::filesystem::last_write_time(_paths[i]) != last_write_time(i)) {
566 write_time_changed = true;
571 return (write_time_changed || calculate_digest() != digest());