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;
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 list<ASDCP::MXF::InterchangeObject*> channels;
389 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
390 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
394 for (auto i: channels) {
395 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
396 if (static_cast<int>(channel->MCAChannelID) > asset->channels()) {
399 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
400 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
401 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
402 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
403 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
404 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
405 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
406 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
407 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
408 if (!channel->MCATagName.empty()) {
409 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
410 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
412 if (!channel->MCAChannelID.empty()) {
413 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
415 if (!channel->RFC5646SpokenLanguage.empty()) {
416 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
417 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
419 if (!channel->SoundfieldGroupLinkID.empty()) {
420 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
421 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
428 /** Write a CompositionMetadataAsset node as a child of @param node provided
429 * the required metadata is stored in the object. If any required metadata
430 * is missing this method will do nothing.
433 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
436 !_main_sound_configuration ||
437 !_main_sound_sample_rate ||
438 !_main_picture_stored_area ||
439 !_main_picture_active_area ||
441 !_reels.front()->main_picture()) {
445 auto meta = node->add_child("meta:CompositionMetadataAsset");
446 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
448 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
450 auto mp = _reels.front()->main_picture();
451 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
452 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
454 auto fctt = meta->add_child("FullContentTitleText", "meta");
455 if (_full_content_title_text && !_full_content_title_text->empty()) {
456 fctt->add_child_text (*_full_content_title_text);
458 if (_full_content_title_text_language) {
459 fctt->set_attribute("language", *_full_content_title_text_language);
462 if (_release_territory) {
463 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
466 if (_version_number) {
467 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
468 vn->add_child_text(raw_convert<string>(*_version_number));
470 vn->set_attribute("status", status_to_string(*_status));
475 meta->add_child("Chain", "meta")->add_child_text(*_chain);
479 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
483 meta->add_child("Facility", "meta")->add_child_text(*_facility);
486 if (_content_versions.size() > 1) {
487 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
488 for (size_t i = 1; i < _content_versions.size(); ++i) {
489 _content_versions[i].as_xml (vc);
494 _luminance->as_xml (meta, "meta");
497 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
498 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
500 auto stored = meta->add_child("MainPictureStoredArea", "meta");
501 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
502 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
504 auto active = meta->add_child("MainPictureActiveArea", "meta");
505 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
506 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
508 optional<string> first_subtitle_language;
509 for (auto i: _reels) {
510 if (i->main_subtitle()) {
511 first_subtitle_language = i->main_subtitle()->language();
512 if (first_subtitle_language) {
518 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
520 if (first_subtitle_language) {
521 lang = *first_subtitle_language;
523 for (auto const& i: _additional_subtitle_languages) {
529 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
532 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
534 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
535 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
536 extension->set_attribute("scope", scope);
537 extension->add_child("Name", "meta")->add_child_text(name);
538 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
539 property->add_child("Name", "meta")->add_child_text(property_name);
540 property->add_child("Value", "meta")->add_child_text(property_value);
543 /* SMPTE Bv2.1 8.6.3 */
544 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
546 if (_sign_language_video_language) {
547 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
550 if (_reels.front()->main_sound()) {
551 auto asset = _reels.front()->main_sound()->asset();
553 write_mca_subdescriptors(meta, asset);
561 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
563 for (auto i: reels) {
564 if (i->main_picture ()) {
565 assets.push_back (i->main_picture());
567 if (i->main_sound ()) {
568 assets.push_back (i->main_sound());
570 if (i->main_subtitle ()) {
571 assets.push_back (i->main_subtitle());
573 for (auto j: i->closed_captions()) {
574 assets.push_back (j);
577 assets.push_back (i->atmos());
583 vector<shared_ptr<ReelFileAsset>>
584 CPL::reel_file_assets ()
586 vector<shared_ptr<ReelFileAsset>> c;
587 add_file_assets (c, _reels);
592 vector<shared_ptr<const ReelFileAsset>>
593 CPL::reel_file_assets () const
595 vector<shared_ptr<const ReelFileAsset>> c;
596 add_file_assets (c, _reels);
602 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
604 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
609 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
610 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
611 note (NoteType::ERROR, s);
615 if (_content_kind != other_cpl->_content_kind) {
616 note (NoteType::ERROR, "CPL: content kinds differ");
620 if (_reels.size() != other_cpl->_reels.size()) {
621 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
625 auto a = _reels.begin();
626 auto b = other_cpl->_reels.begin();
628 while (a != _reels.end ()) {
629 if (!(*a)->equals (*b, opt, note)) {
641 CPL::any_encrypted () const
643 for (auto i: _reels) {
644 if (i->any_encrypted()) {
654 CPL::all_encrypted () const
656 for (auto i: _reels) {
657 if (!i->all_encrypted()) {
667 CPL::add (DecryptedKDM const & kdm)
669 for (auto i: _reels) {
675 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
677 for (auto i: _reels) {
678 i->resolve_refs (assets);
683 CPL::pkl_type (Standard standard) const
685 return static_pkl_type (standard);
689 CPL::static_pkl_type (Standard standard)
692 case Standard::INTEROP:
693 return "text/xml;asdcpKind=CPL";
694 case Standard::SMPTE:
702 CPL::duration () const
705 for (auto i: _reels) {
713 CPL::set_version_number (int v)
716 throw BadSettingError ("CPL version number cannot be negative");
724 CPL::unset_version_number ()
726 _version_number = boost::none;
731 CPL::set_content_versions (vector<ContentVersion> v)
733 std::set<string> ids;
735 if (!ids.insert(i.id).second) {
736 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
740 _content_versions = v;
744 optional<ContentVersion>
745 CPL::content_version () const
747 if (_content_versions.empty()) {
748 return optional<ContentVersion>();
751 return _content_versions[0];
756 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
758 _additional_subtitle_languages.clear ();
759 for (auto const& i: langs) {
760 _additional_subtitle_languages.push_back (i.to_string());
766 CPL::set_main_picture_active_area(dcp::Size area)
768 if (area.width % 2) {
769 throw BadSettingError("Main picture active area width is not a multiple of 2");
772 if (area.height % 2) {
773 throw BadSettingError("Main picture active area height is not a multiple of 2");
776 _main_picture_active_area = area;