Skip to content

Commit e000461

Browse files
committed
feat(ui): add a composable preview view
1 parent 5cf4276 commit e000461

File tree

18 files changed

+221
-2
lines changed

18 files changed

+221
-2
lines changed

gradle/libs.versions.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ videoApiClient = "1.6.7"
77
androidxActivity = "1.10.1"
88
androidxAppcompat = "1.7.1"
99
androidxCamera = "1.4.0-alpha13"
10+
androidxComposeBom = "2026.01.00"
1011
androidxConstraintlayout = "2.2.1"
1112
androidxCore = "1.17.0"
1213
androidxDatabinding = "8.13.0"
@@ -29,6 +30,7 @@ robolectric = "4.16"
2930
komuxer = "0.3.3"
3031
srtdroid = "1.9.5"
3132
junitKtx = "1.3.0"
33+
compose = "1.10.1"
3234

3335
[libraries]
3436
android-documentation-plugin = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" }
@@ -42,6 +44,7 @@ android-material = { module = "com.google.android.material:material", version.re
4244
androidx-activity = { module = "androidx.activity:activity", version.ref = "androidxActivity" }
4345
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppcompat" }
4446
androidx-camera-viewfinder-view = { module = "androidx.camera.viewfinder:viewfinder-view", version.ref = "androidxCamera" }
47+
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidxComposeBom" }
4548
androidx-concurrent-futures = { module = "androidx.concurrent:concurrent-futures", version.ref = "concurrentFutures" }
4649
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidxConstraintlayout" }
4750
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
@@ -70,10 +73,16 @@ kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.re
7073
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
7174
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
7275
srtdroid-ktx = { module = "io.github.thibaultbee.srtdroid:srtdroid-ktx", version.ref = "srtdroid" }
76+
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "compose" }
77+
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
78+
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" }
79+
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" }
80+
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "compose" }
7381

7482
[plugins]
7583
android-application = { id = "com.android.application", version.ref = "agp" }
7684
android-library = { id = "com.android.library", version.ref = "agp" }
85+
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
7786
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
7887
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
7988
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }

settings.libs.gradle.kts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
// StreamPack libraries
22
include(":core")
33
project(":core").name = "streampack-core"
4-
include(":ui")
5-
project(":ui").name = "streampack-ui"
64
include(":services")
75
project(":services").name = "streampack-services"
86

7+
// UI
8+
include(":ui")
9+
project(":ui").projectDir = File(rootDir, "ui/ui")
10+
project(":ui").name = "streampack-ui"
11+
include(":compose")
12+
project(":compose").projectDir = File(rootDir, "ui/compose")
13+
project(":compose").name = "streampack-compose"
14+
915
// Extensions
1016
include(":extension-flv")
1117
project(":extension-flv").projectDir = File(rootDir, "extensions/flv")
File renamed without changes.

