Compare commits

..

10 Commits

Author SHA1 Message Date
ThibaultBee
5740e3f22b chore(*): modernize the project
Some checks failed
Build / build (push) Has been cancelled
2026-02-04 15:16:56 +01:00
ThibaultBee
529356a052 chore(deps): upgrade AGP to 9.0.0 2026-02-04 14:39:53 +01:00
ThibaultBee
344f270a06 chore(deps): upgrade to StreamPack 3.1.1 2026-02-04 14:34:07 +01:00
ThibaultBee
a50391f588 feat(app): add a view model for a better architecture 2026-01-28 12:03:03 +01:00
ThibaultBee
4e4e34ddbf chore(deps): upgrade to StreamPack 3.1.0 2026-01-17 15:27:16 +01:00
ThibaultBee
5a0104a72e chore(deps): upgrade to StreamPack 3.0.2 2025-11-20 16:34:49 +01:00
ThibaultBee
6b9facb5a8 chore(deps): upgrade to StreamPack 3.0.1 2025-11-15 13:40:58 +01:00
ThibaultBee
8c7991ef4f chore(deps): upgrade to StreamPack 3.0.0 2025-09-18 22:25:49 +02:00
Thibault Beyou
35a467061f chore(deps): upgrade to StreamPack 3.0.0-RC2 2025-06-29 22:45:26 +02:00
ThibaultBee
da6e0f033f fix(ci): upgrade upload action to v4 2025-04-10 10:58:28 +02:00
19 changed files with 550 additions and 364 deletions

View File

@@ -21,7 +21,7 @@ jobs:
- name: Assemble - name: Assemble
run: ./gradlew assembleDebug run: ./gradlew assembleDebug
- name: Upload APKs - name: Upload APKs
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: apks name: apks
path: | path: |

View File

@@ -18,5 +18,6 @@ See [StreamPack](https://github.com/ThibaultBee/StreamPack) for more settings an
1. Click on "Use this template" to create a new repository from this template. 1. Click on "Use this template" to create a new repository from this template.
2. Clone your new repository. 2. Clone your new repository.
3. Open the project with Android Studio. 3. Open the project with Android Studio.
4. Set your RTMP or SRT server URL in `MainActivity.kt`. 4. Replace default `rtmp://my.server.url:1935/app/streamKey` by your RTMP or SRT server URL in
`MainViewModel.kt`.
5. Run the application on a device or an emulator. 5. Run the application on a device or an emulator.

View File

@@ -1,62 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'io.github.thibaultbee.streampack.example'
compileSdk 35
defaultConfig {
applicationId "io.github.thibaultbee.streampack.example"
minSdk 26
targetSdk 35
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
packagingOptions {
jniLibs {
pickFirsts += ['**/*.so']
}
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
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'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}

65
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,65 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "io.github.thibaultbee.streampack.app"
compileSdk {
version = release(36)
}
defaultConfig {
applicationId = "io.github.thibaultbee.streampack.app"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_18
targetCompatibility = JavaVersion.VERSION_18
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_18)
}
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.streampack.core)
// For the `PreviewView`
implementation(libs.streampack.ui)
// TODO: Only needed for RTMP live streaming: remove if you don't need it
implementation(libs.streampack.rtmp)
// TODO: Only needed for SRT live streaming: remove if you don't need it
implementation(libs.streampack.srt)
implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.constraintlayout)
implementation(libs.lifecycle.runtime.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}

View File

@@ -14,8 +14,6 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- Use internally. Not use with camera but it avoid a warning -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@@ -0,0 +1,14 @@
package io.github.thibaultbee.streampack.app
import android.content.pm.ActivityInfo
/**
* Application configuration.
*/
object ApplicationConstants {
/**
* Default application orientation.
* Also set in `AndroidManifest.xml` `android:screenOrientation` attribute.
*/
const val supportedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}

View File

