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