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");
142 for (auto i: rating_list->node_children("Rating")) {
143 _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");
153 auto reels = reel_list->node_children("Reel");
154 if (!reels.empty()) {
155 auto asset_list = reels.front()->node_child("AssetList");
156 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
158 read_composition_metadata_asset (metadata);
163 f.ignore_child ("Issuer");
164 f.ignore_child ("Signer");
165 f.ignore_child ("Signature");
172 CPL::add (std::shared_ptr<Reel> reel)
174 _reels.push_back (reel);
179 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
182 xmlpp::Element* root;
183 if (_standard == Standard::INTEROP) {
184 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
186 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
189 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
190 if (_annotation_text) {
191 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
193 root->add_child("IssueDate")->add_child_text (_issue_date);
194 root->add_child("Issuer")->add_child_text (_issuer);
195 root->add_child("Creator")->add_child_text (_creator);
196 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
197 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
198 if (_content_versions.empty()) {
202 _content_versions[0].as_xml (root);
205 auto rating_list = root->add_child("RatingList");
206 for (auto i: _ratings) {
207 i.as_xml (rating_list->add_child("Rating"));
210 auto reel_list = root->add_child ("ReelList");
212 if (_reels.empty()) {
213 throw NoReelsError ();
217 for (auto i: _reels) {
218 auto asset_list = i->write_to_cpl (reel_list, _standard);
219 if (first && _standard == Standard::SMPTE) {
220 maybe_write_composition_metadata_asset (asset_list);
228 signer->sign (root, _standard);
231 doc.write_to_file_formatted (file.string(), "UTF-8");
238 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
240 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
242 auto fctt = node->node_child("FullContentTitleText");
243 _full_content_title_text = fctt->content();
244 _full_content_title_text_language = fctt->optional_string_attribute("language");
246 _release_territory = node->optional_string_child("ReleaseTerritory");
247 if (_release_territory) {
248 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
251 auto vn = node->optional_node_child("VersionNumber");
253 _version_number = raw_convert<int>(vn->content());
254 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
255 auto vn_status = vn->optional_string_attribute("status");
257 _status = string_to_status (*vn_status);
261 _chain = node->optional_string_child("Chain");
262 _distributor = node->optional_string_child("Distributor");
263 _facility = node->optional_string_child("Facility");
265 auto acv = node->optional_node_child("AlternateContentVersionList");
267 for (auto i: acv->node_children("ContentVersion")) {
268 _content_versions.push_back (ContentVersion(i));
272 auto lum = node->optional_node_child("Luminance");
274 _luminance = Luminance (lum);
277 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
279 auto sr = node->optional_string_child("MainSoundSampleRate");
281 vector<string> sr_bits;
282 boost::split (sr_bits, *sr, boost::is_any_of(" "));
283 DCP_ASSERT (sr_bits.size() == 2);
284 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
287 _main_picture_stored_area = dcp::Size (
288 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
289 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
292 _main_picture_active_area = dcp::Size (
293 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
294 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
297 auto sll = node->optional_string_child("MainSubtitleLanguageList");
299 vector<string> sll_split;
300 boost::split (sll_split, *sll, boost::is_any_of(" "));
301 DCP_ASSERT (!sll_split.empty());
303 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
305 if (!_reels.empty()) {
306 auto sub = _reels.front()->main_subtitle();
308 auto lang = sub->language();
309 if (lang && lang == sll_split[0]) {
315 for (auto i = first; i < sll_split.size(); ++i) {
316 _additional_subtitle_languages.push_back (sll_split[i]);
322 /** Write a CompositionMetadataAsset node as a child of @param node provided
323 * the required metadata is stored in the object. If any required metadata
324 * is missing this method will do nothing.
327 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
330 !_main_sound_configuration ||
331 !_main_sound_sample_rate ||
332 !_main_picture_stored_area ||
333 !_main_picture_active_area ||
335 !_reels.front()->main_picture()) {
339 auto meta = node->add_child("meta:CompositionMetadataAsset");
340 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
342 meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
344 auto mp = _reels.front()->main_picture();
345 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
346 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
348 auto fctt = meta->add_child("FullContentTitleText", "meta");
349 if (_full_content_title_text && !_full_content_title_text->empty()) {
350 fctt->add_child_text (*_full_content_title_text);
352 if (_full_content_title_text_language) {
353 fctt->set_attribute("language", *_full_content_title_text_language);
356 if (_release_territory) {
357 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
360 if (_version_number) {
361 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
362 vn->add_child_text(raw_convert<string>(*_version_number));
364 vn->set_attribute("status", status_to_string(*_status));
369 meta->add_child("Chain", "meta")->add_child_text(*_chain);
373 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
377 meta->add_child("Facility", "meta")->add_child_text(*_facility);
380 if (_content_versions.size() > 1) {
381 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
382 for (size_t i = 1; i < _content_versions.size(); ++i) {
383 _content_versions[i].as_xml (vc);
388 _luminance->as_xml (meta, "meta");
391 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
392 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
394 auto stored = meta->add_child("MainPictureStoredArea", "meta");
395 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
396 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
398 auto active = meta->add_child("MainPictureActiveArea", "meta");
399 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
400 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
402 optional<string> first_subtitle_language;
403 for (auto i: _reels) {
404 if (i->main_subtitle()) {
405 first_subtitle_language = i->main_subtitle()->language();
406 if (first_subtitle_language) {
412 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
414 if (first_subtitle_language) {
415 lang = *first_subtitle_language;
417 for (auto const& i: _additional_subtitle_languages) {
423 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
426 /* SMPTE Bv2.1 8.6.3 */
427 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
428 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
429 extension->add_child("Name", "meta")->add_child_text("Application");
430 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
431 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
432 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
434 if (_reels.front()->main_sound()) {
435 auto asset = _reels.front()->main_sound()->asset();
437 auto reader = asset->start_read ();
438 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
439 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
440 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
441 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
444 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
445 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
446 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
447 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
448 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
450 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
451 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
452 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
453 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
454 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
455 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
456 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
457 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
458 if (!soundfield->MCATagName.empty()) {
459 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
460 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
462 if (!soundfield->RFC5646SpokenLanguage.empty()) {
463 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
464 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
467 list<ASDCP::MXF::InterchangeObject*> channels;
468 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
469 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
473 for (auto i: channels) {
474 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
475 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
476 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
477 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
478 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
479 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
480 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
481 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
482 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
483 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
484 if (!channel->MCATagName.empty()) {
485 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
486 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
488 if (!channel->MCAChannelID.empty()) {
489 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
491 if (!channel->RFC5646SpokenLanguage.empty()) {
492 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
493 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
495 if (!channel->SoundfieldGroupLinkID.empty()) {
496 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
497 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
508 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
510 for (auto i: reels) {
511 if (i->main_picture ()) {
512 assets.push_back (i->main_picture());
514 if (i->main_sound ()) {
515 assets.push_back (i->main_sound());
517 if (i->main_subtitle ()) {
518 assets.push_back (i->main_subtitle());
520 for (auto j: i->closed_captions()) {
521 assets.push_back (j);
524 assets.push_back (i->atmos());
530 vector<shared_ptr<ReelFileAsset>>
531 CPL::reel_file_assets ()
533 vector<shared_ptr<ReelFileAsset>> c;
534 add_file_assets (c, _reels);
539 vector<shared_ptr<const ReelFileAsset>>
540 CPL::reel_file_assets () const
542 vector<shared_ptr<const ReelFileAsset>> c;
543 add_file_assets (c, _reels);
549 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
551 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
556 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
557 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
558 note (NoteType::ERROR, s);
562 if (_content_kind != other_cpl->_content_kind) {
563 note (NoteType::ERROR, "CPL: content kinds differ");
567 if (_reels.size() != other_cpl->_reels.size()) {
568 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
572 auto a = _reels.begin();
573 auto b = other_cpl->_reels.begin();
575 while (a != _reels.end ()) {
576 if (!(*a)->equals (*b, opt, note)) {
588 CPL::any_encrypted () const
590 for (auto i: _reels) {
591 if (i->any_encrypted()) {
601 CPL::all_encrypted () const
603 for (auto i: _reels) {
604 if (!i->all_encrypted()) {
614 CPL::add (DecryptedKDM const & kdm)
616 for (auto i: _reels) {
622 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
624 for (auto i: _reels) {
625 i->resolve_refs (assets);
630 CPL::pkl_type (Standard standard) const
632 return static_pkl_type (standard);
636 CPL::static_pkl_type (Standard standard)
639 case Standard::INTEROP:
640 return "text/xml;asdcpKind=CPL";
641 case Standard::SMPTE:
649 CPL::duration () const
652 for (auto i: _reels) {
660 CPL::set_version_number (int v)
663 throw BadSettingError ("CPL version number cannot be negative");
671 CPL::unset_version_number ()
673 _version_number = boost::none;
678 CPL::set_content_versions (vector<ContentVersion> v)
682 if (!ids.insert(i.id).second) {
683 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
687 _content_versions = v;
691 optional<ContentVersion>
692 CPL::content_version () const
694 if (_content_versions.empty()) {
695 return optional<ContentVersion>();
698 return _content_versions[0];
703 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
705 _additional_subtitle_languages.clear ();
706 for (auto const& i: langs) {
707 _additional_subtitle_languages.push_back (i.to_string());