Skip to content

Commit 5d5f3ba

Browse files
committed
refactor(core): move snapshot API to a specific interface
1 parent fb3f8cb commit 5d5f3ba

File tree

4 files changed

+130
-90
lines changed

4 files changed

+130
-90
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package io.github.thibaultbee.streampack.core.elements.interfaces
2+
3+
import android.graphics.Bitmap
4+
import androidx.annotation.IntRange
5+
import kotlinx.coroutines.CoroutineDispatcher
6+
import kotlinx.coroutines.Dispatchers
7+
import kotlinx.coroutines.withContext
8+
import java.io.File
9+
import java.io.FileOutputStream
10+
import java.io.OutputStream
11+
12+
/**
13+
* An interface to take a snapshot of the current video frame.
14+
*/
15+
interface ISnapshotable {
16+
/**
17+
* Takes a snapshot of the current video frame.
18+
*
19+
* The snapshot is returned as a [Bitmap].
20+
*
21+
* @param rotationDegrees The rotation to apply to the snapshot, in degrees. 0 means no rotation.
22+
* @return The snapshot as a [Bitmap].
23+
*/
24+
suspend fun takeSnapshot(@IntRange(from = 0, to = 359) rotationDegrees: Int = 0): Bitmap
25+
}
26+
27+
/**
28+
* Takes a JPEG snapshot of the current video frame.
29+
*
30+
* The snapshot is saved to the specified file.
31+
*
32+
* @param filePathString The path of the file to save the snapshot to.
33+
* @param quality The quality of the JPEG, from 0 to 100.
34+
* @param rotationDegrees The rotation to apply to the snapshot, in degrees.
35+
*/
36+
suspend fun ISnapshotable.takeJpegSnapshot(
37+
filePathString: String,
38+
@IntRange(from = 0, to = 100) quality: Int = 100,
39+
@IntRange(from = 0, to = 359) rotationDegrees: Int = 0,
40+
dispatcher: CoroutineDispatcher = Dispatchers.IO
41+
) = takeJpegSnapshot(withContext(dispatcher) {
42+
FileOutputStream(filePathString)
43+
}, quality, rotationDegrees)
44+
45+
46+
/**
47+
* Takes a JPEG snapshot of the current video frame.
48+
*
49+
* The snapshot is saved to the specified file.
50+
*
51+
* @param file The file to save the snapshot to.
52+
* @param quality The quality of the JPEG, from 0 to 100.
53+
* @param rotationDegrees The rotation to apply to the snapshot, in degrees.
54+
*/
55+
suspend fun ISnapshotable.takeJpegSnapshot(
56+
file: File,
57+
@IntRange(from = 0, to = 100) quality: Int = 100,
58+
@IntRange(from = 0, to = 359) rotationDegrees: Int = 0,
59+
dispatcher: CoroutineDispatcher = Dispatchers.IO
60+
) = takeJpegSnapshot(withContext(dispatcher) {
61+
FileOutputStream(file)
62+
}, quality, rotationDegrees)
63+
64+
/**
65+
* Takes a snapshot of the current video frame.
66+
*
67+
* The snapshot is saved as a JPEG to the specified output stream.
68+
* @param outputStream The output stream to save the snapshot to.
69+
* @param quality The quality of the JPEG, from 0 to 100.
70+
* @param rotationDegrees The rotation to apply to the snapshot, in degrees.
71+
*/
72+
suspend fun ISnapshotable.takeJpegSnapshot(
73+
outputStream: OutputStream,
74+
@IntRange(from = 0, to = 100) quality: Int = 100,
75+
@IntRange(from = 0, to = 359) rotationDegrees: Int = 0
76+
) {
77+
val bitmap = takeSnapshot(rotationDegrees)
78+
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
79+
}

core/src/main/java/io/github/thibaultbee/streampack/core/elements/processing/video/DefaultSurfaceProcessor.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import android.view.Surface
2323
import androidx.annotation.IntRange
2424
import androidx.concurrent.futures.CallbackToFutureAdapter
2525
import com.google.common.util.concurrent.ListenableFuture
26+
import io.github.thibaultbee.streampack.core.elements.interfaces.ISnapshotable
2627
import io.github.thibaultbee.streampack.core.elements.processing.video.outputs.ISurfaceOutput
2728
import io.github.thibaultbee.streampack.core.elements.processing.video.outputs.SurfaceOutput
2829
import io.github.thibaultbee.streampack.core.elements.processing.video.utils.GLUtils
@@ -38,12 +39,14 @@ import io.github.thibaultbee.streampack.core.pipelines.DispatcherProvider.Compan
3839
import io.github.thibaultbee.streampack.core.pipelines.IVideoDispatcherProvider
3940
import io.github.thibaultbee.streampack.core.pipelines.utils.HandlerThreadExecutor
4041
import java.util.concurrent.atomic.AtomicBoolean
42+
import kotlin.coroutines.resume
43+
import kotlin.coroutines.suspendCoroutine
4144

