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
run: ./gradlew assembleDebug
- name: Upload APKs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: apks
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.
2. Clone your new repository.
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.

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.CAMERA" />
<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
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.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 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

@@ -21,4 +21,14 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
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
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
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

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