ui/compose/build.gradle.kts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
plugins {
2+
id(libs.plugins.android.library.get().pluginId)
3+
id(libs.plugins.kotlin.android.get().pluginId)
4+
id("android-library-convention")
5+
alias(libs.plugins.compose.compiler)
6+
}
7+
8+
description = "Jetpack compose components for StreamPack."
9+
10+
android {
11+
namespace = "io.github.thibaultbee.streampack.compose"
12+
13+
defaultConfig {
14+
minSdk = 23
15+
}
16+
buildFeatures {
17+
compose = true
18+
}
19+
}
20+
21+
dependencies {
22+
implementation(project(":streampack-core"))
23+
implementation(project(":streampack-ui"))
24+
25+
val composeBom = platform(libs.androidx.compose.bom)
26+
implementation(composeBom)
27+
implementation(libs.androidx.compose.runtime)
28+
implementation(libs.androidx.compose.ui)
29+
implementation(libs.androidx.compose.ui.tooling.preview)
30+
implementation(libs.androidx.compose.foundation)
31+
32+
androidTestImplementation(composeBom)
33+
34+
debugImplementation(libs.androidx.compose.ui.tooling)
35+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<manifest />
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2026 Thibault B.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.thibaultbee.streampack.compose
17+
18+
import androidx.compose.foundation.layout.fillMaxSize
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.LaunchedEffect
21+
import androidx.compose.runtime.rememberCoroutineScope
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.platform.LocalContext
24+
import androidx.compose.ui.tooling.preview.Preview
25+
import androidx.compose.ui.viewinterop.AndroidView
26+
import io.github.thibaultbee.streampack.compose.utils.BitmapUtils
27+
import io.github.thibaultbee.streampack.core.elements.sources.video.IPreviewableSource
28+
import io.github.thibaultbee.streampack.core.elements.sources.video.bitmap.BitmapSourceFactory
29+
import io.github.thibaultbee.streampack.core.elements.sources.video.camera.CameraSettings.FocusMetering.Companion.DEFAULT_AUTO_CANCEL_DURATION_MS
30+
import io.github.thibaultbee.streampack.core.interfaces.IWithVideoSource
31+
import io.github.thibaultbee.streampack.core.logger.Logger
32+
import io.github.thibaultbee.streampack.core.streamers.single.SingleStreamer
33+
import io.github.thibaultbee.streampack.ui.views.PreviewView
34+
import kotlinx.coroutines.launch
35+
import kotlinx.coroutines.sync.withLock
36+
37+
private const val TAG = "ComposePreviewView"
38+
39+
/**
40+
* Displays the preview of a [IWithVideoSource].
41+
*
42+
* A [IWithVideoSource] must have a video sourc.
43+
*
44+
* @param videoSource the [IWithVideoSource] to preview
45+
* @param modifier the [Modifier] to apply to the [PreviewView]
46+
* @param enableZoomOnPinch enable zoom on pinch gesture
47+
* @param enableTapToFocus enable tap to focus
48+
* @param onTapToFocusTimeoutMs the duration in milliseconds after which the focus area set by tap-to-focus is cleared
49+
*/
50+
@Composable
51+
fun PreviewScreen(
52+
videoSource: IWithVideoSource,
53+
modifier: Modifier = Modifier,
54+
enableZoomOnPinch: Boolean = true,
55+
enableTapToFocus: Boolean = true,
56+
onTapToFocusTimeoutMs: Long = DEFAULT_AUTO_CANCEL_DURATION_MS
57+
) {
58+
val scope = rememberCoroutineScope()
59+
60+
AndroidView(
61+
factory = { context ->
62+
PreviewView(context).apply {
63+
this.enableZoomOnPinch = enableZoomOnPinch
64+
this.enableTapToFocus = enableTapToFocus
65+
this.onTapToFocusTimeoutMs = onTapToFocusTimeoutMs
66+
67+
scope.launch {
68+
try {
69+
setVideoSourceProvider(videoSource)
70+
} catch (e: Exception) {
71+
Logger.e(TAG, "Failed to start preview", e)
72+
}
73+
}
74+
}
75+
},
76+
modifier = modifier,
77+
onRelease = {
78+
scope.launch {
79+
val source = videoSource.videoInput?.sourceFlow?.value as? IPreviewableSource
80+
source?.previewMutex?.withLock {
81+
source.stopPreview()
82+
source.resetPreview()
83+
}
84+
}
85+
})
86+
}
87+
88+
@Preview
89+
@Composable
90+
fun PreviewScreenPreview() {
91+
val context = LocalContext.current
92+
val streamer = SingleStreamer(context)
93+
LaunchedEffect(Unit) {
94+
streamer.setVideoSource(
95+
BitmapSourceFactory(
96+
BitmapUtils.createImage(
97+
1280,
98+
720
99+
)
100+
)
101+
)
102+
}
103+
104+
PreviewScreen(streamer, modifier = Modifier.fillMaxSize())
105+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2026 Thibault B.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.thibaultbee.streampack.compose.utils
17+
18+
import android.graphics.Bitmap
19+
import android.graphics.Canvas
20+
import android.graphics.Color
21+
import android.graphics.Paint
22+
23+
object BitmapUtils {
24+
/**
25+
* Creates a bitmap with the given width, height and color.
26+
*
27+
* @param width The width of the bitmap.
28+
* @param height The height of the bitmap.
29+
* @param color The color of the bitmap.
30+
* @return The created bitmap.
31+
*/
32+
fun createImage(width: Int, height: Int, color: Int = Color.RED): Bitmap {
33+
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
34+
val canvas = Canvas(bitmap)
35+
val paint = Paint().apply {
36+
this.color = color
37+
}
38+
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
39+
return bitmap
40+
}
41+
}

ui/ui/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

0 commit comments

Comments
 (0)