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