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