summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2026-02-01 21:59:32 +0100
committerCarl Hetherington <cth@carlh.net>2026-02-02 00:36:54 +0100
commit56b5f7e06e89f136a9f2ef91f4e7a34d6c451730 (patch)
tree7115179ebf80da4fd988ad1891813d9d06125818
parentf4582b67f10e5cdbe13ef3f3112c709e950d2190 (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.cc95
-rw-r--r--src/cpl.h6
-rw-r--r--src/mca_sub_descriptor.cc84
-rw-r--r--src/mca_sub_descriptor.h65
-rw-r--r--src/wscript2
-rw-r--r--test/cpl_metadata_test.cc30
6 files changed, 251 insertions, 31 deletions
diff --git a/src/cpl.cc b/src/cpl.cc
index 2fbe373a..3e7ca7a3 100644
--- a/src/cpl.cc
+++ b/src/cpl.cc
@@ -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);
+ }
}
}
}
diff --git a/src/cpl.h b/src/cpl.h
index c7e41353..7bbb2d53 100644
--- a/src/cpl.h
+++ b/src/cpl.h
@@ -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"}
+ );
+}
+