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 _content_kind = content_kind_from_string (f.string_child ("ContentKind"));
126 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
127 if (content_version) {
128 /* XXX: SMPTE should insist that Id is present */
129 _content_versions.push_back (
131 content_version->optional_string_child("Id").get_value_or(""),
132 content_version->string_child("LabelText")
135 content_version->done ();
136 } else if (_standard == Standard::SMPTE) {
137 /* ContentVersion is required in SMPTE */
138 throw XMLError ("Missing ContentVersion tag in CPL");
140 auto rating_list = f.node_child ("RatingList");
141 for (auto i: rating_list->node_children("Rating")) {
142 _ratings.push_back (Rating(i));
145 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
146 _reels.push_back (make_shared<Reel>(i, _standard));
149 auto reel_list = f.node_child ("ReelList");
150 auto reels = reel_list->node_children("Reel");
151 if (!reels.empty()) {
152 auto asset_list = reels.front()->node_child("AssetList");
153 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
155 read_composition_metadata_asset (metadata);
159 f.ignore_child ("Issuer");
160 f.ignore_child ("Signer");
161 f.ignore_child ("Signature");
168 CPL::add (std::shared_ptr<Reel> reel)
170 _reels.push_back (reel);
175 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
178 xmlpp::Element* root;
179 if (_standard == Standard::INTEROP) {
180 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
182 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
185 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
186 if (_annotation_text) {
187 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
189 root->add_child("IssueDate")->add_child_text (_issue_date);
190 root->add_child("Issuer")->add_child_text (_issuer);
191 root->add_child("Creator")->add_child_text (_creator);
192 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
193 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
194 if (_content_versions.empty()) {
198 _content_versions[0].as_xml (root);
201 auto rating_list = root->add_child("RatingList");
202 for (auto i: _ratings) {
203 i.as_xml (rating_list->add_child("Rating"));
206 auto reel_list = root->add_child ("ReelList");
208 if (_reels.empty()) {
209 throw NoReelsError ();
213 for (auto i: _reels) {
214 auto asset_list = i->write_to_cpl (reel_list, _standard);
215 if (first && _standard == Standard::SMPTE) {
216 maybe_write_composition_metadata_asset (asset_list);
224 signer->sign (root, _standard);
227 doc.write_to_file_formatted (file.string(), "UTF-8");
234 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
236 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
238 auto fctt = node->node_child("FullContentTitleText");
239 _full_content_title_text = fctt->content();
240 _full_content_title_text_language = fctt->optional_string_attribute("language");
242 _release_territory = node->optional_string_child("ReleaseTerritory");
243 if (_release_territory) {
244 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
247 auto vn = node->optional_node_child("VersionNumber");
249 _version_number = raw_convert<int>(vn->content());
250 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
251 auto vn_status = vn->optional_string_attribute("status");
253 _status = string_to_status (*vn_status);
257 _chain = node->optional_string_child("Chain");
258 _distributor = node->optional_string_child("Distributor");
259 _facility = node->optional_string_child("Facility");
261 auto acv = node->optional_node_child("AlternateContentVersionList");
263 for (auto i: acv->node_children("ContentVersion")) {
264 _content_versions.push_back (ContentVersion(i));
268 auto lum = node->optional_node_child("Luminance");
270 _luminance = Luminance (lum);
273 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
275 auto sr = node->optional_string_child("MainSoundSampleRate");
277 vector<string> sr_bits;
278 boost::split (sr_bits, *sr, boost::is_any_of(" "));
279 DCP_ASSERT (sr_bits.size() == 2);
280 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
283 _main_picture_stored_area = dcp::Size (
284 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
285 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
288 _main_picture_active_area = dcp::Size (
289 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
290 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
293 auto sll = node->optional_string_child("MainSubtitleLanguageList");
295 vector<string> sll_split;
296 boost::split (sll_split, *sll, boost::is_any_of(" "));
297 DCP_ASSERT (!sll_split.empty());
299 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
301 if (!_reels.empty()) {
302 auto sub = _reels.front()->main_subtitle();
304 auto lang = sub->language();
305 if (lang && lang == sll_split[0]) {
311 for (auto i = first; i < sll_split.size(); ++i) {
312 _additional_subtitle_languages.push_back (sll_split[i]);
318 /** Write a CompositionMetadataAsset node as a child of @param node provided
319 * the required metadata is stored in the object. If any required metadata
320 * is missing this method will do nothing.
323 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
326 !_main_sound_configuration ||
327 !_main_sound_sample_rate ||
328 !_main_picture_stored_area ||
329 !_main_picture_active_area ||
331 !_reels.front()->main_picture()) {
335 auto meta = node->add_child("meta:CompositionMetadataAsset");
336 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
338 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
340 auto mp = _reels.front()->main_picture();
341 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
342 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
344 auto fctt = meta->add_child("FullContentTitleText", "meta");
345 if (_full_content_title_text && !_full_content_title_text->empty()) {
346 fctt->add_child_text (*_full_content_title_text);
348 if (_full_content_title_text_language) {
349 fctt->set_attribute("language", *_full_content_title_text_language);
352 if (_release_territory) {
353 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
356 if (_version_number) {
357 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
358 vn->add_child_text(raw_convert<string>(*_version_number));
360 vn->set_attribute("status", status_to_string(*_status));
365 meta->add_child("Chain", "meta")->add_child_text(*_chain);
369 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
373 meta->add_child("Facility", "meta")->add_child_text(*_facility);
376 if (_content_versions.size() > 1) {
377 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
378 for (size_t i = 1; i < _content_versions.size(); ++i) {
379 _content_versions[i].as_xml (vc);
384 _luminance->as_xml (meta, "meta");
387 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
388 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
390 auto stored = meta->add_child("MainPictureStoredArea", "meta");
391 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
392 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
394 auto active = meta->add_child("MainPictureActiveArea", "meta");
395 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
396 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
398 optional<string> first_subtitle_language;
399 for (auto i: _reels) {
400 if (i->main_subtitle()) {
401 first_subtitle_language = i->main_subtitle()->language();
402 if (first_subtitle_language) {
408 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
410 if (first_subtitle_language) {
411 lang = *first_subtitle_language;
413 for (auto const& i: _additional_subtitle_languages) {
419 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
422 auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
424 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
425 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
426 extension->set_attribute("scope", scope);
427 extension->add_child("Name", "meta")->add_child_text(name);
428 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
429 property->add_child("Name", "meta")->add_child_text(property_name);
430 property->add_child("Value", "meta")->add_child_text(property_value);
433 /* SMPTE Bv2.1 8.6.3 */
434 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
436 if (_reels.front()->main_sound()) {
437 auto asset = _reels.front()->main_sound()->asset();
439 auto reader = asset->start_read ();
440 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
441 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
442 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
443 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
446 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
447 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
448 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
449 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
450 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
452 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
453 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
454 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
455 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
456 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
457 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
458 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
459 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
460 if (!soundfield->MCATagName.empty()) {
461 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
462 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
464 if (!soundfield->RFC5646SpokenLanguage.empty()) {
465 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
466 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
469 list<ASDCP::MXF::InterchangeObject*> channels;
470 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
471 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
475 for (auto i: channels) {
476 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
477 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
478 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
479 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
480 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
481 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
482 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
483 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
484 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
485 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
486 if (!channel->MCATagName.empty()) {
487 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
488 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
490 if (!channel->MCAChannelID.empty()) {
491 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
493 if (!channel->RFC5646SpokenLanguage.empty()) {
494 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
495 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
497 if (!channel->SoundfieldGroupLinkID.empty()) {
498 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
499 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
510 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
512 for (auto i: reels) {
513 if (i->main_picture ()) {
514 assets.push_back (i->main_picture());
516 if (i->main_sound ()) {
517 assets.push_back (i->main_sound());
519 if (i->main_subtitle ()) {
520 assets.push_back (i->main_subtitle());
522 for (auto j: i->closed_captions()) {
523 assets.push_back (j);
526 assets.push_back (i->atmos());
532 vector<shared_ptr<ReelFileAsset>>
533 CPL::reel_file_assets ()
535 vector<shared_ptr<ReelFileAsset>> c;
536 add_file_assets (c, _reels);
541 vector<shared_ptr<const ReelFileAsset>>
542 CPL::reel_file_assets () const
544 vector<shared_ptr<const ReelFileAsset>> c;
545 add_file_assets (c, _reels);
551 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
553 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
558 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
559 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
560 note (NoteType::ERROR, s);
564 if (_content_kind != other_cpl->_content_kind) {
565 note (NoteType::ERROR, "CPL: content kinds differ");
569 if (_reels.size() != other_cpl->_reels.size()) {
570 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
574 auto a = _reels.begin();
575 auto b = other_cpl->_reels.begin();
577 while (a != _reels.end ()) {
578 if (!(*a)->equals (*b, opt, note)) {
590 CPL::any_encrypted () const
592 for (auto i: _reels) {
593 if (i->any_encrypted()) {
603 CPL::all_encrypted () const
605 for (auto i: _reels) {
606 if (!i->all_encrypted()) {
616 CPL::add (DecryptedKDM const & kdm)
618 for (auto i: _reels) {
624 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
626 for (auto i: _reels) {
627 i->resolve_refs (assets);
632 CPL::pkl_type (Standard standard) const
634 return static_pkl_type (standard);
638 CPL::static_pkl_type (Standard standard)
641 case Standard::INTEROP:
642 return "text/xml;asdcpKind=CPL";
643 case Standard::SMPTE:
651 CPL::duration () const
654 for (auto i: _reels) {
662 CPL::set_version_number (int v)
665 throw BadSettingError ("CPL version number cannot be negative");
673 CPL::unset_version_number ()
675 _version_number = boost::none;
680 CPL::set_content_versions (vector<ContentVersion> v)
684 if (!ids.insert(i.id).second) {
685 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
689 _content_versions = v;
693 optional<ContentVersion>
694 CPL::content_version () const
696 if (_content_versions.empty()) {
697 return optional<ContentVersion>();
700 return _content_versions[0];
705 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
707 _additional_subtitle_languages.clear ();
708 for (auto const& i: langs) {
709 _additional_subtitle_languages.push_back (i.to_string());