Rename ReelMXF -> ReelFileAsset.
[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 template <class T>
495 void
496 add_file_assets (vector<shared_ptr<T>>& assets, vector<shared_ptr<Reel>> reels)
497 {
498         for (auto i: reels) {
499                 if (i->main_picture ()) {
500                         assets.push_back (i->main_picture());
501                 }
502                 if (i->main_sound ()) {
503                         assets.push_back (i->main_sound());
504                 }
505                 if (i->main_subtitle ()) {
506                         assets.push_back (i->main_subtitle());
507                 }
508                 for (auto j: i->closed_captions()) {
509                         assets.push_back (j);
510                 }
511                 if (i->atmos ()) {
512                         assets.push_back (i->atmos());
513                 }
514         }
515 }
516
517
518 vector<shared_ptr<ReelFileAsset>>
519 CPL::reel_file_assets ()
520 {
521         vector<shared_ptr<ReelFileAsset>> c;
522         add_file_assets (c, _reels);
523         return c;
524 }
525
526 vector<shared_ptr<const ReelFileAsset>>
527 CPL::reel_file_assets () const
528 {
529         vector<shared_ptr<const ReelFileAsset>> c;
530         add_file_assets (c, _reels);
531         return c;
532 }
533
534
535 bool
536 CPL::equals (shared_ptr<const Asset> other, EqualityOptions opt, NoteHandler note) const
537 {
538         auto other_cpl = dynamic_pointer_cast<const CPL>(other);
539         if (!other_cpl) {
540                 return false;
541         }
542
543         if (_annotation_text != other_cpl->_annotation_text && !opt.cpl_annotation_texts_can_differ) {
544                 string const s = "CPL: annotation texts differ: " + _annotation_text.get_value_or("") + " vs " + other_cpl->_annotation_text.get_value_or("") + "\n";
545                 note (NoteType::ERROR, s);
546                 return false;
547         }
548
549         if (_content_kind != other_cpl->_content_kind) {
550                 note (NoteType::ERROR, "CPL: content kinds differ");
551                 return false;
552         }
553
554         if (_reels.size() != other_cpl->_reels.size()) {
555                 note (NoteType::ERROR, String::compose ("CPL: reel counts differ (%1 vs %2)", _reels.size(), other_cpl->_reels.size()));
556                 return false;
557         }
558
559         auto a = _reels.begin();
560         auto b = other_cpl->_reels.begin();
561
562         while (a != _reels.end ()) {
563                 if (!(*a)->equals (*b, opt, note)) {
564                         return false;
565                 }
566                 ++a;
567                 ++b;
568         }
569
570         return true;
571 }
572
573
574 bool
575 CPL::any_encrypted () const
576 {
577         for (auto i: _reels) {
578                 if (i->any_encrypted()) {
579                         return true;
580                 }
581         }
582
583         return false;
584 }
585
586
587 bool
588 CPL::all_encrypted () const
589 {
590         for (auto i: _reels) {
591                 if (!i->all_encrypted()) {
592                         return false;
593                 }
594         }
595
596         return true;
597 }
598
599
600 void
601 CPL::add (DecryptedKDM const & kdm)
602 {
603         for (auto i: _reels) {
604                 i->add (kdm);
605         }
606 }
607
608 void
609 CPL::resolve_refs (vector<shared_ptr<Asset>> assets)
610 {
611         for (auto i: _reels) {
612                 i->resolve_refs (assets);
613         }
614 }
615
616 string
617 CPL::pkl_type (Standard standard) const
618 {
619         return static_pkl_type (standard);
620 }
621
622 string
623 CPL::static_pkl_type (Standard standard)
624 {
625         switch (standard) {
626         case Standard::INTEROP:
627                 return "text/xml;asdcpKind=CPL";
628         case Standard::SMPTE:
629                 return "text/xml";
630         default:
631                 DCP_ASSERT (false);
632         }
633 }
634
635 int64_t
636 CPL::duration () const
637 {
638         int64_t d = 0;
639         for (auto i: _reels) {
640                 d += i->duration ();
641         }
642         return d;
643 }
644
645
646 void
647 CPL::set_version_number (int v)
648 {
649         if (v < 0) {
650                 throw BadSettingError ("CPL version number cannot be negative");
651         }
652
653         _version_number = v;
654 }
655
656
657 void
658 CPL::unset_version_number ()
659 {
660         _version_number = boost::none;
661 }
662
663
664 void
665 CPL::set_content_versions (vector<ContentVersion> v)
666 {
667         set<string> ids;
668         for (auto i: v) {
669                 if (!ids.insert(i.id).second) {
670                         throw DuplicateIdError ("Duplicate ID in ContentVersion list");
671                 }
672         }
673
674         _content_versions = v;
675 }
676
677
678 optional<ContentVersion>
679 CPL::content_version () const
680 {
681         if (_content_versions.empty()) {
682                 return optional<ContentVersion>();
683         }
684
685         return _content_versions[0];
686 }
687
688
689 void
690 CPL::set_additional_subtitle_languages (vector<dcp::LanguageTag> const& langs)
691 {
692         _additional_subtitle_languages.clear ();
693         for (auto const& i: langs) {
694                 _additional_subtitle_languages.push_back (i.to_string());
695         }
696 }