Compare commits

...

15 Commits

Author SHA1 Message Date
pie
707e1295c5 Fix extra signing block problem on Fdroid
Some checks failed
Build / build (push) Has been cancelled
2026-04-29 23:07:37 +02:00
pie
ca26daa3ee Added minimal metadata
Some checks failed
Build / build (push) Has been cancelled
2026-04-29 16:39:07 +02:00
pie
fd712c54b8 Renamed main project
Some checks failed
Build / build (push) Has been cancelled
2026-04-29 15:11:23 +02:00
pie
2ed0cb1a52 Changed app id
Some checks failed
Build / build (push) Has been cancelled
2026-04-28 15:34:49 +02:00
pie
1f136f9aea Terrible settings to make the amazing streampack boilerplate code usable.
Some checks failed
Build / build (push) Has been cancelled
2026-04-15 21:12:26 +02:00
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
66 changed files with 803 additions and 596 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'
}

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

@@ -0,0 +1,73 @@
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)
}
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
defaultConfig {
applicationId = "org.esiliati.repo.picostreaming.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)
implementation(libs.preference)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}

View File

@@ -1,21 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<!-- For play store --> <!-- For play store -->
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="true" /> android:required="true" />
<uses-feature <uses-feature
android:name="android.hardware.camera.autofocus" android:name="android.hardware.camera.autofocus"
android:required="false" /> android:required="false" /> <!-- Required permissions -->
<!-- Required permissions -->
<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.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -23,12 +19,13 @@
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.MyStreamingApp" android:theme="@style/Theme.PicoStreamingApp"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -40,6 +37,15 @@
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="" /> android:value="" />
</activity> </activity>
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".MainActivity" >
<!-- Parent activity meta-data to support 4.0 and lower -->
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
</application> </application>
</manifest> </manifest>

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

