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