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";
76 CPL::CPL (string annotation_text, ContentKind content_kind)
77 /* default _content_title_text to annotation_text */
78 : _issuer ("libdcp" LIBDCP_VERSION)
79 , _creator ("libdcp" LIBDCP_VERSION)
80 , _issue_date (LocalTime().as_string())
81 , _annotation_text (annotation_text)
82 , _content_title_text (annotation_text)
83 , _content_kind (content_kind)
86 cv.label_text = cv.id + LocalTime().as_string();
87 _content_versions.push_back (cv);
90 /** Construct a CPL object from a XML file */
91 CPL::CPL (boost::filesystem::path file)
93 , _content_kind (FEATURE)
95 cxml::Document f ("CompositionPlaylist");
98 if (f.namespace_uri() == cpl_interop_ns) {
100 } else if (f.namespace_uri() == cpl_smpte_ns) {
103 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
106 _id = remove_urn_uuid (f.string_child ("Id"));
107 _annotation_text = f.optional_string_child("AnnotationText").get_value_or("");
108 _issuer = f.optional_string_child("Issuer").get_value_or("");
109 _creator = f.optional_string_child("Creator").get_value_or("");
110 _issue_date = f.string_child ("IssueDate");
111 _content_title_text = f.string_child ("ContentTitleText");
112 _content_kind = content_kind_from_string (f.string_child ("ContentKind"));
113 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
114 if (content_version) {
115 /* XXX: SMPTE should insist that Id is present */
116 _content_versions.push_back (
118 content_version->optional_string_child("Id").get_value_or(""),
119 content_version->string_child("LabelText")
122 content_version->done ();
123 } else if (_standard == SMPTE) {
124 /* ContentVersion is required in SMPTE */
125 throw XMLError ("Missing ContentVersion tag in CPL");
127 cxml::ConstNodePtr rating_list = f.node_child ("RatingList");
129 BOOST_FOREACH (cxml::ConstNodePtr i, rating_list->node_children("Rating")) {
130 _ratings.push_back (Rating(i));
133 _reels = type_grand_children<Reel> (f, "ReelList", "Reel");
135 cxml::ConstNodePtr reel_list = f.node_child ("ReelList");
137 list<cxml::NodePtr> reels = reel_list->node_children("Reel");
138 if (!reels.empty()) {
139 cxml::ConstNodePtr asset_list = reels.front()->node_child("AssetList");
140 cxml::ConstNodePtr metadata = asset_list->optional_node_child("CompositionMetadataAsset");
142 read_composition_metadata_asset (metadata);
148 f.ignore_child ("Issuer");
149 f.ignore_child ("Signer");
150 f.ignore_child ("Signature");
155 /** Add a reel to this CPL.
156 * @param reel Reel to add.
159 CPL::add (boost::shared_ptr<Reel> reel)
161 _reels.push_back (reel);
164 /** Write an CompositonPlaylist XML file.
166 * @param file Filename to write.
167 * @param standard INTEROP or SMPTE.
168 * @param signer Signer to sign the CPL, or 0 to add no signature.
171 CPL::write_xml (boost::filesystem::path file, Standard standard, shared_ptr<const CertificateChain> signer) const
174 xmlpp::Element* root;
175 if (standard == INTEROP) {
176 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
178 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
181 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
182 root->add_child("AnnotationText")->add_child_text (_annotation_text);
183 root->add_child("IssueDate")->add_child_text (_issue_date);
184 root->add_child("Issuer")->add_child_text (_issuer);
185 root->add_child("Creator")->add_child_text (_creator);
186 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
187 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
188 if (_content_versions.empty()) {
192 _content_versions[0].as_xml (root);
195 xmlpp::Element* rating_list = root->add_child("RatingList");
196 BOOST_FOREACH (Rating i, _ratings) {
197 i.as_xml (rating_list->add_child("Rating"));
200 xmlpp::Element* reel_list = root->add_child ("ReelList");
203 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
204 xmlpp::Element* asset_list = i->write_to_cpl (reel_list, standard);
205 if (first && standard == dcp::SMPTE) {
206 maybe_write_composition_metadata_asset (asset_list);
214 signer->sign (root, standard);
217 doc.write_to_file_formatted (file.string(), "UTF-8");
224 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
226 cxml::ConstNodePtr fctt = node->node_child("FullContentTitleText");
227 _full_content_title_text = fctt->content();
228 _full_content_title_text_language = fctt->optional_string_attribute("language");
230 _release_territory = node->optional_string_child("ReleaseTerritory");
232 cxml::ConstNodePtr vn = node->optional_node_child("VersionNumber");
234 _version_number = raw_convert<int>(vn->content());
235 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
236 optional<string> vn_status = vn->optional_string_attribute("status");
238 _status = string_to_status (*vn_status);
242 _chain = node->optional_string_child("Chain");
243 _distributor = node->optional_string_child("Distributor");
244 _facility = node->optional_string_child("Facility");
246 cxml::ConstNodePtr acv = node->optional_node_child("AlternateContentVersionList");
248 BOOST_FOREACH (cxml::ConstNodePtr i, acv->node_children("ContentVersion")) {
249 _content_versions.push_back (ContentVersion(i));
253 cxml::ConstNodePtr lum = node->optional_node_child("Luminance");
255 _luminance = Luminance (lum);
258 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
260 optional<string> sr = node->optional_string_child("MainSoundSampleRate");
262 vector<string> sr_bits;
263 boost::split (sr_bits, *sr, boost::is_any_of(" "));
264 DCP_ASSERT (sr_bits.size() == 2);
265 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
268 _main_picture_stored_area = dcp::Size (
269 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
270 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
273 _main_picture_active_area = dcp::Size (
274 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
275 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
278 optional<string> sll = node->optional_string_child("MainSubtitleLanguageList");
280 vector<string> sll_split;
281 boost::split (sll_split, *sll, boost::is_any_of(" "));
282 DCP_ASSERT (!sll_split.empty());
284 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
286 if (!_reels.empty()) {
287 shared_ptr<dcp::ReelSubtitleAsset> sub = _reels.front()->main_subtitle();
289 optional<string> lang = sub->language();
290 if (lang && lang == sll_split[0]) {
296 for (size_t i = first; i < sll_split.size(); ++i) {
297 _additional_subtitle_languages.push_back (sll_split[i]);
303 /** Write a CompositionMetadataAsset node as a child of @param node provided
304 * the required metadata is stored in the object. If any required metadata
305 * is missing this method will do nothing.
308 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
311 !_main_sound_configuration ||
312 !_main_sound_sample_rate ||
313 !_main_picture_stored_area ||
314 !_main_picture_active_area ||
316 !_reels.front()->main_picture()) {
320 xmlpp::Element* meta = node->add_child("meta:CompositionMetadataAsset");
321 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
323 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
325 shared_ptr<dcp::ReelPictureAsset> mp = _reels.front()->main_picture();
326 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
327 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
329 xmlpp::Element* fctt = meta->add_child("FullContentTitleText", "meta");
330 if (_full_content_title_text) {
331 fctt->add_child_text (*_full_content_title_text);
333 if (_full_content_title_text_language) {
334 fctt->set_attribute("language", *_full_content_title_text_language);
337 if (_release_territory) {
338 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
341 if (_version_number) {
342 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
343 vn->add_child_text(raw_convert<string>(*_version_number));
345 vn->set_attribute("status", status_to_string(*_status));
350 meta->add_child("Chain", "meta")->add_child_text(*_chain);
354 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
358 meta->add_child("Facility", "meta")->add_child_text(*_facility);
361 if (_content_versions.size() > 1) {
362 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
363 for (size_t i = 1; i < _content_versions.size(); ++i) {
364 _content_versions[i].as_xml (vc);
369 _luminance->as_xml (meta, "meta");
372 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
373 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
375 xmlpp::Element* stored = meta->add_child("MainPictureStoredArea", "meta");
376 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
377 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
379 xmlpp::Element* active = meta->add_child("MainPictureActiveArea", "meta");
380 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
381 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
383 optional<string> first_subtitle_language;
384 BOOST_FOREACH (shared_ptr<const Reel> i, _reels) {
385 if (i->main_subtitle()) {
386 first_subtitle_language = i->main_subtitle()->language();
387 if (first_subtitle_language) {
393 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
395 if (first_subtitle_language) {
396 lang = *first_subtitle_language;
398 BOOST_FOREACH (string const& i, _additional_subtitle_languages) {
404 meta->add_child("MainSubtitleLanguageList")->add_child_text(lang);
407 /* SMPTE Bv2.1 8.6.3 */
408 xmlpp::Element* extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
409 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
410 extension->add_child("Name", "meta")->add_child_text("Application");
411 xmlpp::Element* property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
412 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
413 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
415 if (_reels.front()->main_sound()) {
416 shared_ptr<const SoundAsset> asset = _reels.front()->main_sound()->asset();
418 shared_ptr<SoundAssetReader> reader = asset->start_read ();
419 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
420 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
421 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
422 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
425 xmlpp::Element* mca_subs = meta->add_child("mca:MCASubDescriptors");
426 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
427 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
428 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
429 xmlpp::Element* sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
431 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
432 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
433 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
434 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
435 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
436 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
437 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
438 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
439 if (!soundfield->MCATagName.empty()) {
440 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
441 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
443 if (!soundfield->RFC5646SpokenLanguage.empty()) {
444 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
445 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
448 list<ASDCP::MXF::InterchangeObject*> channels;
449 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectsByType(
450 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
454 BOOST_FOREACH (ASDCP::MXF::InterchangeObject* i, channels) {
455 ASDCP::MXF::AudioChannelLabelSubDescriptor* channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
456 xmlpp::Element* ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
457 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
458 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
459 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
460 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
461 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
462 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
463 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
464 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
465 if (!channel->MCATagName.empty()) {
466 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
467 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
469 if (!channel->MCAChannelID.empty()) {
470 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
472 if (!channel->RFC5646SpokenLanguage.empty()) {
473 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
474 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
476 if (!channel->SoundfieldGroupLinkID.empty()) {
477 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
478 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
487 list<shared_ptr<ReelMXF> >
490 list<shared_ptr<ReelMXF> > c;
492 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
493 if (i->main_picture ()) {
494 c.push_back (i->main_picture());
496 if (i->main_sound ()) {
497 c.push_back (i->main_sound());
499 if (i->main_subtitle ()) {
500 c.push_back (i->main_subtitle());
502 BOOST_FOREACH (shared_ptr<ReelClosedCaptionAsset> j, i->closed_captions()) {
506 c.push_back (i->atmos());
513 list<shared_ptr<const ReelMXF> >
514 CPL::reel_mxfs () const
516 list<shared_ptr<const ReelMXF> > c;
518 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
519 if (i->main_picture ()) {
520 c.push_back (i->main_picture());
522 if (i->main_sound ()) {
523 c.push_back (i->main_sound());
525 if (i->main_subtitle ()) {
526 c.push_back (i->main_subtitle());
528 BOOST_FOREACH (shared_ptr<ReelClosedCaptionAsset> j, i->closed_captions()) {
532 c.push_back (i->atmos());
540 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
542 shared_ptr<const CPL> other_cpl = dynamic_pointer_cast<const CPL> (other);
547 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
548 string const s = "CPL: annotation texts differ: " + _annotation_text + " vs " + other_cpl->_annotation_text + "\n";
553 if (_content_kind != other_cpl->_content_kind) {
554 note (DCP_ERROR, "CPL: content kinds differ");
558 if (_reels.size() != other_cpl->_reels.size()) {
559 note (DCP_ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
563 list<shared_ptr<Reel> >::const_iterator a = _reels.begin ();
564 list<shared_ptr<Reel> >::const_iterator b = other_cpl->_reels.begin ();
566 while (a != _reels.end ()) {
567 if (!(*a)->equals (*b, opt, note)) {
577 /** @return true if we have any encrypted content */
579 CPL::encrypted () const
581 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
582 if (i->encrypted ()) {
590 /** Add a KDM to this CPL. If the KDM is for any of this CPLs assets it will be used
591 * to decrypt those assets.
595 CPL::add (DecryptedKDM const & kdm)
597 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
603 CPL::resolve_refs (list<shared_ptr<Asset> > assets)
605 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
606 i->resolve_refs (assets);
611 CPL::pkl_type (Standard standard) const
613 return static_pkl_type (standard);
617 CPL::static_pkl_type (Standard standard)
621 return "text/xml;asdcpKind=CPL";
630 CPL::duration () const
633 BOOST_FOREACH (shared_ptr<Reel> i, _reels) {
641 CPL::set_version_number (int v)
644 throw BadSettingError ("CPL version number cannot be negative");
652 CPL::set_content_versions (vector<ContentVersion> v)
655 BOOST_FOREACH (ContentVersion i, v) {
656 if (!ids.insert(i.id).second) {
657 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
661 _content_versions = v;
665 optional<ContentVersion>
666 CPL::content_version () const
668 if (_content_versions.empty()) {
669 return optional<ContentVersion>();
672 return _content_versions[0];
677 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
679 _additional_subtitle_languages.clear ();
680 BOOST_FOREACH (dcp::LanguageTag const& i, langs) {
681 _additional_subtitle_languages.push_back (i.to_string());