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/>.
21 /** @file src/lib/content.cc
22 * @brief Content class.
26 #include "change_signaller.h"
28 #include "content_factory.h"
29 #include "video_content.h"
30 #include "audio_content.h"
31 #include "text_content.h"
32 #include "exceptions.h"
35 #include "compose.hpp"
36 #include <dcp/locale_convert.h>
37 #include <dcp/raw_convert.h>
38 #include <libcxml/cxml.h>
39 #include <libxml++/libxml++.h>
40 #include <boost/thread/mutex.hpp>
51 using std::shared_ptr;
52 using boost::optional;
53 using dcp::raw_convert;
54 using dcp::locale_convert;
55 using namespace dcpomatic;
57 int const ContentProperty::PATH = 400;
58 int const ContentProperty::POSITION = 401;
59 int const ContentProperty::LENGTH = 402;
60 int const ContentProperty::TRIM_START = 403;
61 int const ContentProperty::TRIM_END = 404;
62 int const ContentProperty::VIDEO_FRAME_RATE = 405;
68 , _change_signals_frequent (false)
73 Content::Content (DCPTime p)
77 , _change_signals_frequent (false)
82 Content::Content (boost::filesystem::path p)
86 , _change_signals_frequent (false)
91 Content::Content (cxml::ConstNodePtr node)
92 : _change_signals_frequent (false)
94 for (auto i: node->node_children("Path")) {
95 _paths.push_back (i->content());
96 auto const mod = i->optional_number_attribute<time_t>("mtime");
98 _last_write_times.push_back (*mod);
100 boost::system::error_code ec;
101 auto last_write = boost::filesystem::last_write_time(i->content(), ec);
102 _last_write_times.push_back (ec ? 0 : last_write);
105 _digest = node->optional_string_child ("Digest").get_value_or ("X");
106 _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
107 _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
108 _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
109 _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]);
147 Content::as_xml (xmlpp::Node* node, bool with_paths) const
149 boost::mutex::scoped_lock lm (_mutex);
152 for (size_t i = 0; i < _paths.size(); ++i) {
153 xmlpp::Element* p = node->add_child("Path");
154 p->add_child_text (_paths[i].string());
155 p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
158 node->add_child("Digest")->add_child_text (_digest);
159 node->add_child("Position")->add_child_text (raw_convert<string> (_position.get ()));
160 node->add_child("TrimStart")->add_child_text (raw_convert<string> (_trim_start.get ()));
161 node->add_child("TrimEnd")->add_child_text (raw_convert<string> (_trim_end.get ()));
162 if (_video_frame_rate) {
163 node->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate.get()));
168 Content::calculate_digest () const
170 boost::mutex::scoped_lock lm (_mutex);
174 /* Some content files are very big, so we use a poor man's
175 digest here: a digest of the first and last 1e6 bytes with the
176 size of the first file tacked on the end as a string.
178 return digest_head_tail(p, 1000000) + raw_convert<string>(boost::filesystem::file_size(p.front()));
182 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
185 job->sub (_("Computing digest"));
188 auto const d = calculate_digest ();
190 boost::mutex::scoped_lock lm (_mutex);
193 _last_write_times.clear ();
194 for (auto i: _paths) {
195 boost::system::error_code ec;
196 auto last_write = boost::filesystem::last_write_time(i, ec);
197 _last_write_times.push_back (ec ? 0 : last_write);
202 Content::signal_change (ChangeType c, int p)
205 if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
206 Change (c, shared_from_this(), p, _change_signals_frequent);
208 emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
210 } catch (std::bad_weak_ptr &) {
211 /* This must be during construction; never mind */
216 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
218 /* video and audio content can modify its position */
221 video->modify_position (film, p);
224 /* Only allow the audio to modify if we have no video;
225 sometimes p can't be on an integer video AND audio frame,
226 and in these cases we want the video constraint to be
227 satisfied since (I think) the audio code is better able to
230 if (!video && audio) {
231 audio->modify_position (film, p);
234 ContentChangeSignaller cc (this, ContentProperty::POSITION);
237 boost::mutex::scoped_lock lm (_mutex);
238 if (p == _position && !force_emit) {
248 Content::set_trim_start (ContentTime t)
250 /* video and audio content can modify its start trim */
253 video->modify_trim_start (t);
256 /* See note in ::set_position */
257 if (!video && audio) {
258 audio->modify_trim_start (t);
261 ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
264 boost::mutex::scoped_lock lm (_mutex);
270 Content::set_trim_end (ContentTime t)
272 ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
275 boost::mutex::scoped_lock lm (_mutex);
282 Content::clone () const
284 /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
286 auto node = doc.create_root_node ("Content");
289 /* notes is unused here (we assume) */
291 return content_factory (cxml::NodePtr(new cxml::Node(node)), Film::current_state_version, notes);
295 Content::technical_summary () const
297 auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
298 if (_video_frame_rate) {
299 s += String::compose(" %1", *_video_frame_rate);
305 Content::length_after_trim (shared_ptr<const Film> film) const
307 auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
309 length = length.round(film->video_frame_rate());
314 /** @return string which changes when something about this content changes which affects
315 * the appearance of its video.
318 Content::identifier () const
322 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
323 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
329 Content::paths_valid () const
331 for (auto i: _paths) {
332 if (!boost::filesystem::exists (i)) {
341 Content::set_paths (vector<boost::filesystem::path> paths)
343 ContentChangeSignaller cc (this, ContentProperty::PATH);
346 boost::mutex::scoped_lock lm (_mutex);
348 _last_write_times.clear ();
349 for (auto i: _paths) {
350 boost::system::error_code ec;
351 auto last_write = boost::filesystem::last_write_time(i, ec);
352 _last_write_times.push_back (ec ? 0 : last_write);
358 Content::path_summary () const
360 /* XXX: should handle multiple paths more gracefully */
362 DCPOMATIC_ASSERT (number_of_paths ());
364 auto s = path(0).filename().string();
365 if (number_of_paths() > 1) {
372 /** @return a list of properties that might be interesting to the user */
374 Content::user_properties (shared_ptr<const Film> film) const
376 list<UserProperty> p;
377 add_properties (film, p);
381 /** @return DCP times of points within this content where a reel split could occur */
383 Content::reel_split_points (shared_ptr<const Film>) const
386 /* This is only called for video content and such content has its position forced
387 to start on a frame boundary.
389 t.push_back (position());
394 Content::set_video_frame_rate (double r)
396 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
399 boost::mutex::scoped_lock lm (_mutex);
400 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
403 _video_frame_rate = r;
406 /* Make sure trim is still on a frame boundary */
408 set_trim_start (trim_start());
413 Content::unset_video_frame_rate ()
415 ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
418 boost::mutex::scoped_lock lm (_mutex);
419 _video_frame_rate = optional<double>();
424 Content::active_video_frame_rate (shared_ptr<const Film> film) const
427 boost::mutex::scoped_lock lm (_mutex);
428 if (_video_frame_rate) {
429 return _video_frame_rate.get ();
433 /* No frame rate specified, so assume this content has been
434 prepared for any concurrent video content or perhaps
437 return film->active_frame_rate_change(position()).source;
441 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
443 p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
445 if (_video_frame_rate) {
451 locale_convert<string> (_video_frame_rate.get(), 5),
452 _("frames per second")
458 UserProperty::GENERAL,
459 _("Prepared for video frame rate"),
460 locale_convert<string> (_video_frame_rate.get(), 5),
461 _("frames per second")
468 /** Take settings from the given content if it is of the correct type */
470 Content::take_settings_from (shared_ptr<const Content> c)
472 if (video && c->video) {
473 video->take_settings_from (c->video);
475 if (audio && c->audio) {
476 audio->take_settings_from (c->audio);
479 auto i = text.begin ();
480 auto j = c->text.begin ();
481 while (i != text.end() && j != c->text.end()) {
482 (*i)->take_settings_from (*j);
488 shared_ptr<TextContent>
489 Content::only_text () const
491 DCPOMATIC_ASSERT (text.size() < 2);
493 return shared_ptr<TextContent> ();
495 return text.front ();
498 shared_ptr<TextContent>
499 Content::text_of_original_type (TextType type) const
502 if (i->original_type() == type) {
507 return shared_ptr<TextContent> ();
511 Content::add_path (boost::filesystem::path p)
513 boost::mutex::scoped_lock lm (_mutex);
514 _paths.push_back (p);
515 boost::system::error_code ec;
516 auto last_write = boost::filesystem::last_write_time(p, ec);
517 _last_write_times.push_back (ec ? 0 : last_write);