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.
44 #include "certificate_chain.h"
46 #include "reel_picture_asset.h"
47 #include "reel_sound_asset.h"
48 #include "reel_subtitle_asset.h"
49 #include "reel_closed_caption_asset.h"
50 #include "reel_atmos_asset.h"
51 #include "local_time.h"
52 #include "dcp_assert.h"
53 #include "compose.hpp"
54 #include "raw_convert.h"
55 #include <asdcp/Metadata.h>
56 #include <libxml/parser.h>
57 #include <libxml++/libxml++.h>
58 #include <boost/algorithm/string.hpp>
68 using std::shared_ptr;
69 using boost::optional;
70 using std::dynamic_pointer_cast;
74 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
75 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
76 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
77 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
78 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
79 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
82 CPL::CPL (string annotation_text, ContentKind content_kind)
83 /* default _content_title_text to annotation_text */
84 : _issuer ("libdcp" LIBDCP_VERSION)
85 , _creator ("libdcp" LIBDCP_VERSION)
86 , _issue_date (LocalTime().as_string())
87 , _annotation_text (annotation_text)
88 , _content_title_text (annotation_text)
89 , _content_kind (content_kind)
92 cv.label_text = cv.id + LocalTime().as_string();
93 _content_versions.push_back (cv);
97 CPL::CPL (boost::filesystem::path file)
99 , _content_kind (ContentKind::FEATURE)
101 cxml::Document f ("CompositionPlaylist");
104 if (f.namespace_uri() == cpl_interop_ns) {
105 _standard = Standard::INTEROP;
106 } else if (f.namespace_uri() == cpl_smpte_ns) {
107 _standard = Standard::SMPTE;
109 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
112 _id = remove_urn_uuid (f.string_child ("Id"));
113 _annotation_text = f.optional_string_child("AnnotationText");
114 _issuer = f.optional_string_child("Issuer").get_value_or("");
115 _creator = f.optional_string_child("Creator").get_value_or("");
116 _issue_date = f.string_child ("IssueDate");
117 _content_title_text = f.string_child ("ContentTitleText");
118 _content_kind = content_kind_from_string (f.string_child ("ContentKind"));
119 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
120 if (content_version) {
121 /* XXX: SMPTE should insist that Id is present */
122 _content_versions.push_back (
124 content_version->optional_string_child("Id").get_value_or(""),
125 content_version->string_child("LabelText")
128 content_version->done ();
129 } else if (_standard == Standard::SMPTE) {
130 /* ContentVersion is required in SMPTE */
131 throw XMLError ("Missing ContentVersion tag in CPL");
133 auto rating_list = f.node_child ("RatingList");
135 for (auto i: rating_list->node_children("Rating")) {
136 _ratings.push_back (Rating(i));
139 _reels = type_grand_children<Reel> (f, "ReelList", "Reel");
141 auto reel_list = f.node_child ("ReelList");
143 auto reels = reel_list->node_children("Reel");
144 if (!reels.empty()) {
145 auto asset_list = reels.front()->node_child("AssetList");
146 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
148 read_composition_metadata_asset (metadata);
153 f.ignore_child ("Issuer");
154 f.ignore_child ("Signer");
155 f.ignore_child ("Signature");
162 CPL::add (std::shared_ptr<Reel> reel)
164 _reels.push_back (reel);
169 CPL::write_xml (boost::filesystem::path file, Standard standard, shared_ptr<const CertificateChain> signer) const
172 xmlpp::Element* root;
173 if (standard == Standard::INTEROP) {
174 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
176 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
179 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
180 if (_annotation_text) {
181 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
183 root->add_child("IssueDate")->add_child_text (_issue_date);
184 root->add_child("Issuer")->add_child_text (_issuer);
185 root->add_child("Creator")->add_child_text (_creator);
186 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
187 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
188 if (_content_versions.empty()) {
192 _content_versions[0].as_xml (root);
195 auto rating_list = root->add_child("RatingList");
196 for (auto i: _ratings) {
197 i.as_xml (rating_list->add_child("Rating"));
200 auto reel_list = root->add_child ("ReelList");
202 if (_reels.empty()) {
203 throw NoReelsError ();
207 for (auto i: _reels) {
208 auto asset_list = i->write_to_cpl (reel_list, standard);
209 if (first && standard == Standard::SMPTE) {
210 maybe_write_composition_metadata_asset (asset_list);
218 signer->sign (root, standard);
221 doc.write_to_file_formatted (file.string(), "UTF-8");
228 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
230 auto fctt = node->node_child("FullContentTitleText");
231 _full_content_title_text = fctt->content();
232 _full_content_title_text_language = fctt->optional_string_attribute("language");
234 _release_territory = node->optional_string_child("ReleaseTerritory");
235 if (_release_territory) {
236 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
239 auto vn = node->optional_node_child("VersionNumber");
241 _version_number = raw_convert<int>(vn->content());
242 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
243 auto vn_status = vn->optional_string_attribute("status");
245 _status = string_to_status (*vn_status);
249 _chain = node->optional_string_child("Chain");
250 _distributor = node->optional_string_child("Distributor");
251 _facility = node->optional_string_child("Facility");
253 auto acv = node->optional_node_child("AlternateContentVersionList");
255 for (auto i: acv->node_children("ContentVersion")) {
256 _content_versions.push_back (ContentVersion(i));
260 auto lum = node->optional_node_child("Luminance");
262 _luminance = Luminance (lum);
265 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
267 auto sr = node->optional_string_child("MainSoundSampleRate");
269 vector<string> sr_bits;
270 boost::split (sr_bits, *sr, boost::is_any_of(" "));
271 DCP_ASSERT (sr_bits.size() == 2);
272 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
275 _main_picture_stored_area = dcp::Size (
276 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
277 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
280 _main_picture_active_area = dcp::Size (
281 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
282 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
285 auto sll = node->optional_string_child("MainSubtitleLanguageList");
287 vector<string> sll_split;
288 boost::split (sll_split, *sll, boost::is_any_of(" "));
289 DCP_ASSERT (!sll_split.empty());
291 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
293 if (!_reels.empty()) {
294 shared_ptr<dcp::ReelSubtitleAsset> sub = _reels.front()->main_subtitle();
296 optional<string> lang = sub->language();
297 if (lang && lang == sll_split[0]) {
303 for (auto i = first; i < sll_split.size(); ++i) {
304 _additional_subtitle_languages.push_back (sll_split[i]);
310 /** Write a CompositionMetadataAsset node as a child of @param node provided
311 * the required metadata is stored in the object. If any required metadata
312 * is missing this method will do nothing.
315 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
318 !_main_sound_configuration ||
319 !_main_sound_sample_rate ||
320 !_main_picture_stored_area ||
321 !_main_picture_active_area ||
323 !_reels.front()->main_picture()) {
327 auto meta = node->add_child("meta:CompositionMetadataAsset");
328 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
330 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
332 auto mp = _reels.front()->main_picture();
333 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
334 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
336 auto fctt = meta->add_child("FullContentTitleText", "meta");
337 if (_full_content_title_text) {
338 fctt->add_child_text (*_full_content_title_text);
340 if (_full_content_title_text_language) {
341 fctt->set_attribute("language", *_full_content_title_text_language);
344 if (_release_territory) {
345 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
348 if (_version_number) {
349 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
350 vn->add_child_text(raw_convert<string>(*_version_number));
352 vn->set_attribute("status", status_to_string(*_status));
357 meta->add_child("Chain", "meta")->add_child_text(*_chain);
361 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
365 meta->add_child("Facility", "meta")->add_child_text(*_facility);
368 if (_content_versions.size() > 1) {
369 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
370 for (size_t i = 1; i < _content_versions.size(); ++i) {
371 _content_versions[i].as_xml (vc);
376 _luminance->as_xml (meta, "meta");
379 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
380 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
382 auto stored = meta->add_child("MainPictureStoredArea", "meta");
383 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
384 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
386 auto active = meta->add_child("MainPictureActiveArea", "meta");
387 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
388 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
390 optional<string> first_subtitle_language;
391 for (auto i: _reels) {
392 if (i->main_subtitle()) {
393 first_subtitle_language = i->main_subtitle()->language();
394 if (first_subtitle_language) {
400 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
402 if (first_subtitle_language) {
403 lang = *first_subtitle_language;
405 for (auto const& i: _additional_subtitle_languages) {
411 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
414 /* SMPTE Bv2.1 8.6.3 */
415 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
416 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
417 extension->add_child("Name", "meta")->add_child_text("Application");
418 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
419 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
420 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
422 if (_reels.front()->main_sound()) {
423 auto asset = _reels.front()->main_sound()->asset();
425 auto reader = asset->start_read ();
426 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
427 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
428 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
429 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
432 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
433 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
434 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
435 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
436 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
438 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
439 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
440 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
441 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
442 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
443 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
444 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
445 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
446 if (!soundfield->MCATagName.empty()) {
447 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
448 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
450 if (!soundfield->RFC5646SpokenLanguage.empty()) {
451 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
452 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
455 list<ASDCP::MXF::InterchangeObject*> channels;
456 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
457 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
461 for (auto i: channels) {
462 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
463 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
464 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
465 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
466 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
467 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
468 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
469 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
470 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
471 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
472 if (!channel->MCATagName.empty()) {
473 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
474 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
476 if (!channel->MCAChannelID.empty()) {
477 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
479 if (!channel->RFC5646SpokenLanguage.empty()) {
480 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
481 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
483 if (!channel->SoundfieldGroupLinkID.empty()) {
484 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
485 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
496 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
498 for (auto i: reels) {
499 if (i->main_picture ()) {
500 assets.push_back (i->main_picture());
502 if (i->main_sound ()) {
503 assets.push_back (i->main_sound());
505 if (i->main_subtitle ()) {
506 assets.push_back (i->main_subtitle());
508 for (auto j: i->closed_captions()) {
509 assets.push_back (j);
512 assets.push_back (i->atmos());
518 vector<shared_ptr<ReelFileAsset>>
519 CPL::reel_file_assets ()
521 vector<shared_ptr<ReelFileAsset>> c;
522 add_file_assets (c, _reels);
527 vector<shared_ptr<const ReelFileAsset>>
528 CPL::reel_file_assets () const
530 vector<shared_ptr<const ReelFileAsset>> c;
531 add_file_assets (c, _reels);
538 add_encryptable_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
540 for (auto i: reels) {
541 if (i->main_picture ()) {
542 assets.push_back (i->main_picture());
544 if (i->main_sound ()) {
545 assets.push_back (i->main_sound());
547 if (i->main_subtitle ()) {
548 assets.push_back (i->main_subtitle());
550 for (auto j: i->closed_captions()) {
551 assets.push_back (j);
554 assets.push_back (i->atmos());
560 vector<shared_ptr<ReelEncryptableAsset>>
561 CPL::reel_encryptable_assets ()
563 vector<shared_ptr<ReelEncryptableAsset>> c;
564 add_encryptable_assets (c, _reels);
569 vector<shared_ptr<const ReelEncryptableAsset>>
570 CPL::reel_encryptable_assets () const
572 vector<shared_ptr<const ReelEncryptableAsset>> c;
573 add_encryptable_assets (c, _reels);
579 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
581 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
586 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
587 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
588 note (NoteType::ERROR, s);
592 if (_content_kind != other_cpl->_content_kind) {
593 note (NoteType::ERROR, "CPL: content kinds differ");
597 if (_reels.size() != other_cpl->_reels.size()) {
598 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
602 auto a = _reels.begin();
603 auto b = other_cpl->_reels.begin();
605 while (a != _reels.end ()) {
606 if (!(*a)->equals (*b, opt, note)) {
618 CPL::any_encrypted () const
620 for (auto i: _reels) {
621 if (i->any_encrypted()) {
631 CPL::all_encrypted () const
633 for (auto i: _reels) {
634 if (!i->all_encrypted()) {
644 CPL::add (DecryptedKDM const & kdm)
646 for (auto i: _reels) {
652 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
654 for (auto i: _reels) {
655 i->resolve_refs (assets);
660 CPL::pkl_type (Standard standard) const
662 return static_pkl_type (standard);
666 CPL::static_pkl_type (Standard standard)
669 case Standard::INTEROP:
670 return "text/xml;asdcpKind=CPL";
671 case Standard::SMPTE:
679 CPL::duration () const
682 for (auto i: _reels) {
690 CPL::set_version_number (int v)
693 throw BadSettingError ("CPL version number cannot be negative");
701 CPL::unset_version_number ()
703 _version_number = boost::none;
708 CPL::set_content_versions (vector<ContentVersion> v)
712 if (!ids.insert(i.id).second) {
713 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
717 _content_versions = v;
721 optional<ContentVersion>
722 CPL::content_version () const
724 if (_content_versions.empty()) {
725 return optional<ContentVersion>();
728 return _content_versions[0];
733 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
735 _additional_subtitle_languages.clear ();
736 for (auto const& i: langs) {
737 _additional_subtitle_languages.push_back (i.to_string());