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");
237 auto vn = node->optional_node_child("VersionNumber");
239 _version_number = raw_convert<int>(vn->content());
240 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
241 auto vn_status = vn->optional_string_attribute("status");
243 _status = string_to_status (*vn_status);
247 _chain = node->optional_string_child("Chain");
248 _distributor = node->optional_string_child("Distributor");
249 _facility = node->optional_string_child("Facility");
251 auto acv = node->optional_node_child("AlternateContentVersionList");
253 for (auto i: acv->node_children("ContentVersion")) {
254 _content_versions.push_back (ContentVersion(i));
258 auto lum = node->optional_node_child("Luminance");
260 _luminance = Luminance (lum);
263 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
265 auto sr = node->optional_string_child("MainSoundSampleRate");
267 vector<string> sr_bits;
268 boost::split (sr_bits, *sr, boost::is_any_of(" "));
269 DCP_ASSERT (sr_bits.size() == 2);
270 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
273 _main_picture_stored_area = dcp::Size (
274 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
275 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
278 _main_picture_active_area = dcp::Size (
279 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
280 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
283 auto sll = node->optional_string_child("MainSubtitleLanguageList");
285 vector<string> sll_split;
286 boost::split (sll_split, *sll, boost::is_any_of(" "));
287 DCP_ASSERT (!sll_split.empty());
289 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
291 if (!_reels.empty()) {
292 shared_ptr<dcp::ReelSubtitleAsset> sub = _reels.front()->main_subtitle();
294 optional<string> lang = sub->language();
295 if (lang && lang == sll_split[0]) {
301 for (auto i = first; i < sll_split.size(); ++i) {
302 _additional_subtitle_languages.push_back (sll_split[i]);
308 /** Write a CompositionMetadataAsset node as a child of @param node provided
309 * the required metadata is stored in the object. If any required metadata
310 * is missing this method will do nothing.
313 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
316 !_main_sound_configuration ||
317 !_main_sound_sample_rate ||
318 !_main_picture_stored_area ||
319 !_main_picture_active_area ||
321 !_reels.front()->main_picture()) {
325 auto meta = node->add_child("meta:CompositionMetadataAsset");
326 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
328 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
330 auto mp = _reels.front()->main_picture();
331 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
332 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
334 auto fctt = meta->add_child("FullContentTitleText", "meta");
335 if (_full_content_title_text) {
336 fctt->add_child_text (*_full_content_title_text);
338 if (_full_content_title_text_language) {
339 fctt->set_attribute("language", *_full_content_title_text_language);
342 if (_release_territory) {
343 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
346 if (_version_number) {
347 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
348 vn->add_child_text(raw_convert<string>(*_version_number));
350 vn->set_attribute("status", status_to_string(*_status));
355 meta->add_child("Chain", "meta")->add_child_text(*_chain);
359 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
363 meta->add_child("Facility", "meta")->add_child_text(*_facility);
366 if (_content_versions.size() > 1) {
367 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
368 for (size_t i = 1; i < _content_versions.size(); ++i) {
369 _content_versions[i].as_xml (vc);
374 _luminance->as_xml (meta, "meta");
377 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
378 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
380 auto stored = meta->add_child("MainPictureStoredArea", "meta");
381 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
382 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
384 auto active = meta->add_child("MainPictureActiveArea", "meta");
385 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
386 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
388 optional<string> first_subtitle_language;
389 for (auto i: _reels) {
390 if (i->main_subtitle()) {
391 first_subtitle_language = i->main_subtitle()->language();
392 if (first_subtitle_language) {
398 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
400 if (first_subtitle_language) {
401 lang = *first_subtitle_language;
403 for (auto const& i: _additional_subtitle_languages) {
409 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
412 /* SMPTE Bv2.1 8.6.3 */
413 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
414 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
415 extension->add_child("Name", "meta")->add_child_text("Application");
416 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
417 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
418 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
420 if (_reels.front()->main_sound()) {
421 auto asset = _reels.front()->main_sound()->asset();
423 auto reader = asset->start_read ();
424 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
425 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
426 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
427 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
430 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
431 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
432 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
433 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
434 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
436 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
437 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
438 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
439 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
440 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
441 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
442 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
443 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
444 if (!soundfield->MCATagName.empty()) {
445 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
446 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
448 if (!soundfield->RFC5646SpokenLanguage.empty()) {
449 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
450 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
453 list<ASDCP::MXF::InterchangeObject*> channels;
454 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
455 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
459 for (auto i: channels) {
460 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
461 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
462 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
463 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
464 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
465 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
466 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
467 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
468 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
469 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
470 if (!channel->MCATagName.empty()) {
471 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
472 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
474 if (!channel->MCAChannelID.empty()) {
475 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
477 if (!channel->RFC5646SpokenLanguage.empty()) {
478 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
479 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
481 if (!channel->SoundfieldGroupLinkID.empty()) {
482 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
483 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
492 vector<shared_ptr<ReelMXF>>
495 vector<shared_ptr<ReelMXF>> c;
497 for (auto i: _reels) {
498 if (i->main_picture ()) {
499 c.push_back (i->main_picture());
501 if (i->main_sound ()) {
502 c.push_back (i->main_sound());
504 if (i->main_subtitle ()) {
505 c.push_back (i->main_subtitle());
507 for (auto j: i->closed_captions()) {
511 c.push_back (i->atmos());
518 vector<shared_ptr<const ReelMXF>>
519 CPL::reel_mxfs () const
521 vector<shared_ptr<const ReelMXF>> c;
523 for (auto i: _reels) {
524 if (i->main_picture ()) {
525 c.push_back (i->main_picture());
527 if (i->main_sound ()) {
528 c.push_back (i->main_sound());
530 if (i->main_subtitle ()) {
531 c.push_back (i->main_subtitle());
533 for (auto j: i->closed_captions()) {
537 c.push_back (i->atmos());
545 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
547 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
552 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
553 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
558 if (_content_kind != other_cpl->_content_kind) {
559 note (DCP_ERROR, "CPL: content kinds differ");
563 if (_reels.size() != other_cpl->_reels.size()) {
564 note (DCP_ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
568 auto a = _reels.begin();
569 auto b = other_cpl->_reels.begin();
571 while (a != _reels.end ()) {
572 if (!(*a)->equals (*b, opt, note)) {
582 /** @return true if we have any encrypted content */
584 CPL::encrypted () const
586 for (auto i: _reels) {
587 if (i->encrypted ()) {
595 /** Add a KDM to this CPL. If the KDM is for any of this CPLs assets it will be used
596 * to decrypt those assets.
600 CPL::add (DecryptedKDM const & kdm)
602 for (auto i: _reels) {
608 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
610 for (auto i: _reels) {
611 i->resolve_refs (assets);
616 CPL::pkl_type (Standard standard) const
618 return static_pkl_type (standard);
622 CPL::static_pkl_type (Standard standard)
626 return "text/xml;asdcpKind=CPL";
635 CPL::duration () const
638 for (auto i: _reels) {
646 CPL::set_version_number (int v)
649 throw BadSettingError ("CPL version number cannot be negative");
657 CPL::set_content_versions (vector<ContentVersion> v)
661 if (!ids.insert(i.id).second) {
662 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
666 _content_versions = v;
670 optional<ContentVersion>
671 CPL::content_version () const
673 if (_content_versions.empty()) {
674 return optional<ContentVersion>();
677 return _content_versions[0];
682 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
684 _additional_subtitle_languages.clear ();
685 for (auto const& i: langs) {
686 _additional_subtitle_languages.push_back (i.to_string());