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