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 auto content_kind = f.node_child("ContentKind");
126 _content_kind = ContentKind(content_kind->content(), content_kind->optional_string_attribute("scope"));
127 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
128 if (content_version) {
129 /* XXX: SMPTE should insist that Id is present */
130 _content_versions.push_back (
132 content_version->optional_string_child("Id").get_value_or(""),
133 content_version->string_child("LabelText")
136 content_version->done ();
137 } else if (_standard == Standard::SMPTE) {
138 /* ContentVersion is required in SMPTE */
139 throw XMLError ("Missing ContentVersion tag in CPL");
141 auto rating_list = f.node_child ("RatingList");
142 for (auto i: rating_list->node_children("Rating")) {
143 _ratings.push_back (Rating(i));
146 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
147 _reels.push_back (make_shared<Reel>(i, _standard));
150 auto reel_list = f.node_child ("ReelList");
151 auto reels = reel_list->node_children("Reel");
152 if (!reels.empty()) {
153 auto asset_list = reels.front()->node_child("AssetList");
154 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
156 read_composition_metadata_asset (metadata);
157 _read_composition_metadata = true;
161 f.ignore_child ("Issuer");
162 f.ignore_child ("Signer");
163 f.ignore_child ("Signature");
170 CPL::add (std::shared_ptr<Reel> reel)
172 _reels.push_back (reel);
177 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
184 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
187 xmlpp::Element* root;
188 if (_standard == Standard::INTEROP) {
189 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
191 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
194 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
195 if (_annotation_text) {
196 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
198 root->add_child("IssueDate")->add_child_text (_issue_date);
199 root->add_child("Issuer")->add_child_text (_issuer);
200 root->add_child("Creator")->add_child_text (_creator);
201 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
202 auto content_kind = root->add_child("ContentKind");
203 content_kind->add_child_text(_content_kind.name());
204 if (_content_kind.scope()) {
205 content_kind->set_attribute("scope", *_content_kind.scope());
207 if (_content_versions.empty()) {
211 _content_versions[0].as_xml (root);
214 auto rating_list = root->add_child("RatingList");
215 for (auto i: _ratings) {
216 i.as_xml (rating_list->add_child("Rating"));
219 auto reel_list = root->add_child ("ReelList");
221 if (_reels.empty()) {
222 throw NoReelsError ();
226 for (auto i: _reels) {
227 auto asset_list = i->write_to_cpl (reel_list, _standard);
228 if (first && _standard == Standard::SMPTE) {
229 maybe_write_composition_metadata_asset (asset_list);
237 signer->sign (root, _standard);
240 doc.write_to_file_formatted (file.string(), "UTF-8");
247 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
249 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
251 /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
252 * apparently didn't include it, so as usual we have to be defensive.
254 if (auto fctt = node->optional_node_child("FullContentTitleText")) {
255 _full_content_title_text = fctt->content();
256 _full_content_title_text_language = fctt->optional_string_attribute("language");
259 _release_territory = node->optional_string_child("ReleaseTerritory");
260 if (_release_territory) {
261 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
264 auto vn = node->optional_node_child("VersionNumber");
266 _version_number = raw_convert<int>(vn->content());
267 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
268 auto vn_status = vn->optional_string_attribute("status");
270 _status = string_to_status (*vn_status);
274 _chain = node->optional_string_child("Chain");
275 _distributor = node->optional_string_child("Distributor");
276 _facility = node->optional_string_child("Facility");
278 auto acv = node->optional_node_child("AlternateContentVersionList");
280 for (auto i: acv->node_children("ContentVersion")) {
281 _content_versions.push_back (ContentVersion(i));
285 auto lum = node->optional_node_child("Luminance");
287 _luminance = Luminance (lum);
290 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
292 auto sr = node->optional_string_child("MainSoundSampleRate");
294 vector<string> sr_bits;
295 boost::split (sr_bits, *sr, boost::is_any_of(" "));
296 DCP_ASSERT (sr_bits.size() == 2);
297 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
300 if (_standard == dcp::Standard::SMPTE) {
301 _main_picture_stored_area = dcp::Size (
302 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
303 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
307 _main_picture_active_area = dcp::Size (
308 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
309 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
312 auto sll = node->optional_string_child("MainSubtitleLanguageList");
314 vector<string> sll_split;
315 boost::split (sll_split, *sll, boost::is_any_of(" "));
316 DCP_ASSERT (!sll_split.empty());
318 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
320 if (!_reels.empty()) {
321 auto sub = _reels.front()->main_subtitle();
323 auto lang = sub->language();
324 if (lang && lang == sll_split[0]) {
330 for (auto i = first; i < sll_split.size(); ++i) {
331 _additional_subtitle_languages.push_back (sll_split[i]);
335 auto eml = node->optional_node_child ("ExtensionMetadataList");
337 for (auto i: eml->node_children("ExtensionMetadata")) {
338 auto name = i->optional_string_child("Name");
339 if (name && *name == "Sign Language Video") {
340 auto property_list = i->node_child("PropertyList");
341 for (auto j: property_list->node_children("Property")) {
342 auto name = j->optional_string_child("Name");
343 auto value = j->optional_string_child("Value");
344 if (name && value && *name == "Language Tag") {
345 _sign_language_video_language = *value;
354 /** Write a CompositionMetadataAsset node as a child of @param node provided
355 * the required metadata is stored in the object. If any required metadata
356 * is missing this method will do nothing.
359 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
362 !_main_sound_configuration ||
363 !_main_sound_sample_rate ||
364 !_main_picture_stored_area ||
365 !_main_picture_active_area ||
367 !_reels.front()->main_picture()) {
371 auto meta = node->add_child("meta:CompositionMetadataAsset");
372 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
374 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
376 auto mp = _reels.front()->main_picture();
377 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
378 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
380 auto fctt = meta->add_child("FullContentTitleText", "meta");
381 if (_full_content_title_text && !_full_content_title_text->empty()) {
382 fctt->add_child_text (*_full_content_title_text);
384 if (_full_content_title_text_language) {
385 fctt->set_attribute("language", *_full_content_title_text_language);
388 if (_release_territory) {
389 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
392 if (_version_number) {
393 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
394 vn->add_child_text(raw_convert<string>(*_version_number));
396 vn->set_attribute("status", status_to_string(*_status));
401 meta->add_child("Chain", "meta")->add_child_text(*_chain);
405 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
409 meta->add_child("Facility", "meta")->add_child_text(*_facility);
412 if (_content_versions.size() > 1) {
413 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
414 for (size_t i = 1; i < _content_versions.size(); ++i) {
415 _content_versions[i].as_xml (vc);
420 _luminance->as_xml (meta, "meta");
423 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
424 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
426 auto stored = meta->add_child("MainPictureStoredArea", "meta");
427 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
428 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
430 auto active = meta->add_child("MainPictureActiveArea", "meta");
431 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
432 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
434 optional<string> first_subtitle_language;
435 for (auto i: _reels) {
436 if (i->main_subtitle()) {
437 first_subtitle_language = i->main_subtitle()->language();
438 if (first_subtitle_language) {
444 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
446 if (first_subtitle_language) {
447 lang = *first_subtitle_language;
449 for (auto const& i: _additional_subtitle_languages) {
455 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
458 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
460 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
461 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
462 extension->set_attribute("scope", scope);
463 extension->add_child("Name", "meta")->add_child_text(name);
464 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
465 property->add_child("Name", "meta")->add_child_text(property_name);
466 property->add_child("Value", "meta")->add_child_text(property_value);
469 /* SMPTE Bv2.1 8.6.3 */
470 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
472 if (_sign_language_video_language) {
473 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
476 if (_reels.front()->main_sound()) {
477 auto asset = _reels.front()->main_sound()->asset();
479 auto reader = asset->start_read ();
480 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
481 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
482 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
483 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
486 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
487 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
488 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
489 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
490 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
492 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
493 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
494 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
495 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
496 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
497 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
498 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
499 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
500 if (!soundfield->MCATagName.empty()) {
501 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
502 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
504 if (!soundfield->RFC5646SpokenLanguage.empty()) {
505 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
506 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
509 list<ASDCP::MXF::InterchangeObject*> channels;
510 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
511 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
515 for (auto i: channels) {
516 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
517 if (static_cast<int>(channel->MCAChannelID) > asset->channels()) {
520 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
521 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
522 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
523 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
524 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
525 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
526 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
527 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
528 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
529 if (!channel->MCATagName.empty()) {
530 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
531 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
533 if (!channel->MCAChannelID.empty()) {
534 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
536 if (!channel->RFC5646SpokenLanguage.empty()) {
537 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
538 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
540 if (!channel->SoundfieldGroupLinkID.empty()) {
541 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
542 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
553 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
555 for (auto i: reels) {
556 if (i->main_picture ()) {
557 assets.push_back (i->main_picture());
559 if (i->main_sound ()) {
560 assets.push_back (i->main_sound());
562 if (i->main_subtitle ()) {
563 assets.push_back (i->main_subtitle());
565 for (auto j: i->closed_captions()) {
566 assets.push_back (j);
569 assets.push_back (i->atmos());
575 vector<shared_ptr<ReelFileAsset>>
576 CPL::reel_file_assets ()
578 vector<shared_ptr<ReelFileAsset>> c;
579 add_file_assets (c, _reels);
584 vector<shared_ptr<const ReelFileAsset>>
585 CPL::reel_file_assets () const
587 vector<shared_ptr<const ReelFileAsset>> c;
588 add_file_assets (c, _reels);
594 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
596 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
601 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
602 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
603 note (NoteType::ERROR, s);
607 if (_content_kind != other_cpl->_content_kind) {
608 note (NoteType::ERROR, "CPL: content kinds differ");
612 if (_reels.size() != other_cpl->_reels.size()) {
613 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
617 auto a = _reels.begin();
618 auto b = other_cpl->_reels.begin();
620 while (a != _reels.end ()) {
621 if (!(*a)->equals (*b, opt, note)) {
633 CPL::any_encrypted () const
635 for (auto i: _reels) {
636 if (i->any_encrypted()) {
646 CPL::all_encrypted () const
648 for (auto i: _reels) {
649 if (!i->all_encrypted()) {
659 CPL::add (DecryptedKDM const & kdm)
661 for (auto i: _reels) {
667 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
669 for (auto i: _reels) {
670 i->resolve_refs (assets);
675 CPL::pkl_type (Standard standard) const
677 return static_pkl_type (standard);
681 CPL::static_pkl_type (Standard standard)
684 case Standard::INTEROP:
685 return "text/xml;asdcpKind=CPL";
686 case Standard::SMPTE:
694 CPL::duration () const
697 for (auto i: _reels) {
705 CPL::set_version_number (int v)
708 throw BadSettingError ("CPL version number cannot be negative");
716 CPL::unset_version_number ()
718 _version_number = boost::none;
723 CPL::set_content_versions (vector<ContentVersion> v)
725 std::set<string> ids;
727 if (!ids.insert(i.id).second) {
728 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
732 _content_versions = v;
736 optional<ContentVersion>
737 CPL::content_version () const
739 if (_content_versions.empty()) {
740 return optional<ContentVersion>();
743 return _content_versions[0];
748 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
750 _additional_subtitle_languages.clear ();
751 for (auto const& i: langs) {
752 _additional_subtitle_languages.push_back (i.to_string());
758 CPL::set_main_picture_active_area(dcp::Size area)
760 if (area.width % 2) {
761 throw BadSettingError("Main picture active area width is not a multiple of 2");
764 if (area.height % 2) {
765 throw BadSettingError("Main picture active area height is not a multiple of 2");
768 _main_picture_active_area = area;