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 _main_picture_stored_area = dcp::Size (
301 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
302 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
305 _main_picture_active_area = dcp::Size (
306 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
307 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
310 auto sll = node->optional_string_child("MainSubtitleLanguageList");
312 vector<string> sll_split;
313 boost::split (sll_split, *sll, boost::is_any_of(" "));
314 DCP_ASSERT (!sll_split.empty());
316 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
318 if (!_reels.empty()) {
319 auto sub = _reels.front()->main_subtitle();
321 auto lang = sub->language();
322 if (lang && lang == sll_split[0]) {
328 for (auto i = first; i < sll_split.size(); ++i) {
329 _additional_subtitle_languages.push_back (sll_split[i]);
333 auto eml = node->optional_node_child ("ExtensionMetadataList");
335 for (auto i: eml->node_children("ExtensionMetadata")) {
336 auto name = i->optional_string_child("Name");
337 if (name && *name == "Sign Language Video") {
338 auto property_list = i->node_child("PropertyList");
339 for (auto j: property_list->node_children("Property")) {
340 auto name = j->optional_string_child("Name");
341 auto value = j->optional_string_child("Value");
342 if (name && value && *name == "Language Tag") {
343 _sign_language_video_language = *value;
352 /** Write a CompositionMetadataAsset node as a child of @param node provided
353 * the required metadata is stored in the object. If any required metadata
354 * is missing this method will do nothing.
357 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
360 !_main_sound_configuration ||
361 !_main_sound_sample_rate ||
362 !_main_picture_stored_area ||
363 !_main_picture_active_area ||
365 !_reels.front()->main_picture()) {
369 auto meta = node->add_child("meta:CompositionMetadataAsset");
370 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
372 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
374 auto mp = _reels.front()->main_picture();
375 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
376 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
378 auto fctt = meta->add_child("FullContentTitleText", "meta");
379 if (_full_content_title_text && !_full_content_title_text->empty()) {
380 fctt->add_child_text (*_full_content_title_text);
382 if (_full_content_title_text_language) {
383 fctt->set_attribute("language", *_full_content_title_text_language);
386 if (_release_territory) {
387 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
390 if (_version_number) {
391 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
392 vn->add_child_text(raw_convert<string>(*_version_number));
394 vn->set_attribute("status", status_to_string(*_status));
399 meta->add_child("Chain", "meta")->add_child_text(*_chain);
403 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
407 meta->add_child("Facility", "meta")->add_child_text(*_facility);
410 if (_content_versions.size() > 1) {
411 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
412 for (size_t i = 1; i < _content_versions.size(); ++i) {
413 _content_versions[i].as_xml (vc);
418 _luminance->as_xml (meta, "meta");
421 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
422 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
424 auto stored = meta->add_child("MainPictureStoredArea", "meta");
425 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
426 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
428 auto active = meta->add_child("MainPictureActiveArea", "meta");
429 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
430 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
432 optional<string> first_subtitle_language;
433 for (auto i: _reels) {
434 if (i->main_subtitle()) {
435 first_subtitle_language = i->main_subtitle()->language();
436 if (first_subtitle_language) {
442 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
444 if (first_subtitle_language) {
445 lang = *first_subtitle_language;
447 for (auto const& i: _additional_subtitle_languages) {
453 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
456 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
458 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
459 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
460 extension->set_attribute("scope", scope);
461 extension->add_child("Name", "meta")->add_child_text(name);
462 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
463 property->add_child("Name", "meta")->add_child_text(property_name);
464 property->add_child("Value", "meta")->add_child_text(property_value);
467 /* SMPTE Bv2.1 8.6.3 */
468 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
470 if (_sign_language_video_language) {
471 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
474 if (_reels.front()->main_sound()) {
475 auto asset = _reels.front()->main_sound()->asset();
477 auto reader = asset->start_read ();
478 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
479 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
480 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
481 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
484 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
485 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
486 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
487 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
488 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
490 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
491 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
492 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
493 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
494 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
495 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
496 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
497 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
498 if (!soundfield->MCATagName.empty()) {
499 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
500 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
502 if (!soundfield->RFC5646SpokenLanguage.empty()) {
503 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
504 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
507 list<ASDCP::MXF::InterchangeObject*> channels;
508 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
509 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
513 for (auto i: channels) {
514 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
515 if (static_cast<int>(channel->MCAChannelID) > asset->channels()) {
518 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
519 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
520 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
521 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
522 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
523 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
524 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
525 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
526 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
527 if (!channel->MCATagName.empty()) {
528 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
529 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
531 if (!channel->MCAChannelID.empty()) {
532 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
534 if (!channel->RFC5646SpokenLanguage.empty()) {
535 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
536 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
538 if (!channel->SoundfieldGroupLinkID.empty()) {
539 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
540 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
551 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
553 for (auto i: reels) {
554 if (i->main_picture ()) {
555 assets.push_back (i->main_picture());
557 if (i->main_sound ()) {
558 assets.push_back (i->main_sound());
560 if (i->main_subtitle ()) {
561 assets.push_back (i->main_subtitle());
563 for (auto j: i->closed_captions()) {
564 assets.push_back (j);
567 assets.push_back (i->atmos());
573 vector<shared_ptr<ReelFileAsset>>
574 CPL::reel_file_assets ()
576 vector<shared_ptr<ReelFileAsset>> c;
577 add_file_assets (c, _reels);
582 vector<shared_ptr<const ReelFileAsset>>
583 CPL::reel_file_assets () const
585 vector<shared_ptr<const ReelFileAsset>> c;
586 add_file_assets (c, _reels);
592 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
594 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
599 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
600 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
601 note (NoteType::ERROR, s);
605 if (_content_kind != other_cpl->_content_kind) {
606 note (NoteType::ERROR, "CPL: content kinds differ");
610 if (_reels.size() != other_cpl->_reels.size()) {
611 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
615 auto a = _reels.begin();
616 auto b = other_cpl->_reels.begin();
618 while (a != _reels.end ()) {
619 if (!(*a)->equals (*b, opt, note)) {
631 CPL::any_encrypted () const
633 for (auto i: _reels) {
634 if (i->any_encrypted()) {
644 CPL::all_encrypted () const
646 for (auto i: _reels) {
647 if (!i->all_encrypted()) {
657 CPL::add (DecryptedKDM const & kdm)
659 for (auto i: _reels) {
665 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
667 for (auto i: _reels) {
668 i->resolve_refs (assets);
673 CPL::pkl_type (Standard standard) const
675 return static_pkl_type (standard);
679 CPL::static_pkl_type (Standard standard)
682 case Standard::INTEROP:
683 return "text/xml;asdcpKind=CPL";
684 case Standard::SMPTE:
692 CPL::duration () const
695 for (auto i: _reels) {
703 CPL::set_version_number (int v)
706 throw BadSettingError ("CPL version number cannot be negative");
714 CPL::unset_version_number ()
716 _version_number = boost::none;
721 CPL::set_content_versions (vector<ContentVersion> v)
723 std::set<string> ids;
725 if (!ids.insert(i.id).second) {
726 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
730 _content_versions = v;
734 optional<ContentVersion>
735 CPL::content_version () const
737 if (_content_versions.empty()) {
738 return optional<ContentVersion>();
741 return _content_versions[0];
746 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
748 _additional_subtitle_languages.clear ();
749 for (auto const& i: langs) {
750 _additional_subtitle_languages.push_back (i.to_string());
756 CPL::set_main_picture_active_area(dcp::Size area)
758 if (area.width % 2) {
759 throw BadSettingError("Main picture active area width is not a multiple of 2");
762 if (area.height % 2) {
763 throw BadSettingError("Main picture active area height is not a multiple of 2");
766 _main_picture_active_area = area;