Bump libdcp for new method.
[dcpomatic.git] / src / lib / content.cc
1 /*
2     Copyright (C) 2013-2015 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 /** @file  src/lib/content.cc
22  *  @brief Content class.
23  */
24
25 #include "content.h"
26 #include "util.h"
27 #include "content_factory.h"
28 #include "exceptions.h"
29 #include "film.h"
30 #include "job.h"
31 #include "compose.hpp"
32 #include <dcp/locale_convert.h>
33 #include <dcp/raw_convert.h>
34 #include <libcxml/cxml.h>
35 #include <libxml++/libxml++.h>
36 #include <boost/thread/mutex.hpp>
37 #include <iostream>
38
39 #include "i18n.h"
40
41 using std::string;
42 using std::list;
43 using std::cout;
44 using std::vector;
45 using std::max;
46 using std::pair;
47 using boost::shared_ptr;
48 using dcp::raw_convert;
49 using dcp::locale_convert;
50
51 int const ContentProperty::PATH = 400;
52 int const ContentProperty::POSITION = 401;
53 int const ContentProperty::LENGTH = 402;
54 int const ContentProperty::TRIM_START = 403;
55 int const ContentProperty::TRIM_END = 404;
56 int const ContentProperty::VIDEO_FRAME_RATE = 405;
57
58 Content::Content (shared_ptr<const Film> film)
59         : _film (film)
60         , _position (0)
61         , _trim_start (0)
62         , _trim_end (0)
63         , _change_signals_frequent (false)
64 {
65
66 }
67
68 Content::Content (shared_ptr<const Film> film, DCPTime p)
69         : _film (film)
70         , _position (p)
71         , _trim_start (0)
72         , _trim_end (0)
73         , _change_signals_frequent (false)
74 {
75
76 }
77
78 Content::Content (shared_ptr<const Film> film, boost::filesystem::path p)
79         : _film (film)
80         , _position (0)
81         , _trim_start (0)
82         , _trim_end (0)
83         , _change_signals_frequent (false)
84 {
85         _paths.push_back (p);
86 }
87
88 Content::Content (shared_ptr<const Film> film, cxml::ConstNodePtr node)
89         : _film (film)
90         , _change_signals_frequent (false)
91 {
92         list<cxml::NodePtr> path_children = node->node_children ("Path");
93         for (list<cxml::NodePtr>::const_iterator i = path_children.begin(); i != path_children.end(); ++i) {
94                 _paths.push_back ((*i)->content ());
95         }
96         _digest = node->optional_string_child ("Digest").get_value_or ("X");
97         _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
98         _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
99         _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
100         _video_frame_rate = node->optional_number_child<double> ("VideoFrameRate");
101 }
102
103 Content::Content (shared_ptr<const Film> film, vector<shared_ptr<Content> > c)
104         : _film (film)
105         , _position (c.front()->position ())
106         , _trim_start (c.front()->trim_start ())
107         , _trim_end (c.back()->trim_end ())
108         , _video_frame_rate (c.front()->video_frame_rate())
109         , _change_signals_frequent (false)
110 {
111         for (size_t i = 0; i < c.size(); ++i) {
112                 if (i > 0 && c[i]->trim_start() > ContentTime ()) {
113                         throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
114                 }
115
116                 if (i < (c.size() - 1) && c[i]->trim_end () > ContentTime ()) {
117                         throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
118                 }
119
120                 if (
121                         (_video_frame_rate && !c[i]->_video_frame_rate) ||
122                         (!_video_frame_rate && c[i]->_video_frame_rate)
123                         ) {
124                         throw JoinError (_("Content to be joined must have the same video frame rate"));
125                 }
126
127                 if (_video_frame_rate && fabs (_video_frame_rate.get() - c[i]->_video_frame_rate.get()) > VIDEO_FRAME_RATE_EPSILON) {
128                         throw JoinError (_("Content to be joined must have the same video frame rate"));
129                 }
130
131                 for (size_t j = 0; j < c[i]->number_of_paths(); ++j) {
132                         _paths.push_back (c[i]->path (j));
133                 }
134         }
135 }
136
137 void
138 Content::as_xml (xmlpp::Node* node, bool with_paths) const
139 {
140         boost::mutex::scoped_lock lm (_mutex);
141
142         if (with_paths) {
143                 for (vector<boost::filesystem::path>::const_iterator i = _paths.begin(); i != _paths.end(); ++i) {
144                         node->add_child("Path")->add_child_text (i->string ());
145                 }
146         }
147         node->add_child("Digest")->add_child_text (_digest);
148         node->add_child("Position")->add_child_text (raw_convert<string> (_position.get ()));
149         node->add_child("TrimStart")->add_child_text (raw_convert<string> (_trim_start.get ()));
150         node->add_child("TrimEnd")->add_child_text (raw_convert<string> (_trim_end.get ()));
151         if (_video_frame_rate) {
152                 node->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate.get()));
153         }
154 }
155
156 void
157 Content::examine (shared_ptr<Job> job)
158 {
159         if (job) {
160                 job->sub (_("Computing digest"));
161         }
162
163         boost::mutex::scoped_lock lm (_mutex);
164         vector<boost::filesystem::path> p = _paths;
165         lm.unlock ();
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         string const d = digest_head_tail (p, 1000000) + raw_convert<string> (boost::filesystem::file_size (p.front ()));
172
173         lm.lock ();
174         _digest = d;
175 }
176
177 void
178 Content::signal_changed (int p)
179 {
180         try {
181                 emit (boost::bind (boost::ref (Changed), shared_from_this (), p, _change_signals_frequent));
182         } catch (boost::bad_weak_ptr) {
183                 /* This must be during construction; never mind */
184         }
185 }
186
187 void
188 Content::set_position (DCPTime p)
189 {
190         {
191                 boost::mutex::scoped_lock lm (_mutex);
192                 if (p == _position) {
193                         return;
194                 }
195
196                 _position = p;
197         }
198
199         signal_changed (ContentProperty::POSITION);
200 }
201
202 void
203 Content::set_trim_start (ContentTime t)
204 {
205         {
206                 boost::mutex::scoped_lock lm (_mutex);
207                 _trim_start = t;
208         }
209
210         signal_changed (ContentProperty::TRIM_START);
211 }
212
213 void
214 Content::set_trim_end (ContentTime t)
215 {
216         {
217                 boost::mutex::scoped_lock lm (_mutex);
218                 _trim_end = t;
219         }
220
221         signal_changed (ContentProperty::TRIM_END);
222 }
223
224
225 shared_ptr<Content>
226 Content::clone () const
227 {
228         shared_ptr<const Film> film = _film.lock ();
229         if (!film) {
230                 return shared_ptr<Content> ();
231         }
232
233         /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
234         xmlpp::Document doc;
235         xmlpp::Node* node = doc.create_root_node ("Content");
236         as_xml (node);
237
238         /* notes is unused here (we assume) */
239         list<string> notes;
240         return content_factory (film, cxml::NodePtr (new cxml::Node (node)), Film::current_state_version, notes);
241 }
242
243 string
244 Content::technical_summary () const
245 {
246         return String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
247 }
248
249 DCPTime
250 Content::length_after_trim () const
251 {
252         return max (DCPTime (), full_length() - DCPTime (trim_start() + trim_end(), film()->active_frame_rate_change (position ())));
253 }
254
255 /** @return string which changes when something about this content changes which affects
256  *  the appearance of its video.
257  */
258 string
259 Content::identifier () const
260 {
261         char buffer[256];
262         snprintf (
263                 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
264                 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
265                 );
266         return buffer;
267 }
268
269 bool
270 Content::paths_valid () const
271 {
272         for (vector<boost::filesystem::path>::const_iterator i = _paths.begin(); i != _paths.end(); ++i) {
273                 if (!boost::filesystem::exists (*i)) {
274                         return false;
275                 }
276         }
277
278         return true;
279 }
280
281 void
282 Content::set_path (boost::filesystem::path path)
283 {
284         _paths.clear ();
285         _paths.push_back (path);
286         signal_changed (ContentProperty::PATH);
287 }
288
289 string
290 Content::path_summary () const
291 {
292         /* XXX: should handle multiple paths more gracefully */
293
294         DCPOMATIC_ASSERT (number_of_paths ());
295
296         string s = path(0).filename().string ();
297         if (number_of_paths() > 1) {
298                 s += " ...";
299         }
300
301         return s;
302 }
303
304 /** @return a list of properties that might be interesting to the user */
305 list<UserProperty>
306 Content::user_properties () const
307 {
308         list<UserProperty> p;
309         add_properties (p);
310         return p;
311 }
312
313 shared_ptr<const Film>
314 Content::film () const
315 {
316         shared_ptr<const Film> film = _film.lock ();
317         DCPOMATIC_ASSERT (film);
318         return film;
319 }
320
321 /** @return DCP times of points within this content where a reel split could occur */
322 list<DCPTime>
323 Content::reel_split_points () const
324 {
325         list<DCPTime> t;
326         /* XXX: this is questionable; perhaps the position itself should be forced to be on a frame boundary */
327         t.push_back (position().round_up (film()->video_frame_rate()));
328         return t;
329 }
330
331 void
332 Content::set_video_frame_rate (double r)
333 {
334         {
335                 boost::mutex::scoped_lock lm (_mutex);
336                 _video_frame_rate = r;
337         }
338
339         signal_changed (ContentProperty::VIDEO_FRAME_RATE);
340 }
341
342 double
343 Content::active_video_frame_rate () const
344 {
345         {
346                 boost::mutex::scoped_lock lm (_mutex);
347                 if (_video_frame_rate) {
348                         return _video_frame_rate.get ();
349                 }
350         }
351
352         /* No frame rate specified, so assume this content has been
353            prepared for any concurrent video content or perhaps
354            just the DCP rate.
355         */
356         shared_ptr<const Film> film = _film.lock ();
357         DCPOMATIC_ASSERT (film);
358         return film->active_frame_rate_change(position()).source;
359 }
360
361 void
362 Content::add_properties (list<UserProperty>& p) const
363 {
364         p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
365
366         if (_video_frame_rate) {
367                 if (video) {
368                         p.push_back (
369                                 UserProperty (
370                                         UserProperty::VIDEO,
371                                         _("Frame rate"),
372                                         locale_convert<string> (_video_frame_rate.get(), 5),
373                                         _("frames per second")
374                                         )
375                                 );
376                 } else {
377                         p.push_back (
378                                 UserProperty (
379                                         UserProperty::GENERAL,
380                                         _("Prepared for video frame rate"),
381                                         locale_convert<string> (_video_frame_rate.get(), 5),
382                                         _("frames per second")
383                                         )
384                                 );
385                 }
386         }
387 }