4245

4346
private class DefaultSurfaceProcessor(
4447
private val dynamicRangeProfile: DynamicRangeProfile,
4548
private val glThread: HandlerThreadExecutor,
46-
) : ISurfaceProcessorInternal, SurfaceTexture.OnFrameAvailableListener {
49+
) : ISurfaceProcessorInternal, SurfaceTexture.OnFrameAvailableListener, ISnapshotable {
4750
private val renderer = OpenGlRenderer()
4851

4952
private val glHandler = glThread.handler
@@ -225,7 +228,27 @@ private class DefaultSurfaceProcessor(
225228
}
226229
}
227230

228-
override fun snapshot(
231+
/**
232+
* Takes a snapshot of the current video frame.
233+
*
234+
* The snapshot is returned as a [Bitmap].
235+
*
236+
* @param rotationDegrees The rotation to apply to the snapshot, in degrees. 0 means no rotation.
237+
* @return The snapshot as a [Bitmap].
238+
*/
239+
override suspend fun takeSnapshot(rotationDegrees: Int): Bitmap {
240+
return suspendCoroutine { continuation ->
241+
val listener = snapshot(rotationDegrees)
242+
try {
243+
val bitmap = listener.get()
244+
continuation.resume(bitmap)
245+
} catch (e: Exception) {
246+
continuation.resumeWith(Result.failure(e))
247+
}
248+
}
249+
}
250+
251+
private fun snapshot(
229252
@IntRange(from = 0, to = 359) rotationDegrees: Int
230253
): ListenableFuture<Bitmap> {
231254
if (isReleaseRequested.get()) {

core/src/main/java/io/github/thibaultbee/streampack/core/elements/processing/video/ISurfaceProcessor.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ interface ISurfaceProcessorInternal : ISurfaceProcessor, Releasable {
5151

5252
fun removeAllOutputSurfaces()
5353

54-
fun snapshot(@IntRange(from = 0, to = 359) rotationDegrees: Int): ListenableFuture<Bitmap>
55-
5654
/**
5755
* Factory interface for creating instances of [ISurfaceProcessorInternal].
5856
*/

core/src/main/java/io/github/thibaultbee/streampack/core/pipelines/inputs/VideoInput.kt

Lines changed: 26 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import android.content.Context
1919
import android.graphics.Bitmap
2020
import android.view.Surface
2121
import androidx.annotation.IntRange
22+
import io.github.thibaultbee.streampack.core.elements.interfaces.ISnapshotable
2223
import io.github.thibaultbee.streampack.core.elements.processing.video.ISurfaceProcessorInternal
2324
import io.github.thibaultbee.streampack.core.elements.processing.video.outputs.ISurfaceOutput
2425
import io.github.thibaultbee.streampack.core.elements.processing.video.outputs.SurfaceOutput
@@ -45,18 +46,13 @@ import kotlinx.coroutines.launch
4546
import kotlinx.coroutines.sync.Mutex
4647
import kotlinx.coroutines.sync.withLock
4748
import kotlinx.coroutines.withContext
48-
import java.io.File
49-
import java.io.FileOutputStream
50-
import java.io.OutputStream
5149
import java.util.concurrent.atomic.AtomicBoolean
52-
import kotlin.coroutines.resume
53-
import kotlin.coroutines.suspendCoroutine
5450

5551
/**
5652
* The public interface for the video input.
5753
* It provides access to the video source, the video processor, and the streaming state.
5854
*/
59-
interface IVideoInput {
55+
interface IVideoInput : ISnapshotable {
6056

6157
/**
6258
* Whether the video input is streaming.
@@ -85,64 +81,6 @@ interface IVideoInput {
8581
* The video processor for adding effects to the video frames.
8682
*/
8783
val processor: ISurfaceProcessorInternal
88-
89-
/**
90-
* Takes a snapshot of the current video frame.
91-
*
92-
* The snapshot is returned as a [Bitmap].
93-
*
94-
* @param rotationDegrees The rotation to apply to the snapshot, in degrees. 0 means no rotation.
95-
* @return The snapshot as a [Bitmap].
96-
*/
97-
suspend fun takeSnapshot(@IntRange(from = 0, to = 359) rotationDegrees: Int = 0): Bitmap
98-
}
99-
100-
/**
101-
* Takes a JPEG snapshot of the current video frame.
102-
*
103-
* The snapshot is saved to the specified file.
104-
*
105-
* @param filePathString The path of the file to save the snapshot to.
106-
* @param quality The quality of the JPEG, from 0 to 100.
107-
* @param rotationDegrees The rotation to apply to the snapshot, in degrees.
108-
*/
109-
suspend fun IVideoInput.takeJpegSnapshot(
110-
filePathString: String,
111-
@IntRange(from = 0, to = 100) quality: Int = 100,
112-
@IntRange(from = 0, to = 359) rotationDegrees: Int = 0
113-
) = takeJpegSnapshot(FileOutputStream(filePathString), quality, rotationDegrees)
114-
115-
116-
/**
117-
* Takes a JPEG snapshot of the current video frame.
118-
*
119-
* The snapshot is saved to the specified file.
120-
*
121-
* @param file The file to save the snapshot to.
122-
* @param quality The quality of the JPEG, from 0 to 100.
123-
* @param rotationDegrees The rotation to apply to the snapshot, in degrees.
124-
*/
125-
suspend fun IVideoInput.takeJpegSnapshot(
126-
file: File,
127-
@IntRange(from = 0, to = 100) quality: Int = 100,
128-
@IntRange(from = 0, to = 359) rotationDegrees: Int = 0
129-
) = takeJpegSnapshot(FileOutputStream(file), quality, rotationDegrees)
130-
131-
/**
132-
* Takes a snapshot of the current video frame.
133-
*
134-
* The snapshot is saved as a JPEG to the specified output stream.
135-
* @param outputStream The output stream to save the snapshot to.
136-
* @param quality The quality of the JPEG, from 0 to 100.
137-
* @param rotationDegrees The rotation to apply to the snapshot, in degrees.
138-
*/
139-
suspend fun IVideoInput.takeJpegSnapshot(
140-
outputStream: OutputStream,
141-
@IntRange(from = 0, to = 100) quality: Int = 100,
142-
@IntRange(from = 0, to = 359) rotationDegrees: Int = 0
143-
) {
144-
val bitmap = takeSnapshot(rotationDegrees)
145-
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
14684
}
14785

14886
/**
@@ -436,6 +374,13 @@ internal class VideoInput(
436374
return newSurfaceProcessor
437375
}
438376

377+
/**
378+
* Takes a snapshot of the current video frame.
379+
*
380+
* It starts the video source if needed.
381+
*
382+
* The snapshot is returned as a [Bitmap].
383+
*/
439384
override suspend fun takeSnapshot(
440385
@IntRange(
441386
from = 0,
@@ -445,14 +390,13 @@ internal class VideoInput(
445390
if (isReleaseRequested.get()) {
446391
throw IllegalStateException("Input is released")
447392
}
448-
return startStreamForBlock {
449-
suspendCoroutine { continuation ->
450-
val listener = processor.snapshot(rotationDegrees)
451-
try {
452-
val bitmap = listener.get()
453-
continuation.resume(bitmap)
454-
} catch (e: Exception) {
455-
continuation.resumeWith(Result.failure(e))
393+
return withContext(dispatcherProvider.default) {
394+
sourceMutex.withLock {
395+
val processor = processor as? ISnapshotable?
396+
?: throw IllegalStateException("Processor is not a snapshotable")
397+
398+
startStreamForBlockUnsafe {
399+
processor.takeSnapshot(rotationDegrees)
456400
}
457401
}
458402
}
@@ -605,23 +549,19 @@ internal class VideoInput(
605549
*
606550
* If the stream was already running, it will not be stopped after the block.
607551
*/
608-
private suspend fun <T> startStreamForBlock(block: suspend () -> T): T {
552+
private suspend fun <T> startStreamForBlockUnsafe(block: suspend () -> T): T {
609553
if (isReleaseRequested.get()) {
610554
throw IllegalStateException("Input is released")
611555
}
612-
return withContext(dispatcherProvider.default) {
613-
sourceMutex.withLock {
614-
val wasStreaming = isStreamingFlow.value
615-
if (!wasStreaming) {
616-
startStreamUnsafe()
617-
}
618-
try {
619-
block()
620-
} finally {
621-
if (!wasStreaming) {
622-
stopStreamUnsafe()
623-
}
624-
}
556+
val wasStreaming = isStreamingFlow.value
557+
if (!wasStreaming) {
558+
startStreamUnsafe()
559+
}
560+
return try {
561+
block()
562+
} finally {
563+
if (!wasStreaming) {
564+
stopStreamUnsafe()
625565
}
626566
}
627567
}

0 commit comments

Comments
 (0)