@@ -24,13 +24,23 @@ use unienc_common::{
2424use crate :: { MetalTexture , common:: UnsafeSendRetained , metal} ;
2525use unienc_common:: TryFromUnityNativeTexturePointer ;
2626
27+ /// Capacity of the bounded channel carrying encoded frames from the VideoToolbox callback to the
28+ /// consumer. `push` reserves a slot before submitting each frame, so when the consumer falls behind
29+ /// and the channel fills up, `push` suspends (backpressure) instead of the callback dropping
30+ /// already-encoded frames (which would break the P-frame reference chain).
31+ const OUTPUT_CHANNEL_CAPACITY : usize = 32 ;
32+
33+ /// A reserved output-channel slot handed to a single encode callback via the per-frame source-frame
34+ /// ref-con. Completing the send through it can neither block the callback nor drop the frame.
35+ type OutputPermit = mpsc:: OwnedPermit < Result < VideoEncodedData > > ;
36+
2737pub struct VideoToolboxEncoder {
2838 input : VideoToolboxEncoderInput ,
2939 output : VideoToolboxEncoderOutput ,
3040}
3141pub struct VideoToolboxEncoderInput {
3242 session : CompressionSession ,
33- tx : Box < mpsc:: UnboundedSender < Result < VideoEncodedData > > > ,
43+ tx : Box < mpsc:: Sender < Result < VideoEncodedData > > > ,
3444 width : u32 ,
3545 height : u32 ,
3646 bitrate : u32 ,
@@ -43,7 +53,7 @@ struct CompressionSession {
4353unsafe impl Send for VideoToolboxEncoderInput { }
4454
4555pub struct VideoToolboxEncoderOutput {
46- rx : mpsc:: UnboundedReceiver < Result < VideoEncodedData > > ,
56+ rx : mpsc:: Receiver < Result < VideoEncodedData > > ,
4757}
4858
4959pub struct VideoEncodedData {
@@ -105,31 +115,33 @@ impl EncodedData for VideoEncodedData {
105115}
106116
107117unsafe extern "C-unwind" fn handle_video_encode_output (
108- output_callback_ref_con : * mut c_void ,
109- _source_frame_ref_con : * mut c_void ,
118+ _output_callback_ref_con : * mut c_void ,
119+ source_frame_ref_con : * mut c_void ,
110120 status : i32 ,
111121 _info_flags : VTEncodeInfoFlags ,
112122 sample_buffer : * mut CMSampleBuffer ,
113123) {
114- let tx = unsafe {
115- & * ( output_callback_ref_con as * const mpsc:: UnboundedSender < Result < VideoEncodedData > > )
116- } ;
124+ // Each submitted frame carries a pre-reserved output-channel slot (an OwnedPermit) as its
125+ // source-frame ref-con. Completing the send through that permit means this callback never
126+ // blocks (it runs on VideoToolbox's internal queue, which complete_frames() waits on) and
127+ // never drops an encoded frame (dropping would break the reference chain of subsequent
128+ // P-frames). Backpressure is instead applied in `push`, which awaits the reservation before
129+ // submitting the frame, so a full channel throttles the producer rather than corrupting output.
130+ let permit = unsafe { * Box :: from_raw ( source_frame_ref_con as * mut OutputPermit ) } ;
117131
118132 // Propagate encode failures to the output side instead of silently ignoring them.
119133 if let Err ( err) = status. to_result ( ) {
120- // send only fails when the output has already been dropped
121- _ = tx. send ( Err ( err) ) ;
134+ permit. send ( Err ( err) ) ;
122135 return ;
123136 }
124137
125138 if let Some ( sample_buffer) = unsafe { Retained :: retain ( sample_buffer) } {
126- // The channel is unbounded, so encoded frames are never dropped here. Dropping a frame at
127- // this layer would break the reference chain of subsequent P-frames; any backpressure /
128- // dropping policy is applied by the consumer (BoundedEncodedDataBuffer on the C# side).
129- // Blocking this callback is not an option either: it runs on VideoToolbox's internal
130- // queue, and complete_frames() waits for pending callbacks to return.
131- _ = tx. send ( Ok ( VideoEncodedData :: new ( sample_buffer. into ( ) ) ) ) ;
132- } // otherwise the frame was dropped by the encoder itself (e.g. kVTEncodeInfo_FrameDropped)
139+ permit. send ( Ok ( VideoEncodedData :: new ( sample_buffer. into ( ) ) ) ) ;
140+ } else {
141+ // The encoder itself dropped the frame (e.g. kVTEncodeInfo_FrameDropped); dropping the
142+ // permit returns the reserved slot to the channel.
143+ drop ( permit) ;
144+ }
133145}
134146
135147unsafe extern "C-unwind" fn release_pixel_buffer (
@@ -155,6 +167,19 @@ impl EncoderInput for VideoToolboxEncoderInput {
155167 type Data = VideoSample < MetalTexture > ;
156168
157169 async fn push ( & mut self , data : Self :: Data ) -> unienc_common:: Result < ( ) > {
170+ // Reserve an output-channel slot before doing any encode work. When the consumer is behind
171+ // and the channel is full, this await suspends `push`, propagating backpressure up the
172+ // pipeline so the C# side drops *input* frames (via DroppingChannelInput) instead of us
173+ // dropping already-encoded output. Holding the OwnedPermit (Send) across the blit await
174+ // below is fine; if building the frame fails, dropping the permit releases the slot.
175+ let permit = self
176+ . tx
177+ . as_ref ( )
178+ . clone ( )
179+ . reserve_owned ( )
180+ . await
181+ . map_err ( AppleError :: from) ?;
182+
158183 let buffer = match data. frame {
159184 unienc_common:: VideoFrame :: Bgra32 ( bgra32) => {
160185 let buffer = bgra32. buffer ;
@@ -232,30 +257,49 @@ impl EncoderInput for VideoToolboxEncoderInput {
232257 }
233258 } ;
234259
260+ // Hand the reserved slot to this frame's encode callback via the source-frame ref-con. No
261+ // await happens past this point, so the raw pointer is never held across a suspension.
262+ let permit_ptr = Box :: into_raw ( Box :: new ( permit) ) ;
263+
235264 let mut retry = 0 ;
236265
237- loop {
266+ let res = loop {
238267 let res = unsafe {
239268 self . session . inner . encode_frame (
240269 & buffer,
241270 CMTime :: with_seconds ( data. timestamp , 720 ) ,
242271 kCMTimeInvalid,
243272 None ,
244- std :: ptr :: null_mut ( ) ,
273+ permit_ptr as * mut c_void ,
245274 std:: ptr:: null_mut ( ) ,
246275 )
247276 } ;
248277
249278 if res == kVTInvalidSessionErr && retry == 0 {
250- // VTCompressionSession turns invalid when the app enters background on iOS
251- // retrying once
279+ // VTCompressionSession turns invalid when the app enters background on iOS; retry
280+ // once with a fresh session, re-passing the same reserved permit.
252281 retry += 1 ;
253- self . session =
254- CompressionSession :: new ( self . width , self . height , self . bitrate , & * self . tx ) ?;
255- continue ;
282+ match CompressionSession :: new ( self . width , self . height , self . bitrate ) {
283+ Ok ( session) => {
284+ self . session = session;
285+ continue ;
286+ }
287+ Err ( err) => {
288+ // No callback will run for this frame; reclaim the permit to free the slot.
289+ drop ( unsafe { Box :: from_raw ( permit_ptr) } ) ;
290+ return Err ( err. into ( ) ) ;
291+ }
292+ }
256293 }
257294
258- break res. to_result ( ) ?;
295+ break res;
296+ } ;
297+
298+ if let Err ( err) = res. to_result ( ) {
299+ // The frame was not accepted, so the callback will never consume the permit; reclaim it
300+ // to release the reserved slot.
301+ drop ( unsafe { Box :: from_raw ( permit_ptr) } ) ;
302+ return Err ( err. into ( ) ) ;
259303 }
260304
261305 Ok ( ( ) )
@@ -291,12 +335,7 @@ impl Drop for VideoToolboxEncoderInput {
291335}
292336
293337impl CompressionSession {
294- fn new (
295- width : u32 ,
296- height : u32 ,
297- bitrate : u32 ,
298- tx : * const mpsc:: UnboundedSender < Result < VideoEncodedData > > ,
299- ) -> Result < Self > {
338+ fn new ( width : u32 , height : u32 , bitrate : u32 ) -> Result < Self > {
300339 let mut session: * mut VTCompressionSession = std:: ptr:: null_mut ( ) ;
301340
302341 unsafe {
@@ -309,7 +348,9 @@ impl CompressionSession {
309348 None ,
310349 None ,
311350 Some ( handle_video_encode_output) ,
312- tx as * mut c_void ,
351+ // The output callback receives its channel slot per-frame via the source-frame
352+ // ref-con (see `push`), so no session-level ref-con is needed.
353+ std:: ptr:: null_mut ( ) ,
313354 NonNull :: new ( & mut session) . ok_or ( AppleError :: NonNullCreationFailed ) ?,
314355 )
315356 . to_result ( ) ?;
@@ -348,14 +389,14 @@ impl CompressionSession {
348389
349390impl VideoToolboxEncoder {
350391 pub fn new ( options : & impl unienc_common:: VideoEncoderOptions ) -> Result < Self > {
351- let ( tx, rx) = mpsc:: unbounded_channel ( ) ;
392+ let ( tx, rx) = mpsc:: channel ( OUTPUT_CHANNEL_CAPACITY ) ;
352393 let tx = Box :: new ( tx) ;
353394
354395 let ( width, height, bitrate) = ( options. width ( ) , options. height ( ) , options. bitrate ( ) ) ;
355396
356397 Ok ( VideoToolboxEncoder {
357398 input : VideoToolboxEncoderInput {
358- session : CompressionSession :: new ( width, height, bitrate, & * tx ) ?,
399+ session : CompressionSession :: new ( width, height, bitrate) ?,
359400 tx,
360401 width,
361402 height,
0 commit comments