diff options
| author | Carl Hetherington <cth@carlh.net> | 2026-02-01 21:59:32 +0100 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2026-02-02 00:36:54 +0100 |
| commit | 56b5f7e06e89f136a9f2ef91f4e7a34d6c451730 (patch) | |
| tree | 7115179ebf80da4fd988ad1891813d9d06125818 | |
| parent | f4582b67f10e5cdbe13ef3f3112c709e950d2190 (diff) | |
Reproduce MCA subdescriptors when writing XML without the assets being present.
This assumes that we don't read a CPL, modify the sound assets, then
write it out again. Maybe we should make that possible (e.g. by
invalidating the CPL's MCA subdescriptors list when changing sound).
| -rw-r--r-- | src/cpl.cc | 95 | ||||
| -rw-r--r-- | src/cpl.h | 6 | ||||
| -rw-r--r-- | src/mca_sub_descriptor.cc | 84 | ||||
| -rw-r--r-- | src/mca_sub_descriptor.h | 65 | ||||
| -rw-r--r-- | src/wscript | 2 | ||||
| -rw-r--r-- | test/cpl_metadata_test.cc | 30 |
6 files changed, 251 insertions, 31 deletions
@@ -272,6 +272,9 @@ CPL::read_composition_metadata_asset(cxml::ConstNodePtr node, vector<dcp::Verifi { _cpl_metadata_id = remove_urn_uuid(node->string_child("Id")); + _cpl_metadata_edit_rate = dcp::Fraction(node->string_child("EditRate")); + _cpl_metadata_intrinsic_duration = node->number_child<int64_t>("IntrinsicDuration"); + /* FullContentTitleText is compulsory but in DoM #2295 we saw a commercial tool which * apparently didn't include it, so as usual we have to be defensive. */ @@ -398,45 +401,52 @@ CPL::read_composition_metadata_asset(cxml::ConstNodePtr node, vector<dcp::Verifi if (node->optional_node_child("MCASubDescriptors")) { _profile = Profile::SMPTE_BV21; + for (auto descriptor: node->node_child("MCASubDescriptors")->node_children()) { + if (!descriptor->is_text()) { + _mca_sub_descriptors.push_back(MCASubDescriptor(descriptor)); + } + } } else { _profile = Profile::SMPTE_BV20; } } -void -CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsset> asset) const +vector<MCASubDescriptor> +CPL::create_mca_subdescriptors(shared_ptr<const SoundAsset> asset) const { + vector<MCASubDescriptor> descriptors; + auto reader = asset->start_read(); + ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield; ASDCP::Result_t r = reader->reader()->OP1aHeader().GetMDObjectByType( asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor), reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield) ); + if (KM_SUCCESS(r)) { - auto mca_subs = cxml::add_child(parent, "mca:MCASubDescriptors"); - mca_subs->set_namespace_declaration(mca_sub_descriptors_ns, "mca"); - mca_subs->set_namespace_declaration(smpte_395_ns, "r0"); - mca_subs->set_namespace_declaration(smpte_335_ns, "r1"); - auto sf = cxml::add_child(mca_subs, "SoundfieldGroupLabelSubDescriptor", string("r0")); + MCASubDescriptor descriptor("SoundfieldGroupLabelSubDescriptor"); char buffer[64]; soundfield->InstanceUID.EncodeString(buffer, sizeof(buffer)); - cxml::add_child(sf, "InstanceID", string("r1"))->add_child_text("urn:uuid:" + string(buffer)); + descriptor.instance_id = buffer; soundfield->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer)); - cxml::add_child(sf, "MCALabelDictionaryID", string("r1"))->add_child_text("urn:smpte:ul:" + string(buffer)); + descriptor.mca_label_dictionary_id = "urn:smpte:ul:" + string(buffer); soundfield->MCALinkID.EncodeString(buffer, sizeof(buffer)); - cxml::add_child(sf, "MCALinkID", string("r1"))->add_child_text("urn:uuid:" + string(buffer)); + descriptor.mca_link_id = buffer; soundfield->MCATagSymbol.EncodeString(buffer, sizeof(buffer)); - cxml::add_child(sf, "MCATagSymbol", string("r1"))->add_child_text(buffer); + descriptor.mca_tag_symbol = buffer; if (!soundfield->MCATagName.empty()) { soundfield->MCATagName.get().EncodeString(buffer, sizeof(buffer)); - cxml::add_child(sf, "MCATagName", string("r1"))->add_child_text(buffer); + descriptor.mca_tag_name = buffer; } if (!soundfield->RFC5646SpokenLanguage.empty()) { soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer)); - cxml::add_child(sf, "RFC5646SpokenLanguage", string("r1"))->add_child_text(buffer); + descriptor.rfc5646_spoken_language = buffer; } + descriptors.push_back(descriptor); + /* Find the MCA subdescriptors in the MXF so that we can also write them here */ list<ASDCP::MXF::InterchangeObject*> channels; auto r = reader->reader()->OP1aHeader().GetMDObjectsByType( @@ -445,33 +455,36 @@ CPL::write_mca_subdescriptors(xmlpp::Element* parent, shared_ptr<const SoundAsse ); for (auto i: channels) { + MCASubDescriptor descriptor("AudioChannelLabelSubDescriptor"); auto channel = reinterpret_cast<ASDCP::MXF::AudioChannelLabelSubDescriptor*>(i); - auto ch = cxml::add_child(mca_subs, "AudioChannelLabelSubDescriptor", string("r0")); channel->InstanceUID.EncodeString(buffer, sizeof(buffer)); - cxml::add_child(ch, "InstanceID", string("r1"))->add_child_text("urn:uuid:" + string(buffer)); + descriptor.instance_id = buffer; channel->MCALabelDictionaryID.EncodeString(buffer, sizeof(buffer)); - cxml::add_child(ch, "MCALabelDictionaryID", string("r1"))->add_child_text("urn:smpte:ul:" + string(buffer)); + descriptor.mca_label_dictionary_id = "urn:smpte:ul:" + string(buffer); channel->MCALinkID.EncodeString(buffer, sizeof(buffer)); - cxml::add_child(ch, "MCALinkID", string("r1"))->add_child_text("urn:uuid:" + string(buffer)); + descriptor.mca_link_id = string(buffer); channel->MCATagSymbol.EncodeString(buffer, sizeof(buffer)); - cxml::add_child(ch, "MCATagSymbol", string("r1"))->add_child_text(buffer); + descriptor.mca_tag_symbol = buffer; if (!channel->MCATagName.empty()) { channel->MCATagName.get().EncodeString(buffer, sizeof(buffer)); - cxml::add_child(ch, "MCATagName", string("r1"))->add_child_text(buffer); + descriptor.mca_tag_name = buffer; } if (!channel->MCAChannelID.empty()) { - cxml::add_child(ch, "MCAChannelID", string("r1"))->add_child_text(fmt::to_string(channel->MCAChannelID.get())); + descriptor.mca_channel_id = fmt::to_string(channel->MCAChannelID.get()); } if (!channel->RFC5646SpokenLanguage.empty()) { channel->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer)); - cxml::add_child(ch, "RFC5646SpokenLanguage", string("r1"))->add_child_text(buffer); + descriptor.rfc5646_spoken_language = buffer; } if (!channel->SoundfieldGroupLinkID.empty()) { channel->SoundfieldGroupLinkID.get().EncodeString(buffer, sizeof(buffer)); - cxml::add_child(ch, "SoundfieldGroupLinkID", string("r1"))->add_child_text("urn:uuid:" + string(buffer)); + descriptor.soundfield_group_link_id = buffer; } + descriptors.push_back(descriptor); } } + + return descriptors; } @@ -488,7 +501,8 @@ CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node) const !_main_picture_stored_area || !_main_picture_active_area || _reels.empty() || - !_reels.front()->main_picture()) { + (!_reels.front()->main_picture() && (!_cpl_metadata_edit_rate || !_cpl_metadata_intrinsic_duration)) + ) { return; } @@ -497,9 +511,15 @@ CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node) const cxml::add_text_child(meta, "Id", "urn:uuid:" + _cpl_metadata_id); - auto mp = _reels.front()->main_picture(); - cxml::add_text_child(meta, "EditRate", mp->edit_rate().as_string()); - cxml::add_text_child(meta, "IntrinsicDuration", fmt::to_string(mp->intrinsic_duration())); + if (_cpl_metadata_edit_rate && _cpl_metadata_intrinsic_duration) { + /* We read these in, so reproduce them */ + cxml::add_text_child(meta, "EditRate", _cpl_metadata_edit_rate->as_string()); + cxml::add_text_child(meta, "IntrinsicDuration", fmt::to_string(*_cpl_metadata_intrinsic_duration)); + } else { + auto mp = _reels.front()->main_picture(); + cxml::add_text_child(meta, "EditRate", mp->edit_rate().as_string()); + cxml::add_text_child(meta, "IntrinsicDuration", fmt::to_string(mp->intrinsic_duration())); + } auto fctt = cxml::add_child(meta, "FullContentTitleText", string("meta")); if (_full_content_title_text && !_full_content_title_text->empty()) { @@ -604,10 +624,25 @@ CPL::maybe_write_composition_metadata_asset(xmlpp::Element* node) const add_extension_metadata("http://www.dolby.com/schemas/2014/EDR-Metadata", "Dolby EDR", "image transfer function", *_dolby_edr_image_transfer_function); } - if (_reels.front()->main_sound()) { - auto asset = _reels.front()->main_sound()->asset(); - if (asset && _profile == Profile::SMPTE_BV21) { - write_mca_subdescriptors(meta, asset); + if (_profile == Profile::SMPTE_BV21) { + vector<MCASubDescriptor> descriptors; + if (!_mca_sub_descriptors.empty()) { + /* We read these in, so reproduce them */ + descriptors = _mca_sub_descriptors; + } else if (_reels.front()->main_sound()) { + if (auto asset = _reels.front()->main_sound()->asset()) { + descriptors = create_mca_subdescriptors(asset); + } + } + + if (!descriptors.empty()) { + auto mca_subs = cxml::add_child(meta, "mca:MCASubDescriptors"); + mca_subs->set_namespace_declaration(mca_sub_descriptors_ns, "mca"); + mca_subs->set_namespace_declaration(smpte_395_ns, "r0"); + mca_subs->set_namespace_declaration(smpte_335_ns, "r1"); + for (auto const& descriptor: descriptors) { + descriptor.as_xml(mca_subs); + } } } } @@ -47,6 +47,7 @@ #include "key.h" #include "language_tag.h" #include "main_sound_configuration.h" +#include "mca_sub_descriptor.h" #include "picture_encoding.h" #include "profile.h" #include "rating.h" @@ -367,7 +368,7 @@ private: void maybe_write_composition_metadata_asset(xmlpp::Element* node) const; void read_composition_metadata_asset(cxml::ConstNodePtr node, std::vector<dcp::VerificationNote>* notes); - void write_mca_subdescriptors(xmlpp::Element* parent, std::shared_ptr<const SoundAsset> asset) const; + std::vector<MCASubDescriptor> create_mca_subdescriptors(std::shared_ptr<const SoundAsset> asset) const; std::string _issuer; std::string _creator; @@ -381,6 +382,8 @@ private: * or the one read in from the existing CPL. */ std::string _cpl_metadata_id = make_uuid(); + boost::optional<Fraction> _cpl_metadata_edit_rate; + boost::optional<int64_t> _cpl_metadata_intrinsic_duration; /** Human-readable name of the composition, without any metadata (i.e. no -FTR-EN-XX- etc.) */ boost::optional<std::string> _full_content_title_text; boost::optional<std::string> _full_content_title_text_language; @@ -403,6 +406,7 @@ private: std::vector<std::string> _additional_subtitle_languages; boost::optional<std::string> _sign_language_video_language; boost::optional<std::string> _dolby_edr_image_transfer_function; + std::vector<MCASubDescriptor> _mca_sub_descriptors; bool _read_composition_metadata = false; std::vector<std::shared_ptr<Reel>> _reels; diff --git a/src/mca_sub_descriptor.cc b/src/mca_sub_descriptor.cc new file mode 100644 index 00000000..34bde9f8 --- /dev/null +++ b/src/mca_sub_descriptor.cc @@ -0,0 +1,84 @@ +/* + Copyright (C) 2026 Carl Hetherington <cth@carlh.net> + + This file is part of libdcp. + + libdcp is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + libdcp is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libdcp. If not, see <http://www.gnu.org/licenses/>. + + In addition, as a special exception, the copyright holders give + permission to link the code of portions of this program with the + OpenSSL library under certain conditions as described in each + individual source file, and distribute linked combinations + including the two. + + You must obey the GNU General Public License in all respects + for all of the code used other than OpenSSL. If you modify + file(s) with this exception, you may extend this exception to your + version of the file(s), but you are not obligated to do so. If you + do not wish to do so, delete this exception statement from your + version. If you delete this exception statement from all source + files in the program, then also delete it here. +*/ + + +#include "dcp_assert.h" +#include "mca_sub_descriptor.h" +#include "util.h" +LIBDCP_DISABLE_WARNINGS +#include <libxml++/libxml++.h> +LIBDCP_ENABLE_WARNINGS + + +using std::string; +using namespace dcp; + + +MCASubDescriptor::MCASubDescriptor(cxml::ConstNodePtr node) +{ + tag = node->name(); + instance_id = remove_urn_uuid(node->string_child("InstanceID")); + mca_label_dictionary_id = node->string_child("MCALabelDictionaryID"); + mca_link_id = remove_urn_uuid(node->string_child("MCALinkID")); + mca_tag_symbol = node->string_child("MCATagSymbol"); + mca_tag_name = node->optional_string_child("MCATagName"); + mca_channel_id = node->optional_string_child("MCAChannelID"); + rfc5646_spoken_language = node->optional_string_child("RFC5646SpokenLanguage"); + if (auto id = node->optional_string_child("SoundfieldGroupLinkID")) { + soundfield_group_link_id = remove_urn_uuid(*id); + } +} + + +void +MCASubDescriptor::as_xml(xmlpp::Element* parent) const +{ + auto node = cxml::add_child(parent, tag, string("r0")); + cxml::add_child(node, "InstanceID", string("r1"))->add_child_text("urn:uuid:" + instance_id); + cxml::add_child(node, "MCALabelDictionaryID", string("r1"))->add_child_text(mca_label_dictionary_id); + cxml::add_child(node, "MCALinkID", string("r1"))->add_child_text("urn:uuid:" + mca_link_id); + cxml::add_child(node, "MCATagSymbol", string("r1"))->add_child_text(mca_tag_symbol); + if (mca_tag_name) { + cxml::add_child(node, "MCATagName", string("r1"))->add_child_text(*mca_tag_name); + } + if (mca_channel_id) { + cxml::add_child(node, "MCAChannelID", string("r1"))->add_child_text(*mca_channel_id); + } + if (rfc5646_spoken_language) { + cxml::add_child(node, "RFC5646SpokenLanguage", string("r1"))->add_child_text(*rfc5646_spoken_language); + } + if (soundfield_group_link_id) { + cxml::add_child(node, "SoundfieldGroupLinkID", string("r1"))->add_child_text("urn:uuid:" + *soundfield_group_link_id); + } +} + diff --git a/src/mca_sub_descriptor.h b/src/mca_sub_descriptor.h new file mode 100644 index 00000000..4a2e8c09 --- /dev/null +++ b/src/mca_sub_descriptor.h @@ -0,0 +1,65 @@ +/* + Copyright (C) 2026 Carl Hetherington <cth@carlh.net> + + This file is part of libdcp. + + libdcp is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + libdcp is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libdcp. If not, see <http://www.gnu.org/licenses/>. + + In addition, as a special exception, the copyright holders give + permission to link the code of portions of this program with the + OpenSSL library under certain conditions as described in each + individual source file, and distribute linked combinations + including the two. + + You must obey the GNU General Public License in all respects + for all of the code used other than OpenSSL. If you modify + file(s) with this exception, you may extend this exception to your + version of the file(s), but you are not obligated to do so. If you + do not wish to do so, delete this exception statement from your + version. If you delete this exception statement from all source + files in the program, then also delete it here. +*/ + + +#include <libcxml/cxml.h> +#include <boost/optional.hpp> +#include <string> + + +namespace dcp { + + +class MCASubDescriptor +{ +public: + explicit MCASubDescriptor(std::string tag_) + : tag(std::move(tag_)) + {} + explicit MCASubDescriptor(cxml::ConstNodePtr node); + + void as_xml(xmlpp::Element* node) const; + + std::string tag; + std::string instance_id; + std::string mca_label_dictionary_id; + std::string mca_link_id; + std::string mca_tag_symbol; + boost::optional<std::string> mca_tag_name; + boost::optional<std::string> mca_channel_id; + boost::optional<std::string> rfc5646_spoken_language; + boost::optional<std::string> soundfield_group_link_id; +}; + + +} diff --git a/src/wscript b/src/wscript index 4fb4c359..4df80173 100644 --- a/src/wscript +++ b/src/wscript @@ -75,6 +75,7 @@ def build(bld): local_time.cc locale_convert.cc main_sound_configuration.cc + mca_sub_descriptor.cc metadata.cc modified_gamma_transfer_function.cc mono_j2k_picture_asset.cc @@ -189,6 +190,7 @@ def build(bld): local_time.h locale_convert.h main_sound_configuration.h + mca_sub_descriptor.h metadata.h mpeg2_picture_asset_writer.h modified_gamma_transfer_function.h diff --git a/test/cpl_metadata_test.cc b/test/cpl_metadata_test.cc index 2dc6da7d..cc86a37e 100644 --- a/test/cpl_metadata_test.cc +++ b/test/cpl_metadata_test.cc @@ -507,3 +507,33 @@ BOOST_AUTO_TEST_CASE(check_dolby_edr_metadata) BOOST_CHECK_EQUAL(check.dolby_edr_image_transfer_function().get_value_or(""), "PQ10K"); } + +BOOST_AUTO_TEST_CASE(cpl_metadata_passthrough) +{ + boost::filesystem::path tmp = "build/test/ak"; + dcp::filesystem::remove_all(tmp); + + /* Plain dcp::filesystem::copy does not seem work on Ubuntu 16.04 */ + dcp::filesystem::create_directory(tmp); + for (auto file: boost::filesystem::directory_iterator(private_test / "ak")) { + dcp::filesystem::copy_file(file, tmp / file.path().filename()); + } + + dcp::DCP dcp("build/test/ak"); + dcp.read(); + + auto signer = make_shared<dcp::CertificateChain>(); + signer->add(dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem"))); + signer->add(dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem"))); + signer->add(dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem"))); + signer->set_key(dcp::file_to_string("test/ref/crypt/leaf.key")); + + dcp.write_xml(signer); + + check_xml( + dcp::file_to_string(private_test / "ak" / "cpl_48b6b1d3-37fd-4782-8ad1-f883e5020d47.xml"), + dcp::file_to_string(boost::filesystem::path("build/test/ak") / "cpl_48b6b1d3-37fd-4782-8ad1-f883e5020d47.xml"), + {"Signer", "Signature"} + ); +} + |