@@ -0,0 +1,200 @@
package io.github.thibaultbee.streampack.app
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.annotation.RequiresPermission
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import io.github.thibaultbee.streampack.app.databinding.ActivityMainBinding
import io.github.thibaultbee.streampack.core.elements.sources.video.camera.extensions.defaultCameraId
import io.github.thibaultbee.streampack.core.streamers.lifecycle.StreamerActivityLifeCycleObserver
import io.github.thibaultbee.streampack.app.utils.PermissionsManager
import io.github.thibaultbee.streampack.app.utils.showDialog
import io.github.thibaultbee.streampack.app.utils.toast
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels {
MainViewModelFactory(this.application)
}
private val streamerRequiredPermissions =
listOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
/**
* A minimalist permission manager
*/
@SuppressLint("MissingPermission")
private val permissionsManager = PermissionsManager(
this,
streamerRequiredPermissions,
onAllGranted = { onPermissionsGranted() },
onShowPermissionRationale = { permissions, onRequiredPermissionLastTime ->
// Explain why we need permissions
showDialog(
title = "Permissions denied",
message = "Explain why you need to grant $permissions permissions to stream",
positiveButtonText = R.string.accept,
onPositiveButtonClick = { onRequiredPermissionLastTime() },
negativeButtonText = R.string.denied
)
},
onDenied = {
showDialog(
"Permissions denied",
"You need to grant all permissions to stream",
positiveButtonText = 0,
negativeButtonText = 0
)
})
/**
* 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 { StreamerActivityLifeCycleObserver(viewModel.streamer) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
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 {
viewModel.startStream()
} catch (e: Exception) {
binding.liveButton.isChecked = false
Log.e(TAG, "Failed to connect", e)
toast("Connection failed: ${e.message}")
}
}
} else {
lifecycleScope.launch {
viewModel.stopStream()
}
}
}
}
// Register the lifecycle observer
lifecycle.addObserver(streamerLifeCycleObserver)
// Configure the streamer
configureStreamer()
// Bind events
viewModel.closedThrowableLiveData.observe(this) {
toast("Connection error: ${it.message}")
}
viewModel.throwableLiveData.observe(this) {
toast("Error: ${it.message}")
}
viewModel.isStreamingLiveData.observe(this) { isStreaming ->
if (isStreaming) {
lockOrientation()
} else {
unlockOrientation()
}
if (isStreaming) {
binding.liveButton.isChecked = true
} else if (viewModel.isTryingConnectionLiveData.value == true) {
binding.liveButton.isChecked = true
} else {
binding.liveButton.isChecked = false
}
}
viewModel.isTryingConnectionLiveData.observe(this) { isWaitingForConnection ->
if (isWaitingForConnection) {
binding.liveButton.isChecked = true
} else if (viewModel.isStreamingLiveData.value == true) {
binding.liveButton.isChecked = true
} else {
binding.liveButton.isChecked = false
}
}
}
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 = ApplicationConstants.supportedOrientation
}
override fun onStart() {
super.onStart()
permissionsManager.requestPermissions()
}
@RequiresPermission(allOf = [Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO])
private fun onPermissionsGranted() {
setAVSource()
setStreamerView()
}
@RequiresPermission(allOf = [Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO])
private fun setAVSource() {
// Set audio and video sources.
lifecycleScope.launch {
viewModel.setAudioSource()
viewModel.setCameraId(this@MainActivity.defaultCameraId)
}
}
private fun setStreamerView() {
lifecycleScope.launch {
binding.preview.setVideoSourceProvider(viewModel.streamer) // Bind the streamer to the preview
}
}
@SuppressLint("MissingPermission")
private fun configureStreamer() {
lifecycleScope.launch {
viewModel.setAudioConfig()
viewModel.setVideoConfig()
}
}
override fun onDestroy() {
super.onDestroy()
// Unregister the lifecycle observer
lifecycle.removeObserver(streamerLifeCycleObserver)
}
private fun toast(message: String) {
runOnUiThread { applicationContext.toast(message) }
}
companion object {
private const val TAG = "MainActivity"
}
}

View File

@@ -0,0 +1,156 @@
package io.github.thibaultbee.streampack.app
import android.Manifest
import android.media.AudioFormat
import android.media.MediaFormat
import android.util.Size
import androidx.annotation.RequiresPermission
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
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.interfaces.setCameraId
import io.github.thibaultbee.streampack.core.interfaces.startStream
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
class MainViewModel(
private val rotationRepository: RotationRepository,
val streamer: SingleStreamer
) : ViewModel() {
private val defaultDispatcher = Dispatchers.Default
/**
* A LiveData to observe the stream state.
*/
val isStreamingLiveData: LiveData<Boolean>
get() = streamer.isStreamingFlow.asLiveData()
/**
* A LiveData to observe the pending connection state.
*/
private val _isTryingConnectionLiveData = MutableLiveData<Boolean>()
val isTryingConnectionLiveData: LiveData<Boolean> = _isTryingConnectionLiveData
/**
* A LiveData to observe async disconnection errors.
*/
val closedThrowableLiveData: LiveData<Throwable> =
streamer.throwableFlow.filterNotNull().filter { it.isClosedException }.asLiveData()
/**
* A LiveData to observe streamer errors.
*/
val throwableLiveData: LiveData<Throwable> =
streamer.throwableFlow.filterNotNull().filter { !it.isClosedException }.asLiveData()
init {
/**
* Listens to device rotation.
*/
viewModelScope.launch(defaultDispatcher) {
rotationRepository.rotationFlow.collect {
streamer.setTargetRotation(it)
}
}
}
/**
* Starts the stream.
*
* Replace with a valid URL.
*/
suspend fun startStream() {
_isTryingConnectionLiveData.postValue(true)
try {
/**
* For SRT, use srt://my.server.url:9998?streamid=myStreamId&passphrase=myPassphrase
*/
streamer.startStream("rtmp://my.server.url:1935/app/streamKey")
} finally {
_isTryingConnectionLiveData.postValue(false)
}
}
/**
* Stops the stream.
*/
suspend fun stopStream() {
streamer.stopStream()
}
/**
* Sets the audio configuration.
*
* You can verify the device supported configuration with [SingleStreamer.getInfo].
*/
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
suspend fun setAudioConfig() {
/**
* There are other parameters in the [AudioConfig] such as:
* - byteFormat
* - enableEchoCanceler
* - enableNoiseSuppressor
* They will be initialized with an appropriate default value.
*/
val audioConfig = AudioConfig(
mimeType = MediaFormat.MIMETYPE_AUDIO_AAC,
sampleRate = 44100,
channelConfig = AudioFormat.CHANNEL_IN_STEREO
)
streamer.setAudioConfig(audioConfig)
}
/**
* Sets the video configuration.
*
* You can verify the device supported configuration with [SingleStreamer.getInfo].
*/
suspend fun setVideoConfig() {
/**
* There are other parameters in the [VideoConfig] such as:
* - bitrate
* - profile
* - level
* - gopSize
* They will be initialized with an appropriate default value.
*/
val videoConfig = VideoConfig(
mimeType = MediaFormat.MIMETYPE_VIDEO_AVC, resolution = Size(1280, 720), fps = 25
)
streamer.setVideoConfig(videoConfig)
}
/**
* Sets the microphone as the audio source.
*/
suspend fun setAudioSource() {
streamer.setAudioSource(MicrophoneSourceFactory())
}
/**
* Sets the camera with the given id as the video source.
*
* @param cameraId The camera id.
*/
@RequiresPermission(Manifest.permission.CAMERA)
suspend fun setCameraId(cameraId: String) {
streamer.setCameraId(cameraId)
}
companion object {
private const val TAG = "MainViewModel"
}
}

