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