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 = boost::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 = boost::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);
270 Content::set_trim_end (ContentTime t)
272 DCPOMATIC_ASSERT (t.get() >= 0);
274 ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
277 boost::mutex::scoped_lock lm (_mutex);
284 Content::clone () const
286 /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
288 auto node = doc.create_root_node ("Content");
291 /* notes is unused here (we assume) */
293 return content_factory (make_shared<cxml::Node>(node), Film::current_state_version, notes);
298 Content::technical_summary () const
300 auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
301 if (_video_frame_rate) {
302 s += String::compose(" %1", *_video_frame_rate);
309 Content::length_after_trim (shared_ptr<const Film> film) const
311 auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
313 length = length.round(film->video_frame_rate());
319 /** @return string which changes when something about this content changes which affects
320 * the appearance of its video.
323 Content::identifier () const
327 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
328 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
335 Content::paths_valid () const
337 for (auto i: _paths) {
338 if (!boost::filesystem::exists (i)) {
348 Content::set_paths (vector<boost::filesystem::path> paths)
350 ContentChangeSignaller cc (this, ContentProperty::PATH);
353 boost::mutex::scoped_lock lm (_mutex);
355 _last_write_times.clear ();
356 for (auto i: _paths) {
357 boost::system::error_code ec;
358 auto last_write = boost::filesystem::last_write_time(i, ec);
359 _last_write_times.push_back (ec ? 0 : last_write);
366 Content::path_summary () const
368 /* XXX: should handle multiple paths more gracefully */
370 DCPOMATIC_ASSERT (number_of_paths ());
372 auto s = path(0).filename().string();
373 if (number_of_paths() > 1) {
381 /** @return a list of properties that might be interesting to the user */
383 Content::user_properties (shared_ptr<const Film> film) const
385 list<UserProperty> p;
386 add_properties (film, p);
391 /** @return DCP times of points within this content where a reel split could occur */
393 Content::reel_split_points (shared_ptr<const Film>) const
396 /* This is only called for video content and such content has its position forced
397 to start on a frame boundary.
399 t.push_back (position());
405 Content::set_video_frame_rate(shared_ptr<const Film> film, double r)
407 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
410 boost::mutex::scoped_lock lm (_mutex);
411 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
414 _video_frame_rate = r;
417 /* Make sure trim is still on a frame boundary */
419 set_trim_start(film, trim_start());
425 Content::unset_video_frame_rate ()
427 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
430 boost::mutex::scoped_lock lm (_mutex);
431 _video_frame_rate = optional<double>();
437 Content::active_video_frame_rate (shared_ptr<const Film> film) const
440 boost::mutex::scoped_lock lm (_mutex);
441 if (_video_frame_rate) {
442 return _video_frame_rate.get ();
446 /* No frame rate specified, so assume this content has been
447 prepared for any concurrent video content or perhaps
450 return film->active_frame_rate_change(position()).source;
455 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
457 auto paths_to_show = std::min(number_of_paths(), size_t{8});
459 for (auto i = size_t{0}; i < paths_to_show; ++i) {
460 paths += path(i).string();
461 if (i < (paths_to_show - 1)) {
465 if (paths_to_show < number_of_paths()) {
466 paths += String::compose("... and %1 more", number_of_paths() - paths_to_show);
470 UserProperty::GENERAL,
471 paths_to_show > 1 ? _("Filenames") : _("Filename"),
476 if (_video_frame_rate) {
482 locale_convert<string> (_video_frame_rate.get(), 5),
483 _("frames per second")
489 UserProperty::GENERAL,
490 _("Prepared for video frame rate"),
491 locale_convert<string> (_video_frame_rate.get(), 5),
492 _("frames per second")
500 /** Take settings from the given content if it is of the correct type */
502 Content::take_settings_from (shared_ptr<const Content> c)
504 if (video && c->video) {
505 video->take_settings_from (c->video);
507 if (audio && c->audio) {
508 audio->take_settings_from (c->audio);
511 auto i = text.begin ();
512 auto j = c->text.begin ();
513 while (i != text.end() && j != c->text.end()) {
514 (*i)->take_settings_from (*j);
521 shared_ptr<TextContent>
522 Content::only_text () const
524 DCPOMATIC_ASSERT (text.size() < 2);
528 return text.front ();
532 shared_ptr<TextContent>
533 Content::text_of_original_type (TextType type) const
536 if (i->original_type() == type) {
546 Content::add_path (boost::filesystem::path p)
548 boost::mutex::scoped_lock lm (_mutex);
549 _paths.push_back (p);
550 boost::system::error_code ec;
551 auto last_write = boost::filesystem::last_write_time(p, ec);
552 _last_write_times.push_back (ec ? 0 : last_write);
557 Content::changed () const
559 bool write_time_changed = false;
560 for (auto i = 0U; i < _paths.size(); ++i) {
561 if (boost::filesystem::last_write_time(_paths[i]) != last_write_time(i)) {
562 write_time_changed = true;
567 return (write_time_changed || calculate_digest() != digest());