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);
159 f.ignore_child ("Issuer");
160 f.ignore_child ("Signer");
161 f.ignore_child ("Signature");
168 CPL::add (std::shared_ptr<Reel> reel)
170 _reels.push_back (reel);
175 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
178 xmlpp::Element* root;
179 if (_standard == Standard::INTEROP) {
180 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
182 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
185 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
186 if (_annotation_text) {
187 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
189 root->add_child("IssueDate")->add_child_text (_issue_date);
190 root->add_child("Issuer")->add_child_text (_issuer);
191 root->add_child("Creator")->add_child_text (_creator);
192 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
193 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
194 if (_content_versions.empty()) {
198 _content_versions[0].as_xml (root);
201 auto rating_list = root->add_child("RatingList");
202 for (auto i: _ratings) {
203 i.as_xml (rating_list->add_child("Rating"));
206 auto reel_list = root->add_child ("ReelList");
208 if (_reels.empty()) {
209 throw NoReelsError ();
213 for (auto i: _reels) {
214 auto asset_list = i->write_to_cpl (reel_list, _standard);
215 if (first && _standard == Standard::SMPTE) {
216 maybe_write_composition_metadata_asset (asset_list);
224 signer->sign (root, _standard);
227 doc.write_to_file_formatted (file.string(), "UTF-8");
234 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
236 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
238 auto fctt = node->node_child("FullContentTitleText");
239 _full_content_title_text = fctt->content();
240 _full_content_title_text_language = fctt->optional_string_attribute("language");
242 _release_territory = node->optional_string_child("ReleaseTerritory");
243 if (_release_territory) {
244 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
247 auto vn = node->optional_node_child("VersionNumber");
249 _version_number = raw_convert<int>(vn->content());
250 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
251 auto vn_status = vn->optional_string_attribute("status");
253 _status = string_to_status (*vn_status);
257 _chain = node->optional_string_child("Chain");
258 _distributor = node->optional_string_child("Distributor");
259 _facility = node->optional_string_child("Facility");
261 auto acv = node->optional_node_child("AlternateContentVersionList");
263 for (auto i: acv->node_children("ContentVersion")) {
264 _content_versions.push_back (ContentVersion(i));
268 auto lum = node->optional_node_child("Luminance");
270 _luminance = Luminance (lum);
273 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
275 auto sr = node->optional_string_child("MainSoundSampleRate");
277 vector<string> sr_bits;
278 boost::split (sr_bits, *sr, boost::is_any_of(" "));
279 DCP_ASSERT (sr_bits.size() == 2);
280 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
283 _main_picture_stored_area = dcp::Size (
284 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
285 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
288 _main_picture_active_area = dcp::Size (
289 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
290 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
293 auto sll = node->optional_string_child("MainSubtitleLanguageList");
295 vector<string> sll_split;
296 boost::split (sll_split, *sll, boost::is_any_of(" "));
297 DCP_ASSERT (!sll_split.empty());
299 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
301 if (!_reels.empty()) {
302 auto sub = _reels.front()->main_subtitle();
304 auto lang = sub->language();
305 if (lang && lang == sll_split[0]) {
311 for (auto i = first; i < sll_split.size(); ++i) {
312 _additional_subtitle_languages.push_back (sll_split[i]);
316 auto eml = node->optional_node_child ("ExtensionMetadataList");
318 for (auto i: eml->node_children("ExtensionMetadata")) {
319 auto name = i->optional_string_child("Name");
320 if (name && *name == "Sign Language Video") {
321 auto property_list = i->node_child("PropertyList");
322 for (auto j: property_list->node_children("Property")) {
323 auto name = j->optional_string_child("Name");
324 auto value = j->optional_string_child("Value");
325 if (name && value && *name == "Language Tag") {
326 _sign_language_video_language = *value;
335 /** Write a CompositionMetadataAsset node as a child of @param node provided
336 * the required metadata is stored in the object. If any required metadata
337 * is missing this method will do nothing.
340 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
343 !_main_sound_configuration ||
344 !_main_sound_sample_rate ||
345 !_main_picture_stored_area ||
346 !_main_picture_active_area ||
348 !_reels.front()->main_picture()) {
352 auto meta = node->add_child("meta:CompositionMetadataAsset");
353 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
355 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
357 auto mp = _reels.front()->main_picture();
358 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
359 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
361 auto fctt = meta->add_child("FullContentTitleText", "meta");
362 if (_full_content_title_text && !_full_content_title_text->empty()) {
363 fctt->add_child_text (*_full_content_title_text);
365 if (_full_content_title_text_language) {
366 fctt->set_attribute("language", *_full_content_title_text_language);
369 if (_release_territory) {
370 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
373 if (_version_number) {
374 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
375 vn->add_child_text(raw_convert<string>(*_version_number));
377 vn->set_attribute("status", status_to_string(*_status));
382 meta->add_child("Chain", "meta")->add_child_text(*_chain);
386 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
390 meta->add_child("Facility", "meta")->add_child_text(*_facility);
393 if (_content_versions.size() > 1) {
394 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
395 for (size_t i = 1; i < _content_versions.size(); ++i) {
396 _content_versions[i].as_xml (vc);
401 _luminance->as_xml (meta, "meta");
404 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
405 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
407 auto stored = meta->add_child("MainPictureStoredArea", "meta");
408 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
409 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
411 auto active = meta->add_child("MainPictureActiveArea", "meta");
412 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
413 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
415 optional<string> first_subtitle_language;
416 for (auto i: _reels) {
417 if (i->main_subtitle()) {
418 first_subtitle_language = i->main_subtitle()->language();
419 if (first_subtitle_language) {
425 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
427 if (first_subtitle_language) {
428 lang = *first_subtitle_language;
430 for (auto const& i: _additional_subtitle_languages) {
436 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
439 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
441 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
442 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
443 extension->set_attribute("scope", scope);
444 extension->add_child("Name", "meta")->add_child_text(name);
445 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
446 property->add_child("Name", "meta")->add_child_text(property_name);
447 property->add_child("Value", "meta")->add_child_text(property_value);
450 /* SMPTE Bv2.1 8.6.3 */
451 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
453 if (_sign_language_video_language) {
454 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
457 if (_reels.front()->main_sound()) {
458 auto asset = _reels.front()->main_sound()->asset();
460 auto reader = asset->start_read ();
461 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
462 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
463 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
464 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
467 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
468 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
469 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
470 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
471 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
473 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
474 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
475 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
476 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
477 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
478 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
479 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
480 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
481 if (!soundfield->MCATagName.empty()) {
482 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
483 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
485 if (!soundfield->RFC5646SpokenLanguage.empty()) {
486 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
487 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
490 list<ASDCP::MXF::InterchangeObject*> channels;
491 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
492 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
496 for (auto i: channels) {
497 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
498 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
499 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
500 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
501 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
502 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
503 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
504 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
505 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
506 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
507 if (!channel->MCATagName.empty()) {
508 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
509 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
511 if (!channel->MCAChannelID.empty()) {
512 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
514 if (!channel->RFC5646SpokenLanguage.empty()) {
515 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
516 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
518 if (!channel->SoundfieldGroupLinkID.empty()) {
519 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
520 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
531 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
533 for (auto i: reels) {
534 if (i->main_picture ()) {
535 assets.push_back (i->main_picture());
537 if (i->main_sound ()) {
538 assets.push_back (i->main_sound());
540 if (i->main_subtitle ()) {
541 assets.push_back (i->main_subtitle());
543 for (auto j: i->closed_captions()) {
544 assets.push_back (j);
547 assets.push_back (i->atmos());
553 vector<shared_ptr<ReelFileAsset>>
554 CPL::reel_file_assets ()
556 vector<shared_ptr<ReelFileAsset>> c;
557 add_file_assets (c, _reels);
562 vector<shared_ptr<const ReelFileAsset>>
563 CPL::reel_file_assets () const
565 vector<shared_ptr<const ReelFileAsset>> c;
566 add_file_assets (c, _reels);
572 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
574 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
579 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
580 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
581 note (NoteType::ERROR, s);
585 if (_content_kind != other_cpl->_content_kind) {
586 note (NoteType::ERROR, "CPL: content kinds differ");
590 if (_reels.size() != other_cpl->_reels.size()) {
591 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
595 auto a = _reels.begin();
596 auto b = other_cpl->_reels.begin();
598 while (a != _reels.end ()) {
599 if (!(*a)->equals (*b, opt, note)) {
611 CPL::any_encrypted () const
613 for (auto i: _reels) {
614 if (i->any_encrypted()) {
624 CPL::all_encrypted () const
626 for (auto i: _reels) {
627 if (!i->all_encrypted()) {
637 CPL::add (DecryptedKDM const & kdm)
639 for (auto i: _reels) {
645 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
647 for (auto i: _reels) {
648 i->resolve_refs (assets);
653 CPL::pkl_type (Standard standard) const
655 return static_pkl_type (standard);
659 CPL::static_pkl_type (Standard standard)
662 case Standard::INTEROP:
663 return "text/xml;asdcpKind=CPL";
664 case Standard::SMPTE:
672 CPL::duration () const
675 for (auto i: _reels) {
683 CPL::set_version_number (int v)
686 throw BadSettingError ("CPL version number cannot be negative");
694 CPL::unset_version_number ()
696 _version_number = boost::none;
701 CPL::set_content_versions (vector<ContentVersion> v)
705 if (!ids.insert(i.id).second) {
706 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
710 _content_versions = v;
714 optional<ContentVersion>
715 CPL::content_version () const
717 if (_content_versions.empty()) {
718 return optional<ContentVersion>();
721 return _content_versions[0];
726 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
728 _additional_subtitle_languages.clear ();
729 for (auto const& i: langs) {
730 _additional_subtitle_languages.push_back (i.to_string());