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 "equality_options.h"
45 #include "local_time.h"
47 #include "raw_convert.h"
49 #include "reel_atmos_asset.h"
50 #include "reel_closed_caption_asset.h"
51 #include "reel_picture_asset.h"
52 #include "reel_sound_asset.h"
53 #include "reel_subtitle_asset.h"
58 LIBDCP_DISABLE_WARNINGS
59 #include <asdcp/Metadata.h>
60 LIBDCP_ENABLE_WARNINGS
61 #include <libxml/parser.h>
62 LIBDCP_DISABLE_WARNINGS
63 #include <libxml++/libxml++.h>
64 LIBDCP_ENABLE_WARNINGS
65 #include <boost/algorithm/string.hpp>
69 using std::dynamic_pointer_cast;
72 using std::make_shared;
75 using std::shared_ptr;
78 using boost::optional;
82 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
83 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
84 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
85 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
86 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
87 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
90 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
91 /* default _content_title_text to annotation_text */
92 : _issuer("libdcp", dcp::version)
93 , _creator("libdcp", dcp::version)
94 , _issue_date (LocalTime().as_string())
95 , _annotation_text (annotation_text)
96 , _content_title_text (annotation_text)
97 , _content_kind (content_kind)
98 , _standard (standard)
101 cv.label_text = cv.id + LocalTime().as_string();
102 _content_versions.push_back (cv);
106 CPL::CPL (boost::filesystem::path file)
108 , _content_kind (ContentKind::FEATURE)
110 cxml::Document f ("CompositionPlaylist");
113 if (f.namespace_uri() == cpl_interop_ns) {
114 _standard = Standard::INTEROP;
115 } else if (f.namespace_uri() == cpl_smpte_ns) {
116 _standard = Standard::SMPTE;
118 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
121 _id = remove_urn_uuid (f.string_child ("Id"));
122 _annotation_text = f.optional_string_child("AnnotationText");
123 _issuer = f.optional_string_child("Issuer").get_value_or("");
124 _creator = f.optional_string_child("Creator").get_value_or("");
125 _issue_date = f.string_child ("IssueDate");
126 _content_title_text = f.string_child ("ContentTitleText");
127 auto content_kind = f.node_child("ContentKind");
128 _content_kind = ContentKind(content_kind->content(), content_kind->optional_string_attribute("scope"));
129 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
130 if (content_version) {
131 /* XXX: SMPTE should insist that Id is present */
132 _content_versions.push_back (
134 content_version->optional_string_child("Id").get_value_or(""),
135 content_version->string_child("LabelText")
138 content_version->done ();
139 } else if (_standard == Standard::SMPTE) {
140 /* ContentVersion is required in SMPTE */
141 throw XMLError ("Missing ContentVersion tag in CPL");
143 auto rating_list = f.node_child ("RatingList");
144 for (auto i: rating_list->node_children("Rating")) {
145 _ratings.push_back (Rating(i));
148 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
149 _reels.push_back (make_shared<Reel>(i, _standard));
152 auto reel_list = f.node_child ("ReelList");
153 auto reels = reel_list->node_children("Reel");
154 if (!reels.empty()) {
155 auto asset_list = reels.front()->node_child("AssetList");
156 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
158 read_composition_metadata_asset (metadata);
159 _read_composition_metadata = true;
163 f.ignore_child ("Issuer");
164 f.ignore_child ("Signer");
165 f.ignore_child ("Signature");
172 CPL::add (std::shared_ptr<Reel> reel)
174 _reels.push_back (reel);
179 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
186 CPL::write_xml(boost::filesystem::path file, shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors) const
189 xmlpp::Element* root;
190 if (_standard == Standard::INTEROP) {
191 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
193 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
196 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
197 if (_annotation_text) {
198 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
200 root->add_child("IssueDate")->add_child_text (_issue_date);
201 root->add_child("Issuer")->add_child_text (_issuer);
202 root->add_child("Creator")->add_child_text (_creator);
203 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
204 auto content_kind = root->add_child("ContentKind");
205 content_kind->add_child_text(_content_kind.name());
206 if (_content_kind.scope()) {
207 content_kind->set_attribute("scope", *_content_kind.scope());
209 if (_content_versions.empty()) {
213 _content_versions[0].as_xml (root);
216 auto rating_list = root->add_child("RatingList");
217 for (auto i: _ratings) {
218 i.as_xml (rating_list->add_child("Rating"));
221 auto reel_list = root->add_child ("ReelList");
223 if (_reels.empty()) {
224 throw NoReelsError ();
228 for (auto i: _reels) {
229 auto asset_list = i->write_to_cpl (reel_list, _standard);
230 if (first && _standard == Standard::SMPTE) {
231 maybe_write_composition_metadata_asset(asset_list, include_mca_subdescriptors);
239 signer->sign (root, _standard);
242 doc.write_to_file_formatted (file.string(), "UTF-8");
249 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
251 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
253 /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
254 * apparently didn't include it, so as usual we have to be defensive.
256 if (auto fctt = node->optional_node_child("FullContentTitleText")) {
257 _full_content_title_text = fctt->content();
258 _full_content_title_text_language = fctt->optional_string_attribute("language");
261 _release_territory = node->optional_string_child("ReleaseTerritory");
262 if (_release_territory) {
263 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
266 auto vn = node->optional_node_child("VersionNumber");
268 _version_number = raw_convert<int>(vn->content());
269 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
270 auto vn_status = vn->optional_string_attribute("status");
272 _status = string_to_status (*vn_status);
276 _chain = node->optional_string_child("Chain");
277 _distributor = node->optional_string_child("Distributor");
278 _facility = node->optional_string_child("Facility");
280 auto acv = node->optional_node_child("AlternateContentVersionList");
282 for (auto i: acv->node_children("ContentVersion")) {
283 _content_versions.push_back (ContentVersion(i));
287 auto lum = node->optional_node_child("Luminance");
289 _luminance = Luminance (lum);
292 if (auto msc = node->optional_string_child("MainSoundConfiguration")) {
294 _main_sound_configuration = MainSoundConfiguration(*msc);
295 } catch (MainSoundConfigurationError& e) {
296 /* With Interop DCPs this node may not make any sense, but that's OK */
297 if (_standard == dcp::Standard::SMPTE) {
303 auto sr = node->optional_string_child("MainSoundSampleRate");
305 vector<string> sr_bits;
306 boost::split (sr_bits, *sr, boost::is_any_of(" "));
307 DCP_ASSERT (sr_bits.size() == 2);
308 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
311 if (_standard == dcp::Standard::SMPTE) {
312 _main_picture_stored_area = dcp::Size (
313 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
314 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
318 _main_picture_active_area = dcp::Size (
319 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
320 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
323 auto sll = node->optional_string_child("MainSubtitleLanguageList");
325 vector<string> sll_split;
326 boost::split (sll_split, *sll, boost::is_any_of(" "));
327 DCP_ASSERT (!sll_split.empty());
329 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
331 if (!_reels.empty()) {
332 auto sub = _reels.front()->main_subtitle();
334 auto lang = sub->language();
335 if (lang && lang == sll_split[0]) {
341 for (auto i = first; i < sll_split.size(); ++i) {
342 _additional_subtitle_languages.push_back (sll_split[i]);
346 auto eml = node->optional_node_child ("ExtensionMetadataList");
348 for (auto i: eml->node_children("ExtensionMetadata")) {
349 auto name = i->optional_string_child("Name");
350 if (name && *name == "Sign Language Video") {
351 auto property_list = i->node_child("PropertyList");
352 for (auto j: property_list->node_children("Property")) {
353 auto name = j->optional_string_child("Name");
354 auto value = j->optional_string_child("Value");
355 if (name && value && *name == "Language Tag") {
356 _sign_language_video_language = *value;
366 CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsset> asset) const
368 auto reader = asset->start_read ();
369 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
370 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
371 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
372 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
375 auto mca_subs = parent->add_child("mca:MCASubDescriptors");
376 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
377 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
378 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
379 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
381 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
382 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
383 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
384 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
385 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
386 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
387 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
388 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
389 if (!soundfield->MCATagName.empty()) {
390 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
391 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
393 if (!soundfield->RFC5646SpokenLanguage.empty()) {
394 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
395 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
398 /* Find the MCA subdescriptors in the MXF so that we can also write them here */
399 list<ASDCP::MXF::InterchangeObject*> channels;
400 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
401 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
405 for (auto i: channels) {
406 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
407 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
408 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
409 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
410 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
411 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
412 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
413 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
414 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
415 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
416 if (!channel->MCATagName.empty()) {
417 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
418 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
420 if (!channel->MCAChannelID.empty()) {
421 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
423 if (!channel->RFC5646SpokenLanguage.empty()) {
424 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
425 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
427 if (!channel->SoundfieldGroupLinkID.empty()) {
428 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
429 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
436 /** Write a CompositionMetadataAsset node as a child of @param node provided
437 * the required metadata is stored in the object. If any required metadata
438 * is missing this method will do nothing.
441 CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node, bool include_mca_subdescriptors) const
444 !_main_sound_configuration ||
445 !_main_sound_sample_rate ||
446 !_main_picture_stored_area ||
447 !_main_picture_active_area ||
449 !_reels.front()->main_picture()) {
453 auto meta = node->add_child("meta:CompositionMetadataAsset");
454 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
456 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
458 auto mp = _reels.front()->main_picture();
459 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
460 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
462 auto fctt = meta->add_child("FullContentTitleText", "meta");
463 if (_full_content_title_text && !_full_content_title_text->empty()) {
464 fctt->add_child_text (*_full_content_title_text);
466 if (_full_content_title_text_language) {
467 fctt->set_attribute("language", *_full_content_title_text_language);
470 if (_release_territory) {
471 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
474 if (_version_number) {
475 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
476 vn->add_child_text(raw_convert<string>(*_version_number));
478 vn->set_attribute("status", status_to_string(*_status));
483 meta->add_child("Chain", "meta")->add_child_text(*_chain);
487 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
491 meta->add_child("Facility", "meta")->add_child_text(*_facility);
494 if (_content_versions.size() > 1) {
495 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
496 for (size_t i = 1; i < _content_versions.size(); ++i) {
497 _content_versions[i].as_xml (vc);
502 _luminance->as_xml (meta, "meta");
505 if (_main_sound_configuration) {
506 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(_main_sound_configuration->to_string());
508 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
510 auto stored = meta->add_child("MainPictureStoredArea", "meta");
511 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
512 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
514 auto active = meta->add_child("MainPictureActiveArea", "meta");
515 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
516 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
518 optional<string> first_subtitle_language;
519 for (auto i: _reels) {
520 if (i->main_subtitle()) {
521 first_subtitle_language = i->main_subtitle()->language();
522 if (first_subtitle_language) {
528 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
530 if (first_subtitle_language) {
531 lang = *first_subtitle_language;
533 for (auto const& i: _additional_subtitle_languages) {
539 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
542 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
544 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
545 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
546 extension->set_attribute("scope", scope);
547 extension->add_child("Name", "meta")->add_child_text(name);
548 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
549 property->add_child("Name", "meta")->add_child_text(property_name);
550 property->add_child("Value", "meta")->add_child_text(property_value);
553 /* SMPTE Bv2.1 8.6.3 */
554 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
556 if (_sign_language_video_language) {
557 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
560 if (_reels.front()->main_sound()) {
561 auto asset = _reels.front()->main_sound()->asset();
562 if (asset && include_mca_subdescriptors) {
563 write_mca_subdescriptors(meta, asset);
571 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
573 for (auto i: reels) {
574 if (i->main_picture ()) {
575 assets.push_back (i->main_picture());
577 if (i->main_sound ()) {
578 assets.push_back (i->main_sound());
580 if (i->main_subtitle ()) {
581 assets.push_back (i->main_subtitle());
583 for (auto j: i->closed_captions()) {
584 assets.push_back (j);
587 assets.push_back (i->atmos());
593 vector<shared_ptr<ReelFileAsset>>
594 CPL::reel_file_assets ()
596 vector<shared_ptr<ReelFileAsset>> c;
597 add_file_assets (c, _reels);
602 vector<shared_ptr<const ReelFileAsset>>
603 CPL::reel_file_assets () const
605 vector<shared_ptr<const ReelFileAsset>> c;
606 add_file_assets (c, _reels);
612 CPL::equals(shared_ptr<const Asset> other, EqualityOptions const& opt, NoteHandler note) const
614 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
619 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
620 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
621 note (NoteType::ERROR, s);
625 if (_content_kind != other_cpl->_content_kind) {
626 note (NoteType::ERROR, "CPL: content kinds differ");
630 if (_reels.size() != other_cpl->_reels.size()) {
631 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
635 auto a = _reels.begin();
636 auto b = other_cpl->_reels.begin();
638 while (a != _reels.end ()) {
639 if (!(*a)->equals (*b, opt, note)) {
651 CPL::any_encrypted () const
653 for (auto i: _reels) {
654 if (i->any_encrypted()) {
664 CPL::all_encrypted () const
666 for (auto i: _reels) {
667 if (!i->all_encrypted()) {
677 CPL::add (DecryptedKDM const & kdm)
679 for (auto i: _reels) {
685 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
687 for (auto i: _reels) {
688 i->resolve_refs (assets);
693 CPL::pkl_type (Standard standard) const
695 return static_pkl_type (standard);
699 CPL::static_pkl_type (Standard standard)
702 case Standard::INTEROP:
703 return "text/xml;asdcpKind=CPL";
704 case Standard::SMPTE:
712 CPL::duration () const
715 for (auto i: _reels) {
723 CPL::set_version_number (int v)
726 throw BadSettingError ("CPL version number cannot be negative");
734 CPL::unset_version_number ()
736 _version_number = boost::none;
741 CPL::set_content_versions (vector<ContentVersion> v)
743 std::set<string> ids;
745 if (!ids.insert(i.id).second) {
746 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
750 _content_versions = v;
754 optional<ContentVersion>
755 CPL::content_version () const
757 if (_content_versions.empty()) {
758 return optional<ContentVersion>();
761 return _content_versions[0];
766 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
768 _additional_subtitle_languages.clear ();
769 for (auto const& i: langs) {
770 _additional_subtitle_languages.push_back (i.to_string());
776 CPL::set_main_picture_active_area(dcp::Size area)
778 if (area.width % 2) {
779 throw BadSettingError("Main picture active area width is not a multiple of 2");
782 if (area.height % 2) {
783 throw BadSettingError("Main picture active area height is not a multiple of 2");
786 _main_picture_active_area = area;