From 0e23a6b2910ba0a548b764453a96d6ed8e9288d7 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:58:39 -0700 Subject: [PATCH] Halve the per-frame allocation and copy on the m2ts NAL video path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NAL path called length_prefixed_to_annex_b, which allocates a whole-frame Vec of its own, then copied the result into a second whole-frame Vec — two full-frame allocations and two full-frame copies per video frame. The crate already has append_length_prefixed_as_annex_b, which writes the conversion straight into a destination buffer; it is the same code path with the intermediate removed. The destination is also sized once up front instead of starting from Vec::new(), which re-grew from zero capacity inside every conversion. On a UHD HEVC title muxed to m2ts:// — ~200k video frames averaging ~310 KB of ES at 60 Mb/s — that removes roughly 62 GB of allocation and 62 GB of memcpy. Behaviour is unchanged: the existing tsmux conversion tests, including the non-NAL passthrough and Annex-B default pair added last round, all still pass. A first attempt reused a persistent scratch buffer across frames, which does not work: the buffer is handed out as Cow::Owned and so can never be returned. A right-sized single allocation gets most of the win without restructuring the function around the borrow. --- src/mux/tsmux.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/mux/tsmux.rs b/src/mux/tsmux.rs index ec67bf0..bbed3ce 100644 --- a/src/mux/tsmux.rs +++ b/src/mux/tsmux.rs @@ -4,7 +4,7 @@ //! packets. Each frame is wrapped in a PES header, split into TS packets, //! and prepended with the 4-byte TP_extra_header. -use super::hevc::{avcc_to_annex_b, hvcc_to_annex_b, length_prefixed_to_annex_b}; +use super::hevc::{append_length_prefixed_as_annex_b, avcc_to_annex_b, hvcc_to_annex_b}; use crate::disc::Codec; use std::io::{self, Write}; @@ -181,7 +181,10 @@ impl TsMuxer { // unchanged, so borrow `data` directly rather than copying it; only // NAL video needs an owned Annex-B conversion buffer. let es_data: std::borrow::Cow<'_, [u8]> = if is_video && self.is_nal_video(track) { - let mut annex_b = Vec::new(); + // Size the buffer once for the whole frame. `Vec::new()` re-grew from + // zero capacity on every frame, reallocating repeatedly inside a single + // ~310 KB conversion. The slack covers any prepended parameter sets. + let mut annex_b = Vec::with_capacity(data.len() + 1024); if keyframe && !self.params_written[track] { if let Some(ref cp) = self.codec_privates[track] { // avcC and hvcC are DIFFERENT box layouts; parsing one with @@ -198,7 +201,12 @@ impl TsMuxer { } self.params_written[track] = true; } - annex_b.extend_from_slice(&length_prefixed_to_annex_b(data)); + // Write the conversion STRAIGHT into the destination. + // `length_prefixed_to_annex_b` allocates a whole-frame Vec of its own + // and we then copied it in, so every video frame cost two full-frame + // allocations and two full-frame copies. At ~200k frames averaging + // ~310 KB of ES on a UHD, that is ~124 GB of pointless memcpy. + append_length_prefixed_as_annex_b(&mut annex_b, data); std::borrow::Cow::Owned(annex_b) } else { if is_video {