1+ /*
2+ * Copyright 2022 The Android Open Source Project
3+ * Copyright 2025 Thibault B.
4+ *
5+ * Licensed under the Apache License, Version 2.0 (the "License");
6+ * you may not use this file except in compliance with the License.
7+ * You may obtain a copy of the License at
8+ *
9+ * http://www.apache.org/licenses/LICENSE-2.0
10+ *
11+ * Unless required by applicable law or agreed to in writing, software
12+ * distributed under the License is distributed on an "AS IS" BASIS,
13+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+ * See the License for the specific language governing permissions and
15+ * limitations under the License.
16+ */
117package io.github.thibaultbee.streampack.core.elements.processing.video
218
19+ import android.graphics.Bitmap
320import android.graphics.SurfaceTexture
421import android.util.Size
522import android.view.Surface
23+ import androidx.annotation.IntRange
624import androidx.concurrent.futures.CallbackToFutureAdapter
725import com.google.common.util.concurrent.ListenableFuture
826import io.github.thibaultbee.streampack.core.elements.processing.video.outputs.ISurfaceOutput
927import io.github.thibaultbee.streampack.core.elements.processing.video.utils.GLUtils
28+ import io.github.thibaultbee.streampack.core.elements.processing.video.utils.extensions.preRotate
29+ import io.github.thibaultbee.streampack.core.elements.processing.video.utils.extensions.preVerticalFlip
1030import io.github.thibaultbee.streampack.core.elements.utils.av.video.DynamicRangeProfile
31+ import io.github.thibaultbee.streampack.core.elements.utils.extensions.rotate
1132import io.github.thibaultbee.streampack.core.logger.Logger
1233import io.github.thibaultbee.streampack.core.pipelines.DispatcherProvider.Companion.THREAD_NAME_GL
1334import io.github.thibaultbee.streampack.core.pipelines.IVideoDispatcherProvider
1435import io.github.thibaultbee.streampack.core.pipelines.utils.HandlerThreadExecutor
36+ import java.io.IOException
1537import java.util.concurrent.atomic.AtomicBoolean
1638
1739
@@ -21,6 +43,8 @@ private class DefaultSurfaceProcessor(
2143) : ISurfaceProcessorInternal, SurfaceTexture.OnFrameAvailableListener {
2244 private val renderer = OpenGlRenderer ()
2345
46+ private val glHandler = glThread.handler
47+
2448 private val isReleaseRequested = AtomicBoolean (false )
2549 private var isReleased = false
2650
@@ -31,7 +55,7 @@ private class DefaultSurfaceProcessor(
3155 private val surfaceInputs: MutableList <SurfaceInput > = mutableListOf ()
3256 private val surfaceInputsTimestampInNsMap: MutableMap <SurfaceTexture , Long > = hashMapOf()
3357
34- private val glHandler = glThread.handler
58+ private val pendingSnapshots = mutableListOf< PendingSnapshot >()
3559
3660 init {
3761 val future = submitSafely {
@@ -177,6 +201,19 @@ private class DefaultSurfaceProcessor(
177201 }
178202 }
179203
204+ override fun snapshot (
205+ @IntRange(from = 0 , to = 359 ) rotationDegrees : Int
206+ ): ListenableFuture <Bitmap > {
207+ if (isReleaseRequested.get()) {
208+ throw IllegalStateException (" SurfaceProcessor is released" )
209+ }
210+ return CallbackToFutureAdapter .getFuture { completer ->
211+ executeSafely {
212+ pendingSnapshots.add(PendingSnapshot (rotationDegrees, completer))
213+ }
214+ }
215+ }
216+
180217 // Executed on GL thread
181218 override fun onFrameAvailable (surfaceTexture : SurfaceTexture ) {
182219 if (isReleaseRequested.get()) {
@@ -201,6 +238,78 @@ private class DefaultSurfaceProcessor(
201238 Logger .e(TAG , " Error while rendering frame" , t)
202239 }
203240 }
241+
242+ // Surface, size and transform matrix for JPEG Surface if exists
243+ if (pendingSnapshots.isNotEmpty()) {
244+ try {
245+ val first = surfaceOutputs.first()
246+ val snapshotOutput = Pair (
247+ first.descriptor.resolution,
248+ surfaceOutputMatrix.clone()
249+ )
250+
251+ // Execute all pending snapshots.
252+ takeSnapshot(snapshotOutput)
253+ } catch (e: RuntimeException ) {
254+ // Propagates error back to the app if failed to take snapshot.
255+ failAllPendingSnapshots(e)
256+ }
257+ }
258+ }
259+
260+ /* *
261+ * Takes a snapshot of the current frame and draws it to given JPEG surface.
262+ *
263+ * @param snapshotOutput The <Surface size, transform matrix> pair for drawing.
264+ */
265+ private fun takeSnapshot (snapshotOutput : Pair <Size , FloatArray >) {
266+ if (pendingSnapshots.isEmpty()) {
267+ // No pending snapshot requests, do nothing.
268+ return
269+ }
270+
271+ // Write to JPEG surface, once for each snapshot request.
272+ try {
273+ for (pendingSnapshot in pendingSnapshots) {
274+ val (size, transform) = snapshotOutput
275+
276+ // Take a snapshot of the current frame.
277+ val bitmap = getBitmap(size, transform, pendingSnapshot.rotationDegrees)
278+
279+ // Complete the snapshot request.
280+ pendingSnapshot.completer.set(bitmap)
281+ }
282+ pendingSnapshots.clear()
283+ } catch (e: IOException ) {
284+ failAllPendingSnapshots(e)
285+ }
286+ }
287+
288+ private fun failAllPendingSnapshots (throwable : Throwable ) {
289+ for (pendingSnapshot in pendingSnapshots) {
290+ pendingSnapshot.completer.setException(throwable)
291+ }
292+ pendingSnapshots.clear()
293+ }
294+
295+ private fun getBitmap (
296+ size : Size ,
297+ textureTransform : FloatArray ,
298+ rotationDegrees : Int
299+ ): Bitmap {
300+ val snapshotTransform = textureTransform.clone()
301+
302+ // Rotate the output if requested.
303+ snapshotTransform.preRotate(rotationDegrees.toFloat(), 0.5f , 0.5f )
304+
305+ // Flip the snapshot. This is for reverting the GL transform added in SurfaceOutputImpl.
306+ snapshotTransform.preVerticalFlip(0.5f )
307+
308+ // Update the size based on the rotation degrees.
309+ val rotatedSize = size.rotate(rotationDegrees)
310+
311+ // Take a snapshot Bitmap and compress it to JPEG.
312+ return renderer.snapshot(rotatedSize, snapshotTransform)
204313 }
205314
206315 private fun executeSafely (
@@ -242,6 +351,12 @@ private class DefaultSurfaceProcessor(
242351 }
243352
244353 private data class SurfaceInput (val surface : Surface , val surfaceTexture : SurfaceTexture )
354+
355+ private data class PendingSnapshot (
356+ @IntRange(from = 0 , to = 359 )
357+ val rotationDegrees : Int ,
358+ val completer : CallbackToFutureAdapter .Completer <Bitmap >
359+ )
245360}
246361
247362class DefaultSurfaceProcessorFactory :
0 commit comments