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