Remove LIBDCP_VERSION in favour of dcp::version.
[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) 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);
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 /** Write a CompositionMetadataAsset node as a child of @param node provided
356  *  the required metadata is stored in the object.  If any required metadata
357  *  is missing this method will do nothing.
358  */
359 void
360 CPL::maybe_write_composition_metadata_asset (xmlpp::Element* node) const
361 {
362         if (
363                 !_main_sound_configuration ||
364                 !_main_sound_sample_rate ||
365                 !_main_picture_stored_area ||
366                 !_main_picture_active_area ||
367                 _reels.empty() ||
368                 !_reels.front()->main_picture()) {
369                 return;
370         }
371
372         auto meta = node->add_child("meta:CompositionMetadataAsset");
373         meta->set_namespace_declaration (cpl_metadata_ns, "meta");
374
375         meta->add_child("Id")->add_child_text("urn:uuid:" + _cpl_metadata_id);
376
377         auto mp = _reels.front()->main_picture();
378         meta->add_child("EditRate")->add_child_text(mp->edit_rate().as_string());
379         meta->add_child("IntrinsicDuration")->add_child_text(raw_convert<string>(mp->intrinsic_duration()));
380
381         auto fctt = meta->add_child("FullContentTitleText", "meta");
382         if (_full_content_title_text && !_full_content_title_text->empty()) {
383                 fctt->add_child_text (*_full_content_title_text);
384         }
385         if (_full_content_title_text_language) {
386                 fctt->set_attribute("language", *_full_content_title_text_language);
387         }
388
389         if (_release_territory) {
390                 meta->add_child("ReleaseTerritory", "meta")->add_child_text(*_release_territory);
391         }
392
393         if (_version_number) {
394                 xmlpp::Element* vn = meta->add_child("VersionNumber", "meta");
395                 vn->add_child_text(raw_convert<string>(*_version_number));
396                 if (_status) {
397                         vn->set_attribute("status", status_to_string(*_status));
398                 }
399         }
400
401         if (_chain) {
402                 meta->add_child("Chain", "meta")->add_child_text(*_chain);
403         }
404
405         if (_distributor) {
406                 meta->add_child("Distributor", "meta")->add_child_text(*_distributor);
407         }
408
409         if (_facility) {
410                 meta->add_child("Facility", "meta")->add_child_text(*_facility);
411         }
412
413         if (_content_versions.size() > 1) {
414                 xmlpp::Element* vc = meta->add_child("AlternateContentVersionList", "meta");
415                 for (size_t i = 1; i < _content_versions.size(); ++i) {
416                         _content_versions[i].as_xml (vc);
417                 }
418         }
419
420         if (_luminance) {
421                 _luminance->as_xml (meta, "meta");
422         }
423
424         meta->add_child("MainSoundConfiguration", "meta")->add_child_text(*_main_sound_configuration);
425         meta->add_child("MainSoundSampleRate", "meta")->add_child_text(raw_convert<string>(*_main_sound_sample_rate) + " 1");
426
427         auto stored = meta->add_child("MainPictureStoredArea", "meta");
428         stored->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->width));
429         stored->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_stored_area->height));
430
431         auto active = meta->add_child("MainPictureActiveArea", "meta");
432         active->add_child("Width", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->width));
433         active->add_child("Height", "meta")->add_child_text(raw_convert<string>(_main_picture_active_area->height));
434
435         optional<string> first_subtitle_language;
436         for (auto i: _reels) {
437                 if (i->main_subtitle()) {
438                         first_subtitle_language = i->main_subtitle()->language();
439                         if (first_subtitle_language) {
440                                 break;
441                         }
442                 }
443         }
444
445         if (first_subtitle_language || !_additional_subtitle_languages.empty()) {
446                 string lang;
447                 if (first_subtitle_language) {
448                         lang = *first_subtitle_language;
449                 }
450                 for (auto const& i: _additional_subtitle_languages) {
451                         if (!lang.empty()) {
452                                 lang += " ";
453                         }
454                         lang += i;
455                 }
456                 meta->add_child("MainSubtitleLanguageList", "meta")->add_child_text(lang);
457         }
458
459         auto metadata_list = meta->add_child("ExtensionMetadataList", "meta");
460
461         auto add_extension_metadata = [metadata_list](string scope, string name, string property_name, string property_value) {
462                 auto extension = metadata_list->add_child("ExtensionMetadata", "meta");
463                 extension->set_attribute("scope", scope);
464                 extension->add_child("Name", "meta")->add_child_text(name);
465                 auto property = extension->add_child("PropertyList", "meta")->add_child("Property", "meta");
466                 property->add_child("Name", "meta")->add_child_text(property_name);
467                 property->add_child("Value", "meta")->add_child_text(property_value);
468         };
469
470         /* SMPTE Bv2.1 8.6.3 */
471         add_extension_metadata ("http://isdcf.com/ns/cplmd/app", "Application", "DCP Constraints Profile", "SMPTE-RDD-52:2020-Bv2.1");
472
473         if (_sign_language_video_language) {
474                 add_extension_metadata ("http://isdcf.com/2017/10/SignLanguageVideo", "Sign Language Video", "Language Tag", *_sign_language_video_language);
475         }
476
477         if (_reels.front()->main_sound()) {
478                 auto asset = _reels.front()->main_sound()->asset();
479                 if (asset) {
480                         auto reader = asset->start_read ();
481                         ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
482                         ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType(
483                                 asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
484                                 reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
485                                 );
486                         if (KM_SUCCESS(r)) {
487                                 auto mca_subs = meta->add_child("mca:MCASubDescriptors");
488                                 mca_subs->set_namespace_declaration (mca_sub_descriptors_ns, "mca");
489                                 mca_subs->set_namespace_declaration (smpte_395_ns, "r0");
490                                 mca_subs->set_namespace_declaration (smpte_335_ns, "r1");
491                                 auto sf = mca_subs->add_child("SoundfieldGroupLabelSubDescriptor", "r0");
492                                 char buffer[64];
493                                 soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer));
494                                 sf->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
495                                 soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
496                                 sf->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
497                                 soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer));
498                                 sf->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
499                                 soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
500                                 sf->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
501                                 if (!soundfield->MCATagName.empty()) {
502                                         soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer));
503                                         sf->add_child("MCATagName", "r1")->add_child_text(buffer);
504                                 }
505                                 if (!soundfield->RFC5646SpokenLanguage.empty()) {
506                                         soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
507                                         sf->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
508                                 }
509
510                                 list<ASDCP::MXF::InterchangeObject*> channels;
511                                 auto r = reader->reader()->OP1aHeader().GetMDObjectsByType(
512                                         asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
513                                         channels
514                                         );
515
516                                 for (auto i: channels) {
517                                         auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i);
518                                         if (static_cast<int>(channel->MCAChannelID) > asset->channels()) {
519                                                 continue;
520                                         }
521                                         auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
522                                         channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
523                                         ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
524                                         channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
525                                         ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
526                                         channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
527                                         ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
528                                         channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
529                                         ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
530                                         if (!channel->MCATagName.empty()) {
531                                                 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
532                                                 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
533                                         }
534                                         if (!channel->MCAChannelID.empty()) {
535                                                 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
536                                         }
537                                         if (!channel->RFC5646SpokenLanguage.empty()) {
538                                                 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
539                                                 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
540                                         }
541                                         if (!channel->SoundfieldGroupLinkID.empty()) {
542                                                 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
543                                                 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
544                                         }
545                                 }
546                         }
547                 }
548         }
549 }
550
551
552 template <class T>
553 void
554 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
555 {
556         for (auto i: reels) {
557                 if (i->main_picture ()) {
558                         assets.push_back (i->main_picture());
559                 }
560                 if (i->main_sound ()) {
561                         assets.push_back (i->main_sound());
562                 }
563                 if (i->main_subtitle ()) {
564                         assets.push_back (i->main_subtitle());
565                 }
566                 for (auto j: i->closed_captions()) {
567                         assets.push_back (j);
568                 }
569                 if (i->atmos ()) {
570                         assets.push_back (i->atmos());
571                 }
572         }
573 }
574
575
576 vector<shared_ptr<ReelFileAsset>>
577 CPL::reel_file_assets ()
578 {
579         vector<shared_ptr<ReelFileAsset>> c;
580         add_file_assets (c, _reels);
581         return c;
582 }
583
584
585 vector<shared_ptr<const ReelFileAsset>>
586 CPL::reel_file_assets () const
587 {
588         vector<shared_ptr<const ReelFileAsset>> c;
589         add_file_assets (c, _reels);
590         return c;
591 }
592
593
594 bool
595 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
596 {
597         auto other_cpl = dynamic_pointer_cast<const CPL>(other);
598         if (!other_cpl) {
599                 return false;
600         }
601
602         if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
603                 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
604                 note (NoteType::ERROR, s);
605                 return false;
606         }
607
608         if (_content_kind != other_cpl->_content_kind) {
609                 note (NoteType::ERROR, "CPL: content kinds differ");
610                 return false;
611         }
612
613         if (_reels.size() != other_cpl->_reels.size()) {
614                 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
615                 return false;
616         }
617
618         auto a = _reels.begin();
619         auto b = other_cpl->_reels.begin();
620
621         while (a != _reels.end ()) {
622                 if (!(*a)->equals (*b, opt, note)) {
623                         return false;
624                 }
625                 ++a;
626                 ++b;
627         }
628
629         return true;
630 }
631
632
633 bool
634 CPL::any_encrypted () const
635 {
636         for (auto i: _reels) {
637                 if (i->any_encrypted()) {
638                         return true;
639                 }
640         }
641
642         return false;
643 }
644
645
646 bool
647 CPL::all_encrypted () const
648 {
649         for (auto i: _reels) {
650                 if (!i->all_encrypted()) {
651                         return false;
652                 }
653         }
654
655         return true;
656 }
657
658
659 void
660 CPL::add (DecryptedKDM const & kdm)
661 {
662         for (auto i: _reels) {
663                 i->add (kdm);
664         }
665 }
666
667 void
668 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
669 {
670         for (auto i: _reels) {
671                 i->resolve_refs (assets);
672         }
673 }
674
675 string
676 CPL::pkl_type (Standard standard) const
677 {
678         return static_pkl_type (standard);
679 }
680
681 string
682 CPL::static_pkl_type (Standard standard)
683 {
684         switch (standard) {
685         case Standard::INTEROP:
686                 return "text/xml;asdcpKind=CPL";
687         case Standard::SMPTE:
688                 return "text/xml";
689         default:
690                 DCP_ASSERT (false);
691         }
692 }
693
694 int64_t
695 CPL::duration () const
696 {
697         int64_t d = 0;
698         for (auto i: _reels) {
699                 d += i->duration ();
700         }
701         return d;
702 }
703
704
705 void
706 CPL::set_version_number (int v)
707 {
708         if (v < 0) {
709                 throw BadSettingError ("CPL version number cannot be negative");
710         }
711
712         _version_number = v;
713 }
714
715
716 void
717 CPL::unset_version_number ()
718 {
719         _version_number = boost::none;
720 }
721
722
723 void
724 CPL::set_content_versions (vector<ContentVersion> v)
725 {
726         std::set<string> ids;
727         for (auto i: v) {
728                 if (!ids.insert(i.id).second) {
729                         throw DuplicateIdError ("Duplicate ID in ContentVersion list");
730                 }
731         }
732
733         _content_versions = v;
734 }
735
736
737 optional<ContentVersion>
738 CPL::content_version () const
739 {
740         if (_content_versions.empty()) {
741                 return optional<ContentVersion>();
742         }
743
744         return _content_versions[0];
745 }
746
747
748 void
749 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
750 {
751         _additional_subtitle_languages.clear ();
752         for (auto const& i: langs) {
753                 _additional_subtitle_languages.push_back (i.to_string());
754         }
755 }
756
757
758 void
759 CPL::set_main_picture_active_area(dcp::Size area)
760 {
761         if (area.width % 2) {
762                 throw BadSettingError("Main picture active area width is not a multiple of 2");
763         }
764
765         if (area.height % 2) {
766                 throw BadSettingError("Main picture active area height is not a multiple of 2");
767         }
768
769         _main_picture_active_area = area;
770 }
771