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);
526 vector<shared_ptr<const ReelFileAsset>>
527 CPL::reel_file_assets () const
529 vector<shared_ptr<const ReelFileAsset>> c;
530 add_file_assets (c, _reels);
536 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
538 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
543 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
544 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
545 note (NoteType::ERROR, s);
549 if (_content_kind != other_cpl->_content_kind) {
550 note (NoteType::ERROR, "CPL: content kinds differ");
554 if (_reels.size() != other_cpl->_reels.size()) {
555 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
559 auto a = _reels.begin();
560 auto b = other_cpl->_reels.begin();
562 while (a != _reels.end ()) {
563 if (!(*a)->equals (*b, opt, note)) {
575 CPL::any_encrypted () const
577 for (auto i: _reels) {
578 if (i->any_encrypted()) {
588 CPL::all_encrypted () const
590 for (auto i: _reels) {
591 if (!i->all_encrypted()) {
601 CPL::add (DecryptedKDM const & kdm)
603 for (auto i: _reels) {
609 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
611 for (auto i: _reels) {
612 i->resolve_refs (assets);
617 CPL::pkl_type (Standard standard) const
619 return static_pkl_type (standard);
623 CPL::static_pkl_type (Standard standard)
626 case Standard::INTEROP:
627 return "text/xml;asdcpKind=CPL";
628 case Standard::SMPTE:
636 CPL::duration () const
639 for (auto i: _reels) {
647 CPL::set_version_number (int v)
650 throw BadSettingError ("CPL version number cannot be negative");
658 CPL::unset_version_number ()
660 _version_number = boost::none;
665 CPL::set_content_versions (vector<ContentVersion> v)
669 if (!ids.insert(i.id).second) {
670 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
674 _content_versions = v;
678 optional<ContentVersion>
679 CPL::content_version () const
681 if (_content_versions.empty()) {
682 return optional<ContentVersion>();
685 return _content_versions[0];
690 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
692 _additional_subtitle_languages.clear ();
693 for (auto const& i: langs) {
694 _additional_subtitle_languages.push_back (i.to_string());