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