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 _content_kind = ContentKind::from_name(f.string_child("ContentKind"));
126 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
127 if (content_version) {
128 /* XXX: SMPTE should insist that Id is present */
129 _content_versions.push_back (
131 content_version->optional_string_child("Id").get_value_or(""),
132 content_version->string_child("LabelText")
135 content_version->done ();
136 } else if (_standard == Standard::SMPTE) {
137 /* ContentVersion is required in SMPTE */
138 throw XMLError ("Missing ContentVersion tag in CPL");
140 auto rating_list = f.node_child ("RatingList");
141 for (auto i: rating_list->node_children("Rating")) {
142 _ratings.push_back (Rating(i));
145 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
146 _reels.push_back (make_shared<Reel>(i, _standard));
149 auto reel_list = f.node_child ("ReelList");
150 auto reels = reel_list->node_children("Reel");
151 if (!reels.empty()) {
152 auto asset_list = reels.front()->node_child("AssetList");
153 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
155 read_composition_metadata_asset (metadata);
156 _read_composition_metadata = true;
160 f.ignore_child ("Issuer");
161 f.ignore_child ("Signer");
162 f.ignore_child ("Signature");
169 CPL::add (std::shared_ptr<Reel> reel)
171 _reels.push_back (reel);
176 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
183 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
186 xmlpp::Element* root;
187 if (_standard == Standard::INTEROP) {
188 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
190 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
193 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
194 if (_annotation_text) {
195 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
197 root->add_child("IssueDate")->add_child_text (_issue_date);
198 root->add_child("Issuer")->add_child_text (_issuer);
199 root->add_child("Creator")->add_child_text (_creator);
200 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
201 root->add_child("ContentKind")->add_child_text(_content_kind.name());
202 if (_content_versions.empty()) {
206 _content_versions[0].as_xml (root);
209 auto rating_list = root->add_child("RatingList");
210 for (auto i: _ratings) {
211 i.as_xml (rating_list->add_child("Rating"));
214 auto reel_list = root->add_child ("ReelList");
216 if (_reels.empty()) {
217 throw NoReelsError ();
221 for (auto i: _reels) {
222 auto asset_list = i->write_to_cpl (reel_list, _standard);
223 if (first && _standard == Standard::SMPTE) {
224 maybe_write_composition_metadata_asset (asset_list);
232 signer->sign (root, _standard);
235 doc.write_to_file_formatted (file.string(), "UTF-8");
242 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
244 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
246 /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
247 * apparently didn't include it, so as usual we have to be defensive.
249 if (auto fctt = node->optional_node_child("FullContentTitleText")) {
250 _full_content_title_text = fctt->content();
251 _full_content_title_text_language = fctt->optional_string_attribute("language");
254 _release_territory = node->optional_string_child("ReleaseTerritory");
255 if (_release_territory) {
256 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
259 auto vn = node->optional_node_child("VersionNumber");
261 _version_number = raw_convert<int>(vn->content());
262 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
263 auto vn_status = vn->optional_string_attribute("status");
265 _status = string_to_status (*vn_status);
269 _chain = node->optional_string_child("Chain");
270 _distributor = node->optional_string_child("Distributor");
271 _facility = node->optional_string_child("Facility");
273 auto acv = node->optional_node_child("AlternateContentVersionList");
275 for (auto i: acv->node_children("ContentVersion")) {
276 _content_versions.push_back (ContentVersion(i));
280 auto lum = node->optional_node_child("Luminance");
282 _luminance = Luminance (lum);
285 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
287 auto sr = node->optional_string_child("MainSoundSampleRate");
289 vector<string> sr_bits;
290 boost::split (sr_bits, *sr, boost::is_any_of(" "));
291 DCP_ASSERT (sr_bits.size() == 2);
292 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
295 _main_picture_stored_area = dcp::Size (
296 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
297 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
300 _main_picture_active_area = dcp::Size (
301 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
302 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
305 auto sll = node->optional_string_child("MainSubtitleLanguageList");
307 vector<string> sll_split;
308 boost::split (sll_split, *sll, boost::is_any_of(" "));
309 DCP_ASSERT (!sll_split.empty());
311 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
313 if (!_reels.empty()) {
314 auto sub = _reels.front()->main_subtitle();
316 auto lang = sub->language();
317 if (lang && lang == sll_split[0]) {
323 for (auto i = first; i < sll_split.size(); ++i) {
324 _additional_subtitle_languages.push_back (sll_split[i]);
328 auto eml = node->optional_node_child ("ExtensionMetadataList");
330 for (auto i: eml->node_children("ExtensionMetadata")) {
331 auto name = i->optional_string_child("Name");
332 if (name && *name == "Sign Language Video") {
333 auto property_list = i->node_child("PropertyList");
334 for (auto j: property_list->node_children("Property")) {
335 auto name = j->optional_string_child("Name");
336 auto value = j->optional_string_child("Value");
337 if (name && value && *name == "Language Tag") {
338 _sign_language_video_language = *value;
347 /** Write a CompositionMetadataAsset node as a child of @param node provided
348 * the required metadata is stored in the object. If any required metadata
349 * is missing this method will do nothing.
352 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
355 !_main_sound_configuration ||
356 !_main_sound_sample_rate ||
357 !_main_picture_stored_area ||
358 !_main_picture_active_area ||
360 !_reels.front()->main_picture()) {
364 auto meta = node->add_child("meta:CompositionMetadataAsset");
365 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
367 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
369 auto mp = _reels.front()->main_picture();
370 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
371 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
373 auto fctt = meta->add_child("FullContentTitleText", "meta");
374 if (_full_content_title_text && !_full_content_title_text->empty()) {
375 fctt->add_child_text (*_full_content_title_text);
377 if (_full_content_title_text_language) {
378 fctt->set_attribute("language", *_full_content_title_text_language);
381 if (_release_territory) {
382 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
385 if (_version_number) {
386 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
387 vn->add_child_text(raw_convert<string>(*_version_number));
389 vn->set_attribute("status", status_to_string(*_status));
394 meta->add_child("Chain", "meta")->add_child_text(*_chain);
398 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
402 meta->add_child("Facility", "meta")->add_child_text(*_facility);
405 if (_content_versions.size() > 1) {
406 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
407 for (size_t i = 1; i < _content_versions.size(); ++i) {
408 _content_versions[i].as_xml (vc);
413 _luminance->as_xml (meta, "meta");
416 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
417 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
419 auto stored = meta->add_child("MainPictureStoredArea", "meta");
420 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
421 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
423 auto active = meta->add_child("MainPictureActiveArea", "meta");
424 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
425 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
427 optional<string> first_subtitle_language;
428 for (auto i: _reels) {
429 if (i->main_subtitle()) {
430 first_subtitle_language = i->main_subtitle()->language();
431 if (first_subtitle_language) {
437 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
439 if (first_subtitle_language) {
440 lang = *first_subtitle_language;
442 for (auto const& i: _additional_subtitle_languages) {
448 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
451 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
453 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
454 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
455 extension->set_attribute("scope", scope);
456 extension->add_child("Name", "meta")->add_child_text(name);
457 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
458 property->add_child("Name", "meta")->add_child_text(property_name);
459 property->add_child("Value", "meta")->add_child_text(property_value);
462 /* SMPTE Bv2.1 8.6.3 */
463 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
465 if (_sign_language_video_language) {
466 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
469 if (_reels.front()->main_sound()) {
470 auto asset = _reels.front()->main_sound()->asset();
472 auto reader = asset->start_read ();
473 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
474 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
475 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
476 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
479 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
480 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
481 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
482 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
483 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
485 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
486 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
487 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
488 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
489 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
490 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
491 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
492 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
493 if (!soundfield->MCATagName.empty()) {
494 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
495 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
497 if (!soundfield->RFC5646SpokenLanguage.empty()) {
498 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
499 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
502 list<ASDCP::MXF::InterchangeObject*> channels;
503 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
504 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
508 for (auto i: channels) {
509 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
510 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
511 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
512 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
513 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
514 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
515 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
516 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
517 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
518 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
519 if (!channel->MCATagName.empty()) {
520 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
521 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
523 if (!channel->MCAChannelID.empty()) {
524 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
526 if (!channel->RFC5646SpokenLanguage.empty()) {
527 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
528 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
530 if (!channel->SoundfieldGroupLinkID.empty()) {
531 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
532 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
543 add_file_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 assets.push_back (i->main_subtitle());
555 for (auto j: i->closed_captions()) {
556 assets.push_back (j);
559 assets.push_back (i->atmos());
565 vector<shared_ptr<ReelFileAsset>>
566 CPL::reel_file_assets ()
568 vector<shared_ptr<ReelFileAsset>> c;
569 add_file_assets (c, _reels);
574 vector<shared_ptr<const ReelFileAsset>>
575 CPL::reel_file_assets () const
577 vector<shared_ptr<const ReelFileAsset>> c;
578 add_file_assets (c, _reels);
584 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
586 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
591 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
592 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
593 note (NoteType::ERROR, s);
597 if (_content_kind != other_cpl->_content_kind) {
598 note (NoteType::ERROR, "CPL: content kinds differ");
602 if (_reels.size() != other_cpl->_reels.size()) {
603 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
607 auto a = _reels.begin();
608 auto b = other_cpl->_reels.begin();
610 while (a != _reels.end ()) {
611 if (!(*a)->equals (*b, opt, note)) {
623 CPL::any_encrypted () const
625 for (auto i: _reels) {
626 if (i->any_encrypted()) {
636 CPL::all_encrypted () const
638 for (auto i: _reels) {
639 if (!i->all_encrypted()) {
649 CPL::add (DecryptedKDM const & kdm)
651 for (auto i: _reels) {
657 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
659 for (auto i: _reels) {
660 i->resolve_refs (assets);
665 CPL::pkl_type (Standard standard) const
667 return static_pkl_type (standard);
671 CPL::static_pkl_type (Standard standard)
674 case Standard::INTEROP:
675 return "text/xml;asdcpKind=CPL";
676 case Standard::SMPTE:
684 CPL::duration () const
687 for (auto i: _reels) {
695 CPL::set_version_number (int v)
698 throw BadSettingError ("CPL version number cannot be negative");
706 CPL::unset_version_number ()
708 _version_number = boost::none;
713 CPL::set_content_versions (vector<ContentVersion> v)
715 std::set<string> ids;
717 if (!ids.insert(i.id).second) {
718 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
722 _content_versions = v;
726 optional<ContentVersion>
727 CPL::content_version () const
729 if (_content_versions.empty()) {
730 return optional<ContentVersion>();
733 return _content_versions[0];
738 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
740 _additional_subtitle_languages.clear ();
741 for (auto const& i: langs) {
742 _additional_subtitle_languages.push_back (i.to_string());