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 "local_time.h"
46 #include "raw_convert.h"
48 #include "reel_atmos_asset.h"
49 #include "reel_closed_caption_asset.h"
50 #include "reel_picture_asset.h"
51 #include "reel_sound_asset.h"
52 #include "reel_subtitle_asset.h"
56 LIBDCP_DISABLE_WARNINGS
57 #include <asdcp/Metadata.h>
58 LIBDCP_ENABLE_WARNINGS
59 #include <libxml/parser.h>
60 LIBDCP_DISABLE_WARNINGS
61 #include <libxml++/libxml++.h>
62 LIBDCP_ENABLE_WARNINGS
63 #include <boost/algorithm/string.hpp>
67 using std::dynamic_pointer_cast;
70 using std::make_shared;
73 using std::shared_ptr;
76 using boost::optional;
80 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
81 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
82 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
83 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
84 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
85 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
88 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
89 /* default _content_title_text to annotation_text */
90 : _issuer ("libdcp" LIBDCP_VERSION)
91 , _creator ("libdcp" LIBDCP_VERSION)
92 , _issue_date (LocalTime().as_string())
93 , _annotation_text (annotation_text)
94 , _content_title_text (annotation_text)
95 , _content_kind (content_kind)
96 , _standard (standard)
99 cv.label_text = cv.id + LocalTime().as_string();
100 _content_versions.push_back (cv);
104 CPL::CPL (boost::filesystem::path file)
106 , _content_kind (ContentKind::FEATURE)
108 cxml::Document f ("CompositionPlaylist");
111 if (f.namespace_uri() == cpl_interop_ns) {
112 _standard = Standard::INTEROP;
113 } else if (f.namespace_uri() == cpl_smpte_ns) {
114 _standard = Standard::SMPTE;
116 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
119 _id = remove_urn_uuid (f.string_child ("Id"));
120 _annotation_text = f.optional_string_child("AnnotationText");
121 _issuer = f.optional_string_child("Issuer").get_value_or("");
122 _creator = f.optional_string_child("Creator").get_value_or("");
123 _issue_date = f.string_child ("IssueDate");
124 _content_title_text = f.string_child ("ContentTitleText");
125 _content_kind = content_kind_from_string (f.string_child ("ContentKind"));
126 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
127 if (content_version) {
128 /* XXX: SMPTE should insist that Id is present */
129 _content_versions.push_back (
131 content_version->optional_string_child("Id").get_value_or(""),
132 content_version->string_child("LabelText")
135 content_version->done ();
136 } else if (_standard == Standard::SMPTE) {
137 /* ContentVersion is required in SMPTE */
138 throw XMLError ("Missing ContentVersion tag in CPL");
140 auto rating_list = f.node_child ("RatingList");
141 for (auto i: rating_list->node_children("Rating")) {
142 _ratings.push_back (Rating(i));
145 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
146 _reels.push_back (make_shared<Reel>(i, _standard));
149 auto reel_list = f.node_child ("ReelList");
150 auto reels = reel_list->node_children("Reel");
151 if (!reels.empty()) {
152 auto asset_list = reels.front()->node_child("AssetList");
153 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
155 read_composition_metadata_asset (metadata);
156 _read_composition_metadata = true;
160 f.ignore_child ("Issuer");
161 f.ignore_child ("Signer");
162 f.ignore_child ("Signature");
169 CPL::add (std::shared_ptr<Reel> reel)
171 _reels.push_back (reel);
176 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
183 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
186 xmlpp::Element* root;
187 if (_standard == Standard::INTEROP) {
188 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
190 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
193 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
194 if (_annotation_text) {
195 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
197 root->add_child("IssueDate")->add_child_text (_issue_date);
198 root->add_child("Issuer")->add_child_text (_issuer);
199 root->add_child("Creator")->add_child_text (_creator);
200 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
201 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
202 if (_content_versions.empty()) {
206 _content_versions[0].as_xml (root);
209 auto rating_list = root->add_child("RatingList");
210 for (auto i: _ratings) {
211 i.as_xml (rating_list->add_child("Rating"));
214 auto reel_list = root->add_child ("ReelList");
216 if (_reels.empty()) {
217 throw NoReelsError ();
221 for (auto i: _reels) {
222 auto asset_list = i->write_to_cpl (reel_list, _standard);
223 if (first && _standard == Standard::SMPTE) {
224 maybe_write_composition_metadata_asset (asset_list);
232 signer->sign (root, _standard);
235 doc.write_to_file_formatted (file.string(), "UTF-8");
242 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
244 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
246 auto fctt = node->node_child("FullContentTitleText");
247 _full_content_title_text = fctt->content();
248 _full_content_title_text_language = fctt->optional_string_attribute("language");
250 _release_territory = node->optional_string_child("ReleaseTerritory");
251 if (_release_territory) {
252 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
255 auto vn = node->optional_node_child("VersionNumber");
257 _version_number = raw_convert<int>(vn->content());
258 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
259 auto vn_status = vn->optional_string_attribute("status");
261 _status = string_to_status (*vn_status);
265 _chain = node->optional_string_child("Chain");
266 _distributor = node->optional_string_child("Distributor");
267 _facility = node->optional_string_child("Facility");
269 auto acv = node->optional_node_child("AlternateContentVersionList");
271 for (auto i: acv->node_children("ContentVersion")) {
272 _content_versions.push_back (ContentVersion(i));
276 auto lum = node->optional_node_child("Luminance");
278 _luminance = Luminance (lum);
281 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
283 auto sr = node->optional_string_child("MainSoundSampleRate");
285 vector<string> sr_bits;
286 boost::split (sr_bits, *sr, boost::is_any_of(" "));
287 DCP_ASSERT (sr_bits.size() == 2);
288 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
291 _main_picture_stored_area = dcp::Size (
292 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
293 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
296 _main_picture_active_area = dcp::Size (
297 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
298 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
301 auto sll = node->optional_string_child("MainSubtitleLanguageList");
303 vector<string> sll_split;
304 boost::split (sll_split, *sll, boost::is_any_of(" "));
305 DCP_ASSERT (!sll_split.empty());
307 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
309 if (!_reels.empty()) {
310 auto sub = _reels.front()->main_subtitle();
312 auto lang = sub->language();
313 if (lang && lang == sll_split[0]) {
319 for (auto i = first; i < sll_split.size(); ++i) {
320 _additional_subtitle_languages.push_back (sll_split[i]);
324 auto eml = node->optional_node_child ("ExtensionMetadataList");
326 for (auto i: eml->node_children("ExtensionMetadata")) {
327 auto name = i->optional_string_child("Name");
328 if (name && *name == "Sign Language Video") {
329 auto property_list = i->node_child("PropertyList");
330 for (auto j: property_list->node_children("Property")) {
331 auto name = j->optional_string_child("Name");
332 auto value = j->optional_string_child("Value");
333 if (name && value && *name == "Language Tag") {
334 _sign_language_video_language = *value;
343 /** Write a CompositionMetadataAsset node as a child of @param node provided
344 * the required metadata is stored in the object. If any required metadata
345 * is missing this method will do nothing.
348 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
351 !_main_sound_configuration ||
352 !_main_sound_sample_rate ||
353 !_main_picture_stored_area ||
354 !_main_picture_active_area ||
356 !_reels.front()->main_picture()) {
360 auto meta = node->add_child("meta:CompositionMetadataAsset");
361 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
363 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
365 auto mp = _reels.front()->main_picture();
366 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
367 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
369 auto fctt = meta->add_child("FullContentTitleText", "meta");
370 if (_full_content_title_text && !_full_content_title_text->empty()) {
371 fctt->add_child_text (*_full_content_title_text);
373 if (_full_content_title_text_language) {
374 fctt->set_attribute("language", *_full_content_title_text_language);
377 if (_release_territory) {
378 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
381 if (_version_number) {
382 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
383 vn->add_child_text(raw_convert<string>(*_version_number));
385 vn->set_attribute("status", status_to_string(*_status));
390 meta->add_child("Chain", "meta")->add_child_text(*_chain);
394 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
398 meta->add_child("Facility", "meta")->add_child_text(*_facility);
401 if (_content_versions.size() > 1) {
402 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
403 for (size_t i = 1; i < _content_versions.size(); ++i) {
404 _content_versions[i].as_xml (vc);
409 _luminance->as_xml (meta, "meta");
412 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
413 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
415 auto stored = meta->add_child("MainPictureStoredArea", "meta");
416 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
417 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
419 auto active = meta->add_child("MainPictureActiveArea", "meta");
420 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
421 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
423 optional<string> first_subtitle_language;
424 for (auto i: _reels) {
425 if (i->main_subtitle()) {
426 first_subtitle_language = i->main_subtitle()->language();
427 if (first_subtitle_language) {
433 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
435 if (first_subtitle_language) {
436 lang = *first_subtitle_language;
438 for (auto const& i: _additional_subtitle_languages) {
444 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
447 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
449 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
450 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
451 extension->set_attribute("scope", scope);
452 extension->add_child("Name", "meta")->add_child_text(name);
453 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
454 property->add_child("Name", "meta")->add_child_text(property_name);
455 property->add_child("Value", "meta")->add_child_text(property_value);
458 /* SMPTE Bv2.1 8.6.3 */
459 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
461 if (_sign_language_video_language) {
462 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
465 if (_reels.front()->main_sound()) {
466 auto asset = _reels.front()->main_sound()->asset();
468 auto reader = asset->start_read ();
469 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
470 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
471 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
472 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
475 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
476 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
477 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
478 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
479 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
481 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
482 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
483 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
484 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
485 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
486 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
487 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
488 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
489 if (!soundfield->MCATagName.empty()) {
490 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
491 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
493 if (!soundfield->RFC5646SpokenLanguage.empty()) {
494 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
495 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
498 list<ASDCP::MXF::InterchangeObject*> channels;
499 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
500 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
504 for (auto i: channels) {
505 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
506 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
507 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
508 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
509 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
510 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
511 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
512 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
513 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
514 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
515 if (!channel->MCATagName.empty()) {
516 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
517 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
519 if (!channel->MCAChannelID.empty()) {
520 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
522 if (!channel->RFC5646SpokenLanguage.empty()) {
523 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
524 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
526 if (!channel->SoundfieldGroupLinkID.empty()) {
527 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
528 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
539 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
541 for (auto i: reels) {
542 if (i->main_picture ()) {
543 assets.push_back (i->main_picture());
545 if (i->main_sound ()) {
546 assets.push_back (i->main_sound());
548 if (i->main_subtitle ()) {
549 assets.push_back (i->main_subtitle());
551 for (auto j: i->closed_captions()) {
552 assets.push_back (j);
555 assets.push_back (i->atmos());
561 vector<shared_ptr<ReelFileAsset>>
562 CPL::reel_file_assets ()
564 vector<shared_ptr<ReelFileAsset>> c;
565 add_file_assets (c, _reels);
570 vector<shared_ptr<const ReelFileAsset>>
571 CPL::reel_file_assets () const
573 vector<shared_ptr<const ReelFileAsset>> c;
574 add_file_assets (c, _reels);
580 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
582 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
587 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
588 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
589 note (NoteType::ERROR, s);
593 if (_content_kind != other_cpl->_content_kind) {
594 note (NoteType::ERROR, "CPL: content kinds differ");
598 if (_reels.size() != other_cpl->_reels.size()) {
599 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
603 auto a = _reels.begin();
604 auto b = other_cpl->_reels.begin();
606 while (a != _reels.end ()) {
607 if (!(*a)->equals (*b, opt, note)) {
619 CPL::any_encrypted () const
621 for (auto i: _reels) {
622 if (i->any_encrypted()) {
632 CPL::all_encrypted () const
634 for (auto i: _reels) {
635 if (!i->all_encrypted()) {
645 CPL::add (DecryptedKDM const & kdm)
647 for (auto i: _reels) {
653 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
655 for (auto i: _reels) {
656 i->resolve_refs (assets);
661 CPL::pkl_type (Standard standard) const
663 return static_pkl_type (standard);
667 CPL::static_pkl_type (Standard standard)
670 case Standard::INTEROP:
671 return "text/xml;asdcpKind=CPL";
672 case Standard::SMPTE:
680 CPL::duration () const
683 for (auto i: _reels) {
691 CPL::set_version_number (int v)
694 throw BadSettingError ("CPL version number cannot be negative");
702 CPL::unset_version_number ()
704 _version_number = boost::none;
709 CPL::set_content_versions (vector<ContentVersion> v)
711 std::set<string> ids;
713 if (!ids.insert(i.id).second) {
714 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
718 _content_versions = v;
722 optional<ContentVersion>
723 CPL::content_version () const
725 if (_content_versions.empty()) {
726 return optional<ContentVersion>();
729 return _content_versions[0];
734 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
736 _additional_subtitle_languages.clear ();
737 for (auto const& i: langs) {
738 _additional_subtitle_languages.push_back (i.to_string());