Take Film pointer out of Content.
[dcpomatic.git] / src / lib / content.cc
1 /*
2     Copyright (C) 2013-2018 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
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.
10
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.
15
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/>.
18
19 */
20
21 /** @file  src/lib/content.cc
22  *  @brief Content class.
23  */
24
25 #include "content.h"
26 #include "change_signaller.h"
27 #include "util.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"
33 #include "film.h"
34 #include "job.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>
41 #include <iostream>
42
43 #include "i18n.h"
44
45 using std::string;
46 using std::list;
47 using std::cout;
48 using std::vector;
49 using std::max;
50 using std::pair;
51 using boost::shared_ptr;
52 using boost::optional;
53 using dcp::raw_convert;
54 using dcp::locale_convert;
55
56 int const ContentProperty::PATH = 400;
57 int const ContentProperty::POSITION = 401;
58 int const ContentProperty::LENGTH = 402;
59 int const ContentProperty::TRIM_START = 403;
60 int const ContentProperty::TRIM_END = 404;
61 int const ContentProperty::VIDEO_FRAME_RATE = 405;
62
63 Content::Content ()
64         : _position (0)
65         , _trim_start (0)
66         , _trim_end (0)
67         , _change_signals_frequent (false)
68 {
69
70 }
71
72 Content::Content (DCPTime p)
73         : _position (p)
74         , _trim_start (0)
75         , _trim_end (0)
76         , _change_signals_frequent (false)
77 {
78
79 }
80
81 Content::Content (boost::filesystem::path p)
82         : _position (0)
83         , _trim_start (0)
84         , _trim_end (0)
85         , _change_signals_frequent (false)
86 {
87         add_path (p);
88 }
89
90 Content::Content (cxml::ConstNodePtr node)
91         : _change_signals_frequent (false)
92 {
93         list<cxml::NodePtr> path_children = node->node_children ("Path");
94         BOOST_FOREACH (cxml::NodePtr i, path_children) {
95                 _paths.push_back (i->content());
96                 optional<time_t> const mod = i->optional_number_attribute<time_t>("mtime");
97                 if (mod) {
98                         _last_write_times.push_back (*mod);
99                 } else if (boost::filesystem::exists(i->content())) {
100                         _last_write_times.push_back (boost::filesystem::last_write_time(i->content()));
101                 } else {
102                         _last_write_times.push_back (0);
103                 }
104         }
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");
110 }
111
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)
118 {
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."));
122                 }
123
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."));
126                 }
127
128                 if (
129                         (_video_frame_rate && !c[i]->_video_frame_rate) ||
130                         (!_video_frame_rate && c[i]->_video_frame_rate)
131                         ) {
132                         throw JoinError (_("Content to be joined must have the same video frame rate"));
133                 }
134
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"));
137                 }
138
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]);
142                 }
143         }
144 }
145
146 void
147 Content::as_xml (xmlpp::Node* node, bool with_paths) const
148 {
149         boost::mutex::scoped_lock lm (_mutex);
150
151         if (with_paths) {
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]));
156                 }
157         }
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()));
164         }
165 }
166
167 string
168 Content::calculate_digest () const
169 {
170         boost::mutex::scoped_lock lm (_mutex);
171         vector<boost::filesystem::path> p = _paths;
172         lm.unlock ();
173
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.
177         */
178         return digest_head_tail(p, 1000000) + raw_convert<string>(boost::filesystem::file_size(p.front()));
179 }
180
181 void
182 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
183 {
184         if (job) {
185                 job->sub (_("Computing digest"));
186         }
187
188         string const d = calculate_digest ();
189
190         boost::mutex::scoped_lock lm (_mutex);
191         _digest = d;
192
193         _last_write_times.clear ();
194         BOOST_FOREACH (boost::filesystem::path i, _paths) {
195                 _last_write_times.push_back (boost::filesystem::last_write_time(i));
196         }
197 }
198
199 void
200 Content::signal_change (ChangeType c, int p)
201 {
202         try {
203                 if (c == CHANGE_TYPE_PENDING || c == CHANGE_TYPE_CANCELLED) {
204                         Change (c, shared_from_this(), p, _change_signals_frequent);
205                 } else {
206                         emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
207                 }
208         } catch (boost::bad_weak_ptr) {
209                 /* This must be during construction; never mind */
210         }
211 }
212
213 void
214 Content::set_position (shared_ptr<const Film> film, DCPTime p)
215 {
216         /* video and audio content can modify its position */
217
218         if (video) {
219                 video->modify_position (film, p);
220         }
221
222         if (audio) {
223                 audio->modify_position (film, p);
224         }
225
226         ChangeSignaller<Content> cc (this, ContentProperty::POSITION);
227
228         {
229                 boost::mutex::scoped_lock lm (_mutex);
230                 if (p == _position) {
231                         cc.abort ();
232                         return;
233                 }
234
235                 _position = p;
236         }
237 }
238
239 void
240 Content::set_trim_start (ContentTime t)
241 {
242         /* video and audio content can modify its start trim */
243
244         if (video) {
245                 video->modify_trim_start (t);
246         }
247
248         if (audio) {
249                 audio->modify_trim_start (t);
250         }
251
252         ChangeSignaller<Content> cc (this, ContentProperty::TRIM_START);
253
254         {
255                 boost::mutex::scoped_lock lm (_mutex);
256                 _trim_start = t;
257         }
258 }
259
260 void
261 Content::set_trim_end (ContentTime t)
262 {
263         ChangeSignaller<Content> cc (this, ContentProperty::TRIM_END);
264
265         {
266                 boost::mutex::scoped_lock lm (_mutex);
267                 _trim_end = t;
268         }
269 }
270
271
272 shared_ptr<Content>
273 Content::clone (shared_ptr<const Film> film) const
274 {
275         /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
276         xmlpp::Document doc;
277         xmlpp::Node* node = doc.create_root_node ("Content");
278         as_xml (node, true);
279
280         /* notes is unused here (we assume) */
281         list<string> notes;
282         return content_factory (film, cxml::NodePtr(new cxml::Node(node)), Film::current_state_version, notes);
283 }
284
285 string
286 Content::technical_summary () const
287 {
288         string s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
289         if (_video_frame_rate) {
290                 s += String::compose(" %1", *_video_frame_rate);
291         }
292         return s;
293 }
294
295 DCPTime
296 Content::length_after_trim (shared_ptr<const Film> film) const
297 {
298         return max (DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
299 }
300
301 /** @return string which changes when something about this content changes which affects
302  *  the appearance of its video.
303  */
304 string
305 Content::identifier () const
306 {
307         char buffer[256];
308         snprintf (
309                 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
310                 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
311                 );
312         return buffer;
313 }
314
315 bool
316 Content::paths_valid () const
317 {
318         BOOST_FOREACH (boost::filesystem::path i, _paths) {
319                 if (!boost::filesystem::exists (i)) {
320                         return false;
321                 }
322         }
323
324         return true;
325 }
326
327 void
328 Content::set_paths (vector<boost::filesystem::path> paths)
329 {
330         ChangeSignaller<Content> cc (this, ContentProperty::PATH);
331
332         {
333                 boost::mutex::scoped_lock lm (_mutex);
334                 _paths = paths;
335                 _last_write_times.clear ();
336                 BOOST_FOREACH (boost::filesystem::path i, _paths) {
337                         _last_write_times.push_back (boost::filesystem::last_write_time(i));
338                 }
339         }
340 }
341
342 string
343 Content::path_summary () const
344 {
345         /* XXX: should handle multiple paths more gracefully */
346
347         DCPOMATIC_ASSERT (number_of_paths ());
348
349         string s = path(0).filename().string ();
350         if (number_of_paths() > 1) {
351                 s += " ...";
352         }
353
354         return s;
355 }
356
357 /** @return a list of properties that might be interesting to the user */
358 list<UserProperty>
359 Content::user_properties () const
360 {
361         list<UserProperty> p;
362         add_properties (p);
363         return p;
364 }
365
366 /** @return DCP times of points within this content where a reel split could occur */
367 list<DCPTime>
368 Content::reel_split_points () const
369 {
370         list<DCPTime> t;
371         /* This is only called for video content and such content has its position forced
372            to start on a frame boundary.
373         */
374         t.push_back (position());
375         return t;
376 }
377
378 void
379 Content::set_video_frame_rate (double r)
380 {
381         ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
382
383         {
384                 boost::mutex::scoped_lock lm (_mutex);
385                 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
386                         cc.abort();
387                 }
388                 _video_frame_rate = r;
389         }
390
391         /* Make sure trim is still on a frame boundary */
392         if (video) {
393                 set_trim_start (trim_start());
394         }
395 }
396
397 void
398 Content::unset_video_frame_rate ()
399 {
400         ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
401
402         {
403                 boost::mutex::scoped_lock lm (_mutex);
404                 _video_frame_rate = optional<double>();
405         }
406 }
407
408 double
409 Content::active_video_frame_rate (shared_ptr<const Film> film) const
410 {
411         {
412                 boost::mutex::scoped_lock lm (_mutex);
413                 if (_video_frame_rate) {
414                         return _video_frame_rate.get ();
415                 }
416         }
417
418         /* No frame rate specified, so assume this content has been
419            prepared for any concurrent video content or perhaps
420            just the DCP rate.
421         */
422         return film->active_frame_rate_change(position()).source;
423 }
424
425 void
426 Content::add_properties (list<UserProperty>& p) const
427 {
428         p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
429
430         if (_video_frame_rate) {
431                 if (video) {
432                         p.push_back (
433                                 UserProperty (
434                                         UserProperty::VIDEO,
435                                         _("Frame rate"),
436                                         locale_convert<string> (_video_frame_rate.get(), 5),
437                                         _("frames per second")
438                                         )
439                                 );
440                 } else {
441                         p.push_back (
442                                 UserProperty (
443                                         UserProperty::GENERAL,
444                                         _("Prepared for video frame rate"),
445                                         locale_convert<string> (_video_frame_rate.get(), 5),
446                                         _("frames per second")
447                                         )
448                                 );
449                 }
450         }
451 }
452
453 /** Take settings from the given content if it is of the correct type */
454 void
455 Content::take_settings_from (shared_ptr<const Content> c)
456 {
457         if (video && c->video) {
458                 video->take_settings_from (c->video);
459         }
460         if (audio && c->audio) {
461                 audio->take_settings_from (c->audio);
462         }
463
464         list<shared_ptr<TextContent> >::iterator i = text.begin ();
465         list<shared_ptr<TextContent> >::const_iterator j = c->text.begin ();
466         while (i != text.end() && j != c->text.end()) {
467                 (*i)->take_settings_from (*j);
468                 ++i;
469                 ++j;
470         }
471 }
472
473 shared_ptr<TextContent>
474 Content::only_text () const
475 {
476         DCPOMATIC_ASSERT (text.size() < 2);
477         if (text.empty ()) {
478                 return shared_ptr<TextContent> ();
479         }
480         return text.front ();
481 }
482
483 shared_ptr<TextContent>
484 Content::text_of_original_type (TextType type) const
485 {
486         BOOST_FOREACH (shared_ptr<TextContent> i, text) {
487                 if (i->original_type() == type) {
488                         return i;
489                 }
490         }
491
492         return shared_ptr<TextContent> ();
493 }
494
495 void
496 Content::add_path (boost::filesystem::path p)
497 {
498         boost::mutex::scoped_lock lm (_mutex);
499         _paths.push_back (p);
500         _last_write_times.push_back (boost::filesystem::last_write_time(p));
501 }