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.
40 #include "certificate_chain.h"
41 #include "compose.hpp"
43 #include "dcp_assert.h"
44 #include "equality_options.h"
45 #include "filesystem.h"
46 #include "local_time.h"
48 #include "raw_convert.h"
50 #include "reel_atmos_asset.h"
51 #include "reel_closed_caption_asset.h"
52 #include "reel_picture_asset.h"
53 #include "reel_sound_asset.h"
54 #include "reel_subtitle_asset.h"
59 LIBDCP_DISABLE_WARNINGS
60 #include <asdcp/Metadata.h>
61 LIBDCP_ENABLE_WARNINGS
62 #include <libxml/parser.h>
63 LIBDCP_DISABLE_WARNINGS
64 #include <libxml++/libxml++.h>
65 LIBDCP_ENABLE_WARNINGS
66 #include <boost/algorithm/string.hpp>
70 using std::dynamic_pointer_cast;
73 using std::make_shared;
76 using std::shared_ptr;
79 using boost::optional;
83 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
84 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
85 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
86 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
87 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
88 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
91 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
92 /* default _content_title_text to annotation_text */
93 : _issuer("libdcp", dcp::version)
94 , _creator("libdcp", dcp::version)
95 , _issue_date (LocalTime().as_string())
96 , _annotation_text (annotation_text)
97 , _content_title_text (annotation_text)
98 , _content_kind (content_kind)
99 , _standard (standard)
102 cv.label_text = cv.id + LocalTime().as_string();
103 _content_versions.push_back (cv);
107 CPL::CPL (boost::filesystem::path file)
109 , _content_kind (ContentKind::FEATURE)
111 cxml::Document f ("CompositionPlaylist");
112 f.read_file(dcp::filesystem::fix_long_path(file));
114 if (f.namespace_uri() == cpl_interop_ns) {
115 _standard = Standard::INTEROP;
116 } else if (f.namespace_uri() == cpl_smpte_ns) {
117 _standard = Standard::SMPTE;
119 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
122 _id = remove_urn_uuid (f.string_child ("Id"));
123 _annotation_text = f.optional_string_child("AnnotationText");
124 _issuer = f.optional_string_child("Issuer").get_value_or("");
125 _creator = f.optional_string_child("Creator").get_value_or("");
126 _issue_date = f.string_child ("IssueDate");
127 _content_title_text = f.string_child ("ContentTitleText");
128 auto content_kind = f.node_child("ContentKind");
129 _content_kind = ContentKind(content_kind->content(), content_kind->optional_string_attribute("scope"));
130 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
131 if (content_version) {
132 /* XXX: SMPTE should insist that Id is present */
133 _content_versions.push_back (
135 content_version->optional_string_child("Id").get_value_or(""),
136 content_version->string_child("LabelText")
139 content_version->done ();
140 } else if (_standard == Standard::SMPTE) {
141 /* ContentVersion is required in SMPTE */
142 throw XMLError ("Missing ContentVersion tag in CPL");
144 auto rating_list = f.node_child ("RatingList");
145 for (auto i: rating_list->node_children("Rating")) {
146 _ratings.push_back (Rating(i));
149 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
150 _reels.push_back (make_shared<Reel>(i, _standard));
153 auto reel_list = f.node_child ("ReelList");
154 auto reels = reel_list->node_children("Reel");
155 if (!reels.empty()) {
156 auto asset_list = reels.front()->node_child("AssetList");
157 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
159 read_composition_metadata_asset (metadata);
160 _read_composition_metadata = true;
164 f.ignore_child ("Issuer");
165 f.ignore_child ("Signer");
166 f.ignore_child ("Signature");
173 CPL::add (std::shared_ptr<Reel> reel)
175 _reels.push_back (reel);
180 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
187 CPL::write_xml(boost::filesystem::path file, shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors) const
190 xmlpp::Element* root;
191 if (_standard == Standard::INTEROP) {
192 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
194 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
197 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
198 if (_annotation_text) {
199 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
201 root->add_child("IssueDate")->add_child_text (_issue_date);
202 root->add_child("Issuer")->add_child_text (_issuer);
203 root->add_child("Creator")->add_child_text (_creator);
204 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
205 auto content_kind = root->add_child("ContentKind");
206 content_kind->add_child_text(_content_kind.name());
207 if (_content_kind.scope()) {
208 content_kind->set_attribute("scope", *_content_kind.scope());
210 if (_content_versions.empty()) {
214 _content_versions[0].as_xml (root);
217 auto rating_list = root->add_child("RatingList");
218 for (auto i: _ratings) {
219 i.as_xml (rating_list->add_child("Rating"));
222 auto reel_list = root->add_child ("ReelList");
224 if (_reels.empty()) {
225 throw NoReelsError ();
229 for (auto i: _reels) {
230 auto asset_list = i->write_to_cpl (reel_list, _standard);
231 if (first && _standard == Standard::SMPTE) {
232 maybe_write_composition_metadata_asset(asset_list, include_mca_subdescriptors);
240 signer->sign (root, _standard);
243 doc.write_to_file_formatted(dcp::filesystem::fix_long_path(file).string(), "UTF-8");
250 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
252 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
254 /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
255 * apparently didn't include it, so as usual we have to be defensive.
257 if (auto fctt = node->optional_node_child("FullContentTitleText")) {
258 _full_content_title_text = fctt->content();
259 _full_content_title_text_language = fctt->optional_string_attribute("language");
262 _release_territory = node->optional_string_child("ReleaseTerritory");
263 if (_release_territory) {
264 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
267 auto vn = node->optional_node_child("VersionNumber");
269 _version_number = raw_convert<int>(vn->content());
270 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
271 auto vn_status = vn->optional_string_attribute("status");
273 _status = string_to_status (*vn_status);
277 _chain = node->optional_string_child("Chain");
278 _distributor = node->optional_string_child("Distributor");
279 _facility = node->optional_string_child("Facility");
281 auto acv = node->optional_node_child("AlternateContentVersionList");
283 for (auto i: acv->node_children("ContentVersion")) {
284 _content_versions.push_back (ContentVersion(i));
288 auto lum = node->optional_node_child("Luminance");
290 _luminance = Luminance (lum);
293 if (auto msc = node->optional_string_child("MainSoundConfiguration")) {
295 _main_sound_configuration = MainSoundConfiguration(*msc);
296 } catch (MainSoundConfigurationError& e) {
297 /* With Interop DCPs this node may not make any sense, but that's OK */
298 if (_standard == dcp::Standard::SMPTE) {
304 auto sr = node->optional_string_child("MainSoundSampleRate");
306 vector<string> sr_bits;
307 boost::split (sr_bits, *sr, boost::is_any_of(" "));
308 DCP_ASSERT (sr_bits.size() == 2);
309 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
312 if (_standard == dcp::Standard::SMPTE) {
313 _main_picture_stored_area = dcp::Size (
314 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
315 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
319 _main_picture_active_area = dcp::Size (
320 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
321 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
324 auto sll = node->optional_string_child("MainSubtitleLanguageList");
326 vector<string> sll_split;
327 boost::split (sll_split, *sll, boost::is_any_of(" "));
328 DCP_ASSERT (!sll_split.empty());
330 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
332 if (!_reels.empty()) {
333 auto sub = _reels.front()->main_subtitle();
335 auto lang = sub->language();
336 if (lang && lang == sll_split[0]) {
342 for (auto i = first; i < sll_split.size(); ++i) {
343 _additional_subtitle_languages.push_back (sll_split[i]);
347 auto eml = node->optional_node_child ("ExtensionMetadataList");
349 auto extension_metadata = [eml](string scope, string name, string property) -> boost::optional<std::string> {
354 for (auto i: eml->node_children("ExtensionMetadata")) {
355 auto xml_scope = i->optional_string_attribute("scope");
356 auto xml_name = i->optional_string_child("Name");
357 if (xml_scope && *xml_scope == scope && xml_name && *xml_name == name) {
358 auto property_list = i->node_child("PropertyList");
359 for (auto j: property_list->node_children("Property")) {
360 auto property_name = j->optional_string_child("Name");
361 auto property_value = j->optional_string_child("Value");
362 if (property_name && property_value && *property_name == property) {
363 return property_value;
372 _sign_language_video_language = extension_metadata("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag");
377 CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsset> asset) const
379 auto reader = asset->start_read ();
380 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
381 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
382 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
383 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
386 auto mca_subs = parent->add_child("mca:MCASubDescriptors");
387 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
388 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
389 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
390 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
392 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
393 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
394 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
395 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
396 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
397 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
398 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
399 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
400 if (!soundfield->MCATagName.empty()) {
401 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
402 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
404 if (!soundfield->RFC5646SpokenLanguage.empty()) {
405 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
406 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
409 /* Find the MCA subdescriptors in the MXF so that we can also write them here */
410 list<ASDCP::MXF::InterchangeObject*> channels;
411 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
412 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
416 for (auto i: channels) {
417 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
418 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
419 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
420 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
421 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
422 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
423 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
424 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
425 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
426 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
427 if (!channel->MCATagName.empty()) {
428 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
429 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
431 if (!channel->MCAChannelID.empty()) {
432 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
434 if (!channel->RFC5646SpokenLanguage.empty()) {
435 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
436 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
438 if (!channel->SoundfieldGroupLinkID.empty()) {
439 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
440 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
447 /** Write a CompositionMetadataAsset node as a child of @param node provided
448 * the required metadata is stored in the object. If any required metadata
449 * is missing this method will do nothing.
452 CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node, bool include_mca_subdescriptors) const
455 !_main_sound_configuration ||
456 !_main_sound_sample_rate ||
457 !_main_picture_stored_area ||
458 !_main_picture_active_area ||
460 !_reels.front()->main_picture()) {
464 auto meta = node->add_child("meta:CompositionMetadataAsset");
465 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
467 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
469 auto mp = _reels.front()->main_picture();
470 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
471 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
473 auto fctt = meta->add_child("FullContentTitleText", "meta");
474 if (_full_content_title_text && !_full_content_title_text->empty()) {
475 fctt->add_child_text (*_full_content_title_text);
477 if (_full_content_title_text_language) {
478 fctt->set_attribute("language", *_full_content_title_text_language);
481 if (_release_territory) {
482 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
485 if (_version_number) {
486 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
487 vn->add_child_text(raw_convert<string>(*_version_number));
489 vn->set_attribute("status", status_to_string(*_status));
494 meta->add_child("Chain", "meta")->add_child_text(*_chain);
498 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
502 meta->add_child("Facility", "meta")->add_child_text(*_facility);
505 if (_content_versions.size() > 1) {
506 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
507 for (size_t i = 1; i < _content_versions.size(); ++i) {
508 _content_versions[i].as_xml (vc);
513 _luminance->as_xml (meta, "meta");
516 if (_main_sound_configuration) {
517 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(_main_sound_configuration->to_string());
519 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
521 auto stored = meta->add_child("MainPictureStoredArea", "meta");
522 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
523 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
525 auto active = meta->add_child("MainPictureActiveArea", "meta");
526 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
527 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
529 optional<string> first_subtitle_language;
530 for (auto i: _reels) {
531 if (i->main_subtitle()) {
532 first_subtitle_language = i->main_subtitle()->language();
533 if (first_subtitle_language) {
539 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
541 if (first_subtitle_language) {
542 lang = *first_subtitle_language;
544 for (auto const& i: _additional_subtitle_languages) {
550 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
553 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
555 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
556 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
557 extension->set_attribute("scope", scope);
558 extension->add_child("Name", "meta")->add_child_text(name);
559 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
560 property->add_child("Name", "meta")->add_child_text(property_name);
561 property->add_child("Value", "meta")->add_child_text(property_value);
564 /* SMPTE Bv2.1 8.6.3 */
565 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
567 if (_sign_language_video_language) {
568 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
571 if (_reels.front()->main_sound()) {
572 auto asset = _reels.front()->main_sound()->asset();
573 if (asset && include_mca_subdescriptors) {
574 write_mca_subdescriptors(meta, asset);
582 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
584 for (auto i: reels) {
585 if (i->main_picture ()) {
586 assets.push_back (i->main_picture());
588 if (i->main_sound ()) {
589 assets.push_back (i->main_sound());
591 if (i->main_subtitle ()) {
592 assets.push_back (i->main_subtitle());
594 for (auto j: i->closed_captions()) {
595 assets.push_back (j);
598 assets.push_back (i->atmos());
604 vector<shared_ptr<ReelFileAsset>>
605 CPL::reel_file_assets ()
607 vector<shared_ptr<ReelFileAsset>> c;
608 add_file_assets (c, _reels);
613 vector<shared_ptr<const ReelFileAsset>>
614 CPL::reel_file_assets () const
616 vector<shared_ptr<const ReelFileAsset>> c;
617 add_file_assets (c, _reels);
623 CPL::equals(shared_ptr<const Asset> other, EqualityOptions const& opt, NoteHandler note) const
625 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
630 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
631 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
632 note (NoteType::ERROR, s);
636 if (_content_kind != other_cpl->_content_kind) {
637 note (NoteType::ERROR, "CPL: content kinds differ");
641 if (_reels.size() != other_cpl->_reels.size()) {
642 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
646 auto a = _reels.begin();
647 auto b = other_cpl->_reels.begin();
649 while (a != _reels.end ()) {
650 if (!(*a)->equals (*b, opt, note)) {
662 CPL::any_encrypted () const
664 for (auto i: _reels) {
665 if (i->any_encrypted()) {
675 CPL::all_encrypted () const
677 for (auto i: _reels) {
678 if (!i->all_encrypted()) {
688 CPL::add (DecryptedKDM const & kdm)
690 for (auto i: _reels) {
696 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
698 for (auto i: _reels) {
699 i->resolve_refs (assets);
704 CPL::pkl_type (Standard standard) const
706 return static_pkl_type (standard);
710 CPL::static_pkl_type (Standard standard)
713 case Standard::INTEROP:
714 return "text/xml;asdcpKind=CPL";
715 case Standard::SMPTE:
723 CPL::duration () const
726 for (auto i: _reels) {
734 CPL::set_version_number (int v)
737 throw BadSettingError ("CPL version number cannot be negative");
745 CPL::unset_version_number ()
747 _version_number = boost::none;
752 CPL::set_content_versions (vector<ContentVersion> v)
754 std::set<string> ids;
756 if (!ids.insert(i.id).second) {
757 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
761 _content_versions = v;
765 optional<ContentVersion>
766 CPL::content_version () const
768 if (_content_versions.empty()) {
769 return optional<ContentVersion>();
772 return _content_versions[0];
777 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
779 _additional_subtitle_languages.clear ();
780 for (auto const& i: langs) {
781 _additional_subtitle_languages.push_back (i.to_string());
787 CPL::set_main_picture_active_area(dcp::Size area)
789 if (area.width % 2) {
790 throw BadSettingError("Main picture active area width is not a multiple of 2");
793 if (area.height % 2) {
794 throw BadSettingError("Main picture active area height is not a multiple of 2");
797 _main_picture_active_area = area;