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.
44 #include "certificate_chain.h"
46 #include "reel_picture_asset.h"
47 #include "reel_sound_asset.h"
48 #include "reel_subtitle_asset.h"
49 #include "reel_closed_caption_asset.h"
50 #include "reel_atmos_asset.h"
51 #include "local_time.h"
52 #include "dcp_assert.h"
53 #include "compose.hpp"
54 #include "raw_convert.h"
55 #include <asdcp/Metadata.h>
56 #include <libxml/parser.h>
57 #include <libxml++/libxml++.h>
58 #include <boost/algorithm/string.hpp>
62 using std::dynamic_pointer_cast;
65 using std::make_shared;
68 using std::shared_ptr;
71 using boost::optional;
75 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
76 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
77 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
78 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
79 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
80 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
83 CPL::CPL (string annotation_text, ContentKind content_kind)
84 /* default _content_title_text to annotation_text */
85 : _issuer ("libdcp" LIBDCP_VERSION)
86 , _creator ("libdcp" LIBDCP_VERSION)
87 , _issue_date (LocalTime().as_string())
88 , _annotation_text (annotation_text)
89 , _content_title_text (annotation_text)
90 , _content_kind (content_kind)
93 cv.label_text = cv.id + LocalTime().as_string();
94 _content_versions.push_back (cv);
98 CPL::CPL (boost::filesystem::path file)
100 , _content_kind (ContentKind::FEATURE)
102 cxml::Document f ("CompositionPlaylist");
105 if (f.namespace_uri() == cpl_interop_ns) {
106 _standard = Standard::INTEROP;
107 } else if (f.namespace_uri() == cpl_smpte_ns) {
108 _standard = Standard::SMPTE;
110 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
113 _id = remove_urn_uuid (f.string_child ("Id"));
114 _annotation_text = f.optional_string_child("AnnotationText");
115 _issuer = f.optional_string_child("Issuer").get_value_or("");
116 _creator = f.optional_string_child("Creator").get_value_or("");
117 _issue_date = f.string_child ("IssueDate");
118 _content_title_text = f.string_child ("ContentTitleText");
119 _content_kind = content_kind_from_string (f.string_child ("ContentKind"));
120 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
121 if (content_version) {
122 /* XXX: SMPTE should insist that Id is present */
123 _content_versions.push_back (
125 content_version->optional_string_child("Id").get_value_or(""),
126 content_version->string_child("LabelText")
129 content_version->done ();
130 } else if (_standard == Standard::SMPTE) {
131 /* ContentVersion is required in SMPTE */
132 throw XMLError ("Missing ContentVersion tag in CPL");
134 auto rating_list = f.node_child ("RatingList");
136 for (auto i: rating_list->node_children("Rating")) {
137 _ratings.push_back (Rating(i));
140 _reels = type_grand_children<Reel> (f, "ReelList", "Reel");
142 auto reel_list = f.node_child ("ReelList");
144 auto reels = reel_list->node_children("Reel");
145 if (!reels.empty()) {
146 auto asset_list = reels.front()->node_child("AssetList");
147 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
149 read_composition_metadata_asset (metadata);
154 f.ignore_child ("Issuer");
155 f.ignore_child ("Signer");
156 f.ignore_child ("Signature");
163 CPL::add (std::shared_ptr<Reel> reel)
165 _reels.push_back (reel);
170 CPL::write_xml (boost::filesystem::path file, Standard standard, shared_ptr<const CertificateChain> signer) const
173 xmlpp::Element* root;
174 if (standard == 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 if (_annotation_text) {
182 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
184 root->add_child("IssueDate")->add_child_text (_issue_date);
185 root->add_child("Issuer")->add_child_text (_issuer);
186 root->add_child("Creator")->add_child_text (_creator);
187 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
188 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
189 if (_content_versions.empty()) {
193 _content_versions[0].as_xml (root);
196 auto rating_list = root->add_child("RatingList");
197 for (auto i: _ratings) {
198 i.as_xml (rating_list->add_child("Rating"));
201 auto reel_list = root->add_child ("ReelList");
203 if (_reels.empty()) {
204 throw NoReelsError ();
208 for (auto i: _reels) {
209 auto asset_list = i->write_to_cpl (reel_list, standard);
210 if (first && standard == Standard::SMPTE) {
211 maybe_write_composition_metadata_asset (asset_list);
219 signer->sign (root, standard);
222 doc.write_to_file_formatted (file.string(), "UTF-8");
229 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
231 auto fctt = node->node_child("FullContentTitleText");
232 _full_content_title_text = fctt->content();
233 _full_content_title_text_language = fctt->optional_string_attribute("language");
235 _release_territory = node->optional_string_child("ReleaseTerritory");
236 if (_release_territory) {
237 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
240 auto vn = node->optional_node_child("VersionNumber");
242 _version_number = raw_convert<int>(vn->content());
243 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
244 auto vn_status = vn->optional_string_attribute("status");
246 _status = string_to_status (*vn_status);
250 _chain = node->optional_string_child("Chain");
251 _distributor = node->optional_string_child("Distributor");
252 _facility = node->optional_string_child("Facility");
254 auto acv = node->optional_node_child("AlternateContentVersionList");
256 for (auto i: acv->node_children("ContentVersion")) {
257 _content_versions.push_back (ContentVersion(i));
261 auto lum = node->optional_node_child("Luminance");
263 _luminance = Luminance (lum);
266 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
268 auto sr = node->optional_string_child("MainSoundSampleRate");
270 vector<string> sr_bits;
271 boost::split (sr_bits, *sr, boost::is_any_of(" "));
272 DCP_ASSERT (sr_bits.size() == 2);
273 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
276 _main_picture_stored_area = dcp::Size (
277 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
278 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
281 _main_picture_active_area = dcp::Size (
282 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
283 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
286 auto sll = node->optional_string_child("MainSubtitleLanguageList");
288 vector<string> sll_split;
289 boost::split (sll_split, *sll, boost::is_any_of(" "));
290 DCP_ASSERT (!sll_split.empty());
292 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
294 if (!_reels.empty()) {
295 auto sub = _reels.front()->main_subtitle();
297 auto lang = sub->language();
298 if (lang && lang == sll_split[0]) {
304 for (auto i = first; i < sll_split.size(); ++i) {
305 _additional_subtitle_languages.push_back (sll_split[i]);
311 /** Write a CompositionMetadataAsset node as a child of @param node provided
312 * the required metadata is stored in the object. If any required metadata
313 * is missing this method will do nothing.
316 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
319 !_main_sound_configuration ||
320 !_main_sound_sample_rate ||
321 !_main_picture_stored_area ||
322 !_main_picture_active_area ||
324 !_reels.front()->main_picture()) {
328 auto meta = node->add_child("meta:CompositionMetadataAsset");
329 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
331 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
333 auto mp = _reels.front()->main_picture();
334 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
335 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
337 auto fctt = meta->add_child("FullContentTitleText", "meta");
338 if (_full_content_title_text) {
339 fctt->add_child_text (*_full_content_title_text);
341 if (_full_content_title_text_language) {
342 fctt->set_attribute("language", *_full_content_title_text_language);
345 if (_release_territory) {
346 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
349 if (_version_number) {
350 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
351 vn->add_child_text(raw_convert<string>(*_version_number));
353 vn->set_attribute("status", status_to_string(*_status));
358 meta->add_child("Chain", "meta")->add_child_text(*_chain);
362 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
366 meta->add_child("Facility", "meta")->add_child_text(*_facility);
369 if (_content_versions.size() > 1) {
370 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
371 for (size_t i = 1; i < _content_versions.size(); ++i) {
372 _content_versions[i].as_xml (vc);
377 _luminance->as_xml (meta, "meta");
380 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
381 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
383 auto stored = meta->add_child("MainPictureStoredArea", "meta");
384 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
385 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
387 auto active = meta->add_child("MainPictureActiveArea", "meta");
388 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
389 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
391 optional<string> first_subtitle_language;
392 for (auto i: _reels) {
393 if (i->main_subtitle()) {
394 first_subtitle_language = i->main_subtitle()->language();
395 if (first_subtitle_language) {
401 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
403 if (first_subtitle_language) {
404 lang = *first_subtitle_language;
406 for (auto const& i: _additional_subtitle_languages) {
412 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
415 /* SMPTE Bv2.1 8.6.3 */
416 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
417 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
418 extension->add_child("Name", "meta")->add_child_text("Application");
419 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
420 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
421 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
423 if (_reels.front()->main_sound()) {
424 auto asset = _reels.front()->main_sound()->asset();
426 auto reader = asset->start_read ();
427 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
428 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
429 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
430 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
433 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
434 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
435 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
436 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
437 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
439 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
440 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
441 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
442 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
443 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
444 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
445 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
446 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
447 if (!soundfield->MCATagName.empty()) {
448 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
449 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
451 if (!soundfield->RFC5646SpokenLanguage.empty()) {
452 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
453 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
456 list<ASDCP::MXF::InterchangeObject*> channels;
457 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
458 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
462 for (auto i: channels) {
463 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
464 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
465 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
466 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
467 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
468 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
469 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
470 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
471 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
472 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
473 if (!channel->MCATagName.empty()) {
474 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
475 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
477 if (!channel->MCAChannelID.empty()) {
478 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
480 if (!channel->RFC5646SpokenLanguage.empty()) {
481 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
482 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
484 if (!channel->SoundfieldGroupLinkID.empty()) {
485 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
486 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
497 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
499 for (auto i: reels) {
500 if (i->main_picture ()) {
501 assets.push_back (i->main_picture());
503 if (i->main_sound ()) {
504 assets.push_back (i->main_sound());
506 if (i->main_subtitle ()) {
507 assets.push_back (i->main_subtitle());
509 for (auto j: i->closed_captions()) {
510 assets.push_back (j);
513 assets.push_back (i->atmos());
519 vector<shared_ptr<ReelFileAsset>>
520 CPL::reel_file_assets ()
522 vector<shared_ptr<ReelFileAsset>> c;
523 add_file_assets (c, _reels);
528 vector<shared_ptr<const ReelFileAsset>>
529 CPL::reel_file_assets () const
531 vector<shared_ptr<const ReelFileAsset>> c;
532 add_file_assets (c, _reels);
539 add_encryptable_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<ReelEncryptableAsset>>
562 CPL::reel_encryptable_assets ()
564 vector<shared_ptr<ReelEncryptableAsset>> c;
565 add_encryptable_assets (c, _reels);
570 vector<shared_ptr<const ReelEncryptableAsset>>
571 CPL::reel_encryptable_assets () const
573 vector<shared_ptr<const ReelEncryptableAsset>> c;
574 add_encryptable_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)
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());