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