Slightly more informative output from diff.
[libdcp.git] / src / cpl.cc
1 /*
2     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include <fstream>
21 #include "cpl.h"
22 #include "parse/cpl.h"
23 #include "util.h"
24 #include "picture_asset.h"
25 #include "sound_asset.h"
26 #include "subtitle_asset.h"
27 #include "parse/asset_map.h"
28 #include "reel.h"
29 #include "metadata.h"
30 #include "exceptions.h"
31 #include "compose.hpp"
32
33 using std::string;
34 using std::stringstream;
35 using std::ofstream;
36 using std::ostream;
37 using std::list;
38 using boost::shared_ptr;
39 using boost::lexical_cast;
40 using namespace libdcp;
41
42 CPL::CPL (string directory, string name, ContentKind content_kind, int length, int frames_per_second)
43         : _directory (directory)
44         , _name (name)
45         , _content_kind (content_kind)
46         , _length (length)
47         , _fps (frames_per_second)
48 {
49         _uuid = make_uuid ();
50 }
51
52 /** Construct a CPL object from a XML file.
53  *  @param directory The directory containing this CPL's DCP.
54  *  @param file The CPL XML filename.
55  *  @param asset_map The corresponding asset map.
56  *  @param require_mxfs true to throw an exception if a required MXF file does not exist.
57  */
58 CPL::CPL (string directory, string file, shared_ptr<const libdcp::parse::AssetMap> asset_map, bool require_mxfs)
59         : _directory (directory)
60         , _content_kind (FEATURE)
61         , _length (0)
62         , _fps (0)
63 {
64         /* Read the XML */
65         shared_ptr<parse::CPL> cpl;
66         try {
67                 cpl.reset (new parse::CPL (file));
68         } catch (FileError& e) {
69                 boost::throw_exception (FileError ("could not load CPL file", file));
70         }
71         
72         /* Now cherry-pick the required bits into our own data structure */
73         
74         _name = cpl->annotation_text;
75         _content_kind = cpl->content_kind;
76
77         for (list<shared_ptr<libdcp::parse::Reel> >::iterator i = cpl->reels.begin(); i != cpl->reels.end(); ++i) {
78
79                 shared_ptr<parse::Picture> p;
80
81                 if ((*i)->asset_list->main_picture) {
82                         p = (*i)->asset_list->main_picture;
83                 } else {
84                         p = (*i)->asset_list->main_stereoscopic_picture;
85                 }
86                 
87                 _fps = p->edit_rate.numerator;
88                 _length += p->duration;
89
90                 shared_ptr<PictureAsset> picture;
91                 shared_ptr<SoundAsset> sound;
92                 shared_ptr<SubtitleAsset> subtitle;
93
94                 /* Some rather twisted logic to decide if we are 3D or not;
95                    some DCPs give a MainStereoscopicPicture to indicate 3D, others
96                    just have a FrameRate twice the EditRate and apparently
97                    expect you to divine the fact that they are hence 3D.
98                 */
99
100                 if (!(*i)->asset_list->main_stereoscopic_picture && p->edit_rate == p->frame_rate) {
101
102                         try {
103                                 picture.reset (new MonoPictureAsset (
104                                                        _directory,
105                                                        asset_map->asset_from_id (p->id)->chunks.front()->path
106                                                        )
107                                         );
108
109                                 picture->set_entry_point (p->entry_point);
110                                 picture->set_duration (p->duration);
111                         } catch (MXFFileError) {
112                                 if (require_mxfs) {
113                                         throw;
114                                 }
115                         }
116                         
117                 } else {
118                         try {
119                                 picture.reset (new StereoPictureAsset (
120                                                        _directory,
121                                                        asset_map->asset_from_id (p->id)->chunks.front()->path,
122                                                        _fps,
123                                                        p->duration
124                                                        )
125                                         );
126
127                                 picture->set_entry_point (p->entry_point);
128                                 picture->set_duration (p->duration);
129                                 
130                         } catch (MXFFileError) {
131                                 if (require_mxfs) {
132                                         throw;
133                                 }
134                         }
135                         
136                 }
137                 
138                 if ((*i)->asset_list->main_sound) {
139                         
140                         try {
141                                 sound.reset (new SoundAsset (
142                                                      _directory,
143                                                      asset_map->asset_from_id ((*i)->asset_list->main_sound->id)->chunks.front()->path
144                                                      )
145                                         );
146
147                                 sound->set_entry_point ((*i)->asset_list->main_sound->entry_point);
148                                 sound->set_duration ((*i)->asset_list->main_sound->duration);
149                         } catch (MXFFileError) {
150                                 if (require_mxfs) {
151                                         throw;
152                                 }
153                         }
154                 }
155
156                 if ((*i)->asset_list->main_subtitle) {
157                         
158                         subtitle.reset (new SubtitleAsset (
159                                                 _directory,
160                                                 asset_map->asset_from_id ((*i)->asset_list->main_subtitle->id)->chunks.front()->path
161                                                 )
162                                 );
163
164                         subtitle->set_entry_point ((*i)->asset_list->main_subtitle->entry_point);
165                         subtitle->set_duration ((*i)->asset_list->main_subtitle->duration);
166                 }
167                         
168                 _reels.push_back (shared_ptr<Reel> (new Reel (picture, sound, subtitle)));
169         }
170 }
171
172 void
173 CPL::add_reel (shared_ptr<const Reel> reel)
174 {
175         _reels.push_back (reel);
176 }
177
178 void
179 CPL::write_xml (XMLMetadata const & metadata) const
180 {
181         boost::filesystem::path p;
182         p /= _directory;
183         stringstream s;
184         s << _uuid << "_cpl.xml";
185         p /= s.str();
186
187         xmlpp::Document doc;
188         xmlpp::Element* root = doc.create_root_node ("CompositionPlaylist", "http://www.smpte-ra.org/schemas/429-7/2006/CPL");
189         root->add_child("Id")->add_child_text ("urn:uuid:" + _uuid);
190         root->add_child("AnnotationText")->add_child_text (_name);
191         root->add_child("IssueDate")->add_child_text (metadata.issue_date);
192         root->add_child("Creator")->add_child_text (metadata.creator);
193         root->add_child("ContentTitleText")->add_child_text (_name);
194         root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
195         {
196                 xmlpp::Node* cv = root->add_child ("ContentVersion");
197                 cv->add_child ("Id")->add_child_text ("urn:uri:" + _uuid + "_" + metadata.issue_date);
198                 cv->add_child ("LabelText")->add_child_text (_uuid + "_" + metadata.issue_date);
199         }
200         root->add_child("RatingList");
201
202         xmlpp::Node* reel_list = root->add_child ("ReelList");
203         
204         for (list<shared_ptr<const Reel> >::const_iterator i = _reels.begin(); i != _reels.end(); ++i) {
205                 (*i)->write_to_cpl (reel_list);
206         }
207
208         doc.write_to_file_formatted (p.string (), "UTF-8");
209
210         _digest = make_digest (p.string ());
211         _length = boost::filesystem::file_size (p.string ());
212 }
213
214 void
215 CPL::write_to_pkl (xmlpp::Node* node) const
216 {
217         xmlpp::Node* asset = node->add_child ("Asset");
218         asset->add_child("Id")->add_child_text ("urn:uuid:" + _uuid);
219         asset->add_child("Hash")->add_child_text (_digest);
220         asset->add_child("Size")->add_child_text (lexical_cast<string> (_length));
221         asset->add_child("Type")->add_child_text ("text/xml");
222 }
223
224 list<shared_ptr<const Asset> >
225 CPL::assets () const
226 {
227         list<shared_ptr<const Asset> > a;
228         for (list<shared_ptr<const Reel> >::const_iterator i = _reels.begin(); i != _reels.end(); ++i) {
229                 if ((*i)->main_picture ()) {
230                         a.push_back ((*i)->main_picture ());
231                 }
232                 if ((*i)->main_sound ()) {
233                         a.push_back ((*i)->main_sound ());
234                 }
235                 if ((*i)->main_subtitle ()) {
236                         a.push_back ((*i)->main_subtitle ());
237                 }
238         }
239
240         return a;
241 }
242
243 void
244 CPL::write_to_assetmap (xmlpp::Node* node) const
245 {
246         xmlpp::Node* asset = node->add_child ("Asset");
247         asset->add_child("Id")->add_child_text ("urn:uuid:" + _uuid);
248         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
249         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
250         chunk->add_child("Path")->add_child_text (_uuid + "_cpl.xml");
251         chunk->add_child("VolumeIndex")->add_child_text ("1");
252         chunk->add_child("Offset")->add_child_text("0");
253         chunk->add_child("Length")->add_child_text(lexical_cast<string> (_length));
254 }
255         
256         
257         
258 bool
259 CPL::equals (CPL const & other, EqualityOptions opt, boost::function<void (NoteType, string)> note) const
260 {
261         if (_name != other._name && !opt.cpl_names_can_differ) {
262                 stringstream s;
263                 s << "names differ: " << _name << " vs " << other._name << "\n";
264                 note (ERROR, s.str ());
265                 return false;
266         }
267
268         if (_content_kind != other._content_kind) {
269                 note (ERROR, "content kinds differ");
270                 return false;
271         }
272
273         if (_fps != other._fps) {
274                 note (ERROR, String::compose ("frames per second differ (%1 vs %2)", _fps, other._fps));
275                 return false;
276         }
277
278         if (_length != other._length) {
279                 stringstream s;
280                 s << "lengths differ (" << _length << " cf " << other._length << ")";
281                 note (ERROR, String::compose ("lengths differ (%1 vs %2)", _length, other._length));
282                 return false;
283         }
284
285         if (_reels.size() != other._reels.size()) {
286                 note (ERROR, String::compose ("reel counts differ (%1 vs %2)", _reels.size(), other._reels.size()));
287                 return false;
288         }
289         
290         list<shared_ptr<const Reel> >::const_iterator a = _reels.begin ();
291         list<shared_ptr<const Reel> >::const_iterator b = other._reels.begin ();
292         
293         while (a != _reels.end ()) {
294                 if (!(*a)->equals (*b, opt, note)) {
295                         return false;
296                 }
297                 ++a;
298                 ++b;
299         }
300
301         return true;
302 }