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