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"
56 LIBDCP_DISABLE_WARNINGS
57 #include <asdcp/Metadata.h>
58 LIBDCP_ENABLE_WARNINGS
59 #include <libxml/parser.h>
60 LIBDCP_DISABLE_WARNINGS
61 #include <libxml++/libxml++.h>
62 LIBDCP_ENABLE_WARNINGS
63 #include <boost/algorithm/string.hpp>
67 using std::dynamic_pointer_cast;
70 using std::make_shared;
73 using std::shared_ptr;
76 using boost::optional;
80 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
81 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
82 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
83 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
84 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
85 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
88 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
89 /* default _content_title_text to annotation_text */
90 : _issuer ("libdcp" LIBDCP_VERSION)
91 , _creator ("libdcp" LIBDCP_VERSION)
92 , _issue_date (LocalTime().as_string())
93 , _annotation_text (annotation_text)
94 , _content_title_text (annotation_text)
95 , _content_kind (content_kind)
96 , _standard (standard)
99 cv.label_text = cv.id + LocalTime().as_string();
100 _content_versions.push_back (cv);
104 CPL::CPL (boost::filesystem::path file)
106 , _content_kind (ContentKind::FEATURE)
108 cxml::Document f ("CompositionPlaylist");
111 if (f.namespace_uri() == cpl_interop_ns) {
112 _standard = Standard::INTEROP;
113 } else if (f.namespace_uri() == cpl_smpte_ns) {
114 _standard = Standard::SMPTE;
116 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
119 _id = remove_urn_uuid (f.string_child ("Id"));
120 _annotation_text = f.optional_string_child("AnnotationText");
121 _issuer = f.optional_string_child("Issuer").get_value_or("");
122 _creator = f.optional_string_child("Creator").get_value_or("");
123 _issue_date = f.string_child ("IssueDate");
124 _content_title_text = f.string_child ("ContentTitleText");
125 auto content_kind = f.node_child("ContentKind");
126 _content_kind = ContentKind(content_kind->content(), content_kind->optional_string_attribute("scope"));
127 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
128 if (content_version) {
129 /* XXX: SMPTE should insist that Id is present */
130 _content_versions.push_back (
132 content_version->optional_string_child("Id").get_value_or(""),
133 content_version->string_child("LabelText")
136 content_version->done ();
137 } else if (_standard == Standard::SMPTE) {
138 /* ContentVersion is required in SMPTE */
139 throw XMLError ("Missing ContentVersion tag in CPL");
141 auto rating_list = f.node_child ("RatingList");
142 for (auto i: rating_list->node_children("Rating")) {
143 _ratings.push_back (Rating(i));
146 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
147 _reels.push_back (make_shared<Reel>(i, _standard));
150 auto reel_list = f.node_child ("ReelList");
151 auto reels = reel_list->node_children("Reel");
152 if (!reels.empty()) {
153 auto asset_list = reels.front()->node_child("AssetList");
154 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
156 read_composition_metadata_asset (metadata);
157 _read_composition_metadata = true;
161 f.ignore_child ("Issuer");
162 f.ignore_child ("Signer");
163 f.ignore_child ("Signature");
170 CPL::add (std::shared_ptr<Reel> reel)
172 _reels.push_back (reel);
177 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
184 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
187 xmlpp::Element* root;
188 if (_standard == Standard::INTEROP) {
189 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
191 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
194 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
195 if (_annotation_text) {
196 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
198 root->add_child("IssueDate")->add_child_text (_issue_date);
199 root->add_child("Issuer")->add_child_text (_issuer);
200 root->add_child("Creator")->add_child_text (_creator);
201 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
202 auto content_kind = root->add_child("ContentKind");
203 content_kind->add_child_text(_content_kind.name());
204 if (_content_kind.scope()) {
205 content_kind->set_attribute("scope", *_content_kind.scope());
207 if (_content_versions.empty()) {
211 _content_versions[0].as_xml (root);
214 auto rating_list = root->add_child("RatingList");
215 for (auto i: _ratings) {
216 i.as_xml (rating_list->add_child("Rating"));
219 auto reel_list = root->add_child ("ReelList");
221 if (_reels.empty()) {
222 throw NoReelsError ();
226 for (auto i: _reels) {
227 auto asset_list = i->write_to_cpl (reel_list, _standard);
228 if (first && _standard == Standard::SMPTE) {
229 maybe_write_composition_metadata_asset (asset_list);
237 signer->sign (root, _standard);
240 doc.write_to_file_formatted (file.string(), "UTF-8");
247 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
249 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
251 /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
252 * apparently didn't include it, so as usual we have to be defensive.
254 if (auto fctt = node->optional_node_child("FullContentTitleText")) {
255 _full_content_title_text = fctt->content();
256 _full_content_title_text_language = fctt->optional_string_attribute("language");
259 _release_territory = node->optional_string_child("ReleaseTerritory");
260 if (_release_territory) {
261 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
264 auto vn = node->optional_node_child("VersionNumber");
266 _version_number = raw_convert<int>(vn->content());
267 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
268 auto vn_status = vn->optional_string_attribute("status");
270 _status = string_to_status (*vn_status);
274 _chain = node->optional_string_child("Chain");
275 _distributor = node->optional_string_child("Distributor");
276 _facility = node->optional_string_child("Facility");
278 auto acv = node->optional_node_child("AlternateContentVersionList");
280 for (auto i: acv->node_children("ContentVersion")) {
281 _content_versions.push_back (ContentVersion(i));
285 auto lum = node->optional_node_child("Luminance");
287 _luminance = Luminance (lum);
290 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
292 auto sr = node->optional_string_child("MainSoundSampleRate");
294 vector<string> sr_bits;
295 boost::split (sr_bits, *sr, boost::is_any_of(" "));
296 DCP_ASSERT (sr_bits.size() == 2);
297 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
300 _main_picture_stored_area = dcp::Size (
301 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
302 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
305 _main_picture_active_area = dcp::Size (
306 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
307 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
310 auto sll = node->optional_string_child("MainSubtitleLanguageList");
312 vector<string> sll_split;
313 boost::split (sll_split, *sll, boost::is_any_of(" "));
314 DCP_ASSERT (!sll_split.empty());
316 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
318 if (!_reels.empty()) {
319 auto sub = _reels.front()->main_subtitle();
321 auto lang = sub->language();
322 if (lang && lang == sll_split[0]) {
328 for (auto i = first; i < sll_split.size(); ++i) {
329 _additional_subtitle_languages.push_back (sll_split[i]);
333 auto eml = node->optional_node_child ("ExtensionMetadataList");
335 for (auto i: eml->node_children("ExtensionMetadata")) {
336 auto name = i->optional_string_child("Name");
337 if (name && *name == "Sign Language Video") {
338 auto property_list = i->node_child("PropertyList");
339 for (auto j: property_list->node_children("Property")) {
340 auto name = j->optional_string_child("Name");
341 auto value = j->optional_string_child("Value");
342 if (name && value && *name == "Language Tag") {
343 _sign_language_video_language = *value;
352 /** Write a CompositionMetadataAsset node as a child of @param node provided
353 * the required metadata is stored in the object. If any required metadata
354 * is missing this method will do nothing.
357 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
360 !_main_sound_configuration ||
361 !_main_sound_sample_rate ||
362 !_main_picture_stored_area ||
363 !_main_picture_active_area ||
365 !_reels.front()->main_picture()) {
369 auto meta = node->add_child("meta:CompositionMetadataAsset");
370 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
372 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
374 auto mp = _reels.front()->main_picture();
375 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
376 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
378 auto fctt = meta->add_child("FullContentTitleText", "meta");
379 if (_full_content_title_text && !_full_content_title_text->empty()) {
380 fctt->add_child_text (*_full_content_title_text);
382 if (_full_content_title_text_language) {
383 fctt->set_attribute("language", *_full_content_title_text_language);
386 if (_release_territory) {
387 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
390 if (_version_number) {
391 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
392 vn->add_child_text(raw_convert<string>(*_version_number));
394 vn->set_attribute("status", status_to_string(*_status));
399 meta->add_child("Chain", "meta")->add_child_text(*_chain);
403 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
407 meta->add_child("Facility", "meta")->add_child_text(*_facility);
410 if (_content_versions.size() > 1) {
411 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
412 for (size_t i = 1; i < _content_versions.size(); ++i) {
413 _content_versions[i].as_xml (vc);
418 _luminance->as_xml (meta, "meta");
421 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
422 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
424 auto stored = meta->add_child("MainPictureStoredArea", "meta");
425 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
426 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
428 auto active = meta->add_child("MainPictureActiveArea", "meta");
429 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
430 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
432 optional<string> first_subtitle_language;
433 for (auto i: _reels) {
434 if (i->main_subtitle()) {
435 first_subtitle_language = i->main_subtitle()->language();
436 if (first_subtitle_language) {
442 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
444 if (first_subtitle_language) {
445 lang = *first_subtitle_language;
447 for (auto const& i: _additional_subtitle_languages) {
453 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
456 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
458 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
459 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
460 extension->set_attribute("scope", scope);
461 extension->add_child("Name", "meta")->add_child_text(name);
462 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
463 property->add_child("Name", "meta")->add_child_text(property_name);
464 property->add_child("Value", "meta")->add_child_text(property_value);
467 /* SMPTE Bv2.1 8.6.3 */
468 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
470 if (_sign_language_video_language) {
471 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
474 if (_reels.front()->main_sound()) {
475 auto asset = _reels.front()->main_sound()->asset();
477 auto reader = asset->start_read ();
478 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
479 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
480 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
481 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
484 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
485 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
486 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
487 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
488 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
490 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
491 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
492 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
493 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
494 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
495 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
496 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
497 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
498 if (!soundfield->MCATagName.empty()) {
499 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
500 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
502 if (!soundfield->RFC5646SpokenLanguage.empty()) {
503 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
504 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
507 list<ASDCP::MXF::InterchangeObject*> channels;
508 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
509 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
513 for (auto i: channels) {
514 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
515 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
516 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
517 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
518 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
519 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
520 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
521 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
522 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
523 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
524 if (!channel->MCATagName.empty()) {
525 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
526 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
528 if (!channel->MCAChannelID.empty()) {
529 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
531 if (!channel->RFC5646SpokenLanguage.empty()) {
532 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
533 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
535 if (!channel->SoundfieldGroupLinkID.empty()) {
536 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
537 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
548 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
550 for (auto i: reels) {
551 if (i->main_picture ()) {
552 assets.push_back (i->main_picture());
554 if (i->main_sound ()) {
555 assets.push_back (i->main_sound());
557 if (i->main_subtitle ()) {
558 assets.push_back (i->main_subtitle());
560 for (auto j: i->closed_captions()) {
561 assets.push_back (j);
564 assets.push_back (i->atmos());
570 vector<shared_ptr<ReelFileAsset>>
571 CPL::reel_file_assets ()
573 vector<shared_ptr<ReelFileAsset>> c;
574 add_file_assets (c, _reels);
579 vector<shared_ptr<const ReelFileAsset>>
580 CPL::reel_file_assets () const
582 vector<shared_ptr<const ReelFileAsset>> c;
583 add_file_assets (c, _reels);
589 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
591 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
596 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
597 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
598 note (NoteType::ERROR, s);
602 if (_content_kind != other_cpl->_content_kind) {
603 note (NoteType::ERROR, "CPL: content kinds differ");
607 if (_reels.size() != other_cpl->_reels.size()) {
608 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
612 auto a = _reels.begin();
613 auto b = other_cpl->_reels.begin();
615 while (a != _reels.end ()) {
616 if (!(*a)->equals (*b, opt, note)) {
628 CPL::any_encrypted () const
630 for (auto i: _reels) {
631 if (i->any_encrypted()) {
641 CPL::all_encrypted () const
643 for (auto i: _reels) {
644 if (!i->all_encrypted()) {
654 CPL::add (DecryptedKDM const & kdm)
656 for (auto i: _reels) {
662 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
664 for (auto i: _reels) {
665 i->resolve_refs (assets);
670 CPL::pkl_type (Standard standard) const
672 return static_pkl_type (standard);
676 CPL::static_pkl_type (Standard standard)
679 case Standard::INTEROP:
680 return "text/xml;asdcpKind=CPL";
681 case Standard::SMPTE:
689 CPL::duration () const
692 for (auto i: _reels) {
700 CPL::set_version_number (int v)
703 throw BadSettingError ("CPL version number cannot be negative");
711 CPL::unset_version_number ()
713 _version_number = boost::none;
718 CPL::set_content_versions (vector<ContentVersion> v)
720 std::set<string> ids;
722 if (!ids.insert(i.id).second) {
723 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
727 _content_versions = v;
731 optional<ContentVersion>
732 CPL::content_version () const
734 if (_content_versions.empty()) {
735 return optional<ContentVersion>();
738 return _content_versions[0];
743 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
745 _additional_subtitle_languages.clear ();
746 for (auto const& i: langs) {
747 _additional_subtitle_languages.push_back (i.to_string());
753 CPL::set_main_picture_active_area(dcp::Size area)
755 if (area.width % 2) {
756 throw BadSettingError("Main picture active area width is not a multiple of 2");
759 if (area.height % 2) {
760 throw BadSettingError("Main picture active area height is not a multiple of 2");
763 _main_picture_active_area = area;