Compare commits
15 Commits
3b8c52d8b8
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 707e1295c5 | |||
| ca26daa3ee | |||
| fd712c54b8 | |||
| 2ed0cb1a52 | |||
| 1f136f9aea | |||
|
|
5740e3f22b | ||
|
|
529356a052 | ||
|
|
344f270a06 | ||
|
|
a50391f588 | ||
|
|
4e4e34ddbf | ||
|
|
5a0104a72e | ||
|
|
6b9facb5a8 | ||
|
|
8c7991ef4f | ||
|
|
35a467061f | ||
|
|
da6e0f033f |
2
.github/workflows/build.yml
vendored
@@ -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: |
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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", "") ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
10
app/src/main/res/drawable/ic_menu.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
9
app/src/main/res/layout/settings_activity.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 870 B |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 478 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -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>
|
||||||
|
|||||||
28
app/src/main/res/values/arrays.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
33
app/src/main/res/xml/root_preferences.xml
Normal 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>
|
||||||
12
build.gradle
@@ -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
@@ -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
|
||||||
|
}
|
||||||
3
fastlane/metadata/android/en-US/full_description.txt
Normal 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.
|
||||||
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Minimal RTMP streaming app.
|
||||||
1
fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PicoStreamingApp
|
||||||
@@ -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
@@ -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" }
|
||||||
|
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||