chore(deps): upgrade to StreamPack 3.0.0-RC

This commit is contained in:
ThibaultBee
2025-04-10 10:55:44 +02:00
parent 4df8e2f79b
commit 3b8c52d8b8
5 changed files with 212 additions and 85 deletions

View File

@@ -47,11 +47,13 @@ dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "io.github.thibaultbee:streampack:${streampackVersion}"
// Only needed for RTMP live streaming
implementation "io.github.thibaultbee:streampack-extension-rtmp:${streampackVersion}"
// Only needed for SRT live streaming
implementation "io.github.thibaultbee:streampack-extension-srt:${streampackVersion}"
implementation "io.github.thibaultbee.streampack:streampack-core:${streampackVersion}"
// For the `PreviewView`
implementation "io.github.thibaultbee.streampack:streampack-ui:${streampackVersion}"
// TODO: Only needed for RTMP live streaming: remove if you don't need it
implementation "io.github.thibaultbee.streampack:streampack-extension-rtmp:${streampackVersion}"
// TODO: Only needed for SRT live streaming: remove if you don't need it
implementation "io.github.thibaultbee.streampack:streampack-extension-srt:${streampackVersion}"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
testImplementation 'junit:junit:4.13.2'

View File

@@ -10,20 +10,25 @@ import android.util.Log
import android.util.Size
import androidx.annotation.RequiresPermission
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import io.github.thibaultbee.streampack.data.AudioConfig
import io.github.thibaultbee.streampack.data.VideoConfig
import io.github.thibaultbee.streampack.error.StreamPackError
import io.github.thibaultbee.streampack.app.data.rotation.RotationRepository
import io.github.thibaultbee.streampack.core.elements.sources.audio.audiorecord.MicrophoneSourceFactory
import io.github.thibaultbee.streampack.core.elements.sources.video.camera.extensions.defaultCameraId
import io.github.thibaultbee.streampack.core.interfaces.setCameraId
import io.github.thibaultbee.streampack.core.interfaces.startStream
import io.github.thibaultbee.streampack.core.streamers.dual.DualStreamer
import io.github.thibaultbee.streampack.core.streamers.lifecycle.StreamerActivityLifeCycleObserver
import io.github.thibaultbee.streampack.core.streamers.single.AudioConfig
import io.github.thibaultbee.streampack.core.streamers.single.SingleStreamer
import io.github.thibaultbee.streampack.core.streamers.single.VideoConfig
import io.github.thibaultbee.streampack.core.utils.extensions.isClosedException
import io.github.thibaultbee.streampack.example.databinding.ActivityMainBinding
import io.github.thibaultbee.streampack.example.utils.PermissionsManager
import io.github.thibaultbee.streampack.example.utils.showDialog
import io.github.thibaultbee.streampack.example.utils.toast
import io.github.thibaultbee.streampack.ext.rtmp.streamers.CameraRtmpLiveStreamer
import io.github.thibaultbee.streampack.ext.srt.streamers.CameraSrtLiveStreamer
import io.github.thibaultbee.streampack.listeners.OnConnectionListener
import io.github.thibaultbee.streampack.listeners.OnErrorListener
import io.github.thibaultbee.streampack.streamers.StreamerLifeCycleObserver
import io.github.thibaultbee.streampack.streamers.helpers.CameraStreamerConfigurationHelper
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
@@ -33,9 +38,10 @@ class MainActivity : AppCompatActivity() {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
@SuppressLint("MissingPermission")
private val permissionsManager = PermissionsManager(this,
private val permissionsManager = PermissionsManager(
this,
streamerRequiredPermissions,
onAllGranted = { inflateStreamer() },
onAllGranted = { onPermissionsGranted() },
onShowPermissionRationale = { permissions, onRequiredPermissionLastTime ->
// Explain why we need permissions
showDialog(
@@ -53,83 +59,150 @@ class MainActivity : AppCompatActivity() {
positiveButtonText = 0,
negativeButtonText = 0
)
}
)
// Reports and manages error with [OnErrorListener]
private val errorListener = object : OnErrorListener {
override fun onError(error: StreamPackError) {
toast("An error occurred: $error")
}
}
// Reports and manages connection events with [OnConnectionListener]
private val connectionListener = object : OnConnectionListener {
override fun onFailed(message: String) {
toast("Connection failed: $message")
}
override fun onLost(message: String) {
toast("Connection lost: $message")
}
override fun onSuccess() {
toast("Connected")
}
}
})
/**
* The streamer is the central object of StreamPack.
* It is responsible for the capture audio and video and the streaming process.
*
* Use a [CameraRtmpLiveStreamer] For RTMP. Or a [CameraSrtLiveStreamer] for SRT.
* If you need only 1 output (live only or record only), use [SingleStreamer].
* If you need 2 outputs (live and record), use [DualStreamer].
*/
private val streamer by lazy {
CameraRtmpLiveStreamer(
this,
initialOnErrorListener = errorListener,
initialOnConnectionListener = connectionListener
// 1 output
SingleStreamer(
this, withAudio = true, withVideo = true
)
// 2 outputs: uncomment the line below
/*
DualStreamer(
this,
withAudio = true,
withVideo = true
)
*/
}
/**
* Listen to lifecycle events. So we don't have to stop the streamer manually in `onPause` and release in `onDestroy
*/
private val streamerLifeCycleObserver by lazy { StreamerLifeCycleObserver(streamer) }
private val streamerLifeCycleObserver by lazy { StreamerActivityLifeCycleObserver(streamer) }
/**
* Listen to device rotation.
*/
private val rotationRepository by lazy { RotationRepository.getInstance(applicationContext) }
/**
* A LiveData to observe the connection state.
*/
private val isTryingConnectionLiveData = MutableLiveData<Boolean>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.liveButton.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
/**
* Dispatch from main thread is forced to avoid making network call on main thread
* with coroutines.
*/
lifecycleScope.launch {
try {
/**
* Always lock the device orientation during a live streaming to avoid
* to recreate the Activity.
*/
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
/**
* For SRT, use srt://my.server.url:9998?streamid=myStreamId&passphrase=myPassphrase
*/
streamer.startStream("rtmp://my.server.url:1234/app/streamKey")
} catch (e: Exception) {
binding.liveButton.isChecked = false
Log.e(TAG, "Failed to connect", e)
bindProperties()
}
private fun bindProperties() {
binding.liveButton.setOnCheckedChangeListener { view, isChecked ->
if (view.isPressed) {
if (isChecked) {
/**
* Dispatch from main thread is forced to avoid making network call on main thread
* with coroutines.
*/
lifecycleScope.launch {
try {
isTryingConnectionLiveData.postValue(true)
/**
* For SRT, use srt://my.server.url:9998?streamid=myStreamId&passphrase=myPassphrase
*/
streamer.startStream("rtmp://my.server.url:1935/app/streamKey")
} catch (e: Exception) {
binding.liveButton.isChecked = false
Log.e(TAG, "Failed to connect", e)
toast("Connection failed: ${e.message}")
} finally {
isTryingConnectionLiveData.postValue(false)
}
}
} else {
lifecycleScope.launch {
streamer.stopStream()
}
}
} else {
lifecycleScope.launch {
streamer.stopStream()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
}
bindAndPrepareStreamer()
}
private fun bindAndPrepareStreamer() {
// Register the lifecycle observer
lifecycle.addObserver(streamerLifeCycleObserver)
// Configure the streamer
configureStreamer()
// Listen to rotation
lifecycleScope.launch {
rotationRepository.rotationFlow.collect {
streamer.setTargetRotation(it)
}
}
// Lock and unlock orientation on isStreaming state.
lifecycleScope.launch {
streamer.isStreamingFlow.collect { isStreaming ->
if (isStreaming) {
lockOrientation()
} else {
unlockOrientation()
}
if (isStreaming) {
binding.liveButton.isChecked = true
} else if (isTryingConnectionLiveData.value == true) {
binding.liveButton.isChecked = true
} else {
binding.liveButton.isChecked = false
}
}
}
// General error handling
lifecycleScope.launch {
streamer.throwableFlow.filterNotNull().filter { !it.isClosedException }
.collect { throwable ->
Log.e(TAG, "Error: ${throwable.message}", throwable)
toast("Error: ${throwable.message}")
}
}
// Connection error handling
lifecycleScope.launch {
streamer.throwableFlow.filterNotNull().filter { it.isClosedException }
.collect { throwable ->
Log.e(TAG, "Connection lost: ${throwable.message}", throwable)
toast("Connection lost: ${throwable.message}")
}
}
}
private fun lockOrientation() {
/**
* Lock orientation while stream is running to avoid stream interruption if
* user turns the device.
* For landscape only mode, set [requireActivity().requestedOrientation] to
* [ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE].
*/
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
}
private fun unlockOrientation() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
override fun onStart() {
@@ -138,22 +211,32 @@ class MainActivity : AppCompatActivity() {
}
@RequiresPermission(allOf = [Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO])
private fun inflateStreamer() {
lifecycle.addObserver(streamerLifeCycleObserver) // Register the lifecycle observer
/**
* Configure the streamer before calling view.streamer. Because it will start camera preview
* which required a configuration
*/
configureStreamer()
private fun onPermissionsGranted() {
setAVSource()
setStreamerView()
}
@RequiresPermission(allOf = [Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO])
private fun setAVSource() {
// Set audio and video sources.
lifecycleScope.launch {
streamer.setAudioSource(MicrophoneSourceFactory())
streamer.setCameraId(this@MainActivity.defaultCameraId)
}
}
private fun setStreamerView() {
binding.preview.streamer = streamer // Bind the streamer to the preview
lifecycleScope.launch {
binding.preview.startPreview()
}
}
@SuppressLint("MissingPermission")
private fun configureStreamer() {
/**
* To check the parameters supported by the device, you can check parameter against:
* - [CameraStreamerConfigurationHelper.flvHelper] for RTMP live streaming or
* - [CameraStreamerConfigurationHelper.tsHelper] for SRT communication.
* To get the parameters supported by the device, the [SingleStreamer] have a
* [SingleStreamer.getInfo] method.
*/
/**
@@ -180,9 +263,11 @@ class MainActivity : AppCompatActivity() {
sampleRate = 44100,
channelConfig = AudioFormat.CHANNEL_IN_STEREO
)
streamer.configure(audioConfig, videoConfig)
}
lifecycleScope.launch {
streamer.setConfig(audioConfig, videoConfig)
}
}
private fun toast(message: String) {
runOnUiThread { applicationContext.toast(message) }

View File

@@ -0,0 +1,40 @@
package io.github.thibaultbee.streampack.app.data.rotation
import android.content.Context
import io.github.thibaultbee.streampack.core.streamers.orientation.SensorRotationProvider
import io.github.thibaultbee.streampack.core.streamers.orientation.asFlowProvider
import kotlinx.coroutines.flow.Flow
/**
* A repository for orientation data.
*/
class RotationRepository(
context: Context,
) {
/**
* A flow of device rotation.
* `SensorRotationProvider` follows the orientation of the sensor, so it will change when the
* device is rotated.
* If the application orientation is locked, you should use `DisplayRotationProvider` instead.
*/
//private val rotationProvider = DisplayRotationProvider(context).asFlowProvider()
private val rotationProvider = SensorRotationProvider(context).asFlowProvider()
val rotationFlow: Flow<Int> = rotationProvider.rotationFlow
companion object {
@Volatile
private var INSTANCE: RotationRepository? = null
fun getInstance(context: Context): RotationRepository {
return INSTANCE ?: synchronized(this) {
INSTANCE?.let {
return it
}
RotationRepository(context).apply {
INSTANCE = this
}
}
}
}
}

View File

@@ -6,7 +6,7 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
<io.github.thibaultbee.streampack.views.PreviewView
<io.github.thibaultbee.streampack.ui.views.PreviewView
android:id="@+id/preview"
android:layout_width="match_parent"
android:layout_height="match_parent"