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));
141 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
142 _reels.push_back (make_shared<Reel>(i, *_standard));
145 auto reel_list = f.node_child ("ReelList");
147 auto reels = reel_list->node_children("Reel");
148 if (!reels.empty()) {
149 auto asset_list = reels.front()->node_child("AssetList");
150 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
152 read_composition_metadata_asset (metadata);
157 f.ignore_child ("Issuer");
158 f.ignore_child ("Signer");
159 f.ignore_child ("Signature");
166 CPL::add (std::shared_ptr<Reel> reel)
168 _reels.push_back (reel);
173 CPL::write_xml (boost::filesystem::path file, Standard standard, shared_ptr<const CertificateChain> signer) const
176 xmlpp::Element* root;
177 if (standard == Standard::INTEROP) {
178 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
180 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
183 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
184 if (_annotation_text) {
185 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
187 root->add_child("IssueDate")->add_child_text (_issue_date);
188 root->add_child("Issuer")->add_child_text (_issuer);
189 root->add_child("Creator")->add_child_text (_creator);
190 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
191 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
192 if (_content_versions.empty()) {
196 _content_versions[0].as_xml (root);
199 auto rating_list = root->add_child("RatingList");
200 for (auto i: _ratings) {
201 i.as_xml (rating_list->add_child("Rating"));
204 auto reel_list = root->add_child ("ReelList");
206 if (_reels.empty()) {
207 throw NoReelsError ();
211 for (auto i: _reels) {
212 auto asset_list = i->write_to_cpl (reel_list, standard);
213 if (first && standard == Standard::SMPTE) {
214 maybe_write_composition_metadata_asset (asset_list);
222 signer->sign (root, standard);
225 doc.write_to_file_formatted (file.string(), "UTF-8");
232 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
234 auto fctt = node->node_child("FullContentTitleText");
235 _full_content_title_text = fctt->content();
236 _full_content_title_text_language = fctt->optional_string_attribute("language");
238 _release_territory = node->optional_string_child("ReleaseTerritory");
239 if (_release_territory) {
240 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
243 auto vn = node->optional_node_child("VersionNumber");
245 _version_number = raw_convert<int>(vn->content());
246 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
247 auto vn_status = vn->optional_string_attribute("status");
249 _status = string_to_status (*vn_status);
253 _chain = node->optional_string_child("Chain");
254 _distributor = node->optional_string_child("Distributor");
255 _facility = node->optional_string_child("Facility");
257 auto acv = node->optional_node_child("AlternateContentVersionList");
259 for (auto i: acv->node_children("ContentVersion")) {
260 _content_versions.push_back (ContentVersion(i));
264 auto lum = node->optional_node_child("Luminance");
266 _luminance = Luminance (lum);
269 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
271 auto sr = node->optional_string_child("MainSoundSampleRate");
273 vector<string> sr_bits;
274 boost::split (sr_bits, *sr, boost::is_any_of(" "));
275 DCP_ASSERT (sr_bits.size() == 2);
276 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
279 _main_picture_stored_area = dcp::Size (
280 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
281 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
284 _main_picture_active_area = dcp::Size (
285 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
286 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
289 auto sll = node->optional_string_child("MainSubtitleLanguageList");
291 vector<string> sll_split;
292 boost::split (sll_split, *sll, boost::is_any_of(" "));
293 DCP_ASSERT (!sll_split.empty());
295 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
297 if (!_reels.empty()) {
298 auto sub = _reels.front()->main_subtitle();
300 auto lang = sub->language();
301 if (lang && lang == sll_split[0]) {
307 for (auto i = first; i < sll_split.size(); ++i) {
308 _additional_subtitle_languages.push_back (sll_split[i]);
314 /** Write a CompositionMetadataAsset node as a child of @param node provided
315 * the required metadata is stored in the object. If any required metadata
316 * is missing this method will do nothing.
319 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
322 !_main_sound_configuration ||
323 !_main_sound_sample_rate ||
324 !_main_picture_stored_area ||
325 !_main_picture_active_area ||
327 !_reels.front()->main_picture()) {
331 auto meta = node->add_child("meta:CompositionMetadataAsset");
332 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
334 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
336 auto mp = _reels.front()->main_picture();
337 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
338 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
340 auto fctt = meta->add_child("FullContentTitleText", "meta");
341 if (_full_content_title_text) {
342 fctt->add_child_text (*_full_content_title_text);
344 if (_full_content_title_text_language) {
345 fctt->set_attribute("language", *_full_content_title_text_language);
348 if (_release_territory) {
349 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
352 if (_version_number) {
353 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
354 vn->add_child_text(raw_convert<string>(*_version_number));
356 vn->set_attribute("status", status_to_string(*_status));
361 meta->add_child("Chain", "meta")->add_child_text(*_chain);
365 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
369 meta->add_child("Facility", "meta")->add_child_text(*_facility);
372 if (_content_versions.size() > 1) {
373 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
374 for (size_t i = 1; i < _content_versions.size(); ++i) {
375 _content_versions[i].as_xml (vc);
380 _luminance->as_xml (meta, "meta");
383 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
384 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
386 auto stored = meta->add_child("MainPictureStoredArea", "meta");
387 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
388 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
390 auto active = meta->add_child("MainPictureActiveArea", "meta");
391 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
392 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
394 optional<string> first_subtitle_language;
395 for (auto i: _reels) {
396 if (i->main_subtitle()) {
397 first_subtitle_language = i->main_subtitle()->language();
398 if (first_subtitle_language) {
404 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
406 if (first_subtitle_language) {
407 lang = *first_subtitle_language;
409 for (auto const& i: _additional_subtitle_languages) {
415 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
418 /* SMPTE Bv2.1 8.6.3 */
419 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
420 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
421 extension->add_child("Name", "meta")->add_child_text("Application");
422 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
423 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
424 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
426 if (_reels.front()->main_sound()) {
427 auto asset = _reels.front()->main_sound()->asset();
429 auto reader = asset->start_read ();
430 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
431 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
432 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
433 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
436 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
437 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
438 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
439 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
440 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
442 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
443 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
444 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
445 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
446 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
447 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
448 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
449 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
450 if (!soundfield->MCATagName.empty()) {
451 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
452 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
454 if (!soundfield->RFC5646SpokenLanguage.empty()) {
455 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
456 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
459 list<ASDCP::MXF::InterchangeObject*> channels;
460 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
461 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
465 for (auto i: channels) {
466 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
467 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
468 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
469 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
470 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
471 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
472 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
473 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
474 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
475 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
476 if (!channel->MCATagName.empty()) {
477 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
478 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
480 if (!channel->MCAChannelID.empty()) {
481 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
483 if (!channel->RFC5646SpokenLanguage.empty()) {
484 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
485 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
487 if (!channel->SoundfieldGroupLinkID.empty()) {
488 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
489 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
500 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
502 for (auto i: reels) {
503 if (i->main_picture ()) {
504 assets.push_back (i->main_picture());
506 if (i->main_sound ()) {
507 assets.push_back (i->main_sound());
509 if (i->main_subtitle ()) {
510 assets.push_back (i->main_subtitle());
512 for (auto j: i->closed_captions()) {
513 assets.push_back (j);
516 assets.push_back (i->atmos());
522 vector<shared_ptr<ReelFileAsset>>
523 CPL::reel_file_assets ()
525 vector<shared_ptr<ReelFileAsset>> c;
526 add_file_assets (c, _reels);
531 vector<shared_ptr<const ReelFileAsset>>
532 CPL::reel_file_assets () const
534 vector<shared_ptr<const ReelFileAsset>> c;
535 add_file_assets (c, _reels);
542 add_encryptable_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
544 for (auto i: reels) {
545 if (i->main_picture ()) {
546 assets.push_back (i->main_picture());
548 if (i->main_sound ()) {
549 assets.push_back (i->main_sound());
551 if (i->main_subtitle ()) {
552 if (auto enc = dynamic_pointer_cast<ReelEncryptableAsset>(i->main_subtitle())) {
553 assets.push_back (enc);
556 for (auto j: i->closed_captions()) {
557 assets.push_back (j);
560 assets.push_back (i->atmos());
566 vector<shared_ptr<ReelEncryptableAsset>>
567 CPL::reel_encryptable_assets ()
569 vector<shared_ptr<ReelEncryptableAsset>> c;
570 add_encryptable_assets (c, _reels);
575 vector<shared_ptr<const ReelEncryptableAsset>>
576 CPL::reel_encryptable_assets () const
578 vector<shared_ptr<const ReelEncryptableAsset>> c;
579 add_encryptable_assets (c, _reels);
585 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
587 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
592 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
593 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
594 note (NoteType::ERROR, s);
598 if (_content_kind != other_cpl->_content_kind) {
599 note (NoteType::ERROR, "CPL: content kinds differ");
603 if (_reels.size() != other_cpl->_reels.size()) {
604 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
608 auto a = _reels.begin();
609 auto b = other_cpl->_reels.begin();
611 while (a != _reels.end ()) {
612 if (!(*a)->equals (*b, opt, note)) {
624 CPL::any_encrypted () const
626 for (auto i: _reels) {
627 if (i->any_encrypted()) {
637 CPL::all_encrypted () const
639 for (auto i: _reels) {
640 if (!i->all_encrypted()) {
650 CPL::add (DecryptedKDM const & kdm)
652 for (auto i: _reels) {
658 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
660 for (auto i: _reels) {
661 i->resolve_refs (assets);
666 CPL::pkl_type (Standard standard) const
668 return static_pkl_type (standard);
672 CPL::static_pkl_type (Standard standard)
675 case Standard::INTEROP:
676 return "text/xml;asdcpKind=CPL";
677 case Standard::SMPTE:
685 CPL::duration () const
688 for (auto i: _reels) {
696 CPL::set_version_number (int v)
699 throw BadSettingError ("CPL version number cannot be negative");
707 CPL::unset_version_number ()
709 _version_number = boost::none;
714 CPL::set_content_versions (vector<ContentVersion> v)
718 if (!ids.insert(i.id).second) {
719 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
723 _content_versions = v;
727 optional<ContentVersion>
728 CPL::content_version () const
730 if (_content_versions.empty()) {
731 return optional<ContentVersion>();
734 return _content_versions[0];
739 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
741 _additional_subtitle_languages.clear ();
742 for (auto const& i: langs) {
743 _additional_subtitle_languages.push_back (i.to_string());