View File

@@ -0,0 +1,52 @@
package io.github.thibaultbee.streampack.app
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.github.thibaultbee.streampack.app.data.rotation.RotationRepository
import io.github.thibaultbee.streampack.core.streamers.dual.DualStreamer
import io.github.thibaultbee.streampack.core.streamers.single.SingleStreamer
/**
* Factory for constructing [MainViewModelFactory] from the [Application].
*/
class MainViewModelFactory(private val application: Application) :
ViewModelProvider.Factory {
private val rotationRepository = RotationRepository.getInstance(application)
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
val streamer = createStreamer(application)
return MainViewModel(rotationRepository, streamer) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
companion object {
/**
* Creates a streamer instance.
*
* The streamer is the central object of StreamPack.
* It is responsible for the capture audio and video and the streaming process.
*
* If you need only 1 output (live only or record only), use [SingleStreamer].
* If you need 2 outputs (live and record), use [DualStreamer].
*/
private fun createStreamer(application: Application): SingleStreamer {
// 1 output
return SingleStreamer(
application, withAudio = true, withVideo = true
)
// 2 outputs: uncomment the line below
/*
DualStreamer(
this,
withAudio = true,
withVideo = true
)
*/
}
}
}

View File

@@ -1,4 +1,4 @@
package io.github.thibaultbee.streampack.example.utils package io.github.thibaultbee.streampack.app.utils
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface

View File

@@ -1,4 +1,4 @@
package io.github.thibaultbee.streampack.example.utils package io.github.thibaultbee.streampack.app.utils
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity

View File

