Try rounding length_after_trim.
[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         return max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position()))).round(film->video_frame_rate());
307 }
308
309 /** @return string which changes when something about this content changes which affects
310  *  the appearance of its video.
311  */
312 string
313 Content::identifier () const
314 {
315         char buffer[256];
316         snprintf (
317                 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
318                 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
319                 );
320         return buffer;
321 }
322
323 bool
324 Content::paths_valid () const
325 {
326         BOOST_FOREACH (boost::filesystem::path i, _paths) {
327                 if (!boost::filesystem::exists (i)) {
328                         return false;
329                 }
330         }
331
332         return true;
333 }
334
335 void
336 Content::set_paths (vector<boost::filesystem::path> paths)
337 {
338         ChangeSignaller<Content> cc (this, ContentProperty::PATH);
339
340         {
341                 boost::mutex::scoped_lock lm (_mutex);
342                 _paths = paths;
343                 _last_write_times.clear ();
344                 BOOST_FOREACH (boost::filesystem::path i, _paths) {
345                         _last_write_times.push_back (boost::filesystem::last_write_time(i));
346                 }
347         }
348 }
349
350 string
351 Content::path_summary () const
352 {
353         /* XXX: should handle multiple paths more gracefully */
354
355         DCPOMATIC_ASSERT (number_of_paths ());
356
357         string s = path(0).filename().string ();
358         if (number_of_paths() > 1) {
359                 s += " ...";
360         }
361
362         return s;
363 }
364
365 /** @return a list of properties that might be interesting to the user */
366 list<UserProperty>
367 Content::user_properties (shared_ptr<const Film> film) const
368 {
369         list<UserProperty> p;
370         add_properties (film, p);
371         return p;
372 }
373
374 /** @return DCP times of points within this content where a reel split could occur */
375 list<DCPTime>
376 Content::reel_split_points (shared_ptr<const Film>) const
377 {
378         list<DCPTime> t;
379         /* This is only called for video content and such content has its position forced
380            to start on a frame boundary.
381         */
382         t.push_back (position());
383         return t;
384 }
385
386 void
387 Content::set_video_frame_rate (double r)
388 {
389         ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
390
391         {
392                 boost::mutex::scoped_lock lm (_mutex);
393                 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
394                         cc.abort();
395                 }
396                 _video_frame_rate = r;
397         }
398
399         /* Make sure trim is still on a frame boundary */
400         if (video) {
401                 set_trim_start (trim_start());
402         }
403 }
404
405 void
406 Content::unset_video_frame_rate ()
407 {
408         ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
409
410         {
411                 boost::mutex::scoped_lock lm (_mutex);
412                 _video_frame_rate = optional<double>();
413         }
414 }
415
416 double
417 Content::active_video_frame_rate (shared_ptr<const Film> film) const
418 {
419         {
420                 boost::mutex::scoped_lock lm (_mutex);
421                 if (_video_frame_rate) {
422                         return _video_frame_rate.get ();
423                 }
424         }
425
426         /* No frame rate specified, so assume this content has been
427            prepared for any concurrent video content or perhaps
428            just the DCP rate.
429         */
430         return film->active_frame_rate_change(position()).source;
431 }
432
433 void
434 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
435 {
436         p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
437
438         if (_video_frame_rate) {
439                 if (video) {
440                         p.push_back (
441                                 UserProperty (
442                                         UserProperty::VIDEO,
443                                         _("Frame rate"),
444                                         locale_convert<string> (_video_frame_rate.get(), 5),
445                                         _("frames per second")
446                                         )
447                                 );
448                 } else {
449                         p.push_back (
450                                 UserProperty (
451                                         UserProperty::GENERAL,
452                                         _("Prepared for video frame rate"),
453                                         locale_convert<string> (_video_frame_rate.get(), 5),
454                                         _("frames per second")
455                                         )
456                                 );
457                 }
458         }
459 }
460
461 /** Take settings from the given content if it is of the correct type */
462 void
463 Content::take_settings_from (shared_ptr<const Content> c)
464 {
465         if (video && c->video) {
466                 video->take_settings_from (c->video);
467         }
468         if (audio && c->audio) {
469                 audio->take_settings_from (c->audio);
470         }
471
472         list<shared_ptr<TextContent> >::iterator i = text.begin ();
473         list<shared_ptr<TextContent> >::const_iterator j = c->text.begin ();
474         while (i != text.end() && j != c->text.end()) {
475                 (*i)->take_settings_from (*j);
476                 ++i;
477                 ++j;
478         }
479 }
480
481 shared_ptr<TextContent>
482 Content::only_text () const
483 {
484         DCPOMATIC_ASSERT (text.size() < 2);
485         if (text.empty ()) {
486                 return shared_ptr<TextContent> ();
487         }
488         return text.front ();
489 }
490
491 shared_ptr<TextContent>
492 Content::text_of_original_type (TextType type) const
493 {
494         BOOST_FOREACH (shared_ptr<TextContent> i, text) {
495                 if (i->original_type() == type) {
496                         return i;
497                 }
498         }
499
500         return shared_ptr<TextContent> ();
501 }
502
503 void
504 Content::add_path (boost::filesystem::path p)
505 {
506         boost::mutex::scoped_lock lm (_mutex);
507         _paths.push_back (p);
508         _last_write_times.push_back (boost::filesystem::last_write_time(p));
509 }