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");
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 if (_annotation_text) {
182 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
184 root->add_child("IssueDate")->add_child_text (_issue_date);
185 root->add_child("Issuer")->add_child_text (_issuer);
186 root->add_child("Creator")->add_child_text (_creator);
187 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
188 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
189 if (_content_versions.empty()) {
193 _content_versions[0].as_xml (root);
196 auto rating_list = root->add_child("RatingList");
197 for (auto i: _ratings) {
198 i.as_xml (rating_list->add_child("Rating"));
201 auto reel_list = root->add_child ("ReelList");
203 if (_reels.empty()) {
204 throw NoReelsError ();
208 for (auto i: _reels) {
209 auto asset_list = i->write_to_cpl (reel_list, standard);
210 if (first && standard == dcp::SMPTE) {
211 maybe_write_composition_metadata_asset (asset_list);
219 signer->sign (root, standard);
222 doc.write_to_file_formatted (file.string(), "UTF-8");
229 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
231 auto fctt = node->node_child("FullContentTitleText");
232 _full_content_title_text = fctt->content();
233 _full_content_title_text_language = fctt->optional_string_attribute("language");
235 _release_territory = node->optional_string_child("ReleaseTerritory");
236 if (_release_territory) {
237 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
240 auto vn = node->optional_node_child("VersionNumber");
242 _version_number = raw_convert<int>(vn->content());
243 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
244 auto vn_status = vn->optional_string_attribute("status");
246 _status = string_to_status (*vn_status);
250 _chain = node->optional_string_child("Chain");
251 _distributor = node->optional_string_child("Distributor");
252 _facility = node->optional_string_child("Facility");
254 auto acv = node->optional_node_child("AlternateContentVersionList");
256 for (auto i: acv->node_children("ContentVersion")) {
257 _content_versions.push_back (ContentVersion(i));
261 auto lum = node->optional_node_child("Luminance");
263 _luminance = Luminance (lum);
266 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
268 auto sr = node->optional_string_child("MainSoundSampleRate");
270 vector<string> sr_bits;
271 boost::split (sr_bits, *sr, boost::is_any_of(" "));
272 DCP_ASSERT (sr_bits.size() == 2);
273 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
276 _main_picture_stored_area = dcp::Size (
277 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
278 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
281 _main_picture_active_area = dcp::Size (
282 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
283 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
286 auto sll = node->optional_string_child("MainSubtitleLanguageList");
288 vector<string> sll_split;
289 boost::split (sll_split, *sll, boost::is_any_of(" "));
290 DCP_ASSERT (!sll_split.empty());
292 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
294 if (!_reels.empty()) {
295 shared_ptr<dcp::ReelSubtitleAsset> sub = _reels.front()->main_subtitle();
297 optional<string> lang = sub->language();
298 if (lang && lang == sll_split[0]) {
304 for (auto i = first; i < sll_split.size(); ++i) {
305 _additional_subtitle_languages.push_back (sll_split[i]);
311 /** Write a CompositionMetadataAsset node as a child of @param node provided
312 * the required metadata is stored in the object. If any required metadata
313 * is missing this method will do nothing.
316 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
319 !_main_sound_configuration ||
320 !_main_sound_sample_rate ||
321 !_main_picture_stored_area ||
322 !_main_picture_active_area ||
324 !_reels.front()->main_picture()) {
328 auto meta = node->add_child("meta:CompositionMetadataAsset");
329 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
331 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
333 auto mp = _reels.front()->main_picture();
334 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
335 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
337 auto fctt = meta->add_child("FullContentTitleText", "meta");
338 if (_full_content_title_text) {
339 fctt->add_child_text (*_full_content_title_text);
341 if (_full_content_title_text_language) {
342 fctt->set_attribute("language", *_full_content_title_text_language);
345 if (_release_territory) {
346 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
349 if (_version_number) {
350 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
351 vn->add_child_text(raw_convert<string>(*_version_number));
353 vn->set_attribute("status", status_to_string(*_status));
358 meta->add_child("Chain", "meta")->add_child_text(*_chain);
362 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
366 meta->add_child("Facility", "meta")->add_child_text(*_facility);
369 if (_content_versions.size() > 1) {
370 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
371 for (size_t i = 1; i < _content_versions.size(); ++i) {
372 _content_versions[i].as_xml (vc);
377 _luminance->as_xml (meta, "meta");
380 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
381 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
383 auto stored = meta->add_child("MainPictureStoredArea", "meta");
384 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
385 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
387 auto active = meta->add_child("MainPictureActiveArea", "meta");
388 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
389 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
391 optional<string> first_subtitle_language;
392 for (auto i: _reels) {
393 if (i->main_subtitle()) {
394 first_subtitle_language = i->main_subtitle()->language();
395 if (first_subtitle_language) {
401 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
403 if (first_subtitle_language) {
404 lang = *first_subtitle_language;
406 for (auto const& i: _additional_subtitle_languages) {
412 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
415 /* SMPTE Bv2.1 8.6.3 */
416 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
417 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
418 extension->add_child("Name", "meta")->add_child_text("Application");
419 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
420 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
421 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
423 if (_reels.front()->main_sound()) {
424 auto asset = _reels.front()->main_sound()->asset();
426 auto reader = asset->start_read ();
427 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
428 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
429 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
430 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
433 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
434 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
435 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
436 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
437 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
439 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
440 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
441 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
442 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
443 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
444 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
445 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
446 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
447 if (!soundfield->MCATagName.empty()) {
448 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
449 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
451 if (!soundfield->RFC5646SpokenLanguage.empty()) {
452 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
453 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
456 list<ASDCP::MXF::InterchangeObject*> channels;
457 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
458 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
462 for (auto i: channels) {
463 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
464 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
465 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
466 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
467 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
468 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
469 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
470 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
471 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
472 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
473 if (!channel->MCATagName.empty()) {
474 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
475 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
477 if (!channel->MCAChannelID.empty()) {
478 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
480 if (!channel->RFC5646SpokenLanguage.empty()) {
481 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
482 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
484 if (!channel->SoundfieldGroupLinkID.empty()) {
485 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
486 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
495 vector<shared_ptr<ReelMXF>>
498 vector<shared_ptr<ReelMXF>> c;
500 for (auto i: _reels) {
501 if (i->main_picture ()) {
502 c.push_back (i->main_picture());
504 if (i->main_sound ()) {
505 c.push_back (i->main_sound());
507 if (i->main_subtitle ()) {
508 c.push_back (i->main_subtitle());
510 for (auto j: i->closed_captions()) {
514 c.push_back (i->atmos());
521 vector<shared_ptr<const ReelMXF>>
522 CPL::reel_mxfs () const
524 vector<shared_ptr<const ReelMXF>> c;
526 for (auto i: _reels) {
527 if (i->main_picture ()) {
528 c.push_back (i->main_picture());
530 if (i->main_sound ()) {
531 c.push_back (i->main_sound());
533 if (i->main_subtitle ()) {
534 c.push_back (i->main_subtitle());
536 for (auto j: i->closed_captions()) {
540 c.push_back (i->atmos());
548 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
550 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
555 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
556 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
561 if (_content_kind != other_cpl->_content_kind) {
562 note (DCP_ERROR, "CPL: content kinds differ");
566 if (_reels.size() != other_cpl->_reels.size()) {
567 note (DCP_ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
571 auto a = _reels.begin();
572 auto b = other_cpl->_reels.begin();
574 while (a != _reels.end ()) {
575 if (!(*a)->equals (*b, opt, note)) {
585 /** @return true if we have any encrypted content */
587 CPL::encrypted () const
589 for (auto i: _reels) {
590 if (i->encrypted ()) {
598 /** Add a KDM to this CPL. If the KDM is for any of this CPLs assets it will be used
599 * to decrypt those assets.
603 CPL::add (DecryptedKDM const & kdm)
605 for (auto i: _reels) {
611 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
613 for (auto i: _reels) {
614 i->resolve_refs (assets);
619 CPL::pkl_type (Standard standard) const
621 return static_pkl_type (standard);
625 CPL::static_pkl_type (Standard standard)
629 return "text/xml;asdcpKind=CPL";
638 CPL::duration () const
641 for (auto i: _reels) {
649 CPL::set_version_number (int v)
652 throw BadSettingError ("CPL version number cannot be negative");
660 CPL::unset_version_number ()
662 _version_number = boost::none;
667 CPL::set_content_versions (vector<ContentVersion> v)
671 if (!ids.insert(i.id).second) {
672 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
676 _content_versions = v;
680 optional<ContentVersion>
681 CPL::content_version () const
683 if (_content_versions.empty()) {
684 return optional<ContentVersion>();
687 return _content_versions[0];
692 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
694 _additional_subtitle_languages.clear ();
695 for (auto const& i: langs) {
696 _additional_subtitle_languages.push_back (i.to_string());