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 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "io.github.thibaultbee:streampack:${streampackVersion}" implementation "io.github.thibaultbee.streampack:streampack-core:${streampackVersion}"
// Only needed for RTMP live streaming // For the `PreviewView`
implementation "io.github.thibaultbee:streampack-extension-rtmp:${streampackVersion}" implementation "io.github.thibaultbee.streampack:streampack-ui:${streampackVersion}"
// Only needed for SRT live streaming // TODO: Only needed for RTMP live streaming: remove if you don't need it
implementation "io.github.thibaultbee:streampack-extension-srt:${streampackVersion}" 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' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'

View File

@@ -10,20 +10,25 @@ import android.util.Log
import android.util.Size import android.util.Size
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import io.github.thibaultbee.streampack.data.AudioConfig import io.github.thibaultbee.streampack.app.data.rotation.RotationRepository
import io.github.thibaultbee.streampack.data.VideoConfig import io.github.thibaultbee.streampack.core.elements.sources.audio.audiorecord.MicrophoneSourceFactory
import io.github.thibaultbee.streampack.error.StreamPackError 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.databinding.ActivityMainBinding
import io.github.thibaultbee.streampack.example.utils.PermissionsManager import io.github.thibaultbee.streampack.example.utils.PermissionsManager
import io.github.thibaultbee.streampack.example.utils.showDialog import io.github.thibaultbee.streampack.example.utils.showDialog
import io.github.thibaultbee.streampack.example.utils.toast import io.github.thibaultbee.streampack.example.utils.toast
import io.github.thibaultbee.streampack.ext.rtmp.streamers.CameraRtmpLiveStreamer import kotlinx.coroutines.flow.filter
import io.github.thibaultbee.streampack.ext.srt.streamers.CameraSrtLiveStreamer import kotlinx.coroutines.flow.filterNotNull
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.launch import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -33,9 +38,10 @@ class MainActivity : AppCompatActivity() {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private val permissionsManager = PermissionsManager(this, private val permissionsManager = PermissionsManager(
this,
streamerRequiredPermissions, streamerRequiredPermissions,
onAllGranted = { inflateStreamer() }, onAllGranted = { onPermissionsGranted() },
onShowPermissionRationale = { permissions, onRequiredPermissionLastTime -> onShowPermissionRationale = { permissions, onRequiredPermissionLastTime ->
// Explain why we need permissions // Explain why we need permissions
showDialog( showDialog(
@@ -53,83 +59,150 @@ class MainActivity : AppCompatActivity() {
positiveButtonText = 0, positiveButtonText = 0,
negativeButtonText = 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. * The streamer is the central object of StreamPack.
* It is responsible for the capture audio and video and the streaming process. * 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 { private val streamer by lazy {
CameraRtmpLiveStreamer( // 1 output
this, SingleStreamer(
initialOnErrorListener = errorListener, this, withAudio = true, withVideo = true
initialOnConnectionListener = connectionListener
) )
// 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 * 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.liveButton.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) { bindProperties()
/** }
* Dispatch from main thread is forced to avoid making network call on main thread
* with coroutines. private fun bindProperties() {
*/ binding.liveButton.setOnCheckedChangeListener { view, isChecked ->
lifecycleScope.launch { if (view.isPressed) {
try { if (isChecked) {
/** /**
* Always lock the device orientation during a live streaming to avoid * Dispatch from main thread is forced to avoid making network call on main thread
* to recreate the Activity. * with coroutines.
*/ */
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED lifecycleScope.launch {
/** try {
* For SRT, use srt://my.server.url:9998?streamid=myStreamId&passphrase=myPassphrase isTryingConnectionLiveData.postValue(true)
*/ /**
streamer.startStream("rtmp://my.server.url:1234/app/streamKey") * For SRT, use srt://my.server.url:9998?streamid=myStreamId&passphrase=myPassphrase
} catch (e: Exception) { */
binding.liveButton.isChecked = false streamer.startStream("rtmp://my.server.url:1935/app/streamKey")
Log.e(TAG, "Failed to connect", e) } 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() { override fun onStart() {
@@ -138,22 +211,32 @@ class MainActivity : AppCompatActivity() {
} }
@RequiresPermission(allOf = [Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO]) @RequiresPermission(allOf = [Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO])
private fun inflateStreamer() { private fun onPermissionsGranted() {
lifecycle.addObserver(streamerLifeCycleObserver) // Register the lifecycle observer setAVSource()
/** setStreamerView()
* Configure the streamer before calling view.streamer. Because it will start camera preview }
* which required a configuration
*/ @RequiresPermission(allOf = [Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO])
configureStreamer() 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 binding.preview.streamer = streamer // Bind the streamer to the preview
lifecycleScope.launch {
binding.preview.startPreview()
}
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private fun configureStreamer() { private fun configureStreamer() {
/** /**
* To check the parameters supported by the device, you can check parameter against: * To get the parameters supported by the device, the [SingleStreamer] have a
* - [CameraStreamerConfigurationHelper.flvHelper] for RTMP live streaming or * [SingleStreamer.getInfo] method.
* - [CameraStreamerConfigurationHelper.tsHelper] for SRT communication.
*/ */
/** /**
@@ -180,9 +263,11 @@ class MainActivity : AppCompatActivity() {
sampleRate = 44100, sampleRate = 44100,
channelConfig = AudioFormat.CHANNEL_IN_STEREO channelConfig = AudioFormat.CHANNEL_IN_STEREO
) )
streamer.configure(audioConfig, videoConfig)
}
lifecycleScope.launch {
streamer.setConfig(audioConfig, videoConfig)
}
}
private fun toast(message: String) { private fun toast(message: String) {
runOnUiThread { applicationContext.toast(message) } 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" android:layout_height="match_parent"
tools:context=".MainActivity"> tools:context=".MainActivity">
<io.github.thibaultbee.streampack.views.PreviewView <io.github.thibaultbee.streampack.ui.views.PreviewView
android:id="@+id/preview" android:id="@+id/preview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@@ -2,7 +2,7 @@
buildscript { buildscript {
ext { ext {
// Upgrade StreamPack version here // Upgrade StreamPack version here
streampackVersion = '2.6.1' streampackVersion = '3.0.0-RC'
} }
} }