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 "filesystem.h"
46 #include "local_time.h"
48 #include "raw_convert.h"
50 #include "reel_atmos_asset.h"
51 #include "reel_closed_caption_asset.h"
52 #include "reel_picture_asset.h"
53 #include "reel_sound_asset.h"
54 #include "reel_subtitle_asset.h"
59 LIBDCP_DISABLE_WARNINGS
60 #include <asdcp/Metadata.h>
61 LIBDCP_ENABLE_WARNINGS
62 #include <libxml/parser.h>
63 LIBDCP_DISABLE_WARNINGS
64 #include <libxml++/libxml++.h>
65 LIBDCP_ENABLE_WARNINGS
66 #include <boost/algorithm/string.hpp>
70 using std::dynamic_pointer_cast;
73 using std::make_shared;
76 using std::shared_ptr;
79 using boost::optional;
83 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
84 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
85 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
86 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
87 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
88 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
91 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
92 /* default _content_title_text to annotation_text */
93 : _issuer("libdcp", dcp::version)
94 , _creator("libdcp", dcp::version)
95 , _issue_date (LocalTime().as_string())
96 , _annotation_text (annotation_text)
97 , _content_title_text (annotation_text)
98 , _content_kind (content_kind)
99 , _standard (standard)
102 cv.label_text = cv.id + LocalTime().as_string();
103 _content_versions.push_back (cv);
107 CPL::CPL (boost::filesystem::path file)
109 , _content_kind (ContentKind::FEATURE)
111 cxml::Document f ("CompositionPlaylist");
112 f.read_file(dcp::filesystem::fix_long_path(file));
114 if (f.namespace_uri() == cpl_interop_ns) {
115 _standard = Standard::INTEROP;
116 } else if (f.namespace_uri() == cpl_smpte_ns) {
117 _standard = Standard::SMPTE;
119 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
122 _id = remove_urn_uuid (f.string_child ("Id"));
123 _annotation_text = f.optional_string_child("AnnotationText");
124 _issuer = f.optional_string_child("Issuer").get_value_or("");
125 _creator = f.optional_string_child("Creator").get_value_or("");
126 _issue_date = f.string_child ("IssueDate");
127 _content_title_text = f.string_child ("ContentTitleText");
128 auto content_kind = f.node_child("ContentKind");
129 _content_kind = ContentKind(content_kind->content(), content_kind->optional_string_attribute("scope"));
130 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
131 if (content_version) {
132 /* XXX: SMPTE should insist that Id is present */
133 _content_versions.push_back (
135 content_version->optional_string_child("Id").get_value_or(""),
136 content_version->string_child("LabelText")
139 content_version->done ();
140 } else if (_standard == Standard::SMPTE) {
141 /* ContentVersion is required in SMPTE */
142 throw XMLError ("Missing ContentVersion tag in CPL");
144 auto rating_list = f.node_child ("RatingList");
145 for (auto i: rating_list->node_children("Rating")) {
146 _ratings.push_back (Rating(i));
149 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
150 _reels.push_back (make_shared<Reel>(i, _standard));
153 auto reel_list = f.node_child ("ReelList");
154 auto reels = reel_list->node_children("Reel");
155 if (!reels.empty()) {
156 auto asset_list = reels.front()->node_child("AssetList");
157 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
159 read_composition_metadata_asset (metadata);
160 _read_composition_metadata = true;
164 f.ignore_child ("Issuer");
165 f.ignore_child ("Signer");
166 f.ignore_child ("Signature");
173 CPL::add (std::shared_ptr<Reel> reel)
175 _reels.push_back (reel);
180 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
187 CPL::write_xml(boost::filesystem::path file, shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors) const
190 xmlpp::Element* root;
191 if (_standard == Standard::INTEROP) {
192 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
194 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
197 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
198 if (_annotation_text) {
199 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
201 root->add_child("IssueDate")->add_child_text (_issue_date);
202 root->add_child("Issuer")->add_child_text (_issuer);
203 root->add_child("Creator")->add_child_text (_creator);
204 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
205 auto content_kind = root->add_child("ContentKind");
206 content_kind->add_child_text(_content_kind.name());
207 if (_content_kind.scope()) {
208 content_kind->set_attribute("scope", *_content_kind.scope());
210 if (_content_versions.empty()) {
214 _content_versions[0].as_xml (root);
217 auto rating_list = root->add_child("RatingList");
218 for (auto i: _ratings) {
219 i.as_xml (rating_list->add_child("Rating"));
222 auto reel_list = root->add_child ("ReelList");
224 if (_reels.empty()) {
225 throw NoReelsError ();
229 for (auto i: _reels) {
230 auto asset_list = i->write_to_cpl (reel_list, _standard);
231 if (first && _standard == Standard::SMPTE) {
232 maybe_write_composition_metadata_asset(asset_list, include_mca_subdescriptors);
240 signer->sign (root, _standard);
243 doc.write_to_file_formatted(dcp::filesystem::fix_long_path(file).string(), "UTF-8");
250 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
252 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
254 /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
255 * apparently didn't include it, so as usual we have to be defensive.
257 if (auto fctt = node->optional_node_child("FullContentTitleText")) {
258 _full_content_title_text = fctt->content();
259 _full_content_title_text_language = fctt->optional_string_attribute("language");
262 _release_territory = node->optional_string_child("ReleaseTerritory");
263 if (_release_territory) {
264 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
267 auto vn = node->optional_node_child("VersionNumber");
269 _version_number = raw_convert<int>(vn->content());
270 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
271 auto vn_status = vn->optional_string_attribute("status");
273 _status = string_to_status (*vn_status);
277 _chain = node->optional_string_child("Chain");
278 _distributor = node->optional_string_child("Distributor");
279 _facility = node->optional_string_child("Facility");
281 auto acv = node->optional_node_child("AlternateContentVersionList");
283 for (auto i: acv->node_children("ContentVersion")) {
284 _content_versions.push_back (ContentVersion(i));
288 auto lum = node->optional_node_child("Luminance");
290 _luminance = Luminance (lum);
293 if (auto msc = node->optional_string_child("MainSoundConfiguration")) {
295 _main_sound_configuration = MainSoundConfiguration(*msc);
296 } catch (MainSoundConfigurationError& e) {
297 /* With Interop DCPs this node may not make any sense, but that's OK */
298 if (_standard == dcp::Standard::SMPTE) {
304 auto sr = node->optional_string_child("MainSoundSampleRate");
306 vector<string> sr_bits;
307 boost::split (sr_bits, *sr, boost::is_any_of(" "));
308 DCP_ASSERT (sr_bits.size() == 2);
309 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
312 if (_standard == dcp::Standard::SMPTE) {
313 _main_picture_stored_area = dcp::Size (
314 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
315 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
319 _main_picture_active_area = dcp::Size (
320 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
321 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
324 auto sll = node->optional_string_child("MainSubtitleLanguageList");
326 vector<string> sll_split;
327 boost::split (sll_split, *sll, boost::is_any_of(" "));
328 DCP_ASSERT (!sll_split.empty());
330 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
332 if (!_reels.empty()) {
333 auto sub = _reels.front()->main_subtitle();
335 auto lang = sub->language();
336 if (lang && lang == sll_split[0]) {
342 for (auto i = first; i < sll_split.size(); ++i) {
343 _additional_subtitle_languages.push_back (sll_split[i]);
347 auto eml = node->optional_node_child ("ExtensionMetadataList");
349 for (auto i: eml->node_children("ExtensionMetadata")) {
350 auto name = i->optional_string_child("Name");
351 if (name && *name == "Sign Language Video") {
352 auto property_list = i->node_child("PropertyList");
353 for (auto j: property_list->node_children("Property")) {
354 auto name = j->optional_string_child("Name");
355 auto value = j->optional_string_child("Value");
356 if (name && value && *name == "Language Tag") {
357 _sign_language_video_language = *value;
367 CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsset> asset) const
369 auto reader = asset->start_read ();
370 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
371 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
372 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
373 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
376 auto mca_subs = parent->add_child("mca:MCASubDescriptors");
377 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
378 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
379 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
380 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
382 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
383 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
384 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
385 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
386 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
387 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
388 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
389 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
390 if (!soundfield->MCATagName.empty()) {
391 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
392 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
394 if (!soundfield->RFC5646SpokenLanguage.empty()) {
395 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
396 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
399 /* Find the MCA subdescriptors in the MXF so that we can also write them here */
400 list<ASDCP::MXF::InterchangeObject*> channels;
401 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
402 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
406 for (auto i: channels) {
407 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
408 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
409 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
410 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
411 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
412 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
413 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
414 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
415 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
416 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
417 if (!channel->MCATagName.empty()) {
418 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
419 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
421 if (!channel->MCAChannelID.empty()) {
422 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
424 if (!channel->RFC5646SpokenLanguage.empty()) {
425 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
426 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
428 if (!channel->SoundfieldGroupLinkID.empty()) {
429 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
430 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
437 /** Write a CompositionMetadataAsset node as a child of @param node provided
438 * the required metadata is stored in the object. If any required metadata
439 * is missing this method will do nothing.
442 CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node, bool include_mca_subdescriptors) const
445 !_main_sound_configuration ||
446 !_main_sound_sample_rate ||
447 !_main_picture_stored_area ||
448 !_main_picture_active_area ||
450 !_reels.front()->main_picture()) {
454 auto meta = node->add_child("meta:CompositionMetadataAsset");
455 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
457 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
459 auto mp = _reels.front()->main_picture();
460 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
461 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
463 auto fctt = meta->add_child("FullContentTitleText", "meta");
464 if (_full_content_title_text && !_full_content_title_text->empty()) {
465 fctt->add_child_text (*_full_content_title_text);
467 if (_full_content_title_text_language) {
468 fctt->set_attribute("language", *_full_content_title_text_language);
471 if (_release_territory) {
472 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
475 if (_version_number) {
476 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
477 vn->add_child_text(raw_convert<string>(*_version_number));
479 vn->set_attribute("status", status_to_string(*_status));
484 meta->add_child("Chain", "meta")->add_child_text(*_chain);
488 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
492 meta->add_child("Facility", "meta")->add_child_text(*_facility);
495 if (_content_versions.size() > 1) {
496 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
497 for (size_t i = 1; i < _content_versions.size(); ++i) {
498 _content_versions[i].as_xml (vc);
503 _luminance->as_xml (meta, "meta");
506 if (_main_sound_configuration) {
507 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(_main_sound_configuration->to_string());
509 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
511 auto stored = meta->add_child("MainPictureStoredArea", "meta");
512 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
513 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
515 auto active = meta->add_child("MainPictureActiveArea", "meta");
516 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
517 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
519 optional<string> first_subtitle_language;
520 for (auto i: _reels) {
521 if (i->main_subtitle()) {
522 first_subtitle_language = i->main_subtitle()->language();
523 if (first_subtitle_language) {
529 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
531 if (first_subtitle_language) {
532 lang = *first_subtitle_language;
534 for (auto const& i: _additional_subtitle_languages) {
540 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
543 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
545 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
546 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
547 extension->set_attribute("scope", scope);
548 extension->add_child("Name", "meta")->add_child_text(name);
549 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
550 property->add_child("Name", "meta")->add_child_text(property_name);
551 property->add_child("Value", "meta")->add_child_text(property_value);
554 /* SMPTE Bv2.1 8.6.3 */
555 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
557 if (_sign_language_video_language) {
558 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
561 if (_reels.front()->main_sound()) {
562 auto asset = _reels.front()->main_sound()->asset();
563 if (asset && include_mca_subdescriptors) {
564 write_mca_subdescriptors(meta, asset);
572 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
574 for (auto i: reels) {
575 if (i->main_picture ()) {
576 assets.push_back (i->main_picture());
578 if (i->main_sound ()) {
579 assets.push_back (i->main_sound());
581 if (i->main_subtitle ()) {
582 assets.push_back (i->main_subtitle());
584 for (auto j: i->closed_captions()) {
585 assets.push_back (j);
588 assets.push_back (i->atmos());
594 vector<shared_ptr<ReelFileAsset>>
595 CPL::reel_file_assets ()
597 vector<shared_ptr<ReelFileAsset>> c;
598 add_file_assets (c, _reels);
603 vector<shared_ptr<const ReelFileAsset>>
604 CPL::reel_file_assets () const
606 vector<shared_ptr<const ReelFileAsset>> c;
607 add_file_assets (c, _reels);
613 CPL::equals(shared_ptr<const Asset> other, EqualityOptions const& opt, NoteHandler note) const
615 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
620 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
621 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
622 note (NoteType::ERROR, s);
626 if (_content_kind != other_cpl->_content_kind) {
627 note (NoteType::ERROR, "CPL: content kinds differ");
631 if (_reels.size() != other_cpl->_reels.size()) {
632 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
636 auto a = _reels.begin();
637 auto b = other_cpl->_reels.begin();
639 while (a != _reels.end ()) {
640 if (!(*a)->equals (*b, opt, note)) {
652 CPL::any_encrypted () const
654 for (auto i: _reels) {
655 if (i->any_encrypted()) {
665 CPL::all_encrypted () const
667 for (auto i: _reels) {
668 if (!i->all_encrypted()) {
678 CPL::add (DecryptedKDM const & kdm)
680 for (auto i: _reels) {
686 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
688 for (auto i: _reels) {
689 i->resolve_refs (assets);
694 CPL::pkl_type (Standard standard) const
696 return static_pkl_type (standard);
700 CPL::static_pkl_type (Standard standard)
703 case Standard::INTEROP:
704 return "text/xml;asdcpKind=CPL";
705 case Standard::SMPTE:
713 CPL::duration () const
716 for (auto i: _reels) {
724 CPL::set_version_number (int v)
727 throw BadSettingError ("CPL version number cannot be negative");
735 CPL::unset_version_number ()
737 _version_number = boost::none;
742 CPL::set_content_versions (vector<ContentVersion> v)
744 std::set<string> ids;
746 if (!ids.insert(i.id).second) {
747 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
751 _content_versions = v;
755 optional<ContentVersion>
756 CPL::content_version () const
758 if (_content_versions.empty()) {
759 return optional<ContentVersion>();
762 return _content_versions[0];
767 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
769 _additional_subtitle_languages.clear ();
770 for (auto const& i: langs) {
771 _additional_subtitle_languages.push_back (i.to_string());
777 CPL::set_main_picture_active_area(dcp::Size area)
779 if (area.width % 2) {
780 throw BadSettingError("Main picture active area width is not a multiple of 2");
783 if (area.height % 2) {
784 throw BadSettingError("Main picture active area height is not a multiple of 2");
787 _main_picture_active_area = area;