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