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