e77a3638ca6f2d4d73deacf7b2fb3a97f5225b71
[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 = boost::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 = boost::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                 _trim_start = t;
265         }
266 }
267
268
269 void
270 Content::set_trim_end (ContentTime t)
271 {
272         DCPOMATIC_ASSERT (t.get() >= 0);
273
274         ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
275
276         {
277                 boost::mutex::scoped_lock lm (_mutex);
278                 _trim_end = t;
279         }
280 }
281
282
283 shared_ptr<Content>
284 Content::clone () const
285 {
286         /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
287         xmlpp::Document doc;
288         auto node = doc.create_root_node ("Content");
289         as_xml (node, true);
290
291         /* notes is unused here (we assume) */
292         list<string> notes;
293         return content_factory (make_shared<cxml::Node>(node), Film::current_state_version, notes);
294 }
295
296
297 string
298 Content::technical_summary () const
299 {
300         auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
301         if (_video_frame_rate) {
302                 s += String::compose(" %1", *_video_frame_rate);
303         }
304         return s;
305 }
306
307
308 DCPTime
309 Content::length_after_trim (shared_ptr<const Film> film) const
310 {
311         auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
312         if (video) {
313                 length = length.round(film->video_frame_rate());
314         }
315         return length;
316 }
317
318
319 /** @return string which changes when something about this content changes which affects
320  *  the appearance of its video.
321  */
322 string
323 Content::identifier () const
324 {
325         char buffer[256];
326         snprintf (
327                 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
328                 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
329                 );
330         return buffer;
331 }
332
333
334 bool
335 Content::paths_valid () const
336 {
337         for (auto i: _paths) {
338                 if (!boost::filesystem::exists (i)) {
339                         return false;
340                 }
341         }
342
343         return true;
344 }
345
346
347 void
348 Content::set_paths (vector<boost::filesystem::path> paths)
349 {
350         ContentChangeSignaller cc (this, ContentProperty::PATH);
351
352         {
353                 boost::mutex::scoped_lock lm (_mutex);
354                 _paths = paths;
355                 _last_write_times.clear ();
356                 for (auto i: _paths) {
357                         boost::system::error_code ec;
358                         auto last_write = boost::filesystem::last_write_time(i, ec);
359                         _last_write_times.push_back (ec ? 0 : last_write);
360                 }
361         }
362 }
363
364
365 string
366 Content::path_summary () const
367 {
368         /* XXX: should handle multiple paths more gracefully */
369
370         DCPOMATIC_ASSERT (number_of_paths ());
371
372         auto s = path(0).filename().string();
373         if (number_of_paths() > 1) {
374                 s += " ...";
375         }
376
377         return s;
378 }
379
380
381 /** @return a list of properties that might be interesting to the user */
382 list<UserProperty>
383 Content::user_properties (shared_ptr<const Film> film) const
384 {
385         list<UserProperty> p;
386         add_properties (film, p);
387         return p;
388 }
389
390
391 /** @return DCP times of points within this content where a reel split could occur */
392 list<DCPTime>
393 Content::reel_split_points (shared_ptr<const Film>) const
394 {
395         list<DCPTime> t;
396         /* This is only called for video content and such content has its position forced
397            to start on a frame boundary.
398         */
399         t.push_back (position());
400         return t;
401 }
402
403
404 void
405 Content::set_video_frame_rate(shared_ptr<const Film> film, double r)
406 {
407         ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
408
409         {
410                 boost::mutex::scoped_lock lm (_mutex);
411                 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
412                         cc.abort();
413                 }
414                 _video_frame_rate = r;
415         }
416
417         /* Make sure trim is still on a frame boundary */
418         if (video) {
419                 set_trim_start(film, trim_start());
420         }
421 }
422
423
424 void
425 Content::unset_video_frame_rate ()
426 {
427         ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
428
429         {
430                 boost::mutex::scoped_lock lm (_mutex);
431                 _video_frame_rate = optional<double>();
432         }
433 }
434
435
436 double
437 Content::active_video_frame_rate (shared_ptr<const Film> film) const
438 {
439         {
440                 boost::mutex::scoped_lock lm (_mutex);
441                 if (_video_frame_rate) {
442                         return _video_frame_rate.get ();
443                 }
444         }
445
446         /* No frame rate specified, so assume this content has been
447            prepared for any concurrent video content or perhaps
448            just the DCP rate.
449         */
450         return film->active_frame_rate_change(position()).source;
451 }
452
453
454 void
455 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
456 {
457         auto paths_to_show = std::min(number_of_paths(), size_t{8});
458         string paths = "";
459         for (auto i = size_t{0}; i < paths_to_show; ++i) {
460                 paths += path(i).string();
461                 if (i < (paths_to_show - 1)) {
462                         paths += "\n";
463                 }
464         }
465         if (paths_to_show < number_of_paths()) {
466                 paths += String::compose("... and %1 more", number_of_paths() - paths_to_show);
467         }
468         p.push_back (
469                 UserProperty(
470                         UserProperty::GENERAL,
471                         paths_to_show > 1 ? _("Filenames") : _("Filename"),
472                         paths
473                         )
474                 );
475
476         if (_video_frame_rate) {
477                 if (video) {
478                         p.push_back (
479                                 UserProperty (
480                                         UserProperty::VIDEO,
481                                         _("Frame rate"),
482                                         locale_convert<string> (_video_frame_rate.get(), 5),
483                                         _("frames per second")
484                                         )
485                                 );
486                 } else {
487                         p.push_back (
488                                 UserProperty (
489                                         UserProperty::GENERAL,
490                                         _("Prepared for video frame rate"),
491                                         locale_convert<string> (_video_frame_rate.get(), 5),
492                                         _("frames per second")
493                                         )
494                                 );
495                 }
496         }
497 }
498
499
500 /** Take settings from the given content if it is of the correct type */
501 void
502 Content::take_settings_from (shared_ptr<const Content> c)
503 {
504         if (video && c->video) {
505                 video->take_settings_from (c->video);
506         }
507         if (audio && c->audio) {
508                 audio->take_settings_from (c->audio);
509         }
510
511         auto i = text.begin ();
512         auto j = c->text.begin ();
513         while (i != text.end() && j != c->text.end()) {
514                 (*i)->take_settings_from (*j);
515                 ++i;
516                 ++j;
517         }
518 }
519
520
521 shared_ptr<TextContent>
522 Content::only_text () const
523 {
524         DCPOMATIC_ASSERT (text.size() < 2);
525         if (text.empty()) {
526                 return {};
527         }
528         return text.front ();
529 }
530
531
532 shared_ptr<TextContent>
533 Content::text_of_original_type (TextType type) const
534 {
535         for (auto i: text) {
536                 if (i->original_type() == type) {
537                         return i;
538                 }
539         }
540
541         return {};
542 }
543
544
545 void
546 Content::add_path (boost::filesystem::path p)
547 {
548         boost::mutex::scoped_lock lm (_mutex);
549         _paths.push_back (p);
550         boost::system::error_code ec;
551         auto last_write = boost::filesystem::last_write_time(p, ec);
552         _last_write_times.push_back (ec ? 0 : last_write);
553 }
554
555
556 bool
557 Content::changed () const
558 {
559         bool write_time_changed = false;
560         for (auto i = 0U; i < _paths.size(); ++i) {
561                 if (boost::filesystem::last_write_time(_paths[i]) != last_write_time(i)) {
562                         write_time_changed = true;
563                         break;
564                 }
565         }
566
567         return (write_time_changed || calculate_digest() != digest());
568 }