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