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 switch (*_standard) {
148 if (i->optional_node_child("PackingList")) {
151 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
156 optional<string> pkl_bool = i->optional_string_child("PackingList");
157 if (pkl_bool && *pkl_bool == "true") {
160 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
168 boost::throw_exception (XMLError ("No packing list found in asset map"));
171 _pkl.reset (new PKL (_directory / *pkl_path));
173 /* Read all the assets from the asset map */
175 /* Make a list of non-CPL/PKL assets so that we can resolve the references
178 list<shared_ptr<Asset> > other_assets;
180 for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
181 boost::filesystem::path path = _directory / i->second;
183 if (!boost::filesystem::exists (path)) {
184 survivable_error (keep_going, errors, MissingAssetError (path));
188 string const pkl_type = _pkl->type(i->first);
190 if (pkl_type == CPL::static_pkl_type(*_standard) || pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
191 xmlpp::DomParser* p = new xmlpp::DomParser;
193 p->parse_file (path.string());
194 } catch (std::exception& e) {
196 throw DCPReadError(String::compose("XML error in %1", path.string()), e.what());
199 string const root = p->get_document()->get_root_node()->get_name ();
202 if (root == "CompositionPlaylist") {
203 shared_ptr<CPL> cpl (new CPL (path));
204 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get()) {
205 survivable_error (keep_going, errors, MismatchedStandardError ());
207 _cpls.push_back (cpl);
208 } else if (root == "DCSubtitle") {
209 if (_standard && _standard.get() == SMPTE) {
210 survivable_error (keep_going, errors, MismatchedStandardError ());
212 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
215 pkl_type == PictureAsset::static_pkl_type(*_standard) ||
216 pkl_type == SoundAsset::static_pkl_type(*_standard) ||
217 pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
218 pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
221 /* XXX: asdcplib does not appear to support discovery of read MXFs standard
225 ASDCP::EssenceType_t type;
226 if (ASDCP::EssenceType (path.string().c_str(), type) != ASDCP::RESULT_OK) {
227 throw DCPReadError ("Could not find essence type");
230 case ASDCP::ESS_UNKNOWN:
231 case ASDCP::ESS_MPEG2_VES:
232 throw DCPReadError ("MPEG2 video essences are not supported");
233 case ASDCP::ESS_JPEG_2000:
235 other_assets.push_back (shared_ptr<MonoPictureAsset> (new MonoPictureAsset (path)));
236 } catch (dcp::MXFFileError& e) {
237 if (ignore_incorrect_picture_mxf_type && e.number() == ASDCP::RESULT_SFORMAT) {
238 /* Tried to load it as mono but the error says it's stereo; try that instead */
239 other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
245 case ASDCP::ESS_PCM_24b_48k:
246 case ASDCP::ESS_PCM_24b_96k:
247 other_assets.push_back (shared_ptr<SoundAsset> (new SoundAsset (path)));
249 case ASDCP::ESS_JPEG_2000_S:
250 other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
252 case ASDCP::ESS_TIMED_TEXT:
253 other_assets.push_back (shared_ptr<SMPTESubtitleAsset> (new SMPTESubtitleAsset (path)));
255 case ASDCP::ESS_DCDATA_DOLBY_ATMOS:
256 other_assets.push_back (shared_ptr<AtmosAsset> (new AtmosAsset (path)));
259 throw DCPReadError (String::compose ("Unknown MXF essence type %1 in %2", int(type), path.string()));
261 } else if (pkl_type == FontAsset::static_pkl_type(*_standard)) {
262 other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
263 } else if (pkl_type == "image/png") {
264 /* It's an Interop PNG subtitle; let it go */
266 throw DCPReadError (String::compose("Unknown asset type %1 in PKL", pkl_type));
270 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
271 i->resolve_refs (other_assets);
276 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
278 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
279 i->resolve_refs (assets);
284 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
286 list<shared_ptr<CPL> > a = cpls ();
287 list<shared_ptr<CPL> > b = other.cpls ();
289 if (a.size() != b.size()) {
290 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
296 BOOST_FOREACH (shared_ptr<CPL> i, a) {
297 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
298 while (j != b.end() && !(*j)->equals (i, opt, note)) {
311 DCP::add (boost::shared_ptr<CPL> cpl)
313 _cpls.push_back (cpl);
317 DCP::encrypted () const
319 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
320 if (i->encrypted ()) {
328 /** Add a KDM to decrypt this DCP. This method must be called after DCP::read()
329 * or the KDM you specify will be ignored.
330 * @param kdm KDM to use.
333 DCP::add (DecryptedKDM const & kdm)
335 list<DecryptedKDMKey> keys = kdm.keys ();
337 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
338 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
339 if (j.cpl_id() == i->id()) {
346 /** Write the VOLINDEX file.
347 * @param standard DCP standard to use (INTEROP or SMPTE)
350 DCP::write_volindex (Standard standard) const
352 boost::filesystem::path p = _directory;
365 xmlpp::Element* root;
369 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
372 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
378 root->add_child("Index")->add_child_text ("1");
379 doc.write_to_file (p.string (), "UTF-8");
383 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
385 boost::filesystem::path p = _directory;
399 xmlpp::Element* root;
403 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
406 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
412 root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
413 root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
417 root->add_child("VolumeCount")->add_child_text ("1");
418 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
419 root->add_child("Issuer")->add_child_text (metadata.issuer);
420 root->add_child("Creator")->add_child_text (metadata.creator);
423 root->add_child("Creator")->add_child_text (metadata.creator);
424 root->add_child("VolumeCount")->add_child_text ("1");
425 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
426 root->add_child("Issuer")->add_child_text (metadata.issuer);
432 xmlpp::Node* asset_list = root->add_child ("AssetList");
434 xmlpp::Node* asset = asset_list->add_child ("Asset");
435 asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
436 asset->add_child("PackingList")->add_child_text ("true");
437 xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
438 xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
439 chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
440 chunk->add_child("VolumeIndex")->add_child_text ("1");
441 chunk->add_child("Offset")->add_child_text ("0");
442 chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
444 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
445 i->write_to_assetmap (asset_list, _directory);
448 /* This must not be the _formatted version otherwise signature digests will be wrong */
449 doc.write_to_file (p.string (), "UTF-8");
452 /** Write all the XML files for this DCP.
453 * @param standand INTEROP or SMPTE.
454 * @param metadata Metadata to use for PKL and asset map files.
455 * @param signer Signer to use, or 0.
460 XMLMetadata metadata,
461 shared_ptr<const CertificateChain> signer,
462 NameFormat name_format
465 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
466 NameFormat::Map values;
468 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
472 _pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
473 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
474 i->add_to_pkl (_pkl, _directory);
478 NameFormat::Map values;
480 boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + _pkl->id() + ".xml");
481 _pkl->write (pkl_path, signer);
483 write_volindex (standard);
484 write_assetmap (standard, _pkl->id(), pkl_path, metadata);
487 list<shared_ptr<CPL> >
493 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
494 * an exception is thrown if they are found.
495 * @return All assets (including CPLs).
497 list<shared_ptr<Asset> >
498 DCP::assets (bool ignore_unresolved) const
500 list<shared_ptr<Asset> > assets;
501 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
502 assets.push_back (i);
503 BOOST_FOREACH (shared_ptr<const ReelAsset> j, i->reel_assets ()) {
504 if (ignore_unresolved && !j->asset_ref().resolved()) {
507 shared_ptr<Asset> o = j->asset_ref().asset ();
508 assets.push_back (o);
509 /* More Interop special-casing */
510 shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
512 sub->add_font_assets (assets);
520 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
521 vector<boost::filesystem::path>
522 DCP::directories_from_files (vector<boost::filesystem::path> files)
524 vector<boost::filesystem::path> d;
525 BOOST_FOREACH (boost::filesystem::path i, files) {
526 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
527 d.push_back (i.parent_path ());