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.
38 #include "certificate_chain.h"
40 #include "reel_picture_asset.h"
41 #include "reel_sound_asset.h"
42 #include "reel_subtitle_asset.h"
43 #include "reel_closed_caption_asset.h"
44 #include "reel_atmos_asset.h"
45 #include "local_time.h"
46 #include "dcp_assert.h"
47 #include "compose.hpp"
48 #include "raw_convert.h"
49 #include <asdcp/Metadata.h>
50 #include <libxml/parser.h>
51 #include <libxml++/libxml++.h>
52 #include <boost/algorithm/string.hpp>
61 using std::shared_ptr;
62 using boost::optional;
63 using std::dynamic_pointer_cast;
67 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
68 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
69 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
70 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
71 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
72 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
75 CPL::CPL (string annotation_text, ContentKind content_kind)
76 /* default _content_title_text to annotation_text */
77 : _issuer ("libdcp" LIBDCP_VERSION)
78 , _creator ("libdcp" LIBDCP_VERSION)
79 , _issue_date (LocalTime().as_string())
80 , _annotation_text (annotation_text)
81 , _content_title_text (annotation_text)
82 , _content_kind (content_kind)
85 cv.label_text = cv.id + LocalTime().as_string();
86 _content_versions.push_back (cv);
89 /** Construct a CPL object from a XML file */
90 CPL::CPL (boost::filesystem::path file)
92 , _content_kind (FEATURE)
94 cxml::Document f ("CompositionPlaylist");
97 if (f.namespace_uri() == cpl_interop_ns) {
99 } else if (f.namespace_uri() == cpl_smpte_ns) {
102 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
105 _id = remove_urn_uuid (f.string_child ("Id"));
106 _annotation_text = f.optional_string_child("AnnotationText").get_value_or("");
107 _issuer = f.optional_string_child("Issuer").get_value_or("");
108 _creator = f.optional_string_child("Creator").get_value_or("");
109 _issue_date = f.string_child ("IssueDate");
110 _content_title_text = f.string_child ("ContentTitleText");
111 _content_kind = content_kind_from_string (f.string_child ("ContentKind"));
112 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
113 if (content_version) {
114 /* XXX: SMPTE should insist that Id is present */
115 _content_versions.push_back (
117 content_version->optional_string_child("Id").get_value_or(""),
118 content_version->string_child("LabelText")
121 content_version->done ();
122 } else if (_standard == SMPTE) {
123 /* ContentVersion is required in SMPTE */
124 throw XMLError ("Missing ContentVersion tag in CPL");
126 auto rating_list = f.node_child ("RatingList");
128 for (auto i: rating_list->node_children("Rating")) {
129 _ratings.push_back (Rating(i));
132 _reels = type_grand_children<Reel> (f, "ReelList", "Reel");
134 auto reel_list = f.node_child ("ReelList");
136 auto reels = reel_list->node_children("Reel");
137 if (!reels.empty()) {
138 auto asset_list = reels.front()->node_child("AssetList");
139 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
141 read_composition_metadata_asset (metadata);
147 f.ignore_child ("Issuer");
148 f.ignore_child ("Signer");
149 f.ignore_child ("Signature");
154 /** Add a reel to this CPL.
155 * @param reel Reel to add.
158 CPL::add (std::shared_ptr<Reel> reel)
160 _reels.push_back (reel);
163 /** Write an CompositonPlaylist XML file.
165 * @param file Filename to write.
166 * @param standard INTEROP or SMPTE.
167 * @param signer Signer to sign the CPL, or 0 to add no signature.
170 CPL::write_xml (boost::filesystem::path file, Standard standard, shared_ptr<const CertificateChain> signer) const
173 xmlpp::Element* root;
174 if (standard == INTEROP) {
175 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
177 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
180 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
181 root->add_child("AnnotationText")->add_child_text (_annotation_text);
182 root->add_child("IssueDate")->add_child_text (_issue_date);
183 root->add_child("Issuer")->add_child_text (_issuer);
184 root->add_child("Creator")->add_child_text (_creator);
185 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
186 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
187 if (_content_versions.empty()) {
191 _content_versions[0].as_xml (root);
194 auto rating_list = root->add_child("RatingList");
195 for (auto i: _ratings) {
196 i.as_xml (rating_list->add_child("Rating"));
199 auto reel_list = root->add_child ("ReelList");
202 for (auto i: _reels) {
203 auto asset_list = i->write_to_cpl (reel_list, standard);
204 if (first && standard == dcp::SMPTE) {
205 maybe_write_composition_metadata_asset (asset_list);
213 signer->sign (root, standard);
216 doc.write_to_file_formatted (file.string(), "UTF-8");
223 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
225 auto fctt = node->node_child("FullContentTitleText");
226 _full_content_title_text = fctt->content();
227 _full_content_title_text_language = fctt->optional_string_attribute("language");
229 _release_territory = node->optional_string_child("ReleaseTerritory");
231 auto vn = node->optional_node_child("VersionNumber");
233 _version_number = raw_convert<int>(vn->content());
234 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
235 auto vn_status = vn->optional_string_attribute("status");
237 _status = string_to_status (*vn_status);
241 _chain = node->optional_string_child("Chain");
242 _distributor = node->optional_string_child("Distributor");
243 _facility = node->optional_string_child("Facility");
245 auto acv = node->optional_node_child("AlternateContentVersionList");
247 for (auto i: acv->node_children("ContentVersion")) {
248 _content_versions.push_back (ContentVersion(i));
252 auto lum = node->optional_node_child("Luminance");
254 _luminance = Luminance (lum);
257 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
259 auto sr = node->optional_string_child("MainSoundSampleRate");
261 vector<string> sr_bits;
262 boost::split (sr_bits, *sr, boost::is_any_of(" "));
263 DCP_ASSERT (sr_bits.size() == 2);
264 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
267 _main_picture_stored_area = dcp::Size (
268 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
269 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
272 _main_picture_active_area = dcp::Size (
273 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
274 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
277 auto sll = node->optional_string_child("MainSubtitleLanguageList");
279 vector<string> sll_split;
280 boost::split (sll_split, *sll, boost::is_any_of(" "));
281 DCP_ASSERT (!sll_split.empty());
283 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
285 if (!_reels.empty()) {
286 shared_ptr<dcp::ReelSubtitleAsset> sub = _reels.front()->main_subtitle();
288 optional<string> lang = sub->language();
289 if (lang && lang == sll_split[0]) {
295 for (auto i = first; i < sll_split.size(); ++i) {
296 _additional_subtitle_languages.push_back (sll_split[i]);
302 /** Write a CompositionMetadataAsset node as a child of @param node provided
303 * the required metadata is stored in the object. If any required metadata
304 * is missing this method will do nothing.
307 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
310 !_main_sound_configuration ||
311 !_main_sound_sample_rate ||
312 !_main_picture_stored_area ||
313 !_main_picture_active_area ||
315 !_reels.front()->main_picture()) {
319 auto meta = node->add_child("meta:CompositionMetadataAsset");
320 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
322 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
324 auto mp = _reels.front()->main_picture();
325 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
326 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
328 auto fctt = meta->add_child("FullContentTitleText", "meta");
329 if (_full_content_title_text) {
330 fctt->add_child_text (*_full_content_title_text);
332 if (_full_content_title_text_language) {
333 fctt->set_attribute("language", *_full_content_title_text_language);
336 if (_release_territory) {
337 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
340 if (_version_number) {
341 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
342 vn->add_child_text(raw_convert<string>(*_version_number));
344 vn->set_attribute("status", status_to_string(*_status));
349 meta->add_child("Chain", "meta")->add_child_text(*_chain);
353 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
357 meta->add_child("Facility", "meta")->add_child_text(*_facility);
360 if (_content_versions.size() > 1) {
361 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
362 for (size_t i = 1; i < _content_versions.size(); ++i) {
363 _content_versions[i].as_xml (vc);
368 _luminance->as_xml (meta, "meta");
371 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
372 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
374 auto stored = meta->add_child("MainPictureStoredArea", "meta");
375 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
376 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
378 auto active = meta->add_child("MainPictureActiveArea", "meta");
379 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
380 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
382 optional<string> first_subtitle_language;
383 for (auto i: _reels) {
384 if (i->main_subtitle()) {
385 first_subtitle_language = i->main_subtitle()->language();
386 if (first_subtitle_language) {
392 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
394 if (first_subtitle_language) {
395 lang = *first_subtitle_language;
397 for (auto const& i: _additional_subtitle_languages) {
403 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
406 /* SMPTE Bv2.1 8.6.3 */
407 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
408 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
409 extension->add_child("Name", "meta")->add_child_text("Application");
410 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
411 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
412 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
414 if (_reels.front()->main_sound()) {
415 auto asset = _reels.front()->main_sound()->asset();
417 auto reader = asset->start_read ();
418 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
419 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
420 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
421 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
424 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
425 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
426 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
427 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
428 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
430 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
431 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
432 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
433 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
434 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
435 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
436 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
437 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
438 if (!soundfield->MCATagName.empty()) {
439 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
440 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
442 if (!soundfield->RFC5646SpokenLanguage.empty()) {
443 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
444 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
447 list<ASDCP::MXF::InterchangeObject*> channels;
448 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
449 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
453 for (auto i: channels) {
454 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
455 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
456 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
457 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
458 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
459 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
460 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
461 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
462 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
463 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
464 if (!channel->MCATagName.empty()) {
465 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
466 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
468 if (!channel->MCAChannelID.empty()) {
469 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
471 if (!channel->RFC5646SpokenLanguage.empty()) {
472 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
473 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
475 if (!channel->SoundfieldGroupLinkID.empty()) {
476 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
477 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
486 list<shared_ptr<ReelMXF>>
489 list<shared_ptr<ReelMXF>> c;
491 for (auto i: _reels) {
492 if (i->main_picture ()) {
493 c.push_back (i->main_picture());
495 if (i->main_sound ()) {
496 c.push_back (i->main_sound());
498 if (i->main_subtitle ()) {
499 c.push_back (i->main_subtitle());
501 for (auto j: i->closed_captions()) {
505 c.push_back (i->atmos());
512 list<shared_ptr<const ReelMXF>>
513 CPL::reel_mxfs () const
515 list<shared_ptr<const ReelMXF>> c;
517 for (auto i: _reels) {
518 if (i->main_picture ()) {
519 c.push_back (i->main_picture());
521 if (i->main_sound ()) {
522 c.push_back (i->main_sound());
524 if (i->main_subtitle ()) {
525 c.push_back (i->main_subtitle());
527 for (auto j: i->closed_captions()) {
531 c.push_back (i->atmos());
539 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
541 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
546 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
547 string const s = "CPL: annotation texts differ: " + _annotation_text + " vs " + other_cpl->_annotation_text + "\n";
552 if (_content_kind != other_cpl->_content_kind) {
553 note (DCP_ERROR, "CPL: content kinds differ");
557 if (_reels.size() != other_cpl->_reels.size()) {
558 note (DCP_ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
562 auto a = _reels.begin();
563 auto b = other_cpl->_reels.begin();
565 while (a != _reels.end ()) {
566 if (!(*a)->equals (*b, opt, note)) {
576 /** @return true if we have any encrypted content */
578 CPL::encrypted () const
580 for (auto i: _reels) {
581 if (i->encrypted ()) {
589 /** Add a KDM to this CPL. If the KDM is for any of this CPLs assets it will be used
590 * to decrypt those assets.
594 CPL::add (DecryptedKDM const & kdm)
596 for (auto i: _reels) {
602 CPL::resolve_refs (list<shared_ptr<Asset>> assets)
604 for (auto i: _reels) {
605 i->resolve_refs (assets);
610 CPL::pkl_type (Standard standard) const
612 return static_pkl_type (standard);
616 CPL::static_pkl_type (Standard standard)
620 return "text/xml;asdcpKind=CPL";
629 CPL::duration () const
632 for (auto i: _reels) {
640 CPL::set_version_number (int v)
643 throw BadSettingError ("CPL version number cannot be negative");
651 CPL::set_content_versions (vector<ContentVersion> v)
655 if (!ids.insert(i.id).second) {
656 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
660 _content_versions = v;
664 optional<ContentVersion>
665 CPL::content_version () const
667 if (_content_versions.empty()) {
668 return optional<ContentVersion>();
671 return _content_versions[0];
676 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
678 _additional_subtitle_languages.clear ();
679 for (auto const& i: langs) {
680 _additional_subtitle_languages.push_back (i.to_string());