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;
53 using std::shared_ptr;
56 using boost::optional;
57 using dcp::locale_convert;
58 using dcp::raw_convert;
59 using namespace dcpomatic;
62 int const ContentProperty::PATH = 400;
63 int const ContentProperty::POSITION = 401;
64 int const ContentProperty::LENGTH = 402;
65 int const ContentProperty::TRIM_START = 403;
66 int const ContentProperty::TRIM_END = 404;
67 int const ContentProperty::VIDEO_FRAME_RATE = 405;
71 : _change_signals_frequent (false)
77 Content::Content (DCPTime p)
79 , _change_signals_frequent (false)
85 Content::Content (boost::filesystem::path p)
86 : _change_signals_frequent (false)
92 Content::Content (cxml::ConstNodePtr node)
93 : _change_signals_frequent (false)
95 for (auto i: node->node_children("Path")) {
96 _paths.push_back (i->content());
97 auto const mod = i->optional_number_attribute<time_t>("mtime");
99 _last_write_times.push_back (*mod);
101 boost::system::error_code ec;
102 auto last_write = boost::filesystem::last_write_time(i->content(), ec);
103 _last_write_times.push_back (ec ? 0 : last_write);
106 _digest = node->optional_string_child ("Digest").get_value_or ("X");
107 _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
108 _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
109 _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
110 _video_frame_rate = node->optional_number_child<double> ("VideoFrameRate");
114 Content::Content (vector<shared_ptr<Content>> c)
115 : _position (c.front()->position())
116 , _trim_start (c.front()->trim_start())
117 , _trim_end (c.back()->trim_end())
118 , _video_frame_rate (c.front()->video_frame_rate())
119 , _change_signals_frequent (false)
121 for (size_t i = 0; i < c.size(); ++i) {
122 if (i > 0 && c[i]->trim_start() > ContentTime ()) {
123 throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
126 if (i < (c.size() - 1) && c[i]->trim_end () > ContentTime ()) {
127 throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
131 (_video_frame_rate && !c[i]->_video_frame_rate) ||
132 (!_video_frame_rate && c[i]->_video_frame_rate)
134 throw JoinError (_("Content to be joined must have the same video frame rate"));
137 if (_video_frame_rate && fabs (_video_frame_rate.get() - c[i]->_video_frame_rate.get()) > VIDEO_FRAME_RATE_EPSILON) {
138 throw JoinError (_("Content to be joined must have the same video frame rate"));
141 for (size_t j = 0; j < c[i]->number_of_paths(); ++j) {
142 _paths.push_back (c[i]->path(j));
143 _last_write_times.push_back (c[i]->_last_write_times[j]);
150 Content::as_xml (xmlpp::Node* node, bool with_paths) const
152 boost::mutex::scoped_lock lm (_mutex);
155 for (size_t i = 0; i < _paths.size(); ++i) {
156 auto p = node->add_child("Path");
157 p->add_child_text (_paths[i].string());
158 p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
161 node->add_child("Digest")->add_child_text(_digest);
162 node->add_child("Position")->add_child_text(raw_convert<string>(_position.get()));
163 node->add_child("TrimStart")->add_child_text(raw_convert<string>(_trim_start.get()));
164 node->add_child("TrimEnd")->add_child_text(raw_convert<string>(_trim_end.get()));
165 if (_video_frame_rate) {
166 node->add_child("VideoFrameRate")->add_child_text(raw_convert<string>(_video_frame_rate.get()));
172 Content::calculate_digest () const
174 boost::mutex::scoped_lock lm (_mutex);
178 /* Some content files are very big, so we use a poor man's
179 digest here: a digest of the first and last 1e6 bytes with the
180 size of the first file tacked on the end as a string.
182 return digest_head_tail(p, 1000000) + raw_convert<string>(boost::filesystem::file_size(p.front()));
187 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
190 job->sub (_("Computing digest"));
193 auto const d = calculate_digest ();
195 boost::mutex::scoped_lock lm (_mutex);
198 _last_write_times.clear ();
199 for (auto i: _paths) {
200 boost::system::error_code ec;
201 auto last_write = boost::filesystem::last_write_time(i, ec);
202 _last_write_times.push_back (ec ? 0 : last_write);
208 Content::signal_change (ChangeType c, int p)
211 if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
212 Change (c, shared_from_this(), p, _change_signals_frequent);
214 emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
216 } catch (std::bad_weak_ptr &) {
217 /* This must be during construction; never mind */
223 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
225 /* video and audio content can modify its position */
228 video->modify_position (film, p);
231 /* Only allow the audio to modify if we have no video;
232 sometimes p can't be on an integer video AND audio frame,
233 and in these cases we want the video constraint to be
234 satisfied since (I think) the audio code is better able to
237 if (!video && audio) {
238 audio->modify_position (film, p);
241 ContentChangeSignaller cc (this, ContentProperty::POSITION);
244 boost::mutex::scoped_lock lm (_mutex);
245 if (p == _position && !force_emit) {
256 Content::set_trim_start (ContentTime t)
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 ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
284 boost::mutex::scoped_lock lm (_mutex);
291 Content::clone () const
293 /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
295 auto node = doc.create_root_node ("Content");
298 /* notes is unused here (we assume) */
300 return content_factory (make_shared<cxml::Node>(node), Film::current_state_version, notes);
305 Content::technical_summary () const
307 auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
308 if (_video_frame_rate) {
309 s += String::compose(" %1", *_video_frame_rate);
316 Content::length_after_trim (shared_ptr<const Film> film) const
318 auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
320 length = length.round(film->video_frame_rate());
326 /** @return string which changes when something about this content changes which affects
327 * the appearance of its video.
330 Content::identifier () const
334 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
335 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
342 Content::paths_valid () const
344 for (auto i: _paths) {
345 if (!boost::filesystem::exists (i)) {
355 Content::set_paths (vector<boost::filesystem::path> paths)
357 ContentChangeSignaller cc (this, ContentProperty::PATH);
360 boost::mutex::scoped_lock lm (_mutex);
362 _last_write_times.clear ();
363 for (auto i: _paths) {
364 boost::system::error_code ec;
365 auto last_write = boost::filesystem::last_write_time(i, ec);
366 _last_write_times.push_back (ec ? 0 : last_write);
373 Content::path_summary () const
375 /* XXX: should handle multiple paths more gracefully */
377 DCPOMATIC_ASSERT (number_of_paths ());
379 auto s = path(0).filename().string();
380 if (number_of_paths() > 1) {
388 /** @return a list of properties that might be interesting to the user */
390 Content::user_properties (shared_ptr<const Film> film) const
392 list<UserProperty> p;
393 add_properties (film, p);
398 /** @return DCP times of points within this content where a reel split could occur */
400 Content::reel_split_points (shared_ptr<const Film>) const
403 /* This is only called for video content and such content has its position forced
404 to start on a frame boundary.
406 t.push_back (position());
412 Content::set_video_frame_rate (double r)
414 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
417 boost::mutex::scoped_lock lm (_mutex);
418 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
421 _video_frame_rate = r;
424 /* Make sure trim is still on a frame boundary */
426 set_trim_start (trim_start());
432 Content::unset_video_frame_rate ()
434 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
437 boost::mutex::scoped_lock lm (_mutex);
438 _video_frame_rate = optional<double>();
444 Content::active_video_frame_rate (shared_ptr<const Film> film) const
447 boost::mutex::scoped_lock lm (_mutex);
448 if (_video_frame_rate) {
449 return _video_frame_rate.get ();
453 /* No frame rate specified, so assume this content has been
454 prepared for any concurrent video content or perhaps
457 return film->active_frame_rate_change(position()).source;
462 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
464 p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
466 if (_video_frame_rate) {
472 locale_convert<string> (_video_frame_rate.get(), 5),
473 _("frames per second")
479 UserProperty::GENERAL,
480 _("Prepared for video frame rate"),
481 locale_convert<string> (_video_frame_rate.get(), 5),
482 _("frames per second")
490 /** Take settings from the given content if it is of the correct type */
492 Content::take_settings_from (shared_ptr<const Content> c)
494 if (video && c->video) {
495 video->take_settings_from (c->video);
497 if (audio && c->audio) {
498 audio->take_settings_from (c->audio);
501 auto i = text.begin ();
502 auto j = c->text.begin ();
503 while (i != text.end() && j != c->text.end()) {
504 (*i)->take_settings_from (*j);
511 shared_ptr<TextContent>
512 Content::only_text () const
514 DCPOMATIC_ASSERT (text.size() < 2);
518 return text.front ();
522 shared_ptr<TextContent>
523 Content::text_of_original_type (TextType type) const
526 if (i->original_type() == type) {
536 Content::add_path (boost::filesystem::path p)
538 boost::mutex::scoped_lock lm (_mutex);
539 _paths.push_back (p);
540 boost::system::error_code ec;
541 auto last_write = boost::filesystem::last_write_time(p, ec);
542 _last_write_times.push_back (ec ? 0 : last_write);