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