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