2 Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
6 libdcp 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.
11 libdcp 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.
16 You should have received a copy of the GNU General Public License
17 along with libdcp. If not, see <http://www.gnu.org/licenses/>.
19 In addition, as a special exception, the copyright holders give
20 permission to link the code of portions of this program with the
21 OpenSSL library under certain conditions as described in each
22 individual source file, and distribute linked combinations
25 You must obey the GNU General Public License in all respects
26 for all of the code used other than OpenSSL. If you modify
27 file(s) with this exception, you may extend this exception to your
28 version of the file(s), but you are not obligated to do so. If you
29 do not wish to do so, delete this exception statement from your
30 version. If you delete this exception statement from all source
31 files in the program, then also delete it here.
38 #include "raw_convert.h"
40 #include "sound_asset.h"
41 #include "atmos_asset.h"
42 #include "picture_asset.h"
43 #include "interop_subtitle_asset.h"
44 #include "smpte_subtitle_asset.h"
45 #include "mono_picture_asset.h"
46 #include "stereo_picture_asset.h"
47 #include "reel_subtitle_asset.h"
50 #include "exceptions.h"
52 #include "certificate_chain.h"
53 #include "compose.hpp"
54 #include "decrypted_kdm.h"
55 #include "decrypted_kdm_key.h"
56 #include "dcp_assert.h"
57 #include "reel_asset.h"
58 #include "font_asset.h"
60 #include <asdcp/AS_DCP.h>
61 #include <xmlsec/xmldsig.h>
62 #include <xmlsec/app.h>
63 #include <libxml++/libxml++.h>
64 #include <boost/filesystem.hpp>
65 #include <boost/algorithm/string.hpp>
66 #include <boost/foreach.hpp>
76 using boost::shared_ptr;
77 using boost::dynamic_pointer_cast;
78 using boost::optional;
79 using boost::algorithm::starts_with;
82 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
83 static string const assetmap_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
84 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
85 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
87 DCP::DCP (boost::filesystem::path directory)
88 : _directory (directory)
90 if (!boost::filesystem::exists (directory)) {
91 boost::filesystem::create_directories (directory);
94 _directory = boost::filesystem::canonical (_directory);
97 /** Call this instead of throwing an exception if the error can be tolerated */
98 template<class T> void
99 survivable_error (bool keep_going, dcp::DCP::ReadErrors* errors, T const & e)
103 errors->push_back (shared_ptr<T> (new T (e)));
111 DCP::read (bool keep_going, ReadErrors* errors, bool ignore_incorrect_picture_mxf_type)
113 /* Read the ASSETMAP and PKL */
115 boost::filesystem::path asset_map_file;
116 if (boost::filesystem::exists (_directory / "ASSETMAP")) {
117 asset_map_file = _directory / "ASSETMAP";
118 } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
119 asset_map_file = _directory / "ASSETMAP.xml";
121 boost::throw_exception (DCPReadError (String::compose ("could not find AssetMap file in `%1'", _directory.string())));
124 cxml::Document asset_map ("AssetMap");
126 asset_map.read_file (asset_map_file);
127 if (asset_map.namespace_uri() == assetmap_interop_ns) {
129 } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
132 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
135 list<shared_ptr<cxml::Node> > asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
136 map<string, boost::filesystem::path> paths;
137 optional<boost::filesystem::path> pkl_path;
138 BOOST_FOREACH (shared_ptr<cxml::Node> i, asset_nodes) {
139 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
140 boost::throw_exception (XMLError ("unsupported asset chunk count"));
142 string p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
143 if (starts_with (p, "file://")) {
146 optional<string> pkl_bool = i->optional_string_child("PackingList");
147 if (pkl_bool && *pkl_bool == "true") {
150 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
155 boost::throw_exception (XMLError ("No packing list found in asset map"));
158 _pkl.reset (new PKL (_directory / *pkl_path));
160 /* Read all the assets from the asset map */
162 /* Make a list of non-CPL/PKL assets so that we can resolve the references
165 list<shared_ptr<Asset> > other_assets;
167 for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
168 boost::filesystem::path path = _directory / i->second;
170 if (!boost::filesystem::exists (path)) {
171 survivable_error (keep_going, errors, MissingAssetError (path));
175 string const pkl_type = _pkl->type(i->first);
177 if (pkl_type == CPL::static_pkl_type(*_standard) || pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
178 xmlpp::DomParser* p = new xmlpp::DomParser;
180 p->parse_file (path.string());
181 } catch (std::exception& e) {
183 throw DCPReadError(String::compose("XML error in %1", path.string()), e.what());
186 string const root = p->get_document()->get_root_node()->get_name ();
189 if (root == "CompositionPlaylist") {
190 shared_ptr<CPL> cpl (new CPL (path));
191 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get()) {
192 survivable_error (keep_going, errors, MismatchedStandardError ());
194 _cpls.push_back (cpl);
195 } else if (root == "DCSubtitle") {
196 if (_standard && _standard.get() == SMPTE) {
197 survivable_error (keep_going, errors, MismatchedStandardError ());
199 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
202 pkl_type == PictureAsset::static_pkl_type(*_standard) ||
203 pkl_type == SoundAsset::static_pkl_type(*_standard) ||
204 pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
205 pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
208 /* XXX: asdcplib does not appear to support discovery of read MXFs standard
212 ASDCP::EssenceType_t type;
213 if (ASDCP::EssenceType (path.string().c_str(), type) != ASDCP::RESULT_OK) {
214 throw DCPReadError ("Could not find essence type");
217 case ASDCP::ESS_UNKNOWN:
218 case ASDCP::ESS_MPEG2_VES:
219 throw DCPReadError ("MPEG2 video essences are not supported");
220 case ASDCP::ESS_JPEG_2000:
222 other_assets.push_back (shared_ptr<MonoPictureAsset> (new MonoPictureAsset (path)));
223 } catch (dcp::MXFFileError& e) {
224 if (ignore_incorrect_picture_mxf_type && e.number() == ASDCP::RESULT_SFORMAT) {
225 /* Tried to load it as mono but the error says it's stereo; try that instead */
226 other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
232 case ASDCP::ESS_PCM_24b_48k:
233 case ASDCP::ESS_PCM_24b_96k:
234 other_assets.push_back (shared_ptr<SoundAsset> (new SoundAsset (path)));
236 case ASDCP::ESS_JPEG_2000_S:
237 other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
239 case ASDCP::ESS_TIMED_TEXT:
240 other_assets.push_back (shared_ptr<SMPTESubtitleAsset> (new SMPTESubtitleAsset (path)));
242 case ASDCP::ESS_DCDATA_DOLBY_ATMOS:
243 other_assets.push_back (shared_ptr<AtmosAsset> (new AtmosAsset (path)));
246 throw DCPReadError (String::compose ("Unknown MXF essence type %1 in %2", int(type), path.string()));
248 } else if (pkl_type == FontAsset::static_pkl_type(*_standard)) {
249 other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
253 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
254 i->resolve_refs (other_assets);
259 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
261 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
262 i->resolve_refs (assets);
267 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
269 list<shared_ptr<CPL> > a = cpls ();
270 list<shared_ptr<CPL> > b = other.cpls ();
272 if (a.size() != b.size()) {
273 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
279 BOOST_FOREACH (shared_ptr<CPL> i, a) {
280 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
281 while (j != b.end() && !(*j)->equals (i, opt, note)) {
294 DCP::add (boost::shared_ptr<CPL> cpl)
296 _cpls.push_back (cpl);
300 DCP::encrypted () const
302 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
303 if (i->encrypted ()) {
311 /** Add a KDM to decrypt this DCP. This method must be called after DCP::read()
312 * or the KDM you specify will be ignored.
313 * @param kdm KDM to use.
316 DCP::add (DecryptedKDM const & kdm)
318 list<DecryptedKDMKey> keys = kdm.keys ();
320 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
321 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
322 if (j.cpl_id() == i->id()) {
329 /** Write the VOLINDEX file.
330 * @param standard DCP standard to use (INTEROP or SMPTE)
333 DCP::write_volindex (Standard standard) const
335 boost::filesystem::path p = _directory;
348 xmlpp::Element* root;
352 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
355 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
361 root->add_child("Index")->add_child_text ("1");
362 doc.write_to_file (p.string (), "UTF-8");
366 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
368 boost::filesystem::path p = _directory;
382 xmlpp::Element* root;
386 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
389 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
395 root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
396 root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
400 root->add_child("VolumeCount")->add_child_text ("1");
401 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
402 root->add_child("Issuer")->add_child_text (metadata.issuer);
403 root->add_child("Creator")->add_child_text (metadata.creator);
406 root->add_child("Creator")->add_child_text (metadata.creator);
407 root->add_child("VolumeCount")->add_child_text ("1");
408 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
409 root->add_child("Issuer")->add_child_text (metadata.issuer);
415 xmlpp::Node* asset_list = root->add_child ("AssetList");
417 xmlpp::Node* asset = asset_list->add_child ("Asset");
418 asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
419 asset->add_child("PackingList")->add_child_text ("true");
420 xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
421 xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
422 chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
423 chunk->add_child("VolumeIndex")->add_child_text ("1");
424 chunk->add_child("Offset")->add_child_text ("0");
425 chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
427 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
428 i->write_to_assetmap (asset_list, _directory);
431 /* This must not be the _formatted version otherwise signature digests will be wrong */
432 doc.write_to_file (p.string (), "UTF-8");
435 /** Write all the XML files for this DCP.
436 * @param standand INTEROP or SMPTE.
437 * @param metadata Metadata to use for PKL and asset map files.
438 * @param signer Signer to use, or 0.
443 XMLMetadata metadata,
444 shared_ptr<const CertificateChain> signer,
445 NameFormat name_format
448 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
449 NameFormat::Map values;
451 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
455 _pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
456 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
457 i->add_to_pkl (_pkl, _directory);
461 NameFormat::Map values;
463 boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + _pkl->id() + ".xml");
464 _pkl->write (pkl_path, signer);
466 write_volindex (standard);
467 write_assetmap (standard, _pkl->id(), pkl_path, metadata);
470 list<shared_ptr<CPL> >
476 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
477 * an exception is thrown if they are found.
478 * @return All assets (including CPLs).
480 list<shared_ptr<Asset> >
481 DCP::assets (bool ignore_unresolved) const
483 list<shared_ptr<Asset> > assets;
484 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
485 assets.push_back (i);
486 BOOST_FOREACH (shared_ptr<const ReelAsset> j, i->reel_assets ()) {
487 if (ignore_unresolved && !j->asset_ref().resolved()) {
490 shared_ptr<Asset> o = j->asset_ref().asset ();
491 assets.push_back (o);
492 /* More Interop special-casing */
493 shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
495 sub->add_font_assets (assets);
503 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
504 vector<boost::filesystem::path>
505 DCP::directories_from_files (vector<boost::filesystem::path> files)
507 vector<boost::filesystem::path> d;
508 BOOST_FOREACH (boost::filesystem::path i, files) {
509 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
510 d.push_back (i.parent_path ());