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 DCPOMATIC_ASSERT (t.get() >= 0);
260 /* video and audio content can modify its start trim */
263 video->modify_trim_start (t);
266 /* See note in ::set_position */
267 if (!video && audio) {
268 audio->modify_trim_start (t);
271 ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
274 boost::mutex::scoped_lock lm (_mutex);
281 Content::set_trim_end (ContentTime t)
283 DCPOMATIC_ASSERT (t.get() >= 0);
285 ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
288 boost::mutex::scoped_lock lm (_mutex);
295 Content::clone () const
297 /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
299 auto node = doc.create_root_node ("Content");
302 /* notes is unused here (we assume) */
304 return content_factory (make_shared<cxml::Node>(node), Film::current_state_version, notes);
309 Content::technical_summary () const
311 auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
312 if (_video_frame_rate) {
313 s += String::compose(" %1", *_video_frame_rate);
320 Content::length_after_trim (shared_ptr<const Film> film) const
322 auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
324 length = length.round(film->video_frame_rate());
330 /** @return string which changes when something about this content changes which affects
331 * the appearance of its video.
334 Content::identifier () const
338 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
339 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
346 Content::paths_valid () const
348 for (auto i: _paths) {
349 if (!boost::filesystem::exists (i)) {
359 Content::set_paths (vector<boost::filesystem::path> paths)
361 ContentChangeSignaller cc (this, ContentProperty::PATH);
364 boost::mutex::scoped_lock lm (_mutex);
366 _last_write_times.clear ();
367 for (auto i: _paths) {
368 boost::system::error_code ec;
369 auto last_write = boost::filesystem::last_write_time(i, ec);
370 _last_write_times.push_back (ec ? 0 : last_write);
377 Content::path_summary () const
379 /* XXX: should handle multiple paths more gracefully */
381 DCPOMATIC_ASSERT (number_of_paths ());
383 auto s = path(0).filename().string();
384 if (number_of_paths() > 1) {
392 /** @return a list of properties that might be interesting to the user */
394 Content::user_properties (shared_ptr<const Film> film) const
396 list<UserProperty> p;
397 add_properties (film, p);
402 /** @return DCP times of points within this content where a reel split could occur */
404 Content::reel_split_points (shared_ptr<const Film>) const
407 /* This is only called for video content and such content has its position forced
408 to start on a frame boundary.
410 t.push_back (position());
416 Content::set_video_frame_rate (double r)
418 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
421 boost::mutex::scoped_lock lm (_mutex);
422 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
425 _video_frame_rate = r;
428 /* Make sure trim is still on a frame boundary */
430 set_trim_start (trim_start());
436 Content::unset_video_frame_rate ()
438 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
441 boost::mutex::scoped_lock lm (_mutex);
442 _video_frame_rate = optional<double>();
448 Content::active_video_frame_rate (shared_ptr<const Film> film) const
451 boost::mutex::scoped_lock lm (_mutex);
452 if (_video_frame_rate) {
453 return _video_frame_rate.get ();
457 /* No frame rate specified, so assume this content has been
458 prepared for any concurrent video content or perhaps
461 return film->active_frame_rate_change(position()).source;
466 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
468 p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
470 if (_video_frame_rate) {
476 locale_convert<string> (_video_frame_rate.get(), 5),
477 _("frames per second")
483 UserProperty::GENERAL,
484 _("Prepared for video frame rate"),
485 locale_convert<string> (_video_frame_rate.get(), 5),
486 _("frames per second")
494 /** Take settings from the given content if it is of the correct type */
496 Content::take_settings_from (shared_ptr<const Content> c)
498 if (video && c->video) {
499 video->take_settings_from (c->video);
501 if (audio && c->audio) {
502 audio->take_settings_from (c->audio);
505 auto i = text.begin ();
506 auto j = c->text.begin ();
507 while (i != text.end() && j != c->text.end()) {
508 (*i)->take_settings_from (*j);
515 shared_ptr<TextContent>
516 Content::only_text () const
518 DCPOMATIC_ASSERT (text.size() < 2);
522 return text.front ();
526 shared_ptr<TextContent>
527 Content::text_of_original_type (TextType type) const
530 if (i->original_type() == type) {
540 Content::add_path (boost::filesystem::path p)
542 boost::mutex::scoped_lock lm (_mutex);
543 _paths.push_back (p);
544 boost::system::error_code ec;
545 auto last_write = boost::filesystem::last_write_time(p, ec);
546 _last_write_times.push_back (ec ? 0 : last_write);