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, Standard standard)
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)
91 , _standard (standard)
94 cv.label_text = cv.id + LocalTime().as_string();
95 _content_versions.push_back (cv);
99 CPL::CPL (boost::filesystem::path file)
101 , _content_kind (ContentKind::FEATURE)
103 cxml::Document f ("CompositionPlaylist");
106 if (f.namespace_uri() == cpl_interop_ns) {
107 _standard = Standard::INTEROP;
108 } else if (f.namespace_uri() == cpl_smpte_ns) {
109 _standard = Standard::SMPTE;
111 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
114 _id = remove_urn_uuid (f.string_child ("Id"));
115 _annotation_text = f.optional_string_child("AnnotationText");
116 _issuer = f.optional_string_child("Issuer").get_value_or("");
117 _creator = f.optional_string_child("Creator").get_value_or("");
118 _issue_date = f.string_child ("IssueDate");
119 _content_title_text = f.string_child ("ContentTitleText");
120 _content_kind = content_kind_from_string (f.string_child ("ContentKind"));
121 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
122 if (content_version) {
123 /* XXX: SMPTE should insist that Id is present */
124 _content_versions.push_back (
126 content_version->optional_string_child("Id").get_value_or(""),
127 content_version->string_child("LabelText")
130 content_version->done ();
131 } else if (_standard == Standard::SMPTE) {
132 /* ContentVersion is required in SMPTE */
133 throw XMLError ("Missing ContentVersion tag in CPL");
135 auto rating_list = f.node_child ("RatingList");
137 for (auto i: rating_list->node_children("Rating")) {
138 _ratings.push_back (Rating(i));
142 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
143 _reels.push_back (make_shared<Reel>(i, _standard));
146 auto reel_list = f.node_child ("ReelList");
148 auto reels = reel_list->node_children("Reel");
149 if (!reels.empty()) {
150 auto asset_list = reels.front()->node_child("AssetList");
151 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
153 read_composition_metadata_asset (metadata);
158 f.ignore_child ("Issuer");
159 f.ignore_child ("Signer");
160 f.ignore_child ("Signature");
167 CPL::add (std::shared_ptr<Reel> reel)
169 _reels.push_back (reel);
174 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
177 xmlpp::Element* root;
178 if (_standard == Standard::INTEROP) {
179 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
181 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
184 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
185 if (_annotation_text) {
186 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
188 root->add_child("IssueDate")->add_child_text (_issue_date);
189 root->add_child("Issuer")->add_child_text (_issuer);
190 root->add_child("Creator")->add_child_text (_creator);
191 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
192 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
193 if (_content_versions.empty()) {
197 _content_versions[0].as_xml (root);
200 auto rating_list = root->add_child("RatingList");
201 for (auto i: _ratings) {
202 i.as_xml (rating_list->add_child("Rating"));
205 auto reel_list = root->add_child ("ReelList");
207 if (_reels.empty()) {
208 throw NoReelsError ();
212 for (auto i: _reels) {
213 auto asset_list = i->write_to_cpl (reel_list, _standard);
214 if (first && _standard == Standard::SMPTE) {
215 maybe_write_composition_metadata_asset (asset_list);
223 signer->sign (root, _standard);
226 doc.write_to_file_formatted (file.string(), "UTF-8");
233 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
235 auto fctt = node->node_child("FullContentTitleText");
236 _full_content_title_text = fctt->content();
237 _full_content_title_text_language = fctt->optional_string_attribute("language");
239 _release_territory = node->optional_string_child("ReleaseTerritory");
240 if (_release_territory) {
241 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
244 auto vn = node->optional_node_child("VersionNumber");
246 _version_number = raw_convert<int>(vn->content());
247 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
248 auto vn_status = vn->optional_string_attribute("status");
250 _status = string_to_status (*vn_status);
254 _chain = node->optional_string_child("Chain");
255 _distributor = node->optional_string_child("Distributor");
256 _facility = node->optional_string_child("Facility");
258 auto acv = node->optional_node_child("AlternateContentVersionList");
260 for (auto i: acv->node_children("ContentVersion")) {
261 _content_versions.push_back (ContentVersion(i));
265 auto lum = node->optional_node_child("Luminance");
267 _luminance = Luminance (lum);
270 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
272 auto sr = node->optional_string_child("MainSoundSampleRate");
274 vector<string> sr_bits;
275 boost::split (sr_bits, *sr, boost::is_any_of(" "));
276 DCP_ASSERT (sr_bits.size() == 2);
277 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
280 _main_picture_stored_area = dcp::Size (
281 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
282 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
285 _main_picture_active_area = dcp::Size (
286 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
287 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
290 auto sll = node->optional_string_child("MainSubtitleLanguageList");
292 vector<string> sll_split;
293 boost::split (sll_split, *sll, boost::is_any_of(" "));
294 DCP_ASSERT (!sll_split.empty());
296 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
298 if (!_reels.empty()) {
299 auto sub = _reels.front()->main_subtitle();
301 auto lang = sub->language();
302 if (lang && lang == sll_split[0]) {
308 for (auto i = first; i < sll_split.size(); ++i) {
309 _additional_subtitle_languages.push_back (sll_split[i]);
315 /** Write a CompositionMetadataAsset node as a child of @param node provided
316 * the required metadata is stored in the object. If any required metadata
317 * is missing this method will do nothing.
320 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
323 !_main_sound_configuration ||
324 !_main_sound_sample_rate ||
325 !_main_picture_stored_area ||
326 !_main_picture_active_area ||
328 !_reels.front()->main_picture()) {
332 auto meta = node->add_child("meta:CompositionMetadataAsset");
333 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
335 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
337 auto mp = _reels.front()->main_picture();
338 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
339 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
341 auto fctt = meta->add_child("FullContentTitleText", "meta");
342 if (_full_content_title_text) {
343 fctt->add_child_text (*_full_content_title_text);
345 if (_full_content_title_text_language) {
346 fctt->set_attribute("language", *_full_content_title_text_language);
349 if (_release_territory) {
350 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
353 if (_version_number) {
354 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
355 vn->add_child_text(raw_convert<string>(*_version_number));
357 vn->set_attribute("status", status_to_string(*_status));
362 meta->add_child("Chain", "meta")->add_child_text(*_chain);
366 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
370 meta->add_child("Facility", "meta")->add_child_text(*_facility);
373 if (_content_versions.size() > 1) {
374 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
375 for (size_t i = 1; i < _content_versions.size(); ++i) {
376 _content_versions[i].as_xml (vc);
381 _luminance->as_xml (meta, "meta");
384 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
385 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
387 auto stored = meta->add_child("MainPictureStoredArea", "meta");
388 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
389 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
391 auto active = meta->add_child("MainPictureActiveArea", "meta");
392 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
393 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
395 optional<string> first_subtitle_language;
396 for (auto i: _reels) {
397 if (i->main_subtitle()) {
398 first_subtitle_language = i->main_subtitle()->language();
399 if (first_subtitle_language) {
405 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
407 if (first_subtitle_language) {
408 lang = *first_subtitle_language;
410 for (auto const& i: _additional_subtitle_languages) {
416 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
419 /* SMPTE Bv2.1 8.6.3 */
420 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
421 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
422 extension->add_child("Name", "meta")->add_child_text("Application");
423 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
424 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
425 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
427 if (_reels.front()->main_sound()) {
428 auto asset = _reels.front()->main_sound()->asset();
430 auto reader = asset->start_read ();
431 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
432 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
433 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
434 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
437 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
438 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
439 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
440 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
441 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
443 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
444 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
445 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
446 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
447 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
448 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
449 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
450 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
451 if (!soundfield->MCATagName.empty()) {
452 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
453 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
455 if (!soundfield->RFC5646SpokenLanguage.empty()) {
456 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
457 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
460 list<ASDCP::MXF::InterchangeObject*> channels;
461 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
462 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
466 for (auto i: channels) {
467 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
468 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
469 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
470 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
471 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
472 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
473 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
474 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
475 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
476 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
477 if (!channel->MCATagName.empty()) {
478 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
479 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
481 if (!channel->MCAChannelID.empty()) {
482 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
484 if (!channel->RFC5646SpokenLanguage.empty()) {
485 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
486 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
488 if (!channel->SoundfieldGroupLinkID.empty()) {
489 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
490 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
501 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
503 for (auto i: reels) {
504 if (i->main_picture ()) {
505 assets.push_back (i->main_picture());
507 if (i->main_sound ()) {
508 assets.push_back (i->main_sound());
510 if (i->main_subtitle ()) {
511 assets.push_back (i->main_subtitle());
513 for (auto j: i->closed_captions()) {
514 assets.push_back (j);
517 assets.push_back (i->atmos());
523 vector<shared_ptr<ReelFileAsset>>
524 CPL::reel_file_assets ()
526 vector<shared_ptr<ReelFileAsset>> c;
527 add_file_assets (c, _reels);
532 vector<shared_ptr<const ReelFileAsset>>
533 CPL::reel_file_assets () const
535 vector<shared_ptr<const ReelFileAsset>> c;
536 add_file_assets (c, _reels);
543 add_encryptable_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
545 for (auto i: reels) {
546 if (i->main_picture ()) {
547 assets.push_back (i->main_picture());
549 if (i->main_sound ()) {
550 assets.push_back (i->main_sound());
552 if (i->main_subtitle ()) {
553 if (auto enc = dynamic_pointer_cast<ReelEncryptableAsset>(i->main_subtitle())) {
554 assets.push_back (enc);
557 for (auto j: i->closed_captions()) {
558 assets.push_back (j);
561 assets.push_back (i->atmos());
567 vector<shared_ptr<ReelEncryptableAsset>>
568 CPL::reel_encryptable_assets ()
570 vector<shared_ptr<ReelEncryptableAsset>> c;
571 add_encryptable_assets (c, _reels);
576 vector<shared_ptr<const ReelEncryptableAsset>>
577 CPL::reel_encryptable_assets () const
579 vector<shared_ptr<const ReelEncryptableAsset>> c;
580 add_encryptable_assets (c, _reels);
586 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
588 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
593 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
594 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
595 note (NoteType::ERROR, s);
599 if (_content_kind != other_cpl->_content_kind) {
600 note (NoteType::ERROR, "CPL: content kinds differ");
604 if (_reels.size() != other_cpl->_reels.size()) {
605 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
609 auto a = _reels.begin();
610 auto b = other_cpl->_reels.begin();
612 while (a != _reels.end ()) {
613 if (!(*a)->equals (*b, opt, note)) {
625 CPL::any_encrypted () const
627 for (auto i: _reels) {
628 if (i->any_encrypted()) {
638 CPL::all_encrypted () const
640 for (auto i: _reels) {
641 if (!i->all_encrypted()) {
651 CPL::add (DecryptedKDM const & kdm)
653 for (auto i: _reels) {
659 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
661 for (auto i: _reels) {
662 i->resolve_refs (assets);
667 CPL::pkl_type (Standard standard) const
669 return static_pkl_type (standard);
673 CPL::static_pkl_type (Standard standard)
676 case Standard::INTEROP:
677 return "text/xml;asdcpKind=CPL";
678 case Standard::SMPTE:
686 CPL::duration () const
689 for (auto i: _reels) {
697 CPL::set_version_number (int v)
700 throw BadSettingError ("CPL version number cannot be negative");
708 CPL::unset_version_number ()
710 _version_number = boost::none;
715 CPL::set_content_versions (vector<ContentVersion> v)
719 if (!ids.insert(i.id).second) {
720 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
724 _content_versions = v;
728 optional<ContentVersion>
729 CPL::content_version () const
731 if (_content_versions.empty()) {
732 return optional<ContentVersion>();
735 return _content_versions[0];
740 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
742 _additional_subtitle_languages.clear ();
743 for (auto const& i: langs) {
744 _additional_subtitle_languages.push_back (i.to_string());