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) 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);
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;
355 /** Write a CompositionMetadataAsset node as a child of @param node provided
356 * the required metadata is stored in the object. If any required metadata
357 * is missing this method will do nothing.
360 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
363 !_main_sound_configuration ||
364 !_main_sound_sample_rate ||
365 !_main_picture_stored_area ||
366 !_main_picture_active_area ||
368 !_reels.front()->main_picture()) {
372 auto meta = node->add_child("meta:CompositionMetadataAsset");
373 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
375 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
377 auto mp = _reels.front()->main_picture();
378 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
379 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
381 auto fctt = meta->add_child("FullContentTitleText", "meta");
382 if (_full_content_title_text && !_full_content_title_text->empty()) {
383 fctt->add_child_text (*_full_content_title_text);
385 if (_full_content_title_text_language) {
386 fctt->set_attribute("language", *_full_content_title_text_language);
389 if (_release_territory) {
390 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
393 if (_version_number) {
394 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
395 vn->add_child_text(raw_convert<string>(*_version_number));
397 vn->set_attribute("status", status_to_string(*_status));
402 meta->add_child("Chain", "meta")->add_child_text(*_chain);
406 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
410 meta->add_child("Facility", "meta")->add_child_text(*_facility);
413 if (_content_versions.size() > 1) {
414 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
415 for (size_t i = 1; i < _content_versions.size(); ++i) {
416 _content_versions[i].as_xml (vc);
421 _luminance->as_xml (meta, "meta");
424 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
425 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
427 auto stored = meta->add_child("MainPictureStoredArea", "meta");
428 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
429 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
431 auto active = meta->add_child("MainPictureActiveArea", "meta");
432 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
433 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
435 optional<string> first_subtitle_language;
436 for (auto i: _reels) {
437 if (i->main_subtitle()) {
438 first_subtitle_language = i->main_subtitle()->language();
439 if (first_subtitle_language) {
445 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
447 if (first_subtitle_language) {
448 lang = *first_subtitle_language;
450 for (auto const& i: _additional_subtitle_languages) {
456 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
459 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
461 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
462 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
463 extension->set_attribute("scope", scope);
464 extension->add_child("Name", "meta")->add_child_text(name);
465 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
466 property->add_child("Name", "meta")->add_child_text(property_name);
467 property->add_child("Value", "meta")->add_child_text(property_value);
470 /* SMPTE Bv2.1 8.6.3 */
471 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
473 if (_sign_language_video_language) {
474 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
477 if (_reels.front()->main_sound()) {
478 auto asset = _reels.front()->main_sound()->asset();
480 auto reader = asset->start_read ();
481 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
482 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
483 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
484 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
487 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
488 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
489 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
490 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
491 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
493 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
494 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
495 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
496 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
497 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
498 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
499 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
500 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
501 if (!soundfield->MCATagName.empty()) {
502 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
503 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
505 if (!soundfield->RFC5646SpokenLanguage.empty()) {
506 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
507 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
510 list<ASDCP::MXF::InterchangeObject*> channels;
511 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
512 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
516 for (auto i: channels) {
517 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
518 if (static_cast<int>(channel->MCAChannelID) > asset->channels()) {
521 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
522 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
523 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
524 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
525 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
526 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
527 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
528 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
529 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
530 if (!channel->MCATagName.empty()) {
531 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
532 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
534 if (!channel->MCAChannelID.empty()) {
535 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
537 if (!channel->RFC5646SpokenLanguage.empty()) {
538 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
539 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
541 if (!channel->SoundfieldGroupLinkID.empty()) {
542 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
543 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
554 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
556 for (auto i: reels) {
557 if (i->main_picture ()) {
558 assets.push_back (i->main_picture());
560 if (i->main_sound ()) {
561 assets.push_back (i->main_sound());
563 if (i->main_subtitle ()) {
564 assets.push_back (i->main_subtitle());
566 for (auto j: i->closed_captions()) {
567 assets.push_back (j);
570 assets.push_back (i->atmos());
576 vector<shared_ptr<ReelFileAsset>>
577 CPL::reel_file_assets ()
579 vector<shared_ptr<ReelFileAsset>> c;
580 add_file_assets (c, _reels);
585 vector<shared_ptr<const ReelFileAsset>>
586 CPL::reel_file_assets () const
588 vector<shared_ptr<const ReelFileAsset>> c;
589 add_file_assets (c, _reels);
595 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
597 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
602 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
603 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
604 note (NoteType::ERROR, s);
608 if (_content_kind != other_cpl->_content_kind) {
609 note (NoteType::ERROR, "CPL: content kinds differ");
613 if (_reels.size() != other_cpl->_reels.size()) {
614 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
618 auto a = _reels.begin();
619 auto b = other_cpl->_reels.begin();
621 while (a != _reels.end ()) {
622 if (!(*a)->equals (*b, opt, note)) {
634 CPL::any_encrypted () const
636 for (auto i: _reels) {
637 if (i->any_encrypted()) {
647 CPL::all_encrypted () const
649 for (auto i: _reels) {
650 if (!i->all_encrypted()) {
660 CPL::add (DecryptedKDM const & kdm)
662 for (auto i: _reels) {
668 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
670 for (auto i: _reels) {
671 i->resolve_refs (assets);
676 CPL::pkl_type (Standard standard) const
678 return static_pkl_type (standard);
682 CPL::static_pkl_type (Standard standard)
685 case Standard::INTEROP:
686 return "text/xml;asdcpKind=CPL";
687 case Standard::SMPTE:
695 CPL::duration () const
698 for (auto i: _reels) {
706 CPL::set_version_number (int v)
709 throw BadSettingError ("CPL version number cannot be negative");
717 CPL::unset_version_number ()
719 _version_number = boost::none;
724 CPL::set_content_versions (vector<ContentVersion> v)
726 std::set<string> ids;
728 if (!ids.insert(i.id).second) {
729 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
733 _content_versions = v;
737 optional<ContentVersion>
738 CPL::content_version () const
740 if (_content_versions.empty()) {
741 return optional<ContentVersion>();
744 return _content_versions[0];
749 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
751 _additional_subtitle_languages.clear ();
752 for (auto const& i: langs) {
753 _additional_subtitle_languages.push_back (i.to_string());
759 CPL::set_main_picture_active_area(dcp::Size area)
761 if (area.width % 2) {
762 throw BadSettingError("Main picture active area width is not a multiple of 2");
765 if (area.height % 2) {
766 throw BadSettingError("Main picture active area height is not a multiple of 2");
769 _main_picture_active_area = area;