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