@@ -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,227 @@
package io.github.thibaultbee.streampack.app
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.util.Log
import android.view.WindowManager.LayoutParams
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.app.utils.PermissionsManager
import io.github.thibaultbee.streampack.app.utils.showDialog
import io.github.thibaultbee.streampack.app.utils.toast
import io.github.thibaultbee.streampack.core.elements.sources.video.camera.extensions.defaultCameraId
import io.github.thibaultbee.streampack.core.streamers.lifecycle.StreamerActivityLifeCycleObserver
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() {
val getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult())
{
val streamResolution = SettingsActivity.getResolution(applicationContext)
val (w, h) = streamResolution.split("|").map { it.toInt() }.let { it[0] to it[1] }
val streamBitrate = SettingsActivity.getBitrate(applicationContext)
lifecycleScope.launch {
viewModel.setVideoConfig(w, h, streamBitrate.toInt())
}
}
binding.settingsButton.setOnClickListener { view ->
val intent: Intent = Intent(this, SettingsActivity::class.java)
getContent.launch(intent)
}
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 {
val streamUrl = SettingsActivity.getServer(applicationContext)
// }
lifecycleScope.launch {
try {
window.addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
viewModel.startStream(streamUrl)
} catch (e: Exception) {
binding.liveButton.isChecked = false
Log.e(TAG, "Failed to connect", e)
toast("Connection failed: ${e.message}")
}
}
} else {
lifecycleScope.launch {
window.clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
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,188 @@
package io.github.thibaultbee.streampack.app
import android.app.AlertDialog
import android.Manifest
import android.content.DialogInterface
import android.media.AudioFormat
import android.media.MediaFormat
import android.text.InputType
import android.util.Log
import android.util.Size
import android.widget.EditText
import androidx.annotation.RequiresPermission
import androidx.core.content.ContentProviderCompat.requireContext
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.configuration.mediadescriptor.MediaDescriptor
import io.github.thibaultbee.streampack.core.elements.sources.audio.audiorecord.MicrophoneSourceFactory
import io.github.thibaultbee.streampack.core.elements.sources.video.camera.ICameraSource
import io.github.thibaultbee.streampack.core.interfaces.setCameraId
import io.github.thibaultbee.streampack.core.interfaces.startStream
import io.github.thibaultbee.streampack.core.pipelines.inputs.IVideoInput
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.ext.rtmp.configuration.mediadescriptor.RtmpMediaDescriptor
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(streamUrl: String) {
_isTryingConnectionLiveData.postValue(true)
try {
/**
* For SRT, use srt://my.server.url:9998?streamid=myStreamId&passphrase=myPassphrase
*/
streamer.startStream(streamUrl)
} 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 video configuration.
*
* You can verify the device supported configuration with [SingleStreamer.getInfo].
*/
suspend fun setVideoConfig(w: Int, h: Int,fps: Int) {
/**
* 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(w, h), fps = fps
)
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

@@ -0,0 +1,42 @@
package io.github.thibaultbee.streampack.app
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.setDefaultValues(this, R.xml.root_preferences, false);
setContentView(R.layout.settings_activity)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, SettingsFragment())
.commit()
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
}
}
companion object {
fun getServer(context: Context): String =
PreferenceManager.getDefaultSharedPreferences(context).getString("pref_server", "") ?: ""
fun getResolution(context: Context): String =
PreferenceManager.getDefaultSharedPreferences(context).getString("pref_resolution", "") ?: ""
fun getBitrate(context: Context): String =
PreferenceManager.getDefaultSharedPreferences(context).getString("pref_bitrate", "") ?: ""
}
}

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,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</vector>

View File

@@ -15,6 +15,21 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/settingsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:layout_marginEnd="32dp"
android:background="@drawable/ic_menu"
android:contentDescription="Settings"
android:textColor="@color/white"
android:textOff=""
android:textOn=""
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
<ToggleButton <ToggleButton
android:id="@+id/liveButton" android:id="@+id/liveButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -0,0 +1,9 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon> </adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.MyStreamingApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.PicoStreamingApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item> <item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorPrimaryVariant">@color/purple_700</item>

View File

@@ -0,0 +1,28 @@
<resources>
<string-array name="resolution_entries">
<item>480px360p</item>
<item>720px480p</item>
<item>1080px720p</item>
</string-array>
<string-array name="resolution_values">
<item>480|360</item>
<item>720|480</item>
<item>1080|720</item>
</string-array>
<array name="default_resolution">
<item>1080|720</item>
</array>
<string-array name="bitrate_entries">
<item>15 fps</item>
<item>25 fps</item>
<item>50 fps</item>
</string-array>
<string-array name="bitrate_values">
<item>15</item>
<item>25</item>
<item>50</item>
</string-array>
<array name="default_bitrate">
<item>25</item>
</array>
</resources>

View File

@@ -1,5 +1,11 @@
<resources> <resources>
<string name="app_name">My Streaming App</string> <string name="app_name">Pico Streaming App</string>
<string name="denied">Denied</string> <string name="denied">Denied</string>
<string name="accept">Accept</string> <string name="accept">Accept</string>
<string name="title_activity_settings">Settings</string>
<!-- Preference Titles -->
<string name="messages_header">Server</string>
<string name="sync_header">Sync</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.MyStreamingApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.PicoStreamingApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item> <item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorPrimaryVariant">@color/purple_700</item>

View File

@@ -0,0 +1,33 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/messages_header">
<EditTextPreference
app:key="pref_server"
app:title="Server"
app:summary="srt://host:port or rtsp://..."
app:useSimpleSummaryProvider="true"
app:defaultValue=""/>
</PreferenceCategory>
<PreferenceCategory app:title="Video">
<ListPreference
app:key="pref_resolution"
app:title="Resolution"
app:summary="Set video acquisition resolution"
app:useSimpleSummaryProvider="true"
app:entries="@array/resolution_entries"
app:entryValues="@array/resolution_values"
app:defaultValue="@array/default_resolution" />
<ListPreference
app:key="pref_bitrate"
app:title="Bitrate"
app:summary="Video bitrate"
app:useSimpleSummaryProvider="true"
app:entries="@array/bitrate_entries"
app:entryValues="@array/bitrate_values"
app:defaultValue="@array/default_bitrate"/>
</PreferenceCategory>
</PreferenceScreen>

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

@@ -0,0 +1,3 @@
Minimal RTMP streaming app based on StreamPack-boilerplate.
It really just adds a bunch of options to make it flexible enough to stream from an Android phone.

View File

@@ -0,0 +1 @@
Minimal RTMP streaming app.

View File

@@ -0,0 +1 @@
PicoStreamingApp

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

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

@@ -0,0 +1,33 @@
[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"
preference = "1.2.0"
[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" }
preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
[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,16 +0,0 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MyStreamingApp"
include ':app'

23
settings.gradle.kts Normal file
View File

@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "PicoStreamingApp"
include(":app")