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 = content_kind_from_string (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);
159 f.ignore_child ("Issuer");
160 f.ignore_child ("Signer");
161 f.ignore_child ("Signature");
168 CPL::add (std::shared_ptr<Reel> reel)
170 _reels.push_back (reel);
175 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
182 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
185 xmlpp::Element* root;
186 if (_standard == Standard::INTEROP) {
187 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
189 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
192 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
193 if (_annotation_text) {
194 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
196 root->add_child("IssueDate")->add_child_text (_issue_date);
197 root->add_child("Issuer")->add_child_text (_issuer);
198 root->add_child("Creator")->add_child_text (_creator);
199 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
200 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
201 if (_content_versions.empty()) {
205 _content_versions[0].as_xml (root);
208 auto rating_list = root->add_child("RatingList");
209 for (auto i: _ratings) {
210 i.as_xml (rating_list->add_child("Rating"));
213 auto reel_list = root->add_child ("ReelList");
215 if (_reels.empty()) {
216 throw NoReelsError ();
220 for (auto i: _reels) {
221 auto asset_list = i->write_to_cpl (reel_list, _standard);
222 if (first && _standard == Standard::SMPTE) {
223 maybe_write_composition_metadata_asset (asset_list);
231 signer->sign (root, _standard);
234 doc.write_to_file_formatted (file.string(), "UTF-8");
241 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
243 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
245 auto fctt = node->node_child("FullContentTitleText");
246 _full_content_title_text = fctt->content();
247 _full_content_title_text_language = fctt->optional_string_attribute("language");
249 _release_territory = node->optional_string_child("ReleaseTerritory");
250 if (_release_territory) {
251 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
254 auto vn = node->optional_node_child("VersionNumber");
256 _version_number = raw_convert<int>(vn->content());
257 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
258 auto vn_status = vn->optional_string_attribute("status");
260 _status = string_to_status (*vn_status);
264 _chain = node->optional_string_child("Chain");
265 _distributor = node->optional_string_child("Distributor");
266 _facility = node->optional_string_child("Facility");
268 auto acv = node->optional_node_child("AlternateContentVersionList");
270 for (auto i: acv->node_children("ContentVersion")) {
271 _content_versions.push_back (ContentVersion(i));
275 auto lum = node->optional_node_child("Luminance");
277 _luminance = Luminance (lum);
280 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
282 auto sr = node->optional_string_child("MainSoundSampleRate");
284 vector<string> sr_bits;
285 boost::split (sr_bits, *sr, boost::is_any_of(" "));
286 DCP_ASSERT (sr_bits.size() == 2);
287 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
290 _main_picture_stored_area = dcp::Size (
291 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
292 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
295 _main_picture_active_area = dcp::Size (
296 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
297 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
300 auto sll = node->optional_string_child("MainSubtitleLanguageList");
302 vector<string> sll_split;
303 boost::split (sll_split, *sll, boost::is_any_of(" "));
304 DCP_ASSERT (!sll_split.empty());
306 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
308 if (!_reels.empty()) {
309 auto sub = _reels.front()->main_subtitle();
311 auto lang = sub->language();
312 if (lang && lang == sll_split[0]) {
318 for (auto i = first; i < sll_split.size(); ++i) {
319 _additional_subtitle_languages.push_back (sll_split[i]);
323 auto eml = node->optional_node_child ("ExtensionMetadataList");
325 for (auto i: eml->node_children("ExtensionMetadata")) {
326 auto name = i->optional_string_child("Name");
327 if (name && *name == "Sign Language Video") {
328 auto property_list = i->node_child("PropertyList");
329 for (auto j: property_list->node_children("Property")) {
330 auto name = j->optional_string_child("Name");
331 auto value = j->optional_string_child("Value");
332 if (name && value && *name == "Language Tag") {
333 _sign_language_video_language = *value;
342 /** Write a CompositionMetadataAsset node as a child of @param node provided
343 * the required metadata is stored in the object. If any required metadata
344 * is missing this method will do nothing.
347 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
350 !_main_sound_configuration ||
351 !_main_sound_sample_rate ||
352 !_main_picture_stored_area ||
353 !_main_picture_active_area ||
355 !_reels.front()->main_picture()) {
359 auto meta = node->add_child("meta:CompositionMetadataAsset");
360 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
362 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
364 auto mp = _reels.front()->main_picture();
365 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
366 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
368 auto fctt = meta->add_child("FullContentTitleText", "meta");
369 if (_full_content_title_text && !_full_content_title_text->empty()) {
370 fctt->add_child_text (*_full_content_title_text);
372 if (_full_content_title_text_language) {
373 fctt->set_attribute("language", *_full_content_title_text_language);
376 if (_release_territory) {
377 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
380 if (_version_number) {
381 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
382 vn->add_child_text(raw_convert<string>(*_version_number));
384 vn->set_attribute("status", status_to_string(*_status));
389 meta->add_child("Chain", "meta")->add_child_text(*_chain);
393 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
397 meta->add_child("Facility", "meta")->add_child_text(*_facility);
400 if (_content_versions.size() > 1) {
401 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
402 for (size_t i = 1; i < _content_versions.size(); ++i) {
403 _content_versions[i].as_xml (vc);
408 _luminance->as_xml (meta, "meta");
411 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
412 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
414 auto stored = meta->add_child("MainPictureStoredArea", "meta");
415 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
416 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
418 auto active = meta->add_child("MainPictureActiveArea", "meta");
419 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
420 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
422 optional<string> first_subtitle_language;
423 for (auto i: _reels) {
424 if (i->main_subtitle()) {
425 first_subtitle_language = i->main_subtitle()->language();
426 if (first_subtitle_language) {
432 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
434 if (first_subtitle_language) {
435 lang = *first_subtitle_language;
437 for (auto const& i: _additional_subtitle_languages) {
443 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
446 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
448 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
449 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
450 extension->set_attribute("scope", scope);
451 extension->add_child("Name", "meta")->add_child_text(name);
452 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
453 property->add_child("Name", "meta")->add_child_text(property_name);
454 property->add_child("Value", "meta")->add_child_text(property_value);
457 /* SMPTE Bv2.1 8.6.3 */
458 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
460 if (_sign_language_video_language) {
461 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
464 if (_reels.front()->main_sound()) {
465 auto asset = _reels.front()->main_sound()->asset();
467 auto reader = asset->start_read ();
468 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
469 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
470 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
471 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
474 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
475 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
476 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
477 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
478 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
480 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
481 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
482 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
483 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
484 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
485 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
486 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
487 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
488 if (!soundfield->MCATagName.empty()) {
489 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
490 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
492 if (!soundfield->RFC5646SpokenLanguage.empty()) {
493 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
494 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
497 list<ASDCP::MXF::InterchangeObject*> channels;
498 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
499 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
503 for (auto i: channels) {
504 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
505 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
506 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
507 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
508 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
509 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
510 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
511 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
512 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
513 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
514 if (!channel->MCATagName.empty()) {
515 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
516 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
518 if (!channel->MCAChannelID.empty()) {
519 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
521 if (!channel->RFC5646SpokenLanguage.empty()) {
522 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
523 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
525 if (!channel->SoundfieldGroupLinkID.empty()) {
526 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
527 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
538 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
540 for (auto i: reels) {
541 if (i->main_picture ()) {
542 assets.push_back (i->main_picture());
544 if (i->main_sound ()) {
545 assets.push_back (i->main_sound());
547 if (i->main_subtitle ()) {
548 assets.push_back (i->main_subtitle());
550 for (auto j: i->closed_captions()) {
551 assets.push_back (j);
554 assets.push_back (i->atmos());
560 vector<shared_ptr<ReelFileAsset>>
561 CPL::reel_file_assets ()
563 vector<shared_ptr<ReelFileAsset>> c;
564 add_file_assets (c, _reels);
569 vector<shared_ptr<const ReelFileAsset>>
570 CPL::reel_file_assets () const
572 vector<shared_ptr<const ReelFileAsset>> c;
573 add_file_assets (c, _reels);
579 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
581 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
586 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
587 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
588 note (NoteType::ERROR, s);
592 if (_content_kind != other_cpl->_content_kind) {
593 note (NoteType::ERROR, "CPL: content kinds differ");
597 if (_reels.size() != other_cpl->_reels.size()) {
598 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
602 auto a = _reels.begin();
603 auto b = other_cpl->_reels.begin();
605 while (a != _reels.end ()) {
606 if (!(*a)->equals (*b, opt, note)) {
618 CPL::any_encrypted () const
620 for (auto i: _reels) {
621 if (i->any_encrypted()) {
631 CPL::all_encrypted () const
633 for (auto i: _reels) {
634 if (!i->all_encrypted()) {
644 CPL::add (DecryptedKDM const & kdm)
646 for (auto i: _reels) {
652 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
654 for (auto i: _reels) {
655 i->resolve_refs (assets);
660 CPL::pkl_type (Standard standard) const
662 return static_pkl_type (standard);
666 CPL::static_pkl_type (Standard standard)
669 case Standard::INTEROP:
670 return "text/xml;asdcpKind=CPL";
671 case Standard::SMPTE:
679 CPL::duration () const
682 for (auto i: _reels) {
690 CPL::set_version_number (int v)
693 throw BadSettingError ("CPL version number cannot be negative");
701 CPL::unset_version_number ()
703 _version_number = boost::none;
708 CPL::set_content_versions (vector<ContentVersion> v)
710 std::set<string> ids;
712 if (!ids.insert(i.id).second) {
713 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
717 _content_versions = v;
721 optional<ContentVersion>
722 CPL::content_version () const
724 if (_content_versions.empty()) {
725 return optional<ContentVersion>();
728 return _content_versions[0];
733 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
735 _additional_subtitle_languages.clear ();
736 for (auto const& i: langs) {
737 _additional_subtitle_languages.push_back (i.to_string());