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.
40 #include "certificate_chain.h"
41 #include "compose.hpp"
43 #include "dcp_assert.h"
44 #include "local_time.h"
46 #include "raw_convert.h"
48 #include "reel_atmos_asset.h"
49 #include "reel_closed_caption_asset.h"
50 #include "reel_picture_asset.h"
51 #include "reel_sound_asset.h"
52 #include "reel_subtitle_asset.h"
56 LIBDCP_DISABLE_WARNINGS
57 #include <asdcp/Metadata.h>
58 LIBDCP_ENABLE_WARNINGS
59 #include <libxml/parser.h>
60 LIBDCP_DISABLE_WARNINGS
61 #include <libxml++/libxml++.h>
62 LIBDCP_ENABLE_WARNINGS
63 #include <boost/algorithm/string.hpp>
67 using std::dynamic_pointer_cast;
70 using std::make_shared;
73 using std::shared_ptr;
76 using boost::optional;
80 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
81 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
82 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
83 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
84 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
85 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
88 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
89 /* default _content_title_text to annotation_text */
90 : _issuer ("libdcp" LIBDCP_VERSION)
91 , _creator ("libdcp" LIBDCP_VERSION)
92 , _issue_date (LocalTime().as_string())
93 , _annotation_text (annotation_text)
94 , _content_title_text (annotation_text)
95 , _content_kind (content_kind)
96 , _standard (standard)
99 cv.label_text = cv.id + LocalTime().as_string();
100 _content_versions.push_back (cv);
104 CPL::CPL (boost::filesystem::path file)
106 , _content_kind (ContentKind::FEATURE)
108 cxml::Document f ("CompositionPlaylist");
111 if (f.namespace_uri() == cpl_interop_ns) {
112 _standard = Standard::INTEROP;
113 } else if (f.namespace_uri() == cpl_smpte_ns) {
114 _standard = Standard::SMPTE;
116 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
119 _id = remove_urn_uuid (f.string_child ("Id"));
120 _annotation_text = f.optional_string_child("AnnotationText");
121 _issuer = f.optional_string_child("Issuer").get_value_or("");
122 _creator = f.optional_string_child("Creator").get_value_or("");
123 _issue_date = f.string_child ("IssueDate");
124 _content_title_text = f.string_child ("ContentTitleText");
125 _content_kind = content_kind_from_string (f.string_child ("ContentKind"));
126 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
127 if (content_version) {
128 /* XXX: SMPTE should insist that Id is present */
129 _content_versions.push_back (
131 content_version->optional_string_child("Id").get_value_or(""),
132 content_version->string_child("LabelText")
135 content_version->done ();
136 } else if (_standard == Standard::SMPTE) {
137 /* ContentVersion is required in SMPTE */
138 throw XMLError ("Missing ContentVersion tag in CPL");
140 auto rating_list = f.node_child ("RatingList");
142 for (auto i: rating_list->node_children("Rating")) {
143 _ratings.push_back (Rating(i));
147 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
148 _reels.push_back (make_shared<Reel>(i, _standard));
151 auto reel_list = f.node_child ("ReelList");
153 auto reels = reel_list->node_children("Reel");
154 if (!reels.empty()) {
155 auto asset_list = reels.front()->node_child("AssetList");
156 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
158 read_composition_metadata_asset (metadata);
163 f.ignore_child ("Issuer");
164 f.ignore_child ("Signer");
165 f.ignore_child ("Signature");
172 CPL::add (std::shared_ptr<Reel> reel)
174 _reels.push_back (reel);
179 CPL::write_xml (boost::filesystem::path file, shared_ptr<const CertificateChain> signer) const
182 xmlpp::Element* root;
183 if (_standard == Standard::INTEROP) {
184 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
186 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
189 root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
190 if (_annotation_text) {
191 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
193 root->add_child("IssueDate")->add_child_text (_issue_date);
194 root->add_child("Issuer")->add_child_text (_issuer);
195 root->add_child("Creator")->add_child_text (_creator);
196 root->add_child("ContentTitleText")->add_child_text (_content_title_text);
197 root->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
198 if (_content_versions.empty()) {
202 _content_versions[0].as_xml (root);
205 auto rating_list = root->add_child("RatingList");
206 for (auto i: _ratings) {
207 i.as_xml (rating_list->add_child("Rating"));
210 auto reel_list = root->add_child ("ReelList");
212 if (_reels.empty()) {
213 throw NoReelsError ();
217 for (auto i: _reels) {
218 auto asset_list = i->write_to_cpl (reel_list, _standard);
219 if (first && _standard == Standard::SMPTE) {
220 maybe_write_composition_metadata_asset (asset_list);
228 signer->sign (root, _standard);
231 doc.write_to_file_formatted (file.string(), "UTF-8");
238 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
240 auto fctt = node->node_child("FullContentTitleText");
241 _full_content_title_text = fctt->content();
242 _full_content_title_text_language = fctt->optional_string_attribute("language");
244 _release_territory = node->optional_string_child("ReleaseTerritory");
245 if (_release_territory) {
246 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
249 auto vn = node->optional_node_child("VersionNumber");
251 _version_number = raw_convert<int>(vn->content());
252 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
253 auto vn_status = vn->optional_string_attribute("status");
255 _status = string_to_status (*vn_status);
259 _chain = node->optional_string_child("Chain");
260 _distributor = node->optional_string_child("Distributor");
261 _facility = node->optional_string_child("Facility");
263 auto acv = node->optional_node_child("AlternateContentVersionList");
265 for (auto i: acv->node_children("ContentVersion")) {
266 _content_versions.push_back (ContentVersion(i));
270 auto lum = node->optional_node_child("Luminance");
272 _luminance = Luminance (lum);
275 _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
277 auto sr = node->optional_string_child("MainSoundSampleRate");
279 vector<string> sr_bits;
280 boost::split (sr_bits, *sr, boost::is_any_of(" "));
281 DCP_ASSERT (sr_bits.size() == 2);
282 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
285 _main_picture_stored_area = dcp::Size (
286 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
287 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
290 _main_picture_active_area = dcp::Size (
291 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
292 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
295 auto sll = node->optional_string_child("MainSubtitleLanguageList");
297 vector<string> sll_split;
298 boost::split (sll_split, *sll, boost::is_any_of(" "));
299 DCP_ASSERT (!sll_split.empty());
301 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
303 if (!_reels.empty()) {
304 auto sub = _reels.front()->main_subtitle();
306 auto lang = sub->language();
307 if (lang && lang == sll_split[0]) {
313 for (auto i = first; i < sll_split.size(); ++i) {
314 _additional_subtitle_languages.push_back (sll_split[i]);
320 /** Write a CompositionMetadataAsset node as a child of @param node provided
321 * the required metadata is stored in the object. If any required metadata
322 * is missing this method will do nothing.
325 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
328 !_main_sound_configuration ||
329 !_main_sound_sample_rate ||
330 !_main_picture_stored_area ||
331 !_main_picture_active_area ||
333 !_reels.front()->main_picture()) {
337 auto meta = node->add_child("meta:CompositionMetadataAsset");
338 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
340 meta->add_child("Id")->add_child_text("urn:uuid:" + make_uuid());
342 auto mp = _reels.front()->main_picture();
343 meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
344 meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
346 auto fctt = meta->add_child("FullContentTitleText", "meta");
347 if (_full_content_title_text) {
348 fctt->add_child_text (*_full_content_title_text);
350 if (_full_content_title_text_language) {
351 fctt->set_attribute("language", *_full_content_title_text_language);
354 if (_release_territory) {
355 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
358 if (_version_number) {
359 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
360 vn->add_child_text(raw_convert<string>(*_version_number));
362 vn->set_attribute("status", status_to_string(*_status));
367 meta->add_child("Chain", "meta")->add_child_text(*_chain);
371 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
375 meta->add_child("Facility", "meta")->add_child_text(*_facility);
378 if (_content_versions.size() > 1) {
379 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
380 for (size_t i = 1; i < _content_versions.size(); ++i) {
381 _content_versions[i].as_xml (vc);
386 _luminance->as_xml (meta, "meta");
389 meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
390 meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
392 auto stored = meta->add_child("MainPictureStoredArea", "meta");
393 stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
394 stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
396 auto active = meta->add_child("MainPictureActiveArea", "meta");
397 active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
398 active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
400 optional<string> first_subtitle_language;
401 for (auto i: _reels) {
402 if (i->main_subtitle()) {
403 first_subtitle_language = i->main_subtitle()->language();
404 if (first_subtitle_language) {
410 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
412 if (first_subtitle_language) {
413 lang = *first_subtitle_language;
415 for (auto const& i: _additional_subtitle_languages) {
421 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
424 /* SMPTE Bv2.1 8.6.3 */
425 auto extension = meta->add_child("ExtensionMetadataList", "meta")->add_child("ExtensionMetadata", "meta");
426 extension->set_attribute("scope", "http://isdcf.com/ns/cplmd/app");
427 extension->add_child("Name", "meta")->add_child_text("Application");
428 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
429 property->add_child("Name", "meta")->add_child_text("DCP Constraints Profile");
430 property->add_child("Value", "meta")->add_child_text("SMPTE-RDD-52:2020-Bv2.1");
432 if (_reels.front()->main_sound()) {
433 auto asset = _reels.front()->main_sound()->asset();
435 auto reader = asset->start_read ();
436 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
437 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
438 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
439 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
442 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
443 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
444 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
445 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
446 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
448 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
449 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
450 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
451 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
452 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
453 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
454 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
455 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
456 if (!soundfield->MCATagName.empty()) {
457 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
458 sf->add_child("MCATagName", "r1")->add_child_text(buffer);
460 if (!soundfield->RFC5646SpokenLanguage.empty()) {
461 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
462 sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
465 list<ASDCP::MXF::InterchangeObject*> channels;
466 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
467 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
471 for (auto i: channels) {
472 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
473 auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
474 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
475 ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
476 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
477 ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
478 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
479 ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
480 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
481 ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
482 if (!channel->MCATagName.empty()) {
483 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
484 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
486 if (!channel->MCAChannelID.empty()) {
487 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
489 if (!channel->RFC5646SpokenLanguage.empty()) {
490 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
491 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
493 if (!channel->SoundfieldGroupLinkID.empty()) {
494 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
495 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
506 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
508 for (auto i: reels) {
509 if (i->main_picture ()) {
510 assets.push_back (i->main_picture());
512 if (i->main_sound ()) {
513 assets.push_back (i->main_sound());
515 if (i->main_subtitle ()) {
516 assets.push_back (i->main_subtitle());
518 for (auto j: i->closed_captions()) {
519 assets.push_back (j);
522 assets.push_back (i->atmos());
528 vector<shared_ptr<ReelFileAsset>>
529 CPL::reel_file_assets ()
531 vector<shared_ptr<ReelFileAsset>> c;
532 add_file_assets (c, _reels);
537 vector<shared_ptr<const ReelFileAsset>>
538 CPL::reel_file_assets () const
540 vector<shared_ptr<const ReelFileAsset>> c;
541 add_file_assets (c, _reels);
547 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
549 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
554 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
555 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
556 note (NoteType::ERROR, s);
560 if (_content_kind != other_cpl->_content_kind) {
561 note (NoteType::ERROR, "CPL: content kinds differ");
565 if (_reels.size() != other_cpl->_reels.size()) {
566 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
570 auto a = _reels.begin();
571 auto b = other_cpl->_reels.begin();
573 while (a != _reels.end ()) {
574 if (!(*a)->equals (*b, opt, note)) {
586 CPL::any_encrypted () const
588 for (auto i: _reels) {
589 if (i->any_encrypted()) {
599 CPL::all_encrypted () const
601 for (auto i: _reels) {
602 if (!i->all_encrypted()) {
612 CPL::add (DecryptedKDM const & kdm)
614 for (auto i: _reels) {
620 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
622 for (auto i: _reels) {
623 i->resolve_refs (assets);
628 CPL::pkl_type (Standard standard) const
630 return static_pkl_type (standard);
634 CPL::static_pkl_type (Standard standard)
637 case Standard::INTEROP:
638 return "text/xml;asdcpKind=CPL";
639 case Standard::SMPTE:
647 CPL::duration () const
650 for (auto i: _reels) {
658 CPL::set_version_number (int v)
661 throw BadSettingError ("CPL version number cannot be negative");
669 CPL::unset_version_number ()
671 _version_number = boost::none;
676 CPL::set_content_versions (vector<ContentVersion> v)
680 if (!ids.insert(i.id).second) {
681 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
685 _content_versions = v;
689 optional<ContentVersion>
690 CPL::content_version () const
692 if (_content_versions.empty()) {
693 return optional<ContentVersion>();
696 return _content_versions[0];
701 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
703 _additional_subtitle_languages.clear ();
704 for (auto const& i: langs) {
705 _additional_subtitle_languages.push_back (i.to_string());