Throw exceptions when setting odd values for picture active area.
[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                                         auto ch = mca_subs->add_child("AudioChannelLabelSubDescriptor", "r0");
516                                         channel->InstanceUID.EncodeString(buffer, sizeof(buffer));
517                                         ch->add_child("InstanceID", "r1")->add_child_text("urn:uuid:" + string(buffer));
518                                         channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer));
519                                         ch->add_child("MCALabelDictionaryID", "r1")->add_child_text("urn:smpte:ul:" + string(buffer));
520                                         channel->MCALinkID.EncodeString(buffer, sizeof(buffer));
521                                         ch->add_child("MCALinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
522                                         channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer));
523                                         ch->add_child("MCATagSymbol", "r1")->add_child_text(buffer);
524                                         if (!channel->MCATagName.empty()) {
525                                                 channel->MCATagName.get().EncodeString(buffer, sizeof(buffer));
526                                                 ch->add_child("MCATagName", "r1")->add_child_text(buffer);
527                                         }
528                                         if (!channel->MCAChannelID.empty()) {
529                                                 ch->add_child("MCAChannelID", "r1")->add_child_text(raw_convert<string>(channel->MCAChannelID.get()));
530                                         }
531                                         if (!channel->RFC5646SpokenLanguage.empty()) {
532                                                 channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
533                                                 ch->add_child("RFC5646SpokenLanguage", "r1")->add_child_text(buffer);
534                                         }
535                                         if (!channel->SoundfieldGroupLinkID.empty()) {
536                                                 channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer));
537                                                 ch->add_child("SoundfieldGroupLinkID", "r1")->add_child_text("urn:uuid:" + string(buffer));
538                                         }
539                                 }
540                         }
541                 }
542         }
543 }
544
545
546 template <class T>
547 void
548 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
549 {
550         for (auto i: reels) {
551                 if (i->main_picture ()) {
552                         assets.push_back (i->main_picture());
553                 }
554                 if (i->main_sound ()) {
555                         assets.push_back (i->main_sound());
556                 }
557                 if (i->main_subtitle ()) {
558                         assets.push_back (i->main_subtitle());
559                 }
560                 for (auto j: i->closed_captions()) {
561                         assets.push_back (j);
562                 }
563                 if (i->atmos ()) {
564                         assets.push_back (i->atmos());
565                 }
566         }
567 }
568
569
570 vector<shared_ptr<ReelFileAsset>>
571 CPL::reel_file_assets ()
572 {
573         vector<shared_ptr<ReelFileAsset>> c;
574         add_file_assets (c, _reels);
575         return c;
576 }
577
578
579 vector<shared_ptr<const ReelFileAsset>>
580 CPL::reel_file_assets () const
581 {
582         vector<shared_ptr<const ReelFileAsset>> c;
583         add_file_assets (c, _reels);
584         return c;
585 }
586
587
588 bool
589 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
590 {
591         auto other_cpl = dynamic_pointer_cast<const CPL>(other);
592         if (!other_cpl) {
593                 return false;
594         }
595
596         if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
597                 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
598                 note (NoteType::ERROR, s);
599                 return false;
600         }
601
602         if (_content_kind != other_cpl->_content_kind) {
603                 note (NoteType::ERROR, "CPL: content kinds differ");
604                 return false;
605         }
606
607         if (_reels.size() != other_cpl->_reels.size()) {
608                 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
609                 return false;
610         }
611
612         auto a = _reels.begin();
613         auto b = other_cpl->_reels.begin();
614
615         while (a != _reels.end ()) {
616                 if (!(*a)->equals (*b, opt, note)) {
617                         return false;
618                 }
619                 ++a;
620                 ++b;
621         }
622
623         return true;
624 }
625
626
627 bool
628 CPL::any_encrypted () const
629 {
630         for (auto i: _reels) {
631                 if (i->any_encrypted()) {
632                         return true;
633                 }
634         }
635
636         return false;
637 }
638
639
640 bool
641 CPL::all_encrypted () const
642 {
643         for (auto i: _reels) {
644                 if (!i->all_encrypted()) {
645                         return false;
646                 }
647         }
648
649         return true;
650 }
651
652
653 void
654 CPL::add (DecryptedKDM const & kdm)
655 {
656         for (auto i: _reels) {
657                 i->add (kdm);
658         }
659 }
660
661 void
662 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
663 {
664         for (auto i: _reels) {
665                 i->resolve_refs (assets);
666         }
667 }
668
669 string
670 CPL::pkl_type (Standard standard) const
671 {
672         return static_pkl_type (standard);
673 }
674
675 string
676 CPL::static_pkl_type (Standard standard)
677 {
678         switch (standard) {
679         case Standard::INTEROP:
680                 return "text/xml;asdcpKind=CPL";
681         case Standard::SMPTE:
682                 return "text/xml";
683         default:
684                 DCP_ASSERT (false);
685         }
686 }
687
688 int64_t
689 CPL::duration () const
690 {
691         int64_t d = 0;
692         for (auto i: _reels) {
693                 d += i->duration ();
694         }
695         return d;
696 }
697
698
699 void
700 CPL::set_version_number (int v)
701 {
702         if (v < 0) {
703                 throw BadSettingError ("CPL version number cannot be negative");
704         }
705
706         _version_number = v;
707 }
708
709
710 void
711 CPL::unset_version_number ()
712 {
713         _version_number = boost::none;
714 }
715
716
717 void
718 CPL::set_content_versions (vector<ContentVersion> v)
719 {
720         std::set<string> ids;
721         for (auto i: v) {
722                 if (!ids.insert(i.id).second) {
723                         throw DuplicateIdError ("Duplicate ID in ContentVersion list");
724                 }
725         }
726
727         _content_versions = v;
728 }
729
730
731 optional<ContentVersion>
732 CPL::content_version () const
733 {
734         if (_content_versions.empty()) {
735                 return optional<ContentVersion>();
736         }
737
738         return _content_versions[0];
739 }
740
741
742 void
743 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
744 {
745         _additional_subtitle_languages.clear ();
746         for (auto const& i: langs) {
747                 _additional_subtitle_languages.push_back (i.to_string());
748         }
749 }
750
751
752 void
753 CPL::set_main_picture_active_area(dcp::Size area)
754 {
755         if (area.width % 2) {
756                 throw BadSettingError("Main picture active area width is not a multiple of 2");
757         }
758
759         if (area.height % 2) {
760                 throw BadSettingError("Main picture active area height is not a multiple of 2");
761         }
762
763         _main_picture_active_area = area;
764 }
765