Supporters update.
[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
22 /** @file  src/lib/content.cc
23  *  @brief Content class.
24  */
25
26
27 #include "audio_content.h"
28 #include "change_signaller.h"
29 #include "compose.hpp"
30 #include "content.h"
31 #include "content_factory.h"
32 #include "exceptions.h"
33 #include "film.h"
34 #include "job.h"
35 #include "text_content.h"
36 #include "util.h"
37 #include "video_content.h"
38 #include <dcp/locale_convert.h>
39 #include <dcp/raw_convert.h>
40 #include <libcxml/cxml.h>
41 #include <libxml++/libxml++.h>
42 #include <boost/thread/mutex.hpp>
43 #include <iostream>
44
45 #include "i18n.h"
46
47
48 using std::cout;
49 using std::list;
50 using std::make_shared;
51 using std::shared_ptr;
52 using std::string;
53 using std::vector;
54 using boost::optional;
55 using dcp::locale_convert;
56 using dcp::raw_convert;
57 using namespace dcpomatic;
58
59
60 int const ContentProperty::PATH = 400;
61 int const ContentProperty::POSITION = 401;
62 int const ContentProperty::LENGTH = 402;
63 int const ContentProperty::TRIM_START = 403;
64 int const ContentProperty::TRIM_END = 404;
65 int const ContentProperty::VIDEO_FRAME_RATE = 405;
66
67
68 Content::Content ()
69         : _change_signals_frequent (false)
70 {
71
72 }
73
74
75 Content::Content (DCPTime p)
76         : _position (p)
77         , _change_signals_frequent (false)
78 {
79
80 }
81
82
83 Content::Content (boost::filesystem::path p)
84         : _change_signals_frequent (false)
85 {
86         add_path (p);
87 }
88
89
90 Content::Content (cxml::ConstNodePtr node)
91         : _change_signals_frequent (false)
92 {
93         for (auto i: node->node_children("Path")) {
94                 _paths.push_back (i->content());
95                 auto const mod = i->optional_number_attribute<time_t>("mtime");
96                 if (mod) {
97                         _last_write_times.push_back (*mod);
98                 } else {
99                         boost::system::error_code ec;
100                         auto last_write = boost::filesystem::last_write_time(i->content(), ec);
101                         _last_write_times.push_back (ec ? 0 : last_write);
102                 }
103         }
104         _digest = node->optional_string_child ("Digest").get_value_or ("X");
105         _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
106         _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
107         _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
108         _video_frame_rate = node->optional_number_child<double> ("VideoFrameRate");
109 }
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
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                         auto 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
169 string
170 Content::calculate_digest () const
171 {
172         /* Some content files are very big, so we use a poor man's
173            digest here: a digest of the first and last 1e6 bytes with the
174            size of the first file tacked on the end as a string.
175         */
176         return simple_digest (paths());
177 }
178
179
180 void
181 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
182 {
183         if (job) {
184                 job->sub (_("Computing digest"));
185         }
186
187         auto const d = calculate_digest ();
188
189         boost::mutex::scoped_lock lm (_mutex);
190         _digest = d;
191
192         _last_write_times.clear ();
193         for (auto i: _paths) {
194                 boost::system::error_code ec;
195                 auto last_write = boost::filesystem::last_write_time(i, ec);
196                 _last_write_times.push_back (ec ? 0 : last_write);
197         }
198 }
199
200
201 void
202 Content::signal_change (ChangeType c, int p)
203 {
204         try {
205                 if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
206                         Change (c, shared_from_this(), p, _change_signals_frequent);
207                 } else {
208                         emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
209                 }
210         } catch (std::bad_weak_ptr &) {
211                 /* This must be during construction; never mind */
212         }
213 }
214
215
216 void
217 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
218 {
219         /* video and audio content can modify its position */
220
221         if (video) {
222                 video->modify_position (film, p);
223         }
224
225         /* Only allow the audio to modify if we have no video;
226            sometimes p can't be on an integer video AND audio frame,
227            and in these cases we want the video constraint to be
228            satisfied since (I think) the audio code is better able to
229            cope.
230         */
231         if (!video && audio) {
232                 audio->modify_position (film, p);
233         }
234
235         ContentChangeSignaller cc (this, ContentProperty::POSITION);
236
237         {
238                 boost::mutex::scoped_lock lm (_mutex);
239                 if (p == _position && !force_emit) {
240                         cc.abort ();
241                         return;
242                 }
243
244                 _position = p;
245         }
246 }
247
248
249 void
250 Content::set_trim_start (ContentTime t)
251 {
252         DCPOMATIC_ASSERT (t.get() >= 0);
253
254         /* video and audio content can modify its start trim */
255
256         if (video) {
257                 video->modify_trim_start (t);
258         }
259
260         /* See note in ::set_position */
261         if (!video && audio) {
262                 audio->modify_trim_start (t);
263         }
264
265         ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
266
267         {
268                 boost::mutex::scoped_lock lm (_mutex);
269                 _trim_start = t;
270         }
271 }
272
273
274 void
275 Content::set_trim_end (ContentTime t)
276 {
277         DCPOMATIC_ASSERT (t.get() >= 0);
278
279         ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
280
281         {
282                 boost::mutex::scoped_lock lm (_mutex);
283                 _trim_end = t;
284         }
285 }
286
287
288 shared_ptr<Content>
289 Content::clone () const
290 {
291         /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
292         xmlpp::Document doc;
293         auto node = doc.create_root_node ("Content");
294         as_xml (node, true);
295
296         /* notes is unused here (we assume) */
297         list<string> notes;
298         return content_factory (make_shared<cxml::Node>(node), Film::current_state_version, notes);
299 }
300
301
302 string
303 Content::technical_summary () const
304 {
305         auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
306         if (_video_frame_rate) {
307                 s += String::compose(" %1", *_video_frame_rate);
308         }
309         return s;
310 }
311
312
313 DCPTime
314 Content::length_after_trim (shared_ptr<const Film> film) const
315 {
316         auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
317         if (video) {
318                 length = length.round(film->video_frame_rate());
319         }
320         return length;
321 }
322
323
324 /** @return string which changes when something about this content changes which affects
325  *  the appearance of its video.
326  */
327 string
328 Content::identifier () const
329 {
330         char buffer[256];
331         snprintf (
332                 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
333                 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
334                 );
335         return buffer;
336 }
337
338
339 bool
340 Content::paths_valid () const
341 {
342         for (auto i: _paths) {
343                 if (!boost::filesystem::exists (i)) {
344                         return false;
345                 }
346         }
347
348         return true;
349 }
350
351
352 void
353 Content::set_paths (vector<boost::filesystem::path> paths)
354 {
355         ContentChangeSignaller cc (this, ContentProperty::PATH);
356
357         {
358                 boost::mutex::scoped_lock lm (_mutex);
359                 _paths = paths;
360                 _last_write_times.clear ();
361                 for (auto i: _paths) {
362                         boost::system::error_code ec;
363                         auto last_write = boost::filesystem::last_write_time(i, ec);
364                         _last_write_times.push_back (ec ? 0 : last_write);
365                 }
366         }
367 }
368
369
370 string
371 Content::path_summary () const
372 {
373         /* XXX: should handle multiple paths more gracefully */
374
375         DCPOMATIC_ASSERT (number_of_paths ());
376
377         auto s = path(0).filename().string();
378         if (number_of_paths() > 1) {
379                 s += " ...";
380         }
381
382         return s;
383 }
384
385
386 /** @return a list of properties that might be interesting to the user */
387 list<UserProperty>
388 Content::user_properties (shared_ptr<const Film> film) const
389 {
390         list<UserProperty> p;
391         add_properties (film, p);
392         return p;
393 }
394
395
396 /** @return DCP times of points within this content where a reel split could occur */
397 list<DCPTime>
398 Content::reel_split_points (shared_ptr<const Film>) const
399 {
400         list<DCPTime> t;
401         /* This is only called for video content and such content has its position forced
402            to start on a frame boundary.
403         */
404         t.push_back (position());
405         return t;
406 }
407
408
409 void
410 Content::set_video_frame_rate (double r)
411 {
412         ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
413
414         {
415                 boost::mutex::scoped_lock lm (_mutex);
416                 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
417                         cc.abort();
418                 }
419                 _video_frame_rate = r;
420         }
421
422         /* Make sure trim is still on a frame boundary */
423         if (video) {
424                 set_trim_start (trim_start());
425         }
426 }
427
428
429 void
430 Content::unset_video_frame_rate ()
431 {
432         ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
433
434         {
435                 boost::mutex::scoped_lock lm (_mutex);
436                 _video_frame_rate = optional<double>();
437         }
438 }
439
440
441 double
442 Content::active_video_frame_rate (shared_ptr<const Film> film) const
443 {
444         {
445                 boost::mutex::scoped_lock lm (_mutex);
446                 if (_video_frame_rate) {
447                         return _video_frame_rate.get ();
448                 }
449         }
450
451         /* No frame rate specified, so assume this content has been
452            prepared for any concurrent video content or perhaps
453            just the DCP rate.
454         */
455         return film->active_frame_rate_change(position()).source;
456 }
457
458
459 void
460 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
461 {
462         auto paths_to_show = std::min(number_of_paths(), size_t{8});
463         string paths = "";
464         for (auto i = size_t{0}; i < paths_to_show; ++i) {
465                 paths += path(i).string();
466                 if (i < (paths_to_show - 1)) {
467                         paths += "\n";
468                 }
469         }
470         if (paths_to_show < number_of_paths()) {
471                 paths += String::compose("... and %1 more", number_of_paths() - paths_to_show);
472         }
473         p.push_back (
474                 UserProperty(
475                         UserProperty::GENERAL,
476                         paths_to_show > 1 ? _("Filenames") : _("Filename"),
477                         paths
478                         )
479                 );
480
481         if (_video_frame_rate) {
482                 if (video) {
483                         p.push_back (
484                                 UserProperty (
485                                         UserProperty::VIDEO,
486                                         _("Frame rate"),
487                                         locale_convert<string> (_video_frame_rate.get(), 5),
488                                         _("frames per second")
489                                         )
490                                 );
491                 } else {
492                         p.push_back (
493                                 UserProperty (
494                                         UserProperty::GENERAL,
495                                         _("Prepared for video frame rate"),
496                                         locale_convert<string> (_video_frame_rate.get(), 5),
497                                         _("frames per second")
498                                         )
499                                 );
500                 }
501         }
502 }
503
504
505 /** Take settings from the given content if it is of the correct type */
506 void
507 Content::take_settings_from (shared_ptr<const Content> c)
508 {
509         if (video && c->video) {
510                 video->take_settings_from (c->video);
511         }
512         if (audio && c->audio) {
513                 audio->take_settings_from (c->audio);
514         }
515
516         auto i = text.begin ();
517         auto j = c->text.begin ();
518         while (i != text.end() && j != c->text.end()) {
519                 (*i)->take_settings_from (*j);
520                 ++i;
521                 ++j;
522         }
523 }
524
525
526 shared_ptr<TextContent>
527 Content::only_text () const
528 {
529         DCPOMATIC_ASSERT (text.size() < 2);
530         if (text.empty()) {
531                 return {};
532         }
533         return text.front ();
534 }
535
536
537 shared_ptr<TextContent>
538 Content::text_of_original_type (TextType type) const
539 {
540         for (auto i: text) {
541                 if (i->original_type() == type) {
542                         return i;
543                 }
544         }
545
546         return {};
547 }
548
549
550 void
551 Content::add_path (boost::filesystem::path p)
552 {
553         boost::mutex::scoped_lock lm (_mutex);
554         _paths.push_back (p);
555         boost::system::error_code ec;
556         auto last_write = boost::filesystem::last_write_time(p, ec);
557         _last_write_times.push_back (ec ? 0 : last_write);
558 }
559
560
561 bool
562 Content::changed () const
563 {
564         bool write_time_changed = false;
565         for (auto i = 0U; i < _paths.size(); ++i) {
566                 if (boost::filesystem::last_write_time(_paths[i]) != last_write_time(i)) {
567                         write_time_changed = true;
568                         break;
569                 }
570         }
571
572         return (write_time_changed || calculate_digest() != digest());
573 }