Skip to content

Commit 4ed53de

Browse files
committed
refactor(*): introduce streamer pipeline to enable multiple output
1 parent 7920b7b commit 4ed53de

File tree

58 files changed

+3661
-1154
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3661
-1154
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.github.thibaultbee.streampack.core.elements.endpoints
2+
3+
import io.github.thibaultbee.streampack.core.configuration.mediadescriptor.MediaDescriptor
4+
import io.github.thibaultbee.streampack.core.elements.data.Frame
5+
import io.github.thibaultbee.streampack.core.elements.encoders.CodecConfig
6+
import kotlinx.coroutines.flow.MutableStateFlow
7+
import kotlinx.coroutines.flow.StateFlow
8+
9+
class DummyEndpoint : IEndpointInternal {
10+
private val _isOpen = MutableStateFlow(false)
11+
override val isOpen: StateFlow<Boolean> = _isOpen
12+
13+
var numOfAudioFramesWritten = 0
14+
private set
15+
var numOfVideoFramesWritten = 0
16+
private set
17+
val numOfFramesWritten: Int
18+
get() = numOfAudioFramesWritten + numOfVideoFramesWritten
19+
20+
private val _isStreaming = MutableStateFlow(false)
21+
val isStreaming: StateFlow<Boolean> = _isStreaming
22+
23+
override val info: IEndpoint.IEndpointInfo
24+
get() = TODO("Not yet implemented")
25+
26+
override fun getInfo(type: MediaDescriptor.Type): IEndpoint.IEndpointInfo {
27+
TODO("Not yet implemented")
28+
}
29+
30+
override val metrics: Any
31+
get() = TODO("Not yet implemented")
32+
33+
override suspend fun open(descriptor: MediaDescriptor) {
34+
_isOpen.emit(true)
35+
}
36+
37+
override suspend fun close() {
38+
_isOpen.emit(false)
39+
}
40+
41+
override suspend fun write(frame: Frame, streamPid: Int) {
42+
when {
43+
frame.isAudio -> numOfAudioFramesWritten++
44+
frame.isVideo -> numOfVideoFramesWritten++
45+
}
46+
}
47+
48+
override fun addStreams(streamConfigs: List<CodecConfig>): Map<CodecConfig, Int> {
49+
return streamConfigs.associateWith { it.hashCode() }
50+
}
51+
52+
override fun addStream(streamConfig: CodecConfig): Int {
53+
return streamConfig.hashCode()
54+
}
55+
56+
override suspend fun startStream() {
57+
_isStreaming.emit(true)
58+
}
59+
60+
override suspend fun stopStream() {
61+
_isStreaming.emit(false)
62+
}
63+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.github.thibaultbee.streampack.core.pipelines
2+
3+
import org.junit.Assert.*
4+
5+
class StreamerPipelineTest {
6+
7+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (C) 2025 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+
* http://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.core.pipelines.outputs
17+
18+
import io.github.thibaultbee.streampack.core.elements.encoders.IEncoderInternal
19+
import io.github.thibaultbee.streampack.core.logger.Logger
20+
import kotlinx.coroutines.flow.MutableStateFlow
21+
import kotlinx.coroutines.flow.StateFlow
22+
23+
class DummyVideoPipelineOutput : DummyPipelineOutput(hasAudio = false, hasVideo = true), IVideoPipelineOutputInternal {
24+
companion object {
25+
private const val TAG = "DummyVideoPipelineOutput"
26+
}
27+
28+
override var targetRotation: Int = 0
29+
override val surface: StateFlow<SurfaceWithSize?>
30+
get() = TODO("Not yet implemented")
31+
override var videoSourceTimestampOffset: Long
32+
get() = TODO("Not yet implemented")
33+
set(value) {}
34+
override var videoFrameRequestedListener: IEncoderInternal.IByteBufferInput.OnFrameRequestedListener?
35+
get() = TODO("Not yet implemented")
36+
set(value) {}
37+
}
38+
39+
open class DummyPipelineOutput(override val hasAudio: Boolean, override val hasVideo: Boolean) :
40+
IPipelineOutputInternal {
41+
42+
override var streamListener: IPipelineOutputInternal.Listener? = null
43+
44+
private val _throwable = MutableStateFlow<Throwable?>(null)
45+
override val throwable: StateFlow<Throwable?> = _throwable
46+
47+
private val _isStreaming = MutableStateFlow(false)
48+
override val isStreaming: StateFlow<Boolean> = _isStreaming
49+
50+
override suspend fun startStream() {
51+
Logger.i(TAG, "Start stream")
52+
_isStreaming.emit(true)
53+
}
54+
55+
override suspend fun stopStream() {
56+
Logger.i(TAG, "Stop stream")
57+
_isStreaming.emit(false)
58+
}
59+
60+
override suspend fun release() {
61+
Logger.i(TAG, "Release")
62+
_isStreaming.emit(false)
63+
}
64+
65+
companion object {
66+
private const val TAG = "DummyPipelineOutput"
67+
}
68+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.github.thibaultbee.streampack.core.pipelines.outputs.encoding
2+
3+
import android.content.Context
4+
import androidx.test.platform.app.InstrumentationRegistry
5+
import io.github.thibaultbee.streampack.core.configuration.mediadescriptor.UriMediaDescriptor
6+
import io.github.thibaultbee.streampack.core.elements.endpoints.DummyEndpoint
7+
import junit.framework.TestCase.assertTrue
8+
import kotlinx.coroutines.test.runTest
9+
import org.junit.Assert.assertFalse
10+
import org.junit.Test
11+
12+
class EncodingPipelineOutputTest {
13+
private val context: Context = InstrumentationRegistry.getInstrumentation().context
14+
15+
@Test
16+
fun testOpenClose() = runTest {
17+
val output = EncodingPipelineOutput(context, endpointInternal = DummyEndpoint())
18+
output.open(UriMediaDescriptor("file://test.mp4"))
19+
assertTrue(output.isOpen.value)
20+
output.close()
21+
assertFalse(output.isOpen.value)
22+
}
23+
}

core/src/androidTest/java/io/github/thibaultbee/streampack/core/utils/ConfigurationUtils.kt renamed to core/src/androidTest/java/io/github/thibaultbee/streampack/core/pipelines/utils/ConfigurationUtils.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package io.github.thibaultbee.streampack.core.utils
16+
package io.github.thibaultbee.streampack.core.pipelines.utils
1717

1818
import android.media.MediaFormat
1919
import android.util.Size

core/src/androidTest/java/io/github/thibaultbee/streampack/core/utils/FileUtils.kt renamed to core/src/androidTest/java/io/github/thibaultbee/streampack/core/pipelines/utils/FileUtils.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package io.github.thibaultbee.streampack.core.utils
16+
package io.github.thibaultbee.streampack.core.pipelines.utils
1717

1818
import androidx.test.platform.app.InstrumentationRegistry
1919
import java.io.File
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (C) 2025 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+
* http://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.core.pipelines.utils
17+
18+
import android.media.MediaFormat
19+
import android.util.Size
20+
import io.github.thibaultbee.streampack.core.elements.encoders.VideoCodecConfig
21+
import org.junit.Assert.assertEquals
22+
import org.junit.Assert.fail
23+
import org.junit.Test
24+
25+
class SourceConfigUtilsTest {
26+
27+
@Test
28+
fun videoSourceConfigFromEmpty() {
29+
try {
30+
SourceConfigUtils.buildVideoSourceConfig(emptySet())
31+
fail("Video codec configs must not be empty")
32+
} catch (_: Throwable) {
33+
}
34+
}
35+
36+
@Test
37+
fun buildVideoSourceConfigWithSimple() {
38+
// Given
39+
val videoCodecConfigs = setOf(
40+
VideoCodecConfig(
41+
MediaFormat.MIMETYPE_VIDEO_AVC,
42+
resolution = Size(1280, 720),
43+
fps = 30
44+
),
45+
VideoCodecConfig(MediaFormat.MIMETYPE_VIDEO_AVC, resolution = Size(1280, 720), fps = 30)
46+
)
47+
48+
// When
49+
val videoSourceConfig = SourceConfigUtils.buildVideoSourceConfig(videoCodecConfigs)
50+
51+
// Then
52+
assertEquals(1280, videoSourceConfig.resolution.width)
53+
assertEquals(720, videoSourceConfig.resolution.height)
54+
assertEquals(30, videoSourceConfig.fps)
55+
}
56+
57+
@Test
58+
fun buildVideoSourceConfigWithDifferentResolution() {
59+
// Given
60+
val videoCodecConfigs = setOf(
61+
VideoCodecConfig(MediaFormat.MIMETYPE_VIDEO_AVC, resolution = Size(1280, 720)),
62+
VideoCodecConfig(MediaFormat.MIMETYPE_VIDEO_AVC, resolution = Size(1920, 1080))
63+
)
64+
65+
// When
66+
val videoSourceConfig = SourceConfigUtils.buildVideoSourceConfig(videoCodecConfigs)
67+
68+
// Then
69+
assertEquals(1920, videoSourceConfig.resolution.width)
70+
assertEquals(1080, videoSourceConfig.resolution.height)
71+
}
72+
73+
@Test
74+
fun videoSourceConfigWithDifferentFps() {
75+
// Given
76+
val videoCodecConfigs = setOf(
77+
VideoCodecConfig(MediaFormat.MIMETYPE_VIDEO_AVC, fps = 30),
78+
VideoCodecConfig(MediaFormat.MIMETYPE_VIDEO_AVC, fps = 25)
79+
)
80+
81+
// When
82+
try {
83+
SourceConfigUtils.buildVideoSourceConfig(videoCodecConfigs)
84+
fail("All video codec configs must have the same fps")
85+
} catch (e: IllegalArgumentException) {
86+
assertEquals("All video codec configs must have the same fps", e.message)
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)