2 Copyright (C) 2012-2018 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 "certificate_chain.h"
40 #include "reel_picture_asset.h"
41 #include "reel_sound_asset.h"
42 #include "reel_subtitle_asset.h"
43 #include "reel_closed_caption_asset.h"
44 #include "reel_atmos_asset.h"
45 #include "local_time.h"
46 #include "dcp_assert.h"
47 #include "compose.hpp"
48 #include "raw_convert.h"
49 #include <asdcp/Metadata.h>
50 #include <libxml/parser.h>
51 #include <libxml++/libxml++.h>
52 #include <boost/algorithm/string.hpp>
53 #include <boost/foreach.hpp>
62 using boost::shared_ptr;
63 using boost::optional;
64 using boost::dynamic_pointer_cast;
68 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
69 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
70 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
71 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
72 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
73 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
75 CPL::CPL (string annotation_text, ContentKind content_kind)
76 /* default _content_title_text to annotation_text */
77 : _issuer ("libdcp" LIBDCP_VERSION)
78 , _creator ("libdcp" LIBDCP_VERSION)
79 , _issue_date (LocalTime().as_string())
80 , _annotation_text (annotation_text)
81 , _content_title_text (annotation_text)
82 , _content_kind (content_kind)
85 cv.label_text = cv.id + LocalTime().as_string();
86 _content_versions.push_back (cv);
89 /** Construct a CPL object from a XML file */
90 CPL::CPL (boost::filesystem::path file)
92 , _content_kind (FEATURE)
94 cxml::Document f ("CompositionPlaylist");
97 if (f.namespace_uri() == cpl_interop_ns) {
99 } else if (f.namespace_uri() == cpl_smpte_ns) {
102 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
105 _id = remove_urn_uuid (f.string_child ("Id"));
106 _annotation_text = f.optional_string_child("AnnotationText").get_value_or("");
107 _issuer = f.optional_string_child("Issuer").get_value_or("");
108 _creator = f.optional_string_child("Creator").get_value_or("");
109 _issue_date = f.string_child ("IssueDate");
110 _content_title_text = f.string_child ("ContentTitleText");
111 _content_kind = content_kind_from_string (f.string_child ("ContentKind"));
112 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
113 if (content_version) {
114 /* XXX: SMPTE should insist that Id is present */
115 _content_versions.push_back (
117 content_version->optional_string_child("Id").get_value_or(""),
118 content_version->string_child("LabelText")
121 content_version->done ();
122 } else if (_standard == SMPTE) {
123 /* ContentVersion is required in SMPTE */
124 throw XMLError ("Missing ContentVersion tag in CPL");
126 cxml::ConstNodePtr rating_list = f.node_child ("RatingList");
128 BOOST_FOREACH (cxml::ConstNodePtr i, rating_list->node_children("Rating")) {
129 _ratings.push_back (Rating(i));
132 _reels = type_grand_children<Reel> (f, "ReelList", "Reel");
134 cxml::ConstNodePtr reel_list = f.node_child ("ReelList");
136 list<cxml::NodePtr> reels = reel_list->node_children("Reel");
137 if (!reels.empty()) {
138 cxml::ConstNodePtr asset_list = reels.front()->node_child("AssetList");
139 cxml::ConstNodePtr metadata = asset_list->optional_node_child("CompositionMetadataAsset");
141 read_composition_metadata_asset (metadata);
147 f.ignore_child ("Issuer");
148 f.ignore_child ("Signer");
149 f.ignore_child ("Signature");
154 /** Add a reel to this CPL.
155 * @param reel Reel to add.
158 CPL::add (boost::shared_ptr<Reel> reel)
160 _reels.push_back (reel);
163 /** Write an CompositonPlaylist XML file.
165 * @param file Filename to write.
166 * @param standard INTEROP or SMPTE.
167 * @param signer Signer to sign the CPL, or 0 to add no signature.
170 CPL::write_xml (boost::filesystem::path file, Standard standard, shared_ptr<const CertificateChain> signer) const
173 xmlpp::Element* root;
174 if (standard == INTEROP) {
175 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
177 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
180 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
181 root->add_child("AnnotationText")->add_child_text (_annotation_text);
182 root->add_child("IssueDate")->add_child_text (_issue_date);
183 root->add_child("Issuer")->add_child_text (_issuer);
184 root->add_child("Creator")->add_child_text (_creator);
185 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
186 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
187 DCP_ASSERT (!_content_versions.empty());
188 _content_versions[0].as_xml (root);
190 xmlpp::Element* rating_list = root->add_child("RatingList");
191 BOOST_FOREACH (Rating i, _ratings) {
192 i.as_xml (rating_list->add_child("Rating"));
195 xmlpp::Element* reel_list = root->add_child ("ReelList");
198 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
199 xmlpp::Element* asset_list = i->write_to_cpl (reel_list, standard);
200 if (first && standard == dcp::SMPTE) {
201 maybe_write_composition_metadata_asset (asset_list);
209 signer->sign (root, standard);
212 doc.write_to_file_formatted (file.string(), "UTF-8");
219 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
221 cxml::ConstNodePtr fctt = node->node_child("FullContentTitleText");
222 _full_content_title_text = fctt->content();
223 _full_content_title_text_language = fctt->optional_string_attribute("language");
225 _release_territory = node->optional_string_child("ReleaseTerritory");
227 cxml::ConstNodePtr vn = node->optional_node_child("VersionNumber");
229 _version_number = raw_convert<int>(vn->content());
230 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
231 optional<string> vn_status = vn->optional_string_attribute("status");
233 _status = string_to_status (*vn_status);
237 _chain = node->optional_string_child("Chain");
238 _distributor = node->optional_string_child("Distributor");
239 _facility = node->optional_string_child("Facility");
241 cxml::ConstNodePtr acv = node->optional_node_child("AlternateContentVersionList");
243 BOOST_FOREACH (cxml::ConstNodePtr i, acv->node_children("ContentVersion")) {
244 _content_versions.push_back (ContentVersion(i));
248 cxml::ConstNodePtr lum = node->optional_node_child("Luminance");
250 _luminance = Luminance (lum);
253 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
255 optional<string> sr = node->optional_string_child("MainSoundSampleRate");
257 vector<string> sr_bits;
258 boost::split (sr_bits, *sr, boost::is_any_of(" "));
259 DCP_ASSERT (sr_bits.size() == 2);
260 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
263 _main_picture_stored_area = dcp::Size (
264 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
265 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
268 _main_picture_active_area = dcp::Size (
269 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
270 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
273 optional<string> sll = node->optional_string_child("MainSubtitleLanguageList");
275 vector<string> sll_split;
276 boost::split (sll_split, *sll, boost::is_any_of(" "));
277 DCP_ASSERT (!sll_split.empty());
279 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
281 if (!_reels.empty()) {
282 shared_ptr<dcp::ReelSubtitleAsset> sub = _reels.front()->main_subtitle();
284 optional<dcp::LanguageTag> lang = sub->language();
285 if (lang && lang->to_string() == sll_split[0]) {
291 for (size_t i = first; i < sll_split.size(); ++i) {
292 _additional_subtitle_languages.push_back (sll_split[i]);
298 /** Write a CompositionMetadataAsset node as a child of @param node provided
299 * the required metadata is stored in the object. If any required metadata
300 * is missing this method will do nothing.
303 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
306 !_main_sound_configuration ||
307 !_main_sound_sample_rate ||
308 !_main_picture_stored_area ||
309 !_main_picture_active_area ||
311 !_reels.front()->main_picture()) {
315 xmlpp::Element* meta = node->add_child("meta:CompositionMetadataAsset");
316 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
318 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
320 shared_ptr<dcp::ReelPictureAsset> mp = _reels.front()->main_picture();
321 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
322 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
324 xmlpp::Element* fctt = meta->add_child("FullContentTitleText", "meta");
325 if (_full_content_title_text) {
326 fctt->add_child_text (*_full_content_title_text);
328 if (_full_content_title_text_language) {
329 fctt->set_attribute("language", *_full_content_title_text_language);
332 if (_release_territory) {
333 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
336 if (_version_number) {
337 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
338 vn->add_child_text(raw_convert<string>(*_version_number));
340 vn->set_attribute("status", status_to_string(*_status));
345 meta->add_child("Chain", "meta")->add_child_text(*_chain);
349 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
353 meta->add_child("Facility", "meta")->add_child_text(*_facility);
356 if (_content_versions.size() > 1) {
357 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
358 for (size_t i = 1; i < _content_versions.size(); ++i) {
359 _content_versions[i].as_xml (vc);
364 _luminance->as_xml (meta, "meta");
367 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
368 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
370 xmlpp::Element* stored = meta->add_child("MainPictureStoredArea", "meta");
371 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
372 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
374 xmlpp::Element* active = meta->add_child("MainPictureActiveArea", "meta");
375 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
376 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
378 optional<dcp::LanguageTag> first_subtitle_language;
379 BOOST_FOREACH (shared_ptr<const Reel> i, _reels) {
380 if (i->main_subtitle()) {
381 first_subtitle_language = i->main_subtitle()->language();
382 if (first_subtitle_language) {
388 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
390 if (first_subtitle_language) {
391 lang = first_subtitle_language->to_string();
393 BOOST_FOREACH (dcp::LanguageTag const& i, _additional_subtitle_languages) {
397 lang += i.to_string();
399 meta->add_child("MainSubtitleLanguageList")->add_child_text(lang);
402 if (_reels.front()->main_sound()) {
403 shared_ptr<const SoundAsset> asset = _reels.front()->main_sound()->asset();
405 shared_ptr<SoundAssetReader> reader = asset->start_read ();
406 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
407 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
408 ASDCP::DefaultSMPTEDict().ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
409 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
412 xmlpp::Element* mca_subs = meta->add_child("mca:MCASubDescriptors");
413 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
414 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
415 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
416 xmlpp::Element* sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
418 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
419 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
420 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
421 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
422 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
423 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
424 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
425 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
426 if (!soundfield->MCATagName.empty()) {
427 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
428 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
430 if (!soundfield->RFC5646SpokenLanguage.empty()) {
431 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
432 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
435 list<ASDCP::MXF::InterchangeObject*> channels;
436 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectsByType(
437 ASDCP::DefaultSMPTEDict().ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
441 BOOST_FOREACH (ASDCP::MXF::InterchangeObject* i, channels) {
442 ASDCP::MXF::AudioChannelLabelSubDescriptor* channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
443 xmlpp::Element* ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
444 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
445 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
446 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
447 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
448 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
449 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
450 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
451 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
452 if (!channel->MCATagName.empty()) {
453 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
454 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
456 if (!channel->MCAChannelID.empty()) {
457 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
459 if (!channel->RFC5646SpokenLanguage.empty()) {
460 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
461 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
463 if (!channel->SoundfieldGroupLinkID.empty()) {
464 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
465 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
474 list<shared_ptr<ReelMXF> >
477 list<shared_ptr<ReelMXF> > c;
479 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
480 if (i->main_picture ()) {
481 c.push_back (i->main_picture());
483 if (i->main_sound ()) {
484 c.push_back (i->main_sound());
486 if (i->main_subtitle ()) {
487 c.push_back (i->main_subtitle());
489 BOOST_FOREACH (shared_ptr<ReelClosedCaptionAsset> j, i->closed_captions()) {
493 c.push_back (i->atmos());
500 list<shared_ptr<const ReelMXF> >
501 CPL::reel_mxfs () const
503 list<shared_ptr<const ReelMXF> > c;
505 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
506 if (i->main_picture ()) {
507 c.push_back (i->main_picture());
509 if (i->main_sound ()) {
510 c.push_back (i->main_sound());
512 if (i->main_subtitle ()) {
513 c.push_back (i->main_subtitle());
515 BOOST_FOREACH (shared_ptr<ReelClosedCaptionAsset> j, i->closed_captions()) {
519 c.push_back (i->atmos());
527 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
529 shared_ptr<const CPL> other_cpl = dynamic_pointer_cast<const CPL> (other);
534 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
535 string const s = "CPL: annotation texts differ: " + _annotation_text + " vs " + other_cpl->_annotation_text + "\n";
540 if (_content_kind != other_cpl->_content_kind) {
541 note (DCP_ERROR, "CPL: content kinds differ");
545 if (_reels.size() != other_cpl->_reels.size()) {
546 note (DCP_ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
550 list<shared_ptr<Reel> >::const_iterator a = _reels.begin ();
551 list<shared_ptr<Reel> >::const_iterator b = other_cpl->_reels.begin ();
553 while (a != _reels.end ()) {
554 if (!(*a)->equals (*b, opt, note)) {
564 /** @return true if we have any encrypted content */
566 CPL::encrypted () const
568 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
569 if (i->encrypted ()) {
577 /** Add a KDM to this CPL. If the KDM is for any of this CPLs assets it will be used
578 * to decrypt those assets.
582 CPL::add (DecryptedKDM const & kdm)
584 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
590 CPL::resolve_refs (list<shared_ptr<Asset> > assets)
592 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
593 i->resolve_refs (assets);
598 CPL::pkl_type (Standard standard) const
600 return static_pkl_type (standard);
604 CPL::static_pkl_type (Standard standard)
608 return "text/xml;asdcpKind=CPL";
617 CPL::duration () const
620 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
628 CPL::set_version_number (int v)
631 throw BadSettingError ("CPL version number cannot be negative");
639 CPL::set_content_versions (vector<ContentVersion> v)
642 BOOST_FOREACH (ContentVersion i, v) {
643 if (!ids.insert(i.id).second) {
644 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
648 _content_versions = v;
653 CPL::content_version () const
655 DCP_ASSERT (!_content_versions.empty());
656 return _content_versions[0];
661 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
663 _additional_subtitle_languages.clear ();
664 BOOST_FOREACH (dcp::LanguageTag const& i, langs) {
665 _additional_subtitle_languages.push_back (i.to_string());