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 "local_time.h"
46 #include "raw_convert.h"
48 #include "reel_atmos_asset.h"
49 #include "reel_closed_caption_asset.h"
50 #include "reel_picture_asset.h"
51 #include "reel_sound_asset.h"
52 #include "reel_subtitle_asset.h"
57 LIBDCP_DISABLE_WARNINGS
58 #include <asdcp/Metadata.h>
59 LIBDCP_ENABLE_WARNINGS
60 #include <libxml/parser.h>
61 LIBDCP_DISABLE_WARNINGS
62 #include <libxml++/libxml++.h>
63 LIBDCP_ENABLE_WARNINGS
64 #include <boost/algorithm/string.hpp>
68 using std::dynamic_pointer_cast;
71 using std::make_shared;
74 using std::shared_ptr;
77 using boost::optional;
81 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
82 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
83 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
84 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
85 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
86 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
89 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
90 /* default _content_title_text to annotation_text */
91 : _issuer("libdcp", dcp::version)
92 , _creator("libdcp", dcp::version)
93 , _issue_date (LocalTime().as_string())
94 , _annotation_text (annotation_text)
95 , _content_title_text (annotation_text)
96 , _content_kind (content_kind)
97 , _standard (standard)
100 cv.label_text = cv.id + LocalTime().as_string();
101 _content_versions.push_back (cv);
105 CPL::CPL (boost::filesystem::path file)
107 , _content_kind (ContentKind::FEATURE)
109 cxml::Document f ("CompositionPlaylist");
112 if (f.namespace_uri() == cpl_interop_ns) {
113 _standard = Standard::INTEROP;
114 } else if (f.namespace_uri() == cpl_smpte_ns) {
115 _standard = Standard::SMPTE;
117 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
120 _id = remove_urn_uuid (f.string_child ("Id"));
121 _annotation_text = f.optional_string_child("AnnotationText");
122 _issuer = f.optional_string_child("Issuer").get_value_or("");
123 _creator = f.optional_string_child("Creator").get_value_or("");
124 _issue_date = f.string_child ("IssueDate");
125 _content_title_text = f.string_child ("ContentTitleText");
126 auto content_kind = f.node_child("ContentKind");
127 _content_kind = ContentKind(content_kind->content(), content_kind->optional_string_attribute("scope"));
128 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
129 if (content_version) {
130 /* XXX: SMPTE should insist that Id is present */
131 _content_versions.push_back (
133 content_version->optional_string_child("Id").get_value_or(""),
134 content_version->string_child("LabelText")
137 content_version->done ();
138 } else if (_standard == Standard::SMPTE) {
139 /* ContentVersion is required in SMPTE */
140 throw XMLError ("Missing ContentVersion tag in CPL");
142 auto rating_list = f.node_child ("RatingList");
143 for (auto i: rating_list->node_children("Rating")) {
144 _ratings.push_back (Rating(i));
147 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
148 _reels.push_back (make_shared<Reel>(i, _standard));
151 auto reel_list = f.node_child ("ReelList");
152 auto reels = reel_list->node_children("Reel");
153 if (!reels.empty()) {
154 auto asset_list = reels.front()->node_child("AssetList");
155 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
157 read_composition_metadata_asset (metadata);
158 _read_composition_metadata = true;
162 f.ignore_child ("Issuer");
163 f.ignore_child ("Signer");
164 f.ignore_child ("Signature");
171 CPL::add (std::shared_ptr<Reel> reel)
173 _reels.push_back (reel);
178 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
185 CPL::write_xml(boost::filesystem::path file, shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors) const
188 xmlpp::Element* root;
189 if (_standard == Standard::INTEROP) {
190 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
192 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
195 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
196 if (_annotation_text) {
197 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
199 root->add_child("IssueDate")->add_child_text (_issue_date);
200 root->add_child("Issuer")->add_child_text (_issuer);
201 root->add_child("Creator")->add_child_text (_creator);
202 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
203 auto content_kind = root->add_child("ContentKind");
204 content_kind->add_child_text(_content_kind.name());
205 if (_content_kind.scope()) {
206 content_kind->set_attribute("scope", *_content_kind.scope());
208 if (_content_versions.empty()) {
212 _content_versions[0].as_xml (root);
215 auto rating_list = root->add_child("RatingList");
216 for (auto i: _ratings) {
217 i.as_xml (rating_list->add_child("Rating"));
220 auto reel_list = root->add_child ("ReelList");
222 if (_reels.empty()) {
223 throw NoReelsError ();
227 for (auto i: _reels) {
228 auto asset_list = i->write_to_cpl (reel_list, _standard);
229 if (first && _standard == Standard::SMPTE) {
230 maybe_write_composition_metadata_asset(asset_list, include_mca_subdescriptors);
238 signer->sign (root, _standard);
241 doc.write_to_file_formatted (file.string(), "UTF-8");
248 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
250 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
252 /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
253 * apparently didn't include it, so as usual we have to be defensive.
255 if (auto fctt = node->optional_node_child("FullContentTitleText")) {
256 _full_content_title_text = fctt->content();
257 _full_content_title_text_language = fctt->optional_string_attribute("language");
260 _release_territory = node->optional_string_child("ReleaseTerritory");
261 if (_release_territory) {
262 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
265 auto vn = node->optional_node_child("VersionNumber");
267 _version_number = raw_convert<int>(vn->content());
268 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
269 auto vn_status = vn->optional_string_attribute("status");
271 _status = string_to_status (*vn_status);
275 _chain = node->optional_string_child("Chain");
276 _distributor = node->optional_string_child("Distributor");
277 _facility = node->optional_string_child("Facility");
279 auto acv = node->optional_node_child("AlternateContentVersionList");
281 for (auto i: acv->node_children("ContentVersion")) {
282 _content_versions.push_back (ContentVersion(i));
286 auto lum = node->optional_node_child("Luminance");
288 _luminance = Luminance (lum);
291 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
293 auto sr = node->optional_string_child("MainSoundSampleRate");
295 vector<string> sr_bits;
296 boost::split (sr_bits, *sr, boost::is_any_of(" "));
297 DCP_ASSERT (sr_bits.size() == 2);
298 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
301 if (_standard == dcp::Standard::SMPTE) {
302 _main_picture_stored_area = dcp::Size (
303 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
304 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
308 _main_picture_active_area = dcp::Size (
309 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
310 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
313 auto sll = node->optional_string_child("MainSubtitleLanguageList");
315 vector<string> sll_split;
316 boost::split (sll_split, *sll, boost::is_any_of(" "));
317 DCP_ASSERT (!sll_split.empty());
319 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
321 if (!_reels.empty()) {
322 auto sub = _reels.front()->main_subtitle();
324 auto lang = sub->language();
325 if (lang && lang == sll_split[0]) {
331 for (auto i = first; i < sll_split.size(); ++i) {
332 _additional_subtitle_languages.push_back (sll_split[i]);
336 auto eml = node->optional_node_child ("ExtensionMetadataList");
338 for (auto i: eml->node_children("ExtensionMetadata")) {
339 auto name = i->optional_string_child("Name");
340 if (name && *name == "Sign Language Video") {
341 auto property_list = i->node_child("PropertyList");
342 for (auto j: property_list->node_children("Property")) {
343 auto name = j->optional_string_child("Name");
344 auto value = j->optional_string_child("Value");
345 if (name && value && *name == "Language Tag") {
346 _sign_language_video_language = *value;
356 CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsset> asset) const
358 auto reader = asset->start_read ();
359 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
360 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
361 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
362 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
365 auto mca_subs = parent->add_child("mca:MCASubDescriptors");
366 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
367 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
368 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
369 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
371 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
372 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
373 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
374 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
375 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
376 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
377 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
378 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
379 if (!soundfield->MCATagName.empty()) {
380 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
381 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
383 if (!soundfield->RFC5646SpokenLanguage.empty()) {
384 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
385 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
388 /* Find the MCA subdescriptors in the MXF so that we can also write them here */
389 list<ASDCP::MXF::InterchangeObject*> channels;
390 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
391 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
395 for (auto i: channels) {
396 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
397 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
398 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
399 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
400 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
401 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
402 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
403 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
404 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
405 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
406 if (!channel->MCATagName.empty()) {
407 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
408 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
410 if (!channel->MCAChannelID.empty()) {
411 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
413 if (!channel->RFC5646SpokenLanguage.empty()) {
414 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
415 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
417 if (!channel->SoundfieldGroupLinkID.empty()) {
418 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
419 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
426 /** Write a CompositionMetadataAsset node as a child of @param node provided
427 * the required metadata is stored in the object. If any required metadata
428 * is missing this method will do nothing.
431 CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node, bool include_mca_subdescriptors) const
434 !_main_sound_configuration ||
435 !_main_sound_sample_rate ||
436 !_main_picture_stored_area ||
437 !_main_picture_active_area ||
439 !_reels.front()->main_picture()) {
443 auto meta = node->add_child("meta:CompositionMetadataAsset");
444 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
446 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
448 auto mp = _reels.front()->main_picture();
449 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
450 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
452 auto fctt = meta->add_child("FullContentTitleText", "meta");
453 if (_full_content_title_text && !_full_content_title_text->empty()) {
454 fctt->add_child_text (*_full_content_title_text);
456 if (_full_content_title_text_language) {
457 fctt->set_attribute("language", *_full_content_title_text_language);
460 if (_release_territory) {
461 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
464 if (_version_number) {
465 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
466 vn->add_child_text(raw_convert<string>(*_version_number));
468 vn->set_attribute("status", status_to_string(*_status));
473 meta->add_child("Chain", "meta")->add_child_text(*_chain);
477 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
481 meta->add_child("Facility", "meta")->add_child_text(*_facility);
484 if (_content_versions.size() > 1) {
485 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
486 for (size_t i = 1; i < _content_versions.size(); ++i) {
487 _content_versions[i].as_xml (vc);
492 _luminance->as_xml (meta, "meta");
495 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
496 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
498 auto stored = meta->add_child("MainPictureStoredArea", "meta");
499 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
500 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
502 auto active = meta->add_child("MainPictureActiveArea", "meta");
503 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
504 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
506 optional<string> first_subtitle_language;
507 for (auto i: _reels) {
508 if (i->main_subtitle()) {
509 first_subtitle_language = i->main_subtitle()->language();
510 if (first_subtitle_language) {
516 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
518 if (first_subtitle_language) {
519 lang = *first_subtitle_language;
521 for (auto const& i: _additional_subtitle_languages) {
527 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
530 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
532 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
533 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
534 extension->set_attribute("scope", scope);
535 extension->add_child("Name", "meta")->add_child_text(name);
536 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
537 property->add_child("Name", "meta")->add_child_text(property_name);
538 property->add_child("Value", "meta")->add_child_text(property_value);
541 /* SMPTE Bv2.1 8.6.3 */
542 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
544 if (_sign_language_video_language) {
545 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
548 if (_reels.front()->main_sound()) {
549 auto asset = _reels.front()->main_sound()->asset();
550 if (asset && include_mca_subdescriptors) {
551 write_mca_subdescriptors(meta, asset);
559 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
561 for (auto i: reels) {
562 if (i->main_picture ()) {
563 assets.push_back (i->main_picture());
565 if (i->main_sound ()) {
566 assets.push_back (i->main_sound());
568 if (i->main_subtitle ()) {
569 assets.push_back (i->main_subtitle());
571 for (auto j: i->closed_captions()) {
572 assets.push_back (j);
575 assets.push_back (i->atmos());
581 vector<shared_ptr<ReelFileAsset>>
582 CPL::reel_file_assets ()
584 vector<shared_ptr<ReelFileAsset>> c;
585 add_file_assets (c, _reels);
590 vector<shared_ptr<const ReelFileAsset>>
591 CPL::reel_file_assets () const
593 vector<shared_ptr<const ReelFileAsset>> c;
594 add_file_assets (c, _reels);
600 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
602 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
607 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
608 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
609 note (NoteType::ERROR, s);
613 if (_content_kind != other_cpl->_content_kind) {
614 note (NoteType::ERROR, "CPL: content kinds differ");
618 if (_reels.size() != other_cpl->_reels.size()) {
619 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
623 auto a = _reels.begin();
624 auto b = other_cpl->_reels.begin();
626 while (a != _reels.end ()) {
627 if (!(*a)->equals (*b, opt, note)) {
639 CPL::any_encrypted () const
641 for (auto i: _reels) {
642 if (i->any_encrypted()) {
652 CPL::all_encrypted () const
654 for (auto i: _reels) {
655 if (!i->all_encrypted()) {
665 CPL::add (DecryptedKDM const & kdm)
667 for (auto i: _reels) {
673 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
675 for (auto i: _reels) {
676 i->resolve_refs (assets);
681 CPL::pkl_type (Standard standard) const
683 return static_pkl_type (standard);
687 CPL::static_pkl_type (Standard standard)
690 case Standard::INTEROP:
691 return "text/xml;asdcpKind=CPL";
692 case Standard::SMPTE:
700 CPL::duration () const
703 for (auto i: _reels) {
711 CPL::set_version_number (int v)
714 throw BadSettingError ("CPL version number cannot be negative");
722 CPL::unset_version_number ()
724 _version_number = boost::none;
729 CPL::set_content_versions (vector<ContentVersion> v)
731 std::set<string> ids;
733 if (!ids.insert(i.id).second) {
734 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
738 _content_versions = v;
742 optional<ContentVersion>
743 CPL::content_version () const
745 if (_content_versions.empty()) {
746 return optional<ContentVersion>();
749 return _content_versions[0];
754 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
756 _additional_subtitle_languages.clear ();
757 for (auto const& i: langs) {
758 _additional_subtitle_languages.push_back (i.to_string());
764 CPL::set_main_picture_active_area(dcp::Size area)
766 if (area.width % 2) {
767 throw BadSettingError("Main picture active area width is not a multiple of 2");
770 if (area.height % 2) {
771 throw BadSettingError("Main picture active area height is not a multiple of 2");
774 _main_picture_active_area = area;