2 Copyright (C) 2012-2021 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>
61 using std::shared_ptr;
62 using boost::optional;
63 using std::dynamic_pointer_cast;
67 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
68 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
69 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
70 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
71 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
72 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 auto rating_list = f.node_child ("RatingList");
128 for (auto i: rating_list->node_children("Rating")) {
129 _ratings.push_back (Rating(i));
132 _reels = type_grand_children<Reel> (f, "ReelList", "Reel");
134 auto reel_list = f.node_child ("ReelList");
136 auto reels = reel_list->node_children("Reel");
137 if (!reels.empty()) {
138 auto asset_list = reels.front()->node_child("AssetList");
139 auto 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 (std::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 if (_content_versions.empty()) {
191 _content_versions[0].as_xml (root);
194 auto rating_list = root->add_child("RatingList");
195 for (auto i: _ratings) {
196 i.as_xml (rating_list->add_child("Rating"));
199 auto reel_list = root->add_child ("ReelList");
201 if (_reels.empty()) {
202 throw NoReelsError ();
206 for (auto i: _reels) {
207 auto asset_list = i->write_to_cpl (reel_list, standard);
208 if (first && standard == dcp::SMPTE) {
209 maybe_write_composition_metadata_asset (asset_list);
217 signer->sign (root, standard);
220 doc.write_to_file_formatted (file.string(), "UTF-8");
227 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
229 auto fctt = node->node_child("FullContentTitleText");
230 _full_content_title_text = fctt->content();
231 _full_content_title_text_language = fctt->optional_string_attribute("language");
233 _release_territory = node->optional_string_child("ReleaseTerritory");
235 auto vn = node->optional_node_child("VersionNumber");
237 _version_number = raw_convert<int>(vn->content());
238 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
239 auto vn_status = vn->optional_string_attribute("status");
241 _status = string_to_status (*vn_status);
245 _chain = node->optional_string_child("Chain");
246 _distributor = node->optional_string_child("Distributor");
247 _facility = node->optional_string_child("Facility");
249 auto acv = node->optional_node_child("AlternateContentVersionList");
251 for (auto i: acv->node_children("ContentVersion")) {
252 _content_versions.push_back (ContentVersion(i));
256 auto lum = node->optional_node_child("Luminance");
258 _luminance = Luminance (lum);
261 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
263 auto sr = node->optional_string_child("MainSoundSampleRate");
265 vector<string> sr_bits;
266 boost::split (sr_bits, *sr, boost::is_any_of(" "));
267 DCP_ASSERT (sr_bits.size() == 2);
268 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
271 _main_picture_stored_area = dcp::Size (
272 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
273 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
276 _main_picture_active_area = dcp::Size (
277 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
278 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
281 auto sll = node->optional_string_child("MainSubtitleLanguageList");
283 vector<string> sll_split;
284 boost::split (sll_split, *sll, boost::is_any_of(" "));
285 DCP_ASSERT (!sll_split.empty());
287 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
289 if (!_reels.empty()) {
290 shared_ptr<dcp::ReelSubtitleAsset> sub = _reels.front()->main_subtitle();
292 optional<string> lang = sub->language();
293 if (lang && lang == sll_split[0]) {
299 for (auto i = first; i < sll_split.size(); ++i) {
300 _additional_subtitle_languages.push_back (sll_split[i]);
306 /** Write a CompositionMetadataAsset node as a child of @param node provided
307 * the required metadata is stored in the object. If any required metadata
308 * is missing this method will do nothing.
311 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
314 !_main_sound_configuration ||
315 !_main_sound_sample_rate ||
316 !_main_picture_stored_area ||
317 !_main_picture_active_area ||
319 !_reels.front()->main_picture()) {
323 auto meta = node->add_child("meta:CompositionMetadataAsset");
324 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
326 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
328 auto mp = _reels.front()->main_picture();
329 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
330 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
332 auto fctt = meta->add_child("FullContentTitleText", "meta");
333 if (_full_content_title_text) {
334 fctt->add_child_text (*_full_content_title_text);
336 if (_full_content_title_text_language) {
337 fctt->set_attribute("language", *_full_content_title_text_language);
340 if (_release_territory) {
341 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
344 if (_version_number) {
345 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
346 vn->add_child_text(raw_convert<string>(*_version_number));
348 vn->set_attribute("status", status_to_string(*_status));
353 meta->add_child("Chain", "meta")->add_child_text(*_chain);
357 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
361 meta->add_child("Facility", "meta")->add_child_text(*_facility);
364 if (_content_versions.size() > 1) {
365 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
366 for (size_t i = 1; i < _content_versions.size(); ++i) {
367 _content_versions[i].as_xml (vc);
372 _luminance->as_xml (meta, "meta");
375 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
376 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
378 auto stored = meta->add_child("MainPictureStoredArea", "meta");
379 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
380 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
382 auto active = meta->add_child("MainPictureActiveArea", "meta");
383 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
384 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
386 optional<string> first_subtitle_language;
387 for (auto i: _reels) {
388 if (i->main_subtitle()) {
389 first_subtitle_language = i->main_subtitle()->language();
390 if (first_subtitle_language) {
396 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
398 if (first_subtitle_language) {
399 lang = *first_subtitle_language;
401 for (auto const& i: _additional_subtitle_languages) {
407 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
410 /* SMPTE Bv2.1 8.6.3 */
411 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
412 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
413 extension->add_child("Name", "meta")->add_child_text("Application");
414 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
415 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
416 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
418 if (_reels.front()->main_sound()) {
419 auto asset = _reels.front()->main_sound()->asset();
421 auto reader = asset->start_read ();
422 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
423 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
424 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
425 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
428 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
429 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
430 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
431 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
432 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
434 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
435 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
436 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
437 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
438 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
439 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
440 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
441 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
442 if (!soundfield->MCATagName.empty()) {
443 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
444 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
446 if (!soundfield->RFC5646SpokenLanguage.empty()) {
447 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
448 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
451 list<ASDCP::MXF::InterchangeObject*> channels;
452 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
453 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
457 for (auto i: channels) {
458 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
459 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
460 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
461 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
462 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
463 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
464 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
465 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
466 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
467 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
468 if (!channel->MCATagName.empty()) {
469 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
470 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
472 if (!channel->MCAChannelID.empty()) {
473 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
475 if (!channel->RFC5646SpokenLanguage.empty()) {
476 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
477 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
479 if (!channel->SoundfieldGroupLinkID.empty()) {
480 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
481 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
490 list<shared_ptr<ReelMXF>>
493 list<shared_ptr<ReelMXF>> c;
495 for (auto i: _reels) {
496 if (i->main_picture ()) {
497 c.push_back (i->main_picture());
499 if (i->main_sound ()) {
500 c.push_back (i->main_sound());
502 if (i->main_subtitle ()) {
503 c.push_back (i->main_subtitle());
505 for (auto j: i->closed_captions()) {
509 c.push_back (i->atmos());
516 list<shared_ptr<const ReelMXF>>
517 CPL::reel_mxfs () const
519 list<shared_ptr<const ReelMXF>> c;
521 for (auto i: _reels) {
522 if (i->main_picture ()) {
523 c.push_back (i->main_picture());
525 if (i->main_sound ()) {
526 c.push_back (i->main_sound());
528 if (i->main_subtitle ()) {
529 c.push_back (i->main_subtitle());
531 for (auto j: i->closed_captions()) {
535 c.push_back (i->atmos());
543 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
545 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
550 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
551 string const s = "CPL: annotation texts differ: " + _annotation_text + " vs " + other_cpl->_annotation_text + "\n";
556 if (_content_kind != other_cpl->_content_kind) {
557 note (DCP_ERROR, "CPL: content kinds differ");
561 if (_reels.size() != other_cpl->_reels.size()) {
562 note (DCP_ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
566 auto a = _reels.begin();
567 auto b = other_cpl->_reels.begin();
569 while (a != _reels.end ()) {
570 if (!(*a)->equals (*b, opt, note)) {
580 /** @return true if we have any encrypted content */
582 CPL::encrypted () const
584 for (auto i: _reels) {
585 if (i->encrypted ()) {
593 /** Add a KDM to this CPL. If the KDM is for any of this CPLs assets it will be used
594 * to decrypt those assets.
598 CPL::add (DecryptedKDM const & kdm)
600 for (auto i: _reels) {
606 CPL::resolve_refs (list<shared_ptr<Asset>> assets)
608 for (auto i: _reels) {
609 i->resolve_refs (assets);
614 CPL::pkl_type (Standard standard) const
616 return static_pkl_type (standard);
620 CPL::static_pkl_type (Standard standard)
624 return "text/xml;asdcpKind=CPL";
633 CPL::duration () const
636 for (auto i: _reels) {
644 CPL::set_version_number (int v)
647 throw BadSettingError ("CPL version number cannot be negative");
655 CPL::set_content_versions (vector<ContentVersion> v)
659 if (!ids.insert(i.id).second) {
660 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
664 _content_versions = v;
668 optional<ContentVersion>
669 CPL::content_version () const
671 if (_content_versions.empty()) {
672 return optional<ContentVersion>();
675 return _content_versions[0];
680 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
682 _additional_subtitle_languages.clear ();
683 for (auto const& i: langs) {
684 _additional_subtitle_languages.push_back (i.to_string());