@@ -1,279 +0,0 @@
package io.github.thibaultbee.streampack.example
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.media.AudioFormat
import android.media.MediaFormat
import android.os.Bundle
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.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 kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val streamerRequiredPermissions =
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
@SuppressLint("MissingPermission")
private val permissionsManager = PermissionsManager(
this,
streamerRequiredPermissions,
onAllGranted = { onPermissionsGranted() },
onShowPermissionRationale = { permissions, onRequiredPermissionLastTime ->
// Explain why we need permissions
showDialog(
title = "Permissions denied",
message = "Explain why you need to grant $permissions permissions to stream",
positiveButtonText = R.string.accept,
onPositiveButtonClick = { onRequiredPermissionLastTime() },
negativeButtonText = R.string.denied
)
},
onDenied = {
showDialog(
"Permissions denied",
"You need to grant all permissions to stream",
positiveButtonText = 0,
negativeButtonText = 0
)
})
/**
* The streamer is the central object of StreamPack.
* It is responsible for the capture audio and video and the streaming process.
*
* 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 {
// 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 { 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)
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()
}
}
}
}
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() {
super.onStart()
permissionsManager.requestPermissions()
}
@RequiresPermission(allOf = [Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO])
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 get the parameters supported by the device, the [SingleStreamer] have a
* [SingleStreamer.getInfo] method.
*/
/**
* There are other parameters in the [VideoConfig] such as:
* - bitrate
* - profile
* - level
* - gopSize
* They will be initialized with an appropriate default value.
*/
val videoConfig = VideoConfig(
mimeType = MediaFormat.MIMETYPE_VIDEO_AVC, resolution = Size(1280, 720), fps = 25
)
/**
* There are other parameters in the [AudioConfig] such as:
* - byteFormat
* - enableEchoCanceler
* - enableNoiseSuppressor
* They will be initialized with an appropriate default value.
*/
val audioConfig = AudioConfig(
mimeType = MediaFormat.MIMETYPE_AUDIO_AAC,
sampleRate = 44100,
channelConfig = AudioFormat.CHANNEL_IN_STEREO
)
lifecycleScope.launch {
streamer.setConfig(audioConfig, videoConfig)
}
}
private fun toast(message: String) {
runOnUiThread { applicationContext.toast(message) }
}
companion object {
private const val TAG = "MainActivity"
}
}

View File

@@ -1,12 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
// Upgrade StreamPack version here
streampackVersion = '3.0.0-RC'
}
}
plugins {
id 'com.android.application' version '8.9.1' apply false
id 'org.jetbrains.kotlin.android' version '2.1.0' apply false
}

5
build.gradle.kts Normal file
View File

@@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}

View File

@@ -22,3 +22,13 @@ kotlin.code.style=official
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.nonFinalResIds=false android.nonFinalResIds=false
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false

31
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,31 @@
[versions]
agp = "9.0.0"
appcompat = "1.7.1"
constraintlayout = "2.2.1"
coreKtx = "1.17.0"
espressoCore = "3.7.0"
junit = "4.13.2"
junitVersion = "1.3.0"
kotlin = "2.3.0"
lifecycleRuntimeKtx = "2.10.0"
material = "1.13.0"
streampack = "3.1.1"
[libraries]
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
junit = { module = "junit:junit", version.ref = "junit" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
material = { module = "com.google.android.material:material", version.ref = "material" }
streampack-core = { group = "io.github.thibaultbee.streampack", name = "streampack-core", version.ref = "streampack" }
streampack-ui = { group = "io.github.thibaultbee.streampack", name = "streampack-ui", version.ref = "streampack" }
streampack-rtmp = { group = "io.github.thibaultbee.streampack", name = "streampack-rtmp", version.ref = "streampack" }
streampack-srt = { group = "io.github.thibaultbee.streampack", name = "streampack-srt", version.ref = "streampack" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

View File

@@ -1,6 +1,6 @@
#Sun Mar 26 13:16:11 CEST 2023 #Sun Mar 26 13:16:11 CEST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@@ -1,8 +1,14 @@
pluginManagement { pluginManagement {
repositories { repositories {
gradlePluginPortal() google {
google() content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral() mavenCentral()
gradlePluginPortal()
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
@@ -12,5 +18,6 @@ dependencyResolutionManagement {
mavenCentral() mavenCentral()
} }
} }
rootProject.name = "MyStreamingApp" rootProject.name = "MyStreamingApp"
include ':app' include(":app")