Remove now-redundant test.
[libdcp.git] / src / cpl.cc
1 /*
2     Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
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.
10
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.
15
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/>.
18
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
23     including the two.
24
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.
32 */
33
34
35 /** @file  src/cpl.cc
36  *  @brief CPL class
37  */
38
39
40 #include "certificate_chain.h"
41 #include "compose.hpp"
42 #include "cpl.h"
43 #include "dcp_assert.h"
44 #include "local_time.h"
45 #include "metadata.h"
46 #include "raw_convert.h"
47 #include "reel.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"
53 #include "util.h"
54 #include "version.h"
55 #include "warnings.h"
56 #include "xml.h"
57 LIBDCP_DISABLE_WARNINGS
58 #include <asdcp/Metadata.h>
59 LIBDCP_ENABLE_WARNINGS
60 #include <libxml/parser.h>
61 LIBDCP_DISABLE_WARNINGS
62 #include <libxml++/libxml++.h>
63 LIBDCP_ENABLE_WARNINGS
64 #include <boost/algorithm/string.hpp>
65
66
67 using std::cout;
68 using std::dynamic_pointer_cast;
69 using std::list;
70 using std::make_pair;
71 using std::make_shared;
72 using std::pair;
73 using std::set;
74 using std::shared_ptr;
75 using std::string;
76 using std::vector;
77 using boost::optional;
78 using namespace dcp;
79
80
81 static string const cpl_interop_ns = "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#";
82 static string const cpl_smpte_ns   = "http://www.smpte-ra.org/schemas/429-7/2006/CPL";
83 static string const cpl_metadata_ns = "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata";
84 static string const mca_sub_descriptors_ns = "http://isdcf.com/ns/cplmd/mca";
85 static string const smpte_395_ns = "http://www.smpte-ra.org/reg/395/2014/13/1/aaf";
86 static string const smpte_335_ns = "http://www.smpte-ra.org/reg/335/2012";
87
88
89 CPL::CPL (string annotation_text, ContentKind content_kind, Standard standard)
90         /* default _content_title_text to annotation_text */
91         : _issuer("libdcp", dcp::version)
92         , _creator("libdcp", dcp::version)
93         , _issue_date (LocalTime().as_string())
94         , _annotation_text (annotation_text)
95         , _content_title_text (annotation_text)
96         , _content_kind (content_kind)
97         , _standard (standard)
98 {
99         ContentVersion cv;
100         cv.label_text = cv.id + LocalTime().as_string();
101         _content_versions.push_back (cv);
102 }
103
104
105 CPL::CPL (boost::filesystem::path file)
106         : Asset (file)
107         , _content_kind (ContentKind::FEATURE)
108 {
109         cxml::Document f ("CompositionPlaylist");
110         f.read_file (file);
111
112         if (f.namespace_uri() == cpl_interop_ns) {
113                 _standard = Standard::INTEROP;
114         } else if (f.namespace_uri() == cpl_smpte_ns) {
115                 _standard = Standard::SMPTE;
116         } else {
117                 boost::throw_exception (XMLError ("Unrecognised CPL namespace " + f.namespace_uri()));
118         }
119
120         _id = remove_urn_uuid (f.string_child ("Id"));
121         _annotation_text = f.optional_string_child("AnnotationText");
122         _issuer = f.optional_string_child("Issuer").get_value_or("");
123         _creator = f.optional_string_child("Creator").get_value_or("");
124         _issue_date = f.string_child ("IssueDate");
125         _content_title_text = f.string_child ("ContentTitleText");
126         auto content_kind = f.node_child("ContentKind");
127         _content_kind = ContentKind(content_kind->content(), content_kind->optional_string_attribute("scope"));
128         shared_ptr<cxml::Node> content_version = f.optional_node_child ("ContentVersion");
129         if (content_version) {
130                 /* XXX: SMPTE should insist that Id is present */
131                 _content_versions.push_back (
132                         ContentVersion (
133                                 content_version->optional_string_child("Id").get_value_or(""),
134                                 content_version->string_child("LabelText")
135                                 )
136                         );
137                 content_version->done ();
138         } else if (_standard == Standard::SMPTE) {
139                 /* ContentVersion is required in SMPTE */
140                 throw XMLError ("Missing ContentVersion tag in CPL");
141         }
142         auto rating_list = f.node_child ("RatingList");
143         for (auto i: rating_list->node_children("Rating")) {
144                 _ratings.push_back (Rating(i));
145         }
146
147         for (auto i: f.node_child("ReelList")->node_children("Reel")) {
148                 _reels.push_back (make_shared<Reel>(i, _standard));
149         }
150
151         auto reel_list = f.node_child ("ReelList");
152         auto reels = reel_list->node_children("Reel");
153         if (!reels.empty()) {
154                 auto asset_list = reels.front()->node_child("AssetList");
155                 auto metadata = asset_list->optional_node_child("CompositionMetadataAsset");
156                 if (metadata) {
157                         read_composition_metadata_asset (metadata);
158                         _read_composition_metadata = true;
159                 }
160         }
161
162         f.ignore_child ("Issuer");
163         f.ignore_child ("Signer");
164         f.ignore_child ("Signature");
165
166         f.done ();
167 }
168
169
170 void
171 CPL::add (std::shared_ptr<Reel> reel)
172 {
173         _reels.push_back (reel);
174 }
175
176
177 void
178 CPL::set (std::vector<std::shared_ptr<Reel>> reels)
179 {
180         _reels = reels;
181 }
182
183
184 void
185 CPL::write_xml(boost::filesystem::path file, shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors) const
186 {
187         xmlpp::Document doc;
188         xmlpp::Element* root;
189         if (_standard == Standard::INTEROP) {
190                 root = doc.create_root_node ("CompositionPlaylist", cpl_interop_ns);
191         } else {
192                 root = doc.create_root_node ("CompositionPlaylist", cpl_smpte_ns);
193         }
194
195         root->add_child("Id")->add_child_text ("urn:uuid:" + _id);
196         if (_annotation_text) {
197                 root->add_child("AnnotationText")->add_child_text (*_annotation_text);
198         }
199         root->add_child("IssueDate")->add_child_text (_issue_date);
200         root->add_child("Issuer")->add_child_text (_issuer);
201         root->add_child("Creator")->add_child_text (_creator);
202         root->add_child("ContentTitleText")->add_child_text (_content_title_text);
203         auto content_kind = root->add_child("ContentKind");
204         content_kind->add_child_text(_content_kind.name());
205         if (_content_kind.scope()) {
206                 content_kind->set_attribute("scope", *_content_kind.scope());
207         }
208         if (_content_versions.empty()) {
209                 ContentVersion cv;
210                 cv.as_xml (root);
211         } else {
212                 _content_versions[0].as_xml (root);
213         }
214
215         auto rating_list = root->add_child("RatingList");
216         for (auto i: _ratings) {
217                 i.as_xml (rating_list->add_child("Rating"));
218         }
219
220         auto reel_list = root->add_child ("ReelList");
221
222         if (_reels.empty()) {
223                 throw NoReelsError ();
224         }
225
226         bool first = true;
227         for (auto i: _reels) {
228                 auto asset_list = i->write_to_cpl (reel_list, _standard);
229                 if (first && _standard == Standard::SMPTE) {
230                         maybe_write_composition_metadata_asset(asset_list, include_mca_subdescriptors);
231                         first = false;
232                 }
233         }
234
235         indent (root, 0);
236
237         if (signer) {
238                 signer->sign (root, _standard);
239         }
240
241         doc.write_to_file_formatted (file.string(), "UTF-8");
242
243         set_file (file);
244 }
245
246
247 void
248 CPL::read_composition_metadata_asset (cxml::ConstNodePtr node)
249 {
250         _cpl_metadata_id = remove_urn_uuid(node->string_child("Id"));
251
252         /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which
253          * apparently didn't include it, so as usual we have to be defensive.
254          */
255         if (auto fctt = node->optional_node_child("FullContentTitleText")) {
256                 _full_content_title_text = fctt->content();
257                 _full_content_title_text_language = fctt->optional_string_attribute("language");
258         }
259
260         _release_territory = node->optional_string_child("ReleaseTerritory");
261         if (_release_territory) {
262                 _release_territory_scope = node->node_child("ReleaseTerritory")->optional_string_attribute("scope");
263         }
264
265         auto vn = node->optional_node_child("VersionNumber");
266         if (vn) {
267                 _version_number = raw_convert<int>(vn->content());
268                 /* I decided to check for this number being non-negative on being set, and in the verifier, but not here */
269                 auto vn_status = vn->optional_string_attribute("status");
270                 if (vn_status) {
271                         _status = string_to_status (*vn_status);
272                 }
273         }
274
275         _chain = node->optional_string_child("Chain");
276         _distributor = node->optional_string_child("Distributor");
277         _facility = node->optional_string_child("Facility");
278
279         auto acv = node->optional_node_child("AlternateContentVersionList");
280         if (acv) {
281                 for (auto i: acv->node_children("ContentVersion")) {
282                         _content_versions.push_back (ContentVersion(i));
283                 }
284         }
285
286         auto lum = node->optional_node_child("Luminance");
287         if (lum) {
288                 _luminance = Luminance (lum);
289         }
290
291         _main_sound_configuration = node->optional_string_child("MainSoundConfiguration");
292
293         auto sr = node->optional_string_child("MainSoundSampleRate");
294         if (sr) {
295                 vector<string> sr_bits;
296                 boost::split (sr_bits, *sr, boost::is_any_of(" "));
297                 DCP_ASSERT (sr_bits.size() == 2);
298                 _main_sound_sample_rate = raw_convert<int>(sr_bits[0]);
299         }
300
301         if (_standard == dcp::Standard::SMPTE) {
302                 _main_picture_stored_area = dcp::Size (
303                         node->node_child("MainPictureStoredArea")->number_child<int>("Width"),
304                         node->node_child("MainPictureStoredArea")->number_child<int>("Height")
305                         );
306         }
307
308         _main_picture_active_area = dcp::Size (
309                 node->node_child("MainPictureActiveArea")->number_child<int>("Width"),
310                 node->node_child("MainPictureActiveArea")->number_child<int>("Height")
311                 );
312
313         auto sll = node->optional_string_child("MainSubtitleLanguageList");
314         if (sll) {
315                 vector<string> sll_split;
316                 boost::split (sll_split, *sll, boost::is_any_of(" "));
317                 DCP_ASSERT (!sll_split.empty());
318
319                 /* If the first language on SubtitleLanguageList is the same as the language of the first subtitle we'll ignore it */
320                 size_t first = 0;
321                 if (!_reels.empty()) {
322                         auto sub = _reels.front()->main_subtitle();
323                         if (sub) {
324                                 auto lang = sub->language();
325                                 if (lang && lang == sll_split[0]) {
326                                         first = 1;
327                                 }
328                         }
329                 }
330
331                 for (auto i = first; i < sll_split.size(); ++i) {
332                         _additional_subtitle_languages.push_back (sll_split[i]);
333                 }
334         }
335
336         auto eml = node->optional_node_child ("ExtensionMetadataList");
337         if (eml) {
338                 for (auto i: eml->node_children("ExtensionMetadata")) {
339                         auto name = i->optional_string_child("Name");
340                         if (name && *name == "Sign Language Video") {
341                                 auto property_list = i->node_child("PropertyList");
342                                 for (auto j: property_list->node_children("Property")) {
343                                         auto name = j->optional_string_child("Name");
344                                         auto value = j->optional_string_child("Value");
345                                         if (name && value && *name == "Language Tag") {
346                                                 _sign_language_video_language = *value;
347                                         }
348                                 }
349                         }
350                 }
351         }
352 }
353
354
355 void
356 CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsset> asset) const
357 {
358         auto reader = asset->start_read ();
359         ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
360         ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
361                 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
362                 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
363                 );
364         if (KM_SUCCESS(r)) {
365                 auto mca_subs = parent->add_child("mca:MCASubDescriptors");
366                 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
367                 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
368                 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
369                 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
370                 char buffer[64];
371                 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
372                 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
373                 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
374                 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
375                 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
376                 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
377                 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
378                 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
379                 if (!soundfield->MCATagName.empty()) {
380                         soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
381                         sf->add_child("MCATagName", "r1")->add_child_text(buffer);
382                 }
383                 if (!soundfield->RFC5646SpokenLanguage.empty()) {
384                         soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
385                         sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
386                 }
387
388                 /* Find the MCA subdescriptors in the MXF so that we can also write them here */
389                 list<ASDCP::MXF::InterchangeObject*> channels;
390                 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
391                         asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
392                         channels
393                         );
394
395                 for (auto i: channels) {
396                         auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
397                         auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
398                         channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
399                         ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
400                         channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
401                         ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
402                         channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
403                         ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
404                         channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
405                         ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
406                         if (!channel->MCATagName.empty()) {
407                                 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
408                                 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
409                         }
410                         if (!channel->MCAChannelID.empty()) {
411                                 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
412                         }
413                         if (!channel->RFC5646SpokenLanguage.empty()) {
414                                 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
415                                 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
416                         }
417                         if (!channel->SoundfieldGroupLinkID.empty()) {
418                                 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
419                                 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
420                         }
421                 }
422         }
423 }
424
425
426 /** Write a CompositionMetadataAsset node as a child of @param node provided
427  *  the required metadata is stored in the object.  If any required metadata
428  *  is missing this method will do nothing.
429  */
430 void
431 CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node, bool include_mca_subdescriptors) const
432 {
433         if (
434                 !_main_sound_configuration ||
435                 !_main_sound_sample_rate ||
436                 !_main_picture_stored_area ||
437                 !_main_picture_active_area ||
438                 _reels.empty() ||
439                 !_reels.front()->main_picture()) {
440                 return;
441         }
442
443         auto meta = node->add_child("meta:CompositionMetadataAsset");
444         meta->set_namespace_declaration (cpl_metadata_ns, "meta");
445
446         meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
447
448         auto mp = _reels.front()->main_picture();
449         meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
450         meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
451
452         auto fctt = meta->add_child("FullContentTitleText", "meta");
453         if (_full_content_title_text && !_full_content_title_text->empty()) {
454                 fctt->add_child_text (*_full_content_title_text);
455         }
456         if (_full_content_title_text_language) {
457                 fctt->set_attribute("language", *_full_content_title_text_language);
458         }
459
460         if (_release_territory) {
461                 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
462         }
463
464         if (_version_number) {
465                 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
466                 vn->add_child_text(raw_convert<string>(*_version_number));
467                 if (_status) {
468                         vn->set_attribute("status", status_to_string(*_status));
469                 }
470         }
471
472         if (_chain) {
473                 meta->add_child("Chain", "meta")->add_child_text(*_chain);
474         }
475
476         if (_distributor) {
477                 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
478         }
479
480         if (_facility) {
481                 meta->add_child("Facility", "meta")->add_child_text(*_facility);
482         }
483
484         if (_content_versions.size() > 1) {
485                 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
486                 for (size_t i = 1; i < _content_versions.size(); ++i) {
487                         _content_versions[i].as_xml (vc);
488                 }
489         }
490
491         if (_luminance) {
492                 _luminance->as_xml (meta, "meta");
493         }
494
495         meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
496         meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
497
498         auto stored = meta->add_child("MainPictureStoredArea", "meta");
499         stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
500         stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
501
502         auto active = meta->add_child("MainPictureActiveArea", "meta");
503         active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
504         active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
505
506         optional<string> first_subtitle_language;
507         for (auto i: _reels) {
508                 if (i->main_subtitle()) {
509                         first_subtitle_language = i->main_subtitle()->language();
510                         if (first_subtitle_language) {
511                                 break;
512                         }
513                 }
514         }
515
516         if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
517                 string lang;
518                 if (first_subtitle_language) {
519                         lang = *first_subtitle_language;
520                 }
521                 for (auto const& i: _additional_subtitle_languages) {
522                         if (!lang.empty()) {
523                                 lang += " ";
524                         }
525                         lang += i;
526                 }
527                 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
528         }
529
530         auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
531
532         auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
533                 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
534                 extension->set_attribute("scope", scope);
535                 extension->add_child("Name", "meta")->add_child_text(name);
536                 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
537                 property->add_child("Name", "meta")->add_child_text(property_name);
538                 property->add_child("Value", "meta")->add_child_text(property_value);
539         };
540
541         /* SMPTE Bv2.1 8.6.3 */
542         add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
543
544         if (_sign_language_video_language) {
545                 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
546         }
547
548         if (_reels.front()->main_sound()) {
549                 auto asset = _reels.front()->main_sound()->asset();
550                 if (asset && include_mca_subdescriptors) {
551                         write_mca_subdescriptors(meta, asset);
552                 }
553         }
554 }
555
556
557 template <class T>
558 void
559 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
560 {
561         for (auto i: reels) {
562                 if (i->main_picture ()) {
563                         assets.push_back (i->main_picture());
564                 }
565                 if (i->main_sound ()) {
566                         assets.push_back (i->main_sound());
567                 }
568                 if (i->main_subtitle ()) {
569                         assets.push_back (i->main_subtitle());
570                 }
571                 for (auto j: i->closed_captions()) {
572                         assets.push_back (j);
573                 }
574                 if (i->atmos ()) {
575                         assets.push_back (i->atmos());
576                 }
577         }
578 }
579
580
581 vector<shared_ptr<ReelFileAsset>>
582 CPL::reel_file_assets ()
583 {
584         vector<shared_ptr<ReelFileAsset>> c;
585         add_file_assets (c, _reels);
586         return c;
587 }
588
589
590 vector<shared_ptr<const ReelFileAsset>>
591 CPL::reel_file_assets () const
592 {
593         vector<shared_ptr<const ReelFileAsset>> c;
594         add_file_assets (c, _reels);
595         return c;
596 }
597
598
599 bool
600 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
601 {
602         auto other_cpl = dynamic_pointer_cast<const CPL>(other);
603         if (!other_cpl) {
604                 return false;
605         }
606
607         if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
608                 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
609                 note (NoteType::ERROR, s);
610                 return false;
611         }
612
613         if (_content_kind != other_cpl->_content_kind) {
614                 note (NoteType::ERROR, "CPL: content kinds differ");
615                 return false;
616         }
617
618         if (_reels.size() != other_cpl->_reels.size()) {
619                 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
620                 return false;
621         }
622
623         auto a = _reels.begin();
624         auto b = other_cpl->_reels.begin();
625
626         while (a != _reels.end ()) {
627                 if (!(*a)->equals (*b, opt, note)) {
628                         return false;
629                 }
630                 ++a;
631                 ++b;
632         }
633
634         return true;
635 }
636
637
638 bool
639 CPL::any_encrypted () const
640 {
641         for (auto i: _reels) {
642                 if (i->any_encrypted()) {
643                         return true;
644                 }
645         }
646
647         return false;
648 }
649
650
651 bool
652 CPL::all_encrypted () const
653 {
654         for (auto i: _reels) {
655                 if (!i->all_encrypted()) {
656                         return false;
657                 }
658         }
659
660         return true;
661 }
662
663
664 void
665 CPL::add (DecryptedKDM const & kdm)
666 {
667         for (auto i: _reels) {
668                 i->add (kdm);
669         }
670 }
671
672 void
673 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
674 {
675         for (auto i: _reels) {
676                 i->resolve_refs (assets);
677         }
678 }
679
680 string
681 CPL::pkl_type (Standard standard) const
682 {
683         return static_pkl_type (standard);
684 }
685
686 string
687 CPL::static_pkl_type (Standard standard)
688 {
689         switch (standard) {
690         case Standard::INTEROP:
691                 return "text/xml;asdcpKind=CPL";
692         case Standard::SMPTE:
693                 return "text/xml";
694         default:
695                 DCP_ASSERT (false);
696         }
697 }
698
699 int64_t
700 CPL::duration () const
701 {
702         int64_t d = 0;
703         for (auto i: _reels) {
704                 d += i->duration ();
705         }
706         return d;
707 }
708
709
710 void
711 CPL::set_version_number (int v)
712 {
713         if (v < 0) {
714                 throw BadSettingError ("CPL version number cannot be negative");
715         }
716
717         _version_number = v;
718 }
719
720
721 void
722 CPL::unset_version_number ()
723 {
724         _version_number = boost::none;
725 }
726
727
728 void
729 CPL::set_content_versions (vector<ContentVersion> v)
730 {
731         std::set<string> ids;
732         for (auto i: v) {
733                 if (!ids.insert(i.id).second) {
734                         throw DuplicateIdError ("Duplicate ID in ContentVersion list");
735                 }
736         }
737
738         _content_versions = v;
739 }
740
741
742 optional<ContentVersion>
743 CPL::content_version () const
744 {
745         if (_content_versions.empty()) {
746                 return optional<ContentVersion>();
747         }
748
749         return _content_versions[0];
750 }
751
752
753 void
754 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
755 {
756         _additional_subtitle_languages.clear ();
757         for (auto const& i: langs) {
758                 _additional_subtitle_languages.push_back (i.to_string());
759         }
760 }
761
762
763 void
764 CPL::set_main_picture_active_area(dcp::Size area)
765 {
766         if (area.width % 2) {
767                 throw BadSettingError("Main picture active area width is not a multiple of 2");
768         }
769
770         if (area.height % 2) {
771                 throw BadSettingError("Main picture active area height is not a multiple of 2");
772         }
773
774         _main_picture_active_area = area;
775 }
776