@@ -57,6 +57,21 @@ public CIFilter[] Filters
5757 /// <summary>Set by the handler so wrapped frames carry mirroring metadata.</summary>
5858 public volatile bool Mirrored ;
5959
60+ /// <summary>Whether the analyzer is handed the filtered frame - see CameraView.AnalyzerSeesEffects.</summary>
61+ public volatile bool AnalyzerSeesEffects ;
62+
63+ /// <summary>Where a filtered frame is drawn before it is handed on. Created on first use.</summary>
64+ readonly FilteredFrameBuffer filtered = new ( ) ;
65+
66+ /// <summary>Releases the scratch buffer with the delegate that owns it.</summary>
67+ protected override void Dispose ( bool disposing )
68+ {
69+ if ( disposing )
70+ this . filtered . Dispose ( ) ;
71+
72+ base . Dispose ( disposing ) ;
73+ }
74+
6075 /// <summary>When set, each frame is composited with the overlay and appended to the burn-in recording.</summary>
6176 public volatile AppleVideoOverlayRecorder ? Recorder ;
6277
@@ -78,6 +93,19 @@ public override void DidOutputSampleBuffer(AVCaptureOutput captureOutput, CMSamp
7893 var recorder = this . Recorder ;
7994 if ( this . OnFrame != null && this . WantFrames ? . Invoke ( ) == true )
8095 {
96+ // The frame as the preview draws it, for an analyzer that has asked to see what
97+ // the camera looks like rather than what the sensor produced. The buffer is this
98+ // delegate's own and is reused, which is safe because the pipeline runs one
99+ // analysis at a time - see FilteredFrameBuffer.
100+ if ( this . AnalyzerSeesEffects
101+ && chain . Length > 0
102+ && this . RenderForAnalyzer ( chain , pixelBuffer ) is { } effected )
103+ {
104+ this . OnFrame ( AppleCameraFrame . Wrap ( effected , rotation : 0 , mirrored : this . Mirrored ) ) ;
105+ recorder ? . AppendVideo ( sampleBuffer ) ;
106+ return ;
107+ }
108+
81109 // ⚠️ Borrow only when nothing is going to write to this buffer. The recorder composites
82110 // effects and the burn-in overlay back into it (AppleVideoOverlayRecorder.Composite), so
83111 // with one attached a borrowed frame would be read by the analyzer on one thread while
@@ -122,6 +150,40 @@ public override void DidOutputSampleBuffer(AVCaptureOutput captureOutput, CMSamp
122150 // Reused across frames so a steady-state render allocates nothing here.
123151 readonly List < CIImage > produced = [ ] ;
124152
153+ /// <summary>
154+ /// Runs the effect chain into the scratch buffer, for an analyzer rather than for the screen.
155+ /// </summary>
156+ /// <remarks>
157+ /// Separate from <see cref="RenderFiltered"/> because the destinations have nothing in common:
158+ /// that one produces an image for a view, this one fills a pixel buffer another consumer will
159+ /// read. The recipe is identical, and both render at the source extent for the reason spelled
160+ /// out there.
161+ /// </remarks>
162+ CVPixelBuffer ? RenderForAnalyzer ( CIFilter [ ] chain , CVPixelBuffer pixelBuffer )
163+ {
164+ using var input = new CIImage ( pixelBuffer ) ;
165+
166+ this . produced . Clear ( ) ;
167+ var output = AppleCameraFilters . Apply ( input , chain , this . produced ) ;
168+
169+ try
170+ {
171+ return output is null ? null : this . filtered . Render ( this . context , output , input . Extent ) ;
172+ }
173+ catch ( Exception )
174+ {
175+ // One frame without the effect on it beats taking the capture pipeline down.
176+ return null ;
177+ }
178+ finally
179+ {
180+ foreach ( var image in this . produced )
181+ image . Dispose ( ) ;
182+
183+ this . produced . Clear ( ) ;
184+ }
185+ }
186+
125187 void RenderFiltered ( CIFilter [ ] chain , CVPixelBuffer pixelBuffer , UIImageView view )
126188 {
127189 using var input = new CIImage ( pixelBuffer ) ;
0 commit comments