Log content change checking.
[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 "dcpomatic_log.h"
33 #include "exceptions.h"
34 #include "film.h"
35 #include "job.h"
36 #include "text_content.h"
37 #include "util.h"
38 #include "video_content.h"
39 #include <dcp/locale_convert.h>
40 #include <dcp/raw_convert.h>
41 #include <libcxml/cxml.h>
42 #include <libxml++/libxml++.h>
43 #include <boost/thread/mutex.hpp>
44 #include <iostream>
45
46 #include "i18n.h"
47
48
49 using std::cout;
50 using std::list;
51 using std::make_shared;
52 using std::shared_ptr;
53 using std::string;
54 using std::vector;
55 using boost::optional;
56 using dcp::locale_convert;
57 using dcp::raw_convert;
58 using namespace dcpomatic;
59
60
61 int const ContentProperty::PATH = 400;
62 int const ContentProperty::POSITION = 401;
63 int const ContentProperty::LENGTH = 402;
64 int const ContentProperty::TRIM_START = 403;
65 int const ContentProperty::TRIM_END = 404;
66 int const ContentProperty::VIDEO_FRAME_RATE = 405;
67
68
69 Content::Content ()
70 {
71
72 }
73
74
75 Content::Content (DCPTime p)
76         : _position (p)
77 {
78
79 }
80
81
82 Content::Content (boost::filesystem::path p)
83 {
84         add_path (p);
85 }
86
87
88 Content::Content (cxml::ConstNodePtr node)
89 {
90         for (auto i: node->node_children("Path")) {
91                 _paths.push_back (i->content());
92                 auto const mod = i->optional_number_attribute<time_t>("mtime");
93                 if (mod) {
94                         _last_write_times.push_back (*mod);
95                 } else {
96                         boost::system::error_code ec;
97                         auto last_write = boost::filesystem::last_write_time(i->content(), ec);
98                         _last_write_times.push_back (ec ? 0 : last_write);
99                 }
100         }
101         _digest = node->optional_string_child ("Digest").get_value_or ("X");
102         _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
103         _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
104         _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
105         _video_frame_rate = node->optional_number_child<double> ("VideoFrameRate");
106 }
107
108
109 Content::Content (vector<shared_ptr<Content>> c)
110         : _position (c.front()->position())
111         , _trim_start (c.front()->trim_start())
112         , _trim_end (c.back()->trim_end())
113         , _video_frame_rate (c.front()->video_frame_rate())
114 {
115         for (size_t i = 0; i < c.size(); ++i) {
116                 if (i > 0 && c[i]->trim_start() > ContentTime ()) {
117                         throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
118                 }
119
120                 if (i < (c.size() - 1) && c[i]->trim_end () > ContentTime ()) {
121                         throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
122                 }
123
124                 if (
125                         (_video_frame_rate && !c[i]->_video_frame_rate) ||
126                         (!_video_frame_rate && c[i]->_video_frame_rate)
127                         ) {
128                         throw JoinError (_("Content to be joined must have the same video frame rate"));
129                 }
130
131                 if (_video_frame_rate && fabs (_video_frame_rate.get() - c[i]->_video_frame_rate.get()) > VIDEO_FRAME_RATE_EPSILON) {
132                         throw JoinError (_("Content to be joined must have the same video frame rate"));
133                 }
134
135                 for (size_t j = 0; j < c[i]->number_of_paths(); ++j) {
136                         _paths.push_back (c[i]->path(j));
137                         _last_write_times.push_back (c[i]->_last_write_times[j]);
138                 }
139         }
140 }
141
142
143 void
144 Content::as_xml (xmlpp::Node* node, bool with_paths) const
145 {
146         boost::mutex::scoped_lock lm (_mutex);
147
148         if (with_paths) {
149                 for (size_t i = 0; i < _paths.size(); ++i) {
150                         auto p = node->add_child("Path");
151                         p->add_child_text (_paths[i].string());
152                         p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
153                 }
154         }
155         node->add_child("Digest")->add_child_text(_digest);
156         node->add_child("Position")->add_child_text(raw_convert<string>(_position.get()));
157         node->add_child("TrimStart")->add_child_text(raw_convert<string>(_trim_start.get()));
158         node->add_child("TrimEnd")->add_child_text(raw_convert<string>(_trim_end.get()));
159         if (_video_frame_rate) {
160                 node->add_child("VideoFrameRate")->add_child_text(raw_convert<string>(_video_frame_rate.get()));
161         }
162 }
163
164
165 string
166 Content::calculate_digest () const
167 {
168         /* Some content files are very big, so we use a poor man's
169            digest here: a digest of the first and last 1e6 bytes with the
170            size of the first file tacked on the end as a string.
171         */
172         return simple_digest (paths());
173 }
174
175
176 void
177 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
178 {
179         if (job) {
180                 job->sub (_("Computing digest"));
181         }
182
183         auto const d = calculate_digest ();
184
185         boost::mutex::scoped_lock lm (_mutex);
186         _digest = d;
187
188         _last_write_times.clear ();
189         for (auto i: _paths) {
190                 boost::system::error_code ec;
191                 auto last_write = boost::filesystem::last_write_time(i, ec);
192                 _last_write_times.push_back (ec ? 0 : last_write);
193         }
194 }
195
196
197 void
198 Content::signal_change (ChangeType c, int p)
199 {
200         try {
201                 if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
202                         Change (c, shared_from_this(), p, _change_signals_frequent);
203                 } else {
204                         emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
205                 }
206         } catch (std::bad_weak_ptr &) {
207                 /* This must be during construction; never mind */
208         }
209 }
210
211
212 void
213 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
214 {
215         /* video and audio content can modify its position */
216
217         if (video) {
218                 video->modify_position (film, p);
219         }
220
221         /* Only allow the audio to modify if we have no video;
222            sometimes p can't be on an integer video AND audio frame,
223            and in these cases we want the video constraint to be
224            satisfied since (I think) the audio code is better able to
225            cope.
226         */
227         if (!video && audio) {
228                 audio->modify_position (film, p);
229         }
230
231         ContentChangeSignaller cc (this, ContentProperty::POSITION);
232
233         {
234                 boost::mutex::scoped_lock lm (_mutex);
235                 if (p == _position && !force_emit) {
236                         cc.abort ();
237                         return;
238                 }
239
240                 _position = p;
241         }
242 }
243
244
245 void
246 Content::set_trim_start(shared_ptr<const Film> film, ContentTime t)
247 {
248         DCPOMATIC_ASSERT (t.get() >= 0);
249
250         /* video and audio content can modify its start trim */
251
252         if (video) {
253                 video->modify_trim_start (t);
254         }
255
256         /* See note in ::set_position */
257         if (!video && audio) {
258                 audio->modify_trim_start(film, t);
259         }
260
261         ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
262
263         {
264                 boost::mutex::scoped_lock lm (_mutex);
265                 if (_trim_start == t) {
266                         cc.abort();
267                 } else {
268                         _trim_start = t;
269                 }
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(shared_ptr<const Film> film, 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(film, 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                 auto const current_last_write_time = boost::filesystem::last_write_time(_paths[i]);
567                 if (current_last_write_time != last_write_time(i)) {
568                         LOG_GENERAL("Content %1 write time changed from %2 to %3", _paths[i], last_write_time(i), current_last_write_time);
569                         write_time_changed = true;
570                         break;
571                 }
572         }
573
574         /* Only check digest if the write time is the same */
575         auto const digest_changed = !write_time_changed && (calculate_digest() != digest());
576         if (digest_changed) {
577                 LOG_GENERAL("Content %1 digest changed", _paths[0]);
578         }
579
580         return (write_time_changed || digest_changed);
581 }