summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2026-02-08 18:46:30 +0100
committerCarl Hetherington <cth@carlh.net>2026-02-12 21:02:01 +0100
commita680098a14cf40172370fde12c86691b82a36051 (patch)
treec4cb35db18d91c84e8df2ec0bbe369200c2a235b /src/lib
parent52ee81ba2d1091f712806c0c4edeef67c455884f (diff)
Support re-use of Atmos MXF assets.
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/asset_filename_register.cc61
-rw-r--r--src/lib/asset_filename_register.h39
-rw-r--r--src/lib/atmos_mxf_content.cc30
-rw-r--r--src/lib/atmos_mxf_content.h2
-rw-r--r--src/lib/atmos_mxf_decoder.cc5
-rw-r--r--src/lib/atmos_mxf_decoder.h6
-rw-r--r--src/lib/player.cc5
-rw-r--r--src/lib/reel_writer.cc41
-rw-r--r--src/lib/reel_writer.h17
-rw-r--r--src/lib/reusable_reel_asset.cc37
-rw-r--r--src/lib/writer.cc5
-rw-r--r--src/lib/wscript1
12 files changed, 229 insertions, 20 deletions
diff --git a/src/lib/asset_filename_register.cc b/src/lib/asset_filename_register.cc
new file mode 100644
index 000000000..6e93b61d2
--- /dev/null
+++ b/src/lib/asset_filename_register.cc
@@ -0,0 +1,61 @@
+/*
+ Copyright (C) 2026 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic 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.
+
+ DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "asset_filename_register.h"
+#include "dcpomatic_assert.h"
+#include <dcp/raw_convert.h>
+#include <boost/filesystem.hpp>
+#include <boost/regex.hpp>
+
+
+using std::string;
+
+
+boost::filesystem::path
+AssetFilenameRegister::destination_filename(boost::filesystem::path source, string uuid)
+{
+ int tries = 0;
+ boost::filesystem::path destination = source.filename();
+ while (true) {
+ auto iter = _used.find(destination);
+ if (iter == _used.end()) {
+ _used[destination] = uuid;
+ return destination;
+ }
+
+ if (iter->second == uuid) {
+ return destination;
+ }
+
+ /* Try to disambiguate filename */
+
+ boost::smatch what;
+ if (boost::regex_match(destination.stem().string(), what, boost::regex("(.*)_(\\d+)"))) {
+ destination = fmt::format("{}_{}.mxf", what[1].str(), dcp::raw_convert<int>(what[2].str()) + 1);
+ } else {
+ destination = fmt::format("{}_0.mxf", destination.stem().string());
+ }
+
+ ++tries;
+ DCPOMATIC_ASSERT(tries < 100);
+ }
+}
+
diff --git a/src/lib/asset_filename_register.h b/src/lib/asset_filename_register.h
new file mode 100644
index 000000000..be2bdf5b8
--- /dev/null
+++ b/src/lib/asset_filename_register.h
@@ -0,0 +1,39 @@
+/*
+ Copyright (C) 2026 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic 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.
+
+ DCP-o-matic 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 DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include <boost/filesystem.hpp>
+#include <map>
+
+
+class AssetFilenameRegister
+{
+public:
+ AssetFilenameRegister() = default;
+
+ AssetFilenameRegister(AssetFilenameRegister const&) = delete;
+ AssetFilenameRegister& operator=(AssetFilenameRegister const&) = delete;
+
+ boost::filesystem::path destination_filename(boost::filesystem::path source, std::string uuid);
+
+private:
+ std::map<boost::filesystem::path, std::string> _used;
+};
+
diff --git a/src/lib/atmos_mxf_content.cc b/src/lib/atmos_mxf_content.cc
index 89f45e332..a1d2235de 100644
--- a/src/lib/atmos_mxf_content.cc
+++ b/src/lib/atmos_mxf_content.cc
@@ -118,3 +118,33 @@ AtmosMXFContent::approximate_length () const
{
return DCPTime::from_frames (atmos->length(), 24);
}
+
+
+bool
+AtmosMXFContent::can_reuse(shared_ptr<const Film> film) const
+{
+ auto atmos_period = period(film);
+
+ /* Check that this content has been trimmed by an exact number of video frames,
+ * otherwise we can't represent the required trim in the CPL.
+ */
+ if (
+ trim_start().round(film->video_frame_rate()) != trim_start() ||
+ trim_end().round(film->video_frame_rate()) != trim_end()
+ ) {
+ return false;
+ }
+
+ /* We assume that if the content made it into the film it must be valid, so
+ * we just have to check that there isn't any reel which has part of this
+ * asset (with some empty space).
+ */
+
+ auto const reels = film->reels();
+ return std::none_of(reels.begin(), reels.end(), [atmos_period](DCPTimePeriod reel) {
+ /* Check for incomplete overlap */
+ auto overlap = atmos_period.overlap(reel);
+ return overlap && *overlap != reel;
+ });
+}
+
diff --git a/src/lib/atmos_mxf_content.h b/src/lib/atmos_mxf_content.h
index 8d0ec5b2f..167d8b65f 100644
--- a/src/lib/atmos_mxf_content.h
+++ b/src/lib/atmos_mxf_content.h
@@ -50,5 +50,7 @@ public:
dcpomatic::DCPTime full_length (std::shared_ptr<const Film> film) const override;
dcpomatic::DCPTime approximate_length () const override;
+ bool can_reuse(std::shared_ptr<const Film> film) const;
+
static bool valid_mxf (boost::filesystem::path path);
};
diff --git a/src/lib/atmos_mxf_decoder.cc b/src/lib/atmos_mxf_decoder.cc
index 19a19a8c4..b6111eaf2 100644
--- a/src/lib/atmos_mxf_decoder.cc
+++ b/src/lib/atmos_mxf_decoder.cc
@@ -42,12 +42,17 @@ AtmosMXFDecoder::AtmosMXFDecoder (std::shared_ptr<const Film> film, std::shared_
_reader = asset->start_read ();
_reader->set_check_hmac (false);
_metadata = AtmosMetadata (asset);
+ _can_reuse = content->can_reuse(film);
}
bool
AtmosMXFDecoder::pass ()
{
+ if (!_decode_reusable && _can_reuse) {
+ return true;
+ }
+
auto const vfr = _content->active_video_frame_rate (film());
auto const frame = _next.frames_round (vfr);
diff --git a/src/lib/atmos_mxf_decoder.h b/src/lib/atmos_mxf_decoder.h
index b8e7fc53d..64e916ab8 100644
--- a/src/lib/atmos_mxf_decoder.h
+++ b/src/lib/atmos_mxf_decoder.h
@@ -36,10 +36,16 @@ public:
bool pass () override;
void seek (dcpomatic::ContentTime t, bool accurate) override;
+ void set_decode_reusable(bool decode_reusable) {
+ _decode_reusable = decode_reusable;
+ }
+
private:
std::shared_ptr<const AtmosMXFContent> _content;
dcpomatic::ContentTime _next;
std::shared_ptr<dcp::AtmosAssetReader> _reader;
boost::optional<AtmosMetadata> _metadata;
+ bool _decode_reusable = true;
+ bool _can_reuse;
};
diff --git a/src/lib/player.cc b/src/lib/player.cc
index 316cf7e2b..f74b828fa 100644
--- a/src/lib/player.cc
+++ b/src/lib/player.cc
@@ -20,6 +20,7 @@
#include "atmos_decoder.h"
+#include "atmos_mxf_decoder.h"
#include "audio_buffers.h"
#include "audio_content.h"
#include "audio_decoder.h"
@@ -312,6 +313,10 @@ Player::setup_pieces()
}
}
+ if (auto atmos = dynamic_pointer_cast<AtmosMXFDecoder>(decoder)) {
+ atmos->set_decode_reusable(_play_reusable);
+ }
+
auto piece = make_shared<Piece>(content, decoder, frc);
_pieces.push_back(piece);
diff --git a/src/lib/reel_writer.cc b/src/lib/reel_writer.cc
index 2c4fac6e0..55e2bff65 100644
--- a/src/lib/reel_writer.cc
+++ b/src/lib/reel_writer.cc
@@ -19,6 +19,7 @@
*/
+#include "asset_filename_register.h"
#include "audio_buffers.h"
#include "config.h"
#include "constants.h"
@@ -411,7 +412,7 @@ ReelWriter::finish(boost::filesystem::path output_dcp)
template <class Asset>
shared_ptr<Asset>
-maybe_reuse_asset(char const* log_name, list<ReusableReelAsset> const& refs, DCPTimePeriod period, boost::filesystem::path output_dir)
+maybe_reuse_asset(char const* log_name, list<ReusableReelAsset> const& refs, AssetFilenameRegister& filename_register, DCPTimePeriod period, boost::filesystem::path output_dir)
{
shared_ptr<Asset> reusable;
@@ -426,8 +427,13 @@ maybe_reuse_asset(char const* log_name, list<ReusableReelAsset> const& refs, DCP
if (ref.use == ReusableReelAsset::Use::COPY) {
auto mxf = reusable->asset_ref().asset();
DCPOMATIC_ASSERT(mxf->file());
- auto const destination = output_dir / mxf->file()->filename();
- copy_file(*mxf->file(), destination);
+ auto const destination = output_dir / filename_register.destination_filename(mxf->file()->filename(), mxf->id());
+ /* We may be using this more than once, so it's OK if it exists. The AssetFilenameRegister
+ * will have disambiguated anything that needs it.
+ */
+ if (!dcp::filesystem::exists(destination)) {
+ copy_file(*mxf->file(), destination);
+ }
mxf->set_file(destination);
}
/* If we have a hash for this asset in the CPL, assume that it is correct */
@@ -451,6 +457,7 @@ ReelWriter::maybe_add_text(
int64_t picture_duration,
shared_ptr<dcp::Reel> reel,
list<ReusableReelAsset> const & refs,
+ AssetFilenameRegister& filename_register,
boost::filesystem::path output_dcp
) const
{
@@ -491,7 +498,7 @@ ReelWriter::maybe_add_text(
} else {
/* We don't have a subtitle asset of our own; hopefully we have one to reuse */
- reel_asset = maybe_reuse_asset<dcp::ReelTextAsset>("subtitle", refs, _period, _output_dir);
+ reel_asset = maybe_reuse_asset<dcp::ReelTextAsset>("subtitle", refs, filename_register, _period, _output_dir);
}
if (reel_asset) {
@@ -509,7 +516,7 @@ ReelWriter::maybe_add_text(
shared_ptr<dcp::ReelPictureAsset>
-ReelWriter::create_reel_picture(shared_ptr<dcp::Reel> reel, list<ReusableReelAsset> const & refs) const
+ReelWriter::create_reel_picture(shared_ptr<dcp::Reel> reel, list<ReusableReelAsset> const & refs, AssetFilenameRegister& filename_register) const
{
shared_ptr<dcp::ReelPictureAsset> reel_asset;
@@ -526,7 +533,7 @@ ReelWriter::create_reel_picture(shared_ptr<dcp::Reel> reel, list<ReusableReelAss
reel_asset = make_shared<dcp::ReelMonoPictureAsset>(_mpeg2_picture_asset, 0);
} else {
/* We don't have a picture asset of our own; hopefully we have one to reuse */
- reel_asset = maybe_reuse_asset<dcp::ReelPictureAsset>("picture", refs, _period, _output_dir);
+ reel_asset = maybe_reuse_asset<dcp::ReelPictureAsset>("picture", refs, filename_register, _period, _output_dir);
}
Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
@@ -550,7 +557,7 @@ ReelWriter::create_reel_picture(shared_ptr<dcp::Reel> reel, list<ReusableReelAss
void
-ReelWriter::create_reel_sound(shared_ptr<dcp::Reel> reel, list<ReusableReelAsset> const & refs) const
+ReelWriter::create_reel_sound(shared_ptr<dcp::Reel> reel, list<ReusableReelAsset> const & refs, AssetFilenameRegister& filename_register) const
{
shared_ptr<dcp::ReelSoundAsset> reel_asset;
@@ -559,7 +566,7 @@ ReelWriter::create_reel_sound(shared_ptr<dcp::Reel> reel, list<ReusableReelAsset
reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
} else {
/* We don't have a sound asset of our own; hopefully we have one to reuse */
- reel_asset = maybe_reuse_asset<dcp::ReelSoundAsset>("sound", refs, _period, _output_dir);
+ reel_asset = maybe_reuse_asset<dcp::ReelSoundAsset>("sound", refs, filename_register, _period, _output_dir);
}
auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
@@ -584,19 +591,20 @@ void
ReelWriter::create_reel_text(
shared_ptr<dcp::Reel> reel,
list<ReusableReelAsset> const & refs,
+ AssetFilenameRegister& filename_register,
int64_t duration,
boost::filesystem::path output_dcp,
bool ensure_subtitles,
set<DCPTextTrack> ensure_closed_captions
) const
{
- auto subtitle = maybe_add_text(_subtitle_asset, dcp::TextType::OPEN_SUBTITLE, duration, reel, refs, output_dcp);
+ auto subtitle = maybe_add_text(_subtitle_asset, dcp::TextType::OPEN_SUBTITLE, duration, reel, refs, filename_register, output_dcp);
if (!subtitle && ensure_subtitles) {
/* We had no subtitle asset, but we've been asked to make sure there is one */
subtitle = maybe_add_text(
empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
- dcp::TextType::OPEN_SUBTITLE, duration, reel, refs, output_dcp
+ dcp::TextType::OPEN_SUBTITLE, duration, reel, refs, filename_register, output_dcp
);
}
@@ -608,7 +616,7 @@ ReelWriter::create_reel_text(
}
for (auto const& i: _closed_caption_assets) {
- auto a = maybe_add_text(i.second, dcp::TextType::CLOSED_CAPTION, duration, reel, refs, output_dcp);
+ auto a = maybe_add_text(i.second, dcp::TextType::CLOSED_CAPTION, duration, reel, refs, filename_register, output_dcp);
DCPOMATIC_ASSERT(a);
a->set_annotation_text(i.first.name);
if (i.first.language) {
@@ -622,7 +630,7 @@ ReelWriter::create_reel_text(
for (auto i: ensure_closed_captions) {
auto a = maybe_add_text(
empty_text_asset(TextType::CLOSED_CAPTION, i, true),
- dcp::TextType::CLOSED_CAPTION, duration, reel, refs, output_dcp
+ dcp::TextType::CLOSED_CAPTION, duration, reel, refs, filename_register, output_dcp
);
DCPOMATIC_ASSERT(a);
a->set_annotation_text(i.name);
@@ -663,6 +671,7 @@ ReelWriter::create_reel_markers(shared_ptr<dcp::Reel> reel) const
shared_ptr<dcp::Reel>
ReelWriter::create_reel(
list<ReusableReelAsset> const & refs,
+ AssetFilenameRegister& filename_register,
boost::filesystem::path output_dcp,
bool ensure_subtitles,
set<DCPTextTrack> ensure_closed_captions
@@ -678,18 +687,20 @@ ReelWriter::create_reel(
*/
int64_t duration = 0;
if (!_text_only) {
- auto reel_picture_asset = create_reel_picture(reel, refs);
+ auto reel_picture_asset = create_reel_picture(reel, refs, filename_register);
duration = reel_picture_asset->actual_duration();
- create_reel_sound(reel, refs);
+ create_reel_sound(reel, refs, filename_register);
if (!film()->interop()) {
create_reel_markers(reel);
}
}
- create_reel_text(reel, refs, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
+ create_reel_text(reel, refs, filename_register, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
if (_atmos_asset) {
reel->add(make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
+ } else if (auto reuse = maybe_reuse_asset<dcp::ReelAtmosAsset>("atmos", refs, filename_register, _period, _output_dir)) {
+ reel->add(reuse);
}
return reel;
diff --git a/src/lib/reel_writer.h b/src/lib/reel_writer.h
index 24d641a27..dbd47e829 100644
--- a/src/lib/reel_writer.h
+++ b/src/lib/reel_writer.h
@@ -37,6 +37,7 @@
#include <dcp/mpeg2_picture_asset_writer.h>
+class AssetFilenameRegister;
class AudioBuffers;
class Film;
class InfoFileHandle;
@@ -86,6 +87,7 @@ public:
void finish(boost::filesystem::path output_dcp);
std::shared_ptr<dcp::Reel> create_reel(
std::list<ReusableReelAsset> const & refs,
+ AssetFilenameRegister& filename_register,
boost::filesystem::path output_dcp,
bool ensure_subtitles,
std::set<DCPTextTrack> ensure_closed_captions
@@ -110,11 +112,20 @@ private:
bool existing_picture_frame_ok(dcp::File& asset_file, Frame frame);
std::shared_ptr<dcp::TextAsset> empty_text_asset(TextType type, boost::optional<DCPTextTrack> track, bool with_dummy) const;
- std::shared_ptr<dcp::ReelPictureAsset> create_reel_picture(std::shared_ptr<dcp::Reel> reel, std::list<ReusableReelAsset> const & refs) const;
- void create_reel_sound(std::shared_ptr<dcp::Reel> reel, std::list<ReusableReelAsset> const & refs) const;
+ std::shared_ptr<dcp::ReelPictureAsset> create_reel_picture(
+ std::shared_ptr<dcp::Reel> reel,
+ std::list<ReusableReelAsset> const & refs,
+ AssetFilenameRegister& filename_register
+ ) const;
+ void create_reel_sound(
+ std::shared_ptr<dcp::Reel> reel,
+ std::list<ReusableReelAsset> const & refs,
+ AssetFilenameRegister& filename_register
+ ) const;
void create_reel_text(
std::shared_ptr<dcp::Reel> reel,
std::list<ReusableReelAsset> const& refs,
+ AssetFilenameRegister& filename_register,
int64_t duration,
boost::filesystem::path output_dcp,
bool ensure_subtitles,
@@ -124,13 +135,13 @@ private:
float convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const;
void copy_file(boost::filesystem::path from, boost::filesystem::path to);
-
std::shared_ptr<dcp::ReelTextAsset> maybe_add_text(
std::shared_ptr<dcp::TextAsset> asset,
dcp::TextType type,
int64_t picture_duration,
std::shared_ptr<dcp::Reel> reel,
std::list<ReusableReelAsset> const & refs,
+ AssetFilenameRegister& filename_register,
boost::filesystem::path output_dcp
) const;
diff --git a/src/lib/reusable_reel_asset.cc b/src/lib/reusable_reel_asset.cc
index 8d504b1a0..18869298a 100644
--- a/src/lib/reusable_reel_asset.cc
+++ b/src/lib/reusable_reel_asset.cc
@@ -19,6 +19,7 @@
*/
+#include "atmos_mxf_content.h"
#include "dcp_content.h"
#include "dcp_decoder.h"
#include "dcpomatic_assert.h"
@@ -27,6 +28,7 @@
#include "reusable_reel_asset.h"
#include <dcp/reel.h>
#include <dcp/reel_asset.h>
+#include <dcp/reel_atmos_asset.h>
#include <dcp/reel_picture_asset.h>
#include <dcp/reel_sound_asset.h>
#include <dcp/reel_text_asset.h>
@@ -34,10 +36,11 @@
using std::list;
+using std::make_shared;
using std::max;
using std::min;
using std::shared_ptr;
-using boost::dynamic_pointer_cast;
+using std::dynamic_pointer_cast;
using boost::scoped_ptr;
using namespace dcpomatic;
@@ -132,6 +135,36 @@ add_reusable_reel_assets_from_dcp(shared_ptr<const Film> film, shared_ptr<const
+static
+void
+add_reusable_reel_assets_for_atmos(shared_ptr<const Film> film, shared_ptr<const AtmosMXFContent> atmos, list<ReusableReelAsset>& reel_assets)
+{
+ if (!atmos->can_reuse(film)) {
+ return;
+ }
+
+ auto const vfr = film->video_frame_rate();
+ auto asset = make_shared<dcp::AtmosAsset>(atmos->path(0));
+ auto period = atmos->period(film);
+ auto entry_point = atmos->trim_start().frames_round(vfr);
+ for (auto reel: film->reels()) {
+ if (reel.overlap(period)) {
+ /* We already checked that the trimmed Atmos content overlaps complete reels,
+ * so we can assume that here.
+ */
+ auto reel_asset = make_shared<dcp::ReelAtmosAsset>(asset, entry_point);
+ reel_asset->set_duration(reel.duration().frames_round(vfr));
+ entry_point += reel_asset->actual_duration();
+
+ reel_assets.push_back(
+ ReusableReelAsset(reel_asset, reel, ReusableReelAsset::Use::COPY)
+ );
+ }
+ }
+}
+
+
+
/** @return Details of all the DCP assets in a playlist that are marked to refer to */
list<ReusableReelAsset>
get_reusable_reel_assets(shared_ptr<const Film> film, shared_ptr<const Playlist> playlist)
@@ -141,6 +174,8 @@ get_reusable_reel_assets(shared_ptr<const Film> film, shared_ptr<const Playlist>
for (auto content: playlist->content()) {
if (auto dcp = dynamic_pointer_cast<DCPContent>(content)) {
add_reusable_reel_assets_from_dcp(film, dcp, reel_assets);
+ } else if (auto atmos = dynamic_pointer_cast<AtmosMXFContent>(content)) {
+ add_reusable_reel_assets_for_atmos(film, atmos, reel_assets);
}
}
diff --git a/src/lib/writer.cc b/src/lib/writer.cc
index 60a24ee96..5369b7a19 100644
--- a/src/lib/writer.cc
+++ b/src/lib/writer.cc
@@ -19,6 +19,7 @@
*/
+#include "asset_filename_register.h"
#include "audio_buffers.h"
#include "audio_mapping.h"
#include "config.h"
@@ -604,10 +605,12 @@ Writer::finish()
calculate_digests();
+ AssetFilenameRegister filename_register;
+
/* Add reels */
for (auto& i: _reels) {
- cpl->add(i.create_reel(_reel_assets, _output_dir, _have_subtitles, _have_closed_captions));
+ cpl->add(i.create_reel(_reel_assets, filename_register, _output_dir, _have_subtitles, _have_closed_captions));
}
/* Add metadata */
diff --git a/src/lib/wscript b/src/lib/wscript
index c3ec8132e..1fd8b7eb1 100644
--- a/src/lib/wscript
+++ b/src/lib/wscript
@@ -25,6 +25,7 @@ sources = """
analyse_audio_job.cc
analyse_subtitles_job.cc
analytics.cc
+ asset_filename_register.cc
atmos_content.cc
atmos_mxf_content.cc
atmos_decoder.cc