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