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