diff options
| author | Carl Hetherington <cth@carlh.net> | 2026-02-08 18:46:30 +0100 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2026-02-12 21:02:01 +0100 |
| commit | a680098a14cf40172370fde12c86691b82a36051 (patch) | |
| tree | c4cb35db18d91c84e8df2ec0bbe369200c2a235b /src/lib | |
| parent | 52ee81ba2d1091f712806c0c4edeef67c455884f (diff) | |
Support re-use of Atmos MXF assets.
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/asset_filename_register.cc | 61 | ||||
| -rw-r--r-- | src/lib/asset_filename_register.h | 39 | ||||
| -rw-r--r-- | src/lib/atmos_mxf_content.cc | 30 | ||||
| -rw-r--r-- | src/lib/atmos_mxf_content.h | 2 | ||||
| -rw-r--r-- | src/lib/atmos_mxf_decoder.cc | 5 | ||||
| -rw-r--r-- | src/lib/atmos_mxf_decoder.h | 6 | ||||
| -rw-r--r-- | src/lib/player.cc | 5 | ||||
| -rw-r--r-- | src/lib/reel_writer.cc | 41 | ||||
| -rw-r--r-- | src/lib/reel_writer.h | 17 | ||||
| -rw-r--r-- | src/lib/reusable_reel_asset.cc | 37 | ||||
| -rw-r--r-- | src/lib/writer.cc | 5 | ||||
| -rw-r--r-- | src/lib/wscript | 1 |
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 |
