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