Fix case where the is FFmpegContent with 24fps video and 44.1kHz audio
[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, bool force_emit)
215 {
216         /* video and audio content can modify its position */
217
218         if (video) {
219                 video->modify_position (film, p);
220         }
221
222         /* Only allow the audio to modify if we have no video;
223            sometimes p can't be on an integer video AND audio frame,
224            and in these cases we want the video constraint to be
225            satisfied since (I think) the audio code is better able to
226            cope.
227         */
228         if (!video && audio) {
229                 audio->modify_position (film, p);
230         }
231
232         ChangeSignaller<Content> cc (this, ContentProperty::POSITION);
233
234         {
235                 boost::mutex::scoped_lock lm (_mutex);
236                 if (p == _position && !force_emit) {
237                         cc.abort ();
238                         return;
239                 }
240
241                 _position = p;
242         }
243 }
244
245 void
246 Content::set_trim_start (ContentTime t)
247 {
248         /* video and audio content can modify its start trim */
249
250         if (video) {
251                 video->modify_trim_start (t);
252         }
253
254         /* See note in ::set_position */
255         if (!video && audio) {
256                 audio->modify_trim_start (t);
257         }
258
259         ChangeSignaller<Content> cc (this, ContentProperty::TRIM_START);
260
261         {
262                 boost::mutex::scoped_lock lm (_mutex);
263                 _trim_start = t;
264         }
265 }
266
267 void
268 Content::set_trim_end (ContentTime t)
269 {
270         ChangeSignaller<Content> cc (this, ContentProperty::TRIM_END);
271
272         {
273                 boost::mutex::scoped_lock lm (_mutex);
274                 _trim_end = t;
275         }
276 }
277
278
279 shared_ptr<Content>
280 Content::clone () const
281 {
282         /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
283         xmlpp::Document doc;
284         xmlpp::Node* node = doc.create_root_node ("Content");
285         as_xml (node, true);
286
287         /* notes is unused here (we assume) */
288         list<string> notes;
289         return content_factory (cxml::NodePtr(new cxml::Node(node)), Film::current_state_version, notes);
290 }
291
292 string
293 Content::technical_summary () const
294 {
295         string s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
296         if (_video_frame_rate) {
297                 s += String::compose(" %1", *_video_frame_rate);
298         }
299         return s;
300 }
301
302 DCPTime
303 Content::length_after_trim (shared_ptr<const Film> film) const
304 {
305         return max (DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
306 }
307
308 /** @return string which changes when something about this content changes which affects
309  *  the appearance of its video.
310  */
311 string
312 Content::identifier () const
313 {
314         char buffer[256];
315         snprintf (
316                 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
317                 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
318                 );
319         return buffer;
320 }
321
322 bool
323 Content::paths_valid () const
324 {
325         BOOST_FOREACH (boost::filesystem::path i, _paths) {
326                 if (!boost::filesystem::exists (i)) {
327                         return false;
328                 }
329         }
330
331         return true;
332 }
333
334 void
335 Content::set_paths (vector<boost::filesystem::path> paths)
336 {
337         ChangeSignaller<Content> cc (this, ContentProperty::PATH);
338
339         {
340                 boost::mutex::scoped_lock lm (_mutex);
341                 _paths = paths;
342                 _last_write_times.clear ();
343                 BOOST_FOREACH (boost::filesystem::path i, _paths) {
344                         _last_write_times.push_back (boost::filesystem::last_write_time(i));
345                 }
346         }
347 }
348
349 string
350 Content::path_summary () const
351 {
352         /* XXX: should handle multiple paths more gracefully */
353
354         DCPOMATIC_ASSERT (number_of_paths ());
355
356         string s = path(0).filename().string ();
357         if (number_of_paths() > 1) {
358                 s += " ...";
359         }
360
361         return s;
362 }
363
364 /** @return a list of properties that might be interesting to the user */
365 list<UserProperty>
366 Content::user_properties (shared_ptr<const Film> film) const
367 {
368         list<UserProperty> p;
369         add_properties (film, p);
370         return p;
371 }
372
373 /** @return DCP times of points within this content where a reel split could occur */
374 list<DCPTime>
375 Content::reel_split_points (shared_ptr<const Film>) const
376 {
377         list<DCPTime> t;
378         /* This is only called for video content and such content has its position forced
379            to start on a frame boundary.
380         */
381         t.push_back (position());
382         return t;
383 }
384
385 void
386 Content::set_video_frame_rate (double r)
387 {
388         ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
389
390         {
391                 boost::mutex::scoped_lock lm (_mutex);
392                 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
393                         cc.abort();
394                 }
395                 _video_frame_rate = r;
396         }
397
398         /* Make sure trim is still on a frame boundary */
399         if (video) {
400                 set_trim_start (trim_start());
401         }
402 }
403
404 void
405 Content::unset_video_frame_rate ()
406 {
407         ChangeSignaller<Content> cc (this, ContentProperty::VIDEO_FRAME_RATE);
408
409         {
410                 boost::mutex::scoped_lock lm (_mutex);
411                 _video_frame_rate = optional<double>();
412         }
413 }
414
415 double
416 Content::active_video_frame_rate (shared_ptr<const Film> film) const
417 {
418         {
419                 boost::mutex::scoped_lock lm (_mutex);
420                 if (_video_frame_rate) {
421                         return _video_frame_rate.get ();
422                 }
423         }
424
425         /* No frame rate specified, so assume this content has been
426            prepared for any concurrent video content or perhaps
427            just the DCP rate.
428         */
429         return film->active_frame_rate_change(position()).source;
430 }
431
432 void
433 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
434 {
435         p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
436
437         if (_video_frame_rate) {
438                 if (video) {
439                         p.push_back (
440                                 UserProperty (
441                                         UserProperty::VIDEO,
442                                         _("Frame rate"),
443                                         locale_convert<string> (_video_frame_rate.get(), 5),
444                                         _("frames per second")
445                                         )
446                                 );
447                 } else {
448                         p.push_back (
449                                 UserProperty (
450                                         UserProperty::GENERAL,
451                                         _("Prepared for video frame rate"),
452                                         locale_convert<string> (_video_frame_rate.get(), 5),
453                                         _("frames per second")
454                                         )
455                                 );
456                 }
457         }
458 }
459
460 /** Take settings from the given content if it is of the correct type */
461 void
462 Content::take_settings_from (shared_ptr<const Content> c)
463 {
464         if (video && c->video) {
465                 video->take_settings_from (c->video);
466         }
467         if (audio && c->audio) {
468                 audio->take_settings_from (c->audio);
469         }
470
471         list<shared_ptr<TextContent> >::iterator i = text.begin ();
472         list<shared_ptr<TextContent> >::const_iterator j = c->text.begin ();
473         while (i != text.end() && j != c->text.end()) {
474                 (*i)->take_settings_from (*j);
475                 ++i;
476                 ++j;
477         }
478 }
479
480 shared_ptr<TextContent>
481 Content::only_text () const
482 {
483         DCPOMATIC_ASSERT (text.size() < 2);
484         if (text.empty ()) {
485                 return shared_ptr<TextContent> ();
486         }
487         return text.front ();
488 }
489
490 shared_ptr<TextContent>
491 Content::text_of_original_type (TextType type) const
492 {
493         BOOST_FOREACH (shared_ptr<TextContent> i, text) {
494                 if (i->original_type() == type) {
495                         return i;
496                 }
497         }
498
499         return shared_ptr<TextContent> ();
500 }
501
502 void
503 Content::add_path (boost::filesystem::path p)
504 {
505         boost::mutex::scoped_lock lm (_mutex);
506         _paths.push_back (p);
507         _last_write_times.push_back (boost::filesystem::last_write_time(p));
508 }