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, bool include_mca_subdescriptors) 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, include_mca_subdescriptors);
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 if (auto msc = node->optional_string_child("MainSoundConfiguration")) {
293 _main_sound_configuration = MainSoundConfiguration(*msc);
294 } catch (MainSoundConfigurationError& e) {
295 /* With Interop DCPs this node may not make any sense, but that's OK */
296 if (_standard == dcp::Standard::SMPTE) {
302 auto sr = node->optional_string_child("MainSoundSampleRate");
304 vector<string> sr_bits;
305 boost::split (sr_bits, *sr, boost::is_any_of(" "));
306 DCP_ASSERT (sr_bits.size() == 2);
307 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
310 if (_standard == dcp::Standard::SMPTE) {
311 _main_picture_stored_area = dcp::Size (
312 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
313 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
317 _main_picture_active_area = dcp::Size (
318 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
319 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
322 auto sll = node->optional_string_child("MainSubtitleLanguageList");
324 vector<string> sll_split;
325 boost::split (sll_split, *sll, boost::is_any_of(" "));
326 DCP_ASSERT (!sll_split.empty());
328 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
330 if (!_reels.empty()) {
331 auto sub = _reels.front()->main_subtitle();
333 auto lang = sub->language();
334 if (lang && lang == sll_split[0]) {
340 for (auto i = first; i < sll_split.size(); ++i) {
341 _additional_subtitle_languages.push_back (sll_split[i]);
345 auto eml = node->optional_node_child ("ExtensionMetadataList");
347 for (auto i: eml->node_children("ExtensionMetadata")) {
348 auto name = i->optional_string_child("Name");
349 if (name && *name == "Sign Language Video") {
350 auto property_list = i->node_child("PropertyList");
351 for (auto j: property_list->node_children("Property")) {
352 auto name = j->optional_string_child("Name");
353 auto value = j->optional_string_child("Value");
354 if (name && value && *name == "Language Tag") {
355 _sign_language_video_language = *value;
365 CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsset> asset) const
367 auto reader = asset->start_read ();
368 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
369 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
370 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
371 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
374 auto mca_subs = parent->add_child("mca:MCASubDescriptors");
375 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
376 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
377 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
378 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
380 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
381 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
382 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
383 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
384 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
385 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
386 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
387 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
388 if (!soundfield->MCATagName.empty()) {
389 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
390 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
392 if (!soundfield->RFC5646SpokenLanguage.empty()) {
393 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
394 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
397 /* Find the MCA subdescriptors in the MXF so that we can also write them here */
398 list<ASDCP::MXF::InterchangeObject*> channels;
399 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
400 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
404 for (auto i: channels) {
405 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
406 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
407 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
408 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
409 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
410 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
411 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
412 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
413 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
414 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
415 if (!channel->MCATagName.empty()) {
416 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
417 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
419 if (!channel->MCAChannelID.empty()) {
420 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
422 if (!channel->RFC5646SpokenLanguage.empty()) {
423 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
424 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
426 if (!channel->SoundfieldGroupLinkID.empty()) {
427 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
428 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
435 /** Write a CompositionMetadataAsset node as a child of @param node provided
436 * the required metadata is stored in the object. If any required metadata
437 * is missing this method will do nothing.
440 CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node, bool include_mca_subdescriptors) const
443 !_main_sound_configuration ||
444 !_main_sound_sample_rate ||
445 !_main_picture_stored_area ||
446 !_main_picture_active_area ||
448 !_reels.front()->main_picture()) {
452 auto meta = node->add_child("meta:CompositionMetadataAsset");
453 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
455 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
457 auto mp = _reels.front()->main_picture();
458 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
459 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
461 auto fctt = meta->add_child("FullContentTitleText", "meta");
462 if (_full_content_title_text && !_full_content_title_text->empty()) {
463 fctt->add_child_text (*_full_content_title_text);
465 if (_full_content_title_text_language) {
466 fctt->set_attribute("language", *_full_content_title_text_language);
469 if (_release_territory) {
470 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
473 if (_version_number) {
474 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
475 vn->add_child_text(raw_convert<string>(*_version_number));
477 vn->set_attribute("status", status_to_string(*_status));
482 meta->add_child("Chain", "meta")->add_child_text(*_chain);
486 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
490 meta->add_child("Facility", "meta")->add_child_text(*_facility);
493 if (_content_versions.size() > 1) {
494 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
495 for (size_t i = 1; i < _content_versions.size(); ++i) {
496 _content_versions[i].as_xml (vc);
501 _luminance->as_xml (meta, "meta");
504 if (_main_sound_configuration) {
505 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(_main_sound_configuration->to_string());
507 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
509 auto stored = meta->add_child("MainPictureStoredArea", "meta");
510 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
511 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
513 auto active = meta->add_child("MainPictureActiveArea", "meta");
514 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
515 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
517 optional<string> first_subtitle_language;
518 for (auto i: _reels) {
519 if (i->main_subtitle()) {
520 first_subtitle_language = i->main_subtitle()->language();
521 if (first_subtitle_language) {
527 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
529 if (first_subtitle_language) {
530 lang = *first_subtitle_language;
532 for (auto const& i: _additional_subtitle_languages) {
538 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
541 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
543 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
544 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
545 extension->set_attribute("scope", scope);
546 extension->add_child("Name", "meta")->add_child_text(name);
547 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
548 property->add_child("Name", "meta")->add_child_text(property_name);
549 property->add_child("Value", "meta")->add_child_text(property_value);
552 /* SMPTE Bv2.1 8.6.3 */
553 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
555 if (_sign_language_video_language) {
556 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
559 if (_reels.front()->main_sound()) {
560 auto asset = _reels.front()->main_sound()->asset();
561 if (asset && include_mca_subdescriptors) {
562 write_mca_subdescriptors(meta, asset);
570 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
572 for (auto i: reels) {
573 if (i->main_picture ()) {
574 assets.push_back (i->main_picture());
576 if (i->main_sound ()) {
577 assets.push_back (i->main_sound());
579 if (i->main_subtitle ()) {
580 assets.push_back (i->main_subtitle());
582 for (auto j: i->closed_captions()) {
583 assets.push_back (j);
586 assets.push_back (i->atmos());
592 vector<shared_ptr<ReelFileAsset>>
593 CPL::reel_file_assets ()
595 vector<shared_ptr<ReelFileAsset>> c;
596 add_file_assets (c, _reels);
601 vector<shared_ptr<const ReelFileAsset>>
602 CPL::reel_file_assets () const
604 vector<shared_ptr<const ReelFileAsset>> c;
605 add_file_assets (c, _reels);
611 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
613 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
618 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
619 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
620 note (NoteType::ERROR, s);
624 if (_content_kind != other_cpl->_content_kind) {
625 note (NoteType::ERROR, "CPL: content kinds differ");
629 if (_reels.size() != other_cpl->_reels.size()) {
630 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
634 auto a = _reels.begin();
635 auto b = other_cpl->_reels.begin();
637 while (a != _reels.end ()) {
638 if (!(*a)->equals (*b, opt, note)) {
650 CPL::any_encrypted () const
652 for (auto i: _reels) {
653 if (i->any_encrypted()) {
663 CPL::all_encrypted () const
665 for (auto i: _reels) {
666 if (!i->all_encrypted()) {
676 CPL::add (DecryptedKDM const & kdm)
678 for (auto i: _reels) {
684 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
686 for (auto i: _reels) {
687 i->resolve_refs (assets);
692 CPL::pkl_type (Standard standard) const
694 return static_pkl_type (standard);
698 CPL::static_pkl_type (Standard standard)
701 case Standard::INTEROP:
702 return "text/xml;asdcpKind=CPL";
703 case Standard::SMPTE:
711 CPL::duration () const
714 for (auto i: _reels) {
722 CPL::set_version_number (int v)
725 throw BadSettingError ("CPL version number cannot be negative");
733 CPL::unset_version_number ()
735 _version_number = boost::none;
740 CPL::set_content_versions (vector<ContentVersion> v)
742 std::set<string> ids;
744 if (!ids.insert(i.id).second) {
745 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
749 _content_versions = v;
753 optional<ContentVersion>
754 CPL::content_version () const
756 if (_content_versions.empty()) {
757 return optional<ContentVersion>();
760 return _content_versions[0];
765 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
767 _additional_subtitle_languages.clear ();
768 for (auto const& i: langs) {
769 _additional_subtitle_languages.push_back (i.to_string());
775 CPL::set_main_picture_active_area(dcp::Size area)
777 if (area.width % 2) {
778 throw BadSettingError("Main picture active area width is not a multiple of 2");
781 if (area.height % 2) {
782 throw BadSettingError("Main picture active area height is not a multiple of 2");
785 _main_picture_active_area = area;