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 "equality_options.h"
45 #include "filesystem.h"
46 #include "local_time.h"
48 #include "raw_convert.h"
50 #include "reel_atmos_asset.h"
51 #include "reel_closed_caption_asset.h"
52 #include "reel_picture_asset.h"
53 #include "reel_sound_asset.h"
54 #include "reel_subtitle_asset.h"
59 LIBDCP_DISABLE_WARNINGS
60 #include <asdcp/Metadata.h>
61 LIBDCP_ENABLE_WARNINGS
62 #include <libxml/parser.h>
63 LIBDCP_DISABLE_WARNINGS
64 #include <libxml++/libxml++.h>
65 LIBDCP_ENABLE_WARNINGS
66 #include <boost/algorithm/string.hpp>
70 using std::dynamic_pointer_cast;
73 using std::make_shared;
76 using std::shared_ptr;
79 using boost::optional;
83 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
84 static string const cpl_smpte_ns = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
85 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
86 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
87 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
88 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
91 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
92 /* default _content_title_text to annotation_text */
93 : _issuer("libdcp", dcp::version)
94 , _creator("libdcp", dcp::version)
95 , _issue_date (LocalTime().as_string())
96 , _annotation_text (annotation_text)
97 , _content_title_text (annotation_text)
98 , _content_kind (content_kind)
99 , _standard (standard)
102 cv.label_text = cv.id + LocalTime().as_string();
103 _content_versions.push_back (cv);
107 CPL::CPL (boost::filesystem::path file, vector<dcp::VerificationNote>* notes)
109 , _content_kind (ContentKind::FEATURE)
111 cxml::Document f ("CompositionPlaylist");
112 f.read_file(dcp::filesystem::fix_long_path(file));
114 if (f.namespace_uri() == cpl_interop_ns) {
115 _standard = Standard::INTEROP;
116 } else if (f.namespace_uri() == cpl_smpte_ns) {
117 _standard = Standard::SMPTE;
121 dcp::VerificationNote(
122 dcp::VerificationNote::Type::ERROR,
123 dcp::VerificationNote::Code::INVALID_CPL_NAMESPACE,
129 _standard = Standard::INTEROP;
132 _id = remove_urn_uuid (f.string_child ("Id"));
133 _annotation_text = f.optional_string_child("AnnotationText");
134 _issuer = f.optional_string_child("Issuer").get_value_or("");
135 _creator = f.optional_string_child("Creator").get_value_or("");
136 _issue_date = f.string_child ("IssueDate");
137 _content_title_text = f.string_child ("ContentTitleText");
138 auto content_kind = f.node_child("ContentKind");
139 _content_kind = ContentKind(content_kind->content(), content_kind->optional_string_attribute("scope"));
140 shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
141 if (content_version) {
142 /* XXX: SMPTE should insist that Id is present */
143 _content_versions.push_back (
145 content_version->optional_string_child("Id").get_value_or(""),
146 content_version->string_child("LabelText")
149 content_version->done ();
150 } else if (_standard == Standard::SMPTE) {
151 /* ContentVersion is required in SMPTE */
154 dcp::VerificationNote(
155 dcp::VerificationNote::Type::ERROR,
156 dcp::VerificationNote::Code::MISSING_CPL_CONTENT_VERSION,
163 auto rating_list = f.node_child ("RatingList");
164 for (auto i: rating_list->node_children("Rating")) {
165 _ratings.push_back (Rating(i));
168 for (auto i: f.node_child("ReelList")->node_children("Reel")) {
169 _reels.push_back (make_shared<Reel>(i, _standard));
172 auto reel_list = f.node_child ("ReelList");
173 auto reels = reel_list->node_children("Reel");
174 if (!reels.empty()) {
175 auto asset_list = reels.front()->node_child("AssetList");
176 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
178 read_composition_metadata_asset (metadata);
179 _read_composition_metadata = true;
183 f.ignore_child ("Issuer");
184 f.ignore_child ("Signer");
185 f.ignore_child ("Signature");
192 CPL::add (std::shared_ptr<Reel> reel)
194 _reels.push_back (reel);
199 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
206 CPL::write_xml(boost::filesystem::path file, shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors) const
209 xmlpp::Element* root;
210 if (_standard == Standard::INTEROP) {
211 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
213 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
216 cxml::add_text_child(root, "Id", "urn:uuid:" + _id);
217 if (_annotation_text) {
218 cxml::add_text_child(root, "AnnotationText", *_annotation_text);
220 cxml::add_text_child(root, "IssueDate", _issue_date);
221 cxml::add_text_child(root, "Issuer", _issuer);
222 cxml::add_text_child(root, "Creator", _creator);
223 cxml::add_text_child(root, "ContentTitleText", _content_title_text);
224 auto content_kind = cxml::add_child(root, "ContentKind");
225 content_kind->add_child_text(_content_kind.name());
226 if (_content_kind.scope()) {
227 content_kind->set_attribute("scope", *_content_kind.scope());
229 if (_content_versions.empty()) {
233 _content_versions[0].as_xml (root);
236 auto rating_list = cxml::add_child(root, "RatingList");
237 for (auto i: _ratings) {
238 i.as_xml(cxml::add_child(rating_list, "Rating"));
241 auto reel_list = cxml::add_child(root, "ReelList");
243 if (_reels.empty()) {
244 throw NoReelsError ();
248 for (auto i: _reels) {
249 auto asset_list = i->write_to_cpl (reel_list, _standard);
250 if (first && _standard == Standard::SMPTE) {
251 maybe_write_composition_metadata_asset(asset_list, include_mca_subdescriptors);
259 signer->sign (root, _standard);
262 doc.write_to_file_formatted(dcp::filesystem::fix_long_path(file).string(), "UTF-8");
269 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
271 _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
273 /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
274 * apparently didn't include it, so as usual we have to be defensive.
276 if (auto fctt = node->optional_node_child("FullContentTitleText")) {
277 _full_content_title_text = fctt->content();
278 _full_content_title_text_language = fctt->optional_string_attribute("language");
281 _release_territory = node->optional_string_child("ReleaseTerritory");
282 if (_release_territory) {
283 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
286 auto vn = node->optional_node_child("VersionNumber");
288 _version_number = raw_convert<int>(vn->content());
289 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
290 auto vn_status = vn->optional_string_attribute("status");
292 _status = string_to_status (*vn_status);
296 _chain = node->optional_string_child("Chain");
297 _distributor = node->optional_string_child("Distributor");
298 _facility = node->optional_string_child("Facility");
300 auto acv = node->optional_node_child("AlternateContentVersionList");
302 for (auto i: acv->node_children("ContentVersion")) {
303 _content_versions.push_back (ContentVersion(i));
307 auto lum = node->optional_node_child("Luminance");
309 _luminance = Luminance (lum);
312 if (auto msc = node->optional_string_child("MainSoundConfiguration")) {
314 _main_sound_configuration = MainSoundConfiguration(*msc);
315 } catch (MainSoundConfigurationError& e) {
316 /* With Interop DCPs this node may not make any sense, but that's OK */
317 if (_standard == dcp::Standard::SMPTE) {
323 auto sr = node->optional_string_child("MainSoundSampleRate");
325 vector<string> sr_bits;
326 boost::split (sr_bits, *sr, boost::is_any_of(" "));
327 DCP_ASSERT (sr_bits.size() == 2);
328 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
331 if (_standard == dcp::Standard::SMPTE) {
332 _main_picture_stored_area = dcp::Size (
333 node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
334 node->node_child("MainPictureStoredArea")->number_child<int>("Height")
338 _main_picture_active_area = dcp::Size (
339 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
340 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
343 auto sll = node->optional_string_child("MainSubtitleLanguageList");
345 vector<string> sll_split;
346 boost::split (sll_split, *sll, boost::is_any_of(" "));
347 DCP_ASSERT (!sll_split.empty());
349 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
351 if (!_reels.empty()) {
352 auto sub = _reels.front()->main_subtitle();
354 auto lang = sub->language();
355 if (lang && lang == sll_split[0]) {
361 for (auto i = first; i < sll_split.size(); ++i) {
362 _additional_subtitle_languages.push_back (sll_split[i]);
366 auto eml = node->optional_node_child ("ExtensionMetadataList");
368 auto extension_metadata = [eml](string scope, string name, string property) -> boost::optional<std::string> {
373 for (auto i: eml->node_children("ExtensionMetadata")) {
374 auto xml_scope = i->optional_string_attribute("scope");
375 auto xml_name = i->optional_string_child("Name");
376 if (xml_scope && *xml_scope == scope && xml_name && *xml_name == name) {
377 auto property_list = i->node_child("PropertyList");
378 for (auto j: property_list->node_children("Property")) {
379 auto property_name = j->optional_string_child("Name");
380 auto property_value = j->optional_string_child("Value");
381 if (property_name && property_value && *property_name == property) {
382 return property_value;
391 _sign_language_video_language = extension_metadata("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag");
392 _dolby_edr_image_transfer_function = extension_metadata("http://www.dolby.com/schemas/2014/EDR-Metadata", "Dolby EDR", "image transfer function");
397 CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsset> asset) const
399 auto reader = asset->start_read ();
400 ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
401 ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
402 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
403 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
406 auto mca_subs = cxml::add_child(parent, "mca:MCASubDescriptors");
407 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
408 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
409 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
410 auto sf = cxml::add_child(mca_subs, "SoundfieldGroupLabelSubDescriptor", string("r0"));
412 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
413 cxml::add_child(sf, "InstanceID", string("r1"))->add_child_text("urn:uuid:" + string(buffer));
414 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
415 cxml::add_child(sf, "MCALabelDictionaryID", string("r1"))->add_child_text("urn:smpte:ul:" + string(buffer));
416 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
417 cxml::add_child(sf, "MCALinkID", string("r1"))->add_child_text("urn:uuid:" + string(buffer));
418 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
419 cxml::add_child(sf, "MCATagSymbol", string("r1"))->add_child_text(buffer);
420 if (!soundfield->MCATagName.empty()) {
421 soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
422 cxml::add_child(sf, "MCATagName", string("r1"))->add_child_text(buffer);
424 if (!soundfield->RFC5646SpokenLanguage.empty()) {
425 soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
426 cxml::add_child(sf, "RFC5646SpokenLanguage", string("r1"))->add_child_text(buffer);
429 /* Find the MCA subdescriptors in the MXF so that we can also write them here */
430 list<ASDCP::MXF::InterchangeObject*> channels;
431 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
432 asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
436 for (auto i: channels) {
437 auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
438 auto ch = cxml::add_child(mca_subs, "AudioChannelLabelSubDescriptor", string("r0"));
439 channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
440 cxml::add_child(ch, "InstanceID", string("r1"))->add_child_text("urn:uuid:" + string(buffer));
441 channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
442 cxml::add_child(ch, "MCALabelDictionaryID", string("r1"))->add_child_text("urn:smpte:ul:" + string(buffer));
443 channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
444 cxml::add_child(ch, "MCALinkID", string("r1"))->add_child_text("urn:uuid:" + string(buffer));
445 channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
446 cxml::add_child(ch, "MCATagSymbol", string("r1"))->add_child_text(buffer);
447 if (!channel->MCATagName.empty()) {
448 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
449 cxml::add_child(ch, "MCATagName", string("r1"))->add_child_text(buffer);
451 if (!channel->MCAChannelID.empty()) {
452 cxml::add_child(ch, "MCAChannelID", string("r1"))->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
454 if (!channel->RFC5646SpokenLanguage.empty()) {
455 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
456 cxml::add_child(ch, "RFC5646SpokenLanguage", string("r1"))->add_child_text(buffer);
458 if (!channel->SoundfieldGroupLinkID.empty()) {
459 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
460 cxml::add_child(ch, "SoundfieldGroupLinkID", string("r1"))->add_child_text("urn:uuid:" + string(buffer));
467 /** Write a CompositionMetadataAsset node as a child of @param node provided
468 * the required metadata is stored in the object. If any required metadata
469 * is missing this method will do nothing.
472 CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node, bool include_mca_subdescriptors) const
475 !_main_sound_configuration ||
476 !_main_sound_sample_rate ||
477 !_main_picture_stored_area ||
478 !_main_picture_active_area ||
480 !_reels.front()->main_picture()) {
484 auto meta = cxml::add_child(node, "meta:CompositionMetadataAsset");
485 meta->set_namespace_declaration (cpl_metadata_ns, "meta");
487 cxml::add_text_child(meta, "Id", "urn:uuid:" + _cpl_metadata_id);
489 auto mp = _reels.front()->main_picture();
490 cxml::add_text_child(meta, "EditRate", mp->edit_rate().as_string());
491 cxml::add_text_child(meta, "IntrinsicDuration", raw_convert<string>(mp->intrinsic_duration()));
493 auto fctt = cxml::add_child(meta, "FullContentTitleText", string("meta"));
494 if (_full_content_title_text && !_full_content_title_text->empty()) {
495 fctt->add_child_text (*_full_content_title_text);
497 if (_full_content_title_text_language) {
498 fctt->set_attribute("language", *_full_content_title_text_language);
501 if (_release_territory) {
502 cxml::add_child(meta, "ReleaseTerritory", string("meta"))->add_child_text(*_release_territory);
505 if (_version_number) {
506 auto vn = cxml::add_child(meta, "VersionNumber", string("meta"));
507 vn->add_child_text(raw_convert<string>(*_version_number));
509 vn->set_attribute("status", status_to_string(*_status));
514 cxml::add_child(meta, "Chain", string("meta"))->add_child_text(*_chain);
518 cxml::add_child(meta, "Distributor", string("meta"))->add_child_text(*_distributor);
522 cxml::add_child(meta, "Facility", string("meta"))->add_child_text(*_facility);
525 if (_content_versions.size() > 1) {
526 auto vc = cxml::add_child(meta, "AlternateContentVersionList", string("meta"));
527 for (size_t i = 1; i < _content_versions.size(); ++i) {
528 _content_versions[i].as_xml (vc);
533 _luminance->as_xml (meta, "meta");
536 if (_main_sound_configuration) {
537 cxml::add_child(meta, "MainSoundConfiguration", string("meta"))->add_child_text(_main_sound_configuration->to_string());
539 cxml::add_child(meta, "MainSoundSampleRate", string("meta"))->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
541 auto stored = cxml::add_child(meta, "MainPictureStoredArea", string("meta"));
542 cxml::add_child(stored, "Width", string("meta"))->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
543 cxml::add_child(stored, "Height", string("meta"))->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
545 auto active = cxml::add_child(meta, "MainPictureActiveArea", string("meta"));
546 cxml::add_child(active, "Width", string("meta"))->add_child_text(raw_convert<string>(_main_picture_active_area->width));
547 cxml::add_child(active, "Height", string("meta"))->add_child_text(raw_convert<string>(_main_picture_active_area->height));
549 optional<string> first_subtitle_language;
550 for (auto i: _reels) {
551 if (i->main_subtitle()) {
552 first_subtitle_language = i->main_subtitle()->language();
553 if (first_subtitle_language) {
559 if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
561 if (first_subtitle_language) {
562 lang = *first_subtitle_language;
564 for (auto const& i: _additional_subtitle_languages) {
570 cxml::add_child(meta, "MainSubtitleLanguageList", string("meta"))->add_child_text(lang);
573 auto metadata_list = cxml::add_child(meta, "ExtensionMetadataList", string("meta"));
575 auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
576 auto extension = cxml::add_child(metadata_list, "ExtensionMetadata", string("meta"));
577 extension->set_attribute("scope", scope);
578 cxml::add_child(extension, "Name", string("meta"))->add_child_text(name);
579 auto property = cxml::add_child(cxml::add_child(extension, "PropertyList", string("meta")), "Property", string("meta"));
580 cxml::add_child(property, "Name", string("meta"))->add_child_text(property_name);
581 cxml::add_child(property, "Value", string("meta"))->add_child_text(property_value);
584 /* SMPTE Bv2.1 8.6.3 */
585 add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
587 if (_sign_language_video_language) {
588 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
591 if (_dolby_edr_image_transfer_function) {
592 add_extension_metadata("http://www.dolby.com/schemas/2014/EDR-Metadata", "Dolby EDR", "image transfer function", *_dolby_edr_image_transfer_function);
595 if (_reels.front()->main_sound()) {
596 auto asset = _reels.front()->main_sound()->asset();
597 if (asset && include_mca_subdescriptors) {
598 write_mca_subdescriptors(meta, asset);
606 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
608 for (auto i: reels) {
609 if (i->main_picture ()) {
610 assets.push_back (i->main_picture());
612 if (i->main_sound ()) {
613 assets.push_back (i->main_sound());
615 if (i->main_subtitle ()) {
616 assets.push_back (i->main_subtitle());
618 for (auto j: i->closed_captions()) {
619 assets.push_back (j);
622 assets.push_back (i->atmos());
628 vector<shared_ptr<ReelFileAsset>>
629 CPL::reel_file_assets ()
631 vector<shared_ptr<ReelFileAsset>> c;
632 add_file_assets (c, _reels);
637 vector<shared_ptr<const ReelFileAsset>>
638 CPL::reel_file_assets () const
640 vector<shared_ptr<const ReelFileAsset>> c;
641 add_file_assets (c, _reels);
647 CPL::equals(shared_ptr<const Asset> other, EqualityOptions const& opt, NoteHandler note) const
649 auto other_cpl = dynamic_pointer_cast<const CPL>(other);
654 if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
655 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
656 note (NoteType::ERROR, s);
660 if (_content_kind != other_cpl->_content_kind) {
661 note (NoteType::ERROR, "CPL: content kinds differ");
665 if (_reels.size() != other_cpl->_reels.size()) {
666 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
670 auto a = _reels.begin();
671 auto b = other_cpl->_reels.begin();
673 while (a != _reels.end ()) {
674 if (!(*a)->equals (*b, opt, note)) {
686 CPL::any_encrypted () const
688 for (auto i: _reels) {
689 if (i->any_encrypted()) {
699 CPL::all_encrypted () const
701 for (auto i: _reels) {
702 if (!i->all_encrypted()) {
712 CPL::add (DecryptedKDM const & kdm)
714 for (auto i: _reels) {
720 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
722 for (auto i: _reels) {
723 i->resolve_refs (assets);
728 CPL::pkl_type (Standard standard) const
730 return static_pkl_type (standard);
734 CPL::static_pkl_type (Standard standard)
737 case Standard::INTEROP:
738 return "text/xml;asdcpKind=CPL";
739 case Standard::SMPTE:
747 CPL::duration () const
750 for (auto i: _reels) {
758 CPL::set_version_number (int v)
761 throw BadSettingError ("CPL version number cannot be negative");
769 CPL::unset_version_number ()
771 _version_number = boost::none;
776 CPL::set_content_versions (vector<ContentVersion> v)
778 std::set<string> ids;
780 if (!ids.insert(i.id).second) {
781 throw DuplicateIdError ("Duplicate ID in ContentVersion list");
785 _content_versions = v;
789 optional<ContentVersion>
790 CPL::content_version () const
792 if (_content_versions.empty()) {
793 return optional<ContentVersion>();
796 return _content_versions[0];
801 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
803 _additional_subtitle_languages.clear ();
804 for (auto const& i: langs) {
805 _additional_subtitle_languages.push_back (i.to_string());
811 CPL::set_main_picture_active_area(dcp::Size area)
813 if (area.width % 2) {
814 throw BadSettingError("Main picture active area width is not a multiple of 2");
817 if (area.height % 2) {
818 throw BadSettingError("Main picture active area height is not a multiple of 2");
821 _main_picture_active_area = area;