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