a5b0c6e43db811bd7434079f916da808ba9ef8b1
[libdcp.git] / src / dcp.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 /** @file  src/dcp.cc
21  *  @brief A class to create a DCP.
22  */
23
24 #include <sstream>
25 #include <iomanip>
26 #include <cassert>
27 #include <iostream>
28 #include <boost/filesystem.hpp>
29 #include <boost/lexical_cast.hpp>
30 #include <boost/algorithm/string.hpp>
31 #include <boost/lexical_cast.hpp>
32 #include <libxml++/libxml++.h>
33 #include <xmlsec/xmldsig.h>
34 #include <xmlsec/app.h>
35 #include "dcp.h"
36 #include "sound_mxf.h"
37 #include "picture_mxf.h"
38 #include "subtitle_asset.h"
39 #include "util.h"
40 #include "metadata.h"
41 #include "exceptions.h"
42 #include "parse/pkl.h"
43 #include "parse/asset_map.h"
44 #include "reel.h"
45 #include "cpl.h"
46 #include "signer.h"
47 #include "kdm.h"
48
49 using std::string;
50 using std::list;
51 using std::stringstream;
52 using std::ostream;
53 using std::copy;
54 using std::back_inserter;
55 using std::make_pair;
56 using boost::shared_ptr;
57 using boost::lexical_cast;
58 using namespace dcp;
59
60 DCP::DCP (boost::filesystem::path directory)
61         : _directory (directory)
62 {
63         boost::filesystem::create_directories (directory);
64 }
65
66 void
67 DCP::write_xml (bool interop, XMLMetadata const & metadata, shared_ptr<const Signer> signer) const
68 {
69         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
70                 (*i)->write_xml (interop, metadata, signer);
71         }
72
73         string pkl_uuid = make_uuid ();
74         string pkl_path = write_pkl (pkl_uuid, interop, metadata, signer);
75         
76         write_volindex (interop);
77         write_assetmap (pkl_uuid, boost::filesystem::file_size (pkl_path), interop, metadata);
78 }
79
80 std::string
81 DCP::write_pkl (string pkl_uuid, bool interop, XMLMetadata const & metadata, shared_ptr<const Signer> signer) const
82 {
83         assert (!_cpls.empty ());
84         
85         boost::filesystem::path p;
86         p /= _directory;
87         stringstream s;
88         s << pkl_uuid << "_pkl.xml";
89         p /= s.str();
90
91         xmlpp::Document doc;
92         xmlpp::Element* pkl;
93         if (interop) {
94                 pkl = doc.create_root_node("PackingList", "http://www.digicine.com/PROTO-ASDCP-PKL-20040311#");
95         } else {
96                 pkl = doc.create_root_node("PackingList", "http://www.smpte-ra.org/schemas/429-8/2007/PKL");
97         }
98         
99         if (signer) {
100                 pkl->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
101         }
102
103         pkl->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
104         /* XXX: this is a bit of a hack */
105         pkl->add_child("AnnotationText")->add_child_text(_cpls.front()->name());
106         pkl->add_child("IssueDate")->add_child_text (metadata.issue_date);
107         pkl->add_child("Issuer")->add_child_text (metadata.issuer);
108         pkl->add_child("Creator")->add_child_text (metadata.creator);
109
110         xmlpp::Element* asset_list = pkl->add_child("AssetList");
111         list<shared_ptr<const Content> > a = assets ();
112         for (list<shared_ptr<const Content> >::const_iterator i = a.begin(); i != a.end(); ++i) {
113                 (*i)->write_to_pkl (asset_list);
114         }
115         
116         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
117                 (*i)->write_to_pkl (asset_list);
118         }
119
120         if (signer) {
121                 signer->sign (pkl, interop);
122         }
123                 
124         doc.write_to_file (p.string (), "UTF-8");
125         return p.string ();
126 }
127
128 void
129 DCP::write_volindex (bool interop) const
130 {
131         boost::filesystem::path p;
132         p /= _directory;
133         if (interop) {
134                 p /= "VOLINDEX";
135         } else {
136                 p /= "VOLINDEX.xml";
137         }
138
139         xmlpp::Document doc;
140         xmlpp::Element* root = doc.create_root_node ("VolumeIndex", "http://www.smpte-ra.org/schemas/429-9/2007/AM");
141         root->add_child("Index")->add_child_text ("1");
142         doc.write_to_file (p.string (), "UTF-8");
143 }
144
145 void
146 DCP::write_assetmap (string pkl_uuid, int pkl_length, bool interop, XMLMetadata const & metadata) const
147 {
148         boost::filesystem::path p;
149         p /= _directory;
150         if (interop) {
151                 p /= "ASSETMAP";
152         } else {
153                 p /= "ASSETMAP.xml";
154         }
155
156         xmlpp::Document doc;
157         xmlpp::Element* root;
158         if (interop) {
159                 root = doc.create_root_node ("AssetMap", "http://www.digicine.com/PROTO-ASDCP-AM-20040311#");
160         } else {
161                 root = doc.create_root_node ("AssetMap", "http://www.smpte-ra.org/schemas/429-9/2007/AM");
162         }
163
164         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
165         root->add_child("AnnotationText")->add_child_text ("Created by " + metadata.creator);
166         if (interop) {
167                 root->add_child("VolumeCount")->add_child_text ("1");
168                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
169                 root->add_child("Issuer")->add_child_text (metadata.issuer);
170                 root->add_child("Creator")->add_child_text (metadata.creator);
171         } else {
172                 root->add_child("Creator")->add_child_text (metadata.creator);
173                 root->add_child("VolumeCount")->add_child_text ("1");
174                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
175                 root->add_child("Issuer")->add_child_text (metadata.issuer);
176         }
177                 
178         xmlpp::Node* asset_list = root->add_child ("AssetList");
179
180         xmlpp::Node* asset = asset_list->add_child ("Asset");
181         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
182         asset->add_child("PackingList")->add_child_text ("true");
183         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
184         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
185         chunk->add_child("Path")->add_child_text (pkl_uuid + "_pkl.xml");
186         chunk->add_child("VolumeIndex")->add_child_text ("1");
187         chunk->add_child("Offset")->add_child_text ("0");
188         chunk->add_child("Length")->add_child_text (lexical_cast<string> (pkl_length));
189         
190         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
191                 (*i)->write_to_assetmap (asset_list);
192         }
193
194         list<shared_ptr<const Content> > a = assets ();
195         for (list<shared_ptr<const Content> >::const_iterator i = a.begin(); i != a.end(); ++i) {
196                 (*i)->write_to_assetmap (asset_list);
197         }
198
199         /* This must not be the _formatted version otherwise signature digests will be wrong */
200         doc.write_to_file (p.string (), "UTF-8");
201 }
202
203 void
204 DCP::read (bool require_mxfs)
205 {
206         read_assets ();
207         read_cpls (require_mxfs);
208 }
209
210 void
211 DCP::read_assets ()
212 {
213         shared_ptr<parse::AssetMap> asset_map;
214         try {
215                 boost::filesystem::path p = _directory;
216                 p /= "ASSETMAP";
217                 if (boost::filesystem::exists (p)) {
218                         asset_map.reset (new dcp::parse::AssetMap (p.string ()));
219                 } else {
220                         p = _directory;
221                         p /= "ASSETMAP.xml";
222                         if (boost::filesystem::exists (p)) {
223                                 asset_map.reset (new dcp::parse::AssetMap (p.string ()));
224                         } else {
225                                 boost::throw_exception (DCPReadError ("could not find AssetMap file"));
226                         }
227                 }
228                 
229         } catch (FileError& e) {
230                 boost::throw_exception (FileError ("could not load AssetMap file", _files.asset_map, e.number ()));
231         }
232
233         for (list<shared_ptr<dcp::parse::AssetMapAsset> >::const_iterator i = asset_map->assets.begin(); i != asset_map->assets.end(); ++i) {
234                 if ((*i)->chunks.size() != 1) {
235                         boost::throw_exception (XMLError ("unsupported asset chunk count"));
236                 }
237
238                 boost::filesystem::path t = _directory;
239                 t /= (*i)->chunks.front()->path;
240                 
241                 if (boost::algorithm::ends_with (t.string(), ".mxf") || boost::algorithm::ends_with (t.string(), ".ttf")) {
242                         continue;
243                 }
244
245                 xmlpp::DomParser* p = new xmlpp::DomParser;
246                 try {
247                         p->parse_file (t.string());
248                 } catch (std::exception& e) {
249                         delete p;
250                         continue;
251                 }
252
253                 string const root = p->get_document()->get_root_node()->get_name ();
254                 delete p;
255
256                 if (root == "CompositionPlaylist") {
257                         _files.cpls.push_back (t.string());
258                 } else if (root == "PackingList") {
259                         if (_files.pkl.empty ()) {
260                                 _files.pkl = t.string();
261                         } else {
262                                 boost::throw_exception (DCPReadError ("duplicate PKLs found"));
263                         }
264                 }
265         }
266         
267         if (_files.cpls.empty ()) {
268                 boost::throw_exception (DCPReadError ("no CPL files found"));
269         }
270
271         if (_files.pkl.empty ()) {
272                 boost::throw_exception (DCPReadError ("no PKL file found"));
273         }
274
275         shared_ptr<parse::PKL> pkl;
276         try {
277                 pkl.reset (new parse::PKL (_files.pkl));
278         } catch (FileError& e) {
279                 boost::throw_exception (FileError ("could not load PKL file", _files.pkl, e.number ()));
280         }
281
282         _asset_maps.push_back (make_pair (boost::filesystem::absolute (_directory).string(), asset_map));
283 }
284
285 void
286 DCP::read_cpls (bool require_mxfs)
287 {
288         for (list<string>::iterator i = _files.cpls.begin(); i != _files.cpls.end(); ++i) {
289                 _cpls.push_back (shared_ptr<CPL> (new CPL (_directory, *i, _asset_maps, require_mxfs)));
290         }
291 }
292
293 bool
294 DCP::equals (DCP const & other, EqualityOptions opt, boost::function<void (NoteType, string)> note) const
295 {
296         if (_cpls.size() != other._cpls.size()) {
297                 note (ERROR, "CPL counts differ");
298                 return false;
299         }
300
301         list<shared_ptr<CPL> >::const_iterator a = _cpls.begin ();
302         list<shared_ptr<CPL> >::const_iterator b = other._cpls.begin ();
303
304         while (a != _cpls.end ()) {
305                 if (!(*a)->equals (*b->get(), opt, note)) {
306                         return false;
307                 }
308                 ++a;
309                 ++b;
310         }
311
312         return true;
313 }
314
315 void
316 DCP::add_cpl (shared_ptr<CPL> cpl)
317 {
318         _cpls.push_back (cpl);
319 }
320
321 class AssetComparator
322 {
323 public:
324         bool operator() (shared_ptr<const Content> a, shared_ptr<const Content> b) {
325                 return a->id() < b->id();
326         }
327 };
328
329 list<shared_ptr<const Content> >
330 DCP::assets () const
331 {
332         list<shared_ptr<const Content> > a;
333         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
334                 list<shared_ptr<const Content> > t = (*i)->assets ();
335                 a.merge (t);
336         }
337
338         a.sort (AssetComparator ());
339         a.unique ();
340         return a;
341 }
342
343 bool
344 DCP::encrypted () const
345 {
346         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
347                 if ((*i)->encrypted ()) {
348                         return true;
349                 }
350         }
351
352         return false;
353 }
354
355 void
356 DCP::add_kdm (KDM const & kdm)
357 {
358         list<KDMKey> keys = kdm.keys ();
359         
360         for (list<shared_ptr<CPL> >::iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
361                 for (list<KDMKey>::iterator j = keys.begin(); j != keys.end(); ++j) {
362                         if (j->cpl_id() == (*i)->id()) {
363                                 (*i)->add_kdm (kdm);
364                         }                               
365                 }
366         }
367 }
368
369 void
370 DCP::add_assets_from (DCP const & ov)
371 {
372         copy (ov._asset_maps.begin(), ov._asset_maps.end(), back_inserter (_asset_maps));
373 }