删除未使用的相机和记录类来重构记录功能;更新pubspec中的依赖项。Yaml为本地插件路径;简化MainActivity和相关类,以提高性能和可维护性。

This commit is contained in:
2026-07-09 16:17:34 +08:00
parent 75c42ea168
commit 6138eddc16
65 changed files with 3677 additions and 2672 deletions
@@ -0,0 +1,62 @@
group 'video.api.flutter.livestream'
version '1.0-SNAPSHOT'
buildscript {
ext {
kotlin_version = '1.9.22'
streamPackVersion = '2.6.0'
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdk 34
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 21
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
namespace "video.api.flutter.livestream"
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation "io.github.thibaultbee:streampack:$streamPackVersion"
implementation "io.github.thibaultbee:streampack-extension-rtmp:$streamPackVersion"
}
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
@@ -0,0 +1 @@
rootProject.name = 'apivideo_live_stream'
@@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.camera"
android:required="true"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
@@ -0,0 +1,54 @@
package video.api.flutter.livestream
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
/** ApiVideoLiveStreamPlugin */
class ApiVideoLiveStreamPlugin : FlutterPlugin, ActivityAware {
private var permissionsManager: PermissionsManager? = null
private var methodCallHandlerImpl: MethodCallHandlerImpl? = null
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
permissionsManager = PermissionsManager(binding.applicationContext).apply {
methodCallHandlerImpl = MethodCallHandlerImpl(
binding.applicationContext,
binding.binaryMessenger,
this,
binding.textureRegistry
).apply {
startListening()
}
}
}
override fun onDetachedFromEngine(binding: FlutterPluginBinding) {
methodCallHandlerImpl?.stopListening()
methodCallHandlerImpl = null
permissionsManager = null
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
val activity = binding.activity
permissionsManager?.let {
it.activity = activity
binding.addRequestPermissionsResultListener(it)
}
}
override fun onDetachedFromActivityForConfigChanges() {
permissionsManager?.activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
permissionsManager?.let {
it.activity = null
binding.addRequestPermissionsResultListener(it)
}
}
override fun onDetachedFromActivity() {
permissionsManager?.activity = null
}
}
@@ -0,0 +1,274 @@
package video.api.flutter.livestream
import android.Manifest
import android.content.Context
import android.util.Size
import android.view.Surface
import io.flutter.view.TextureRegistry
import io.github.thibaultbee.streampack.data.AudioConfig
import io.github.thibaultbee.streampack.data.VideoConfig
import io.github.thibaultbee.streampack.error.StreamPackError
import io.github.thibaultbee.streampack.ext.rtmp.streamers.CameraRtmpLiveStreamer
import io.github.thibaultbee.streampack.listeners.OnConnectionListener
import io.github.thibaultbee.streampack.listeners.OnErrorListener
import io.github.thibaultbee.streampack.utils.backCameraList
import io.github.thibaultbee.streampack.utils.externalCameraList
import io.github.thibaultbee.streampack.utils.frontCameraList
import io.github.thibaultbee.streampack.utils.isBackCamera
import io.github.thibaultbee.streampack.utils.isExternalCamera
import io.github.thibaultbee.streampack.utils.isFrontCamera
import kotlinx.coroutines.runBlocking
class FlutterLiveStreamView(
private val context: Context,
textureRegistry: TextureRegistry,
private val permissionsManager: PermissionsManager,
private val onConnectionSucceeded: () -> Unit,
private val onDisconnected: () -> Unit,
private val onConnectionFailed: (String) -> Unit,
private val onGenericError: (Exception) -> Unit,
private val onVideoSizeChanged: (Size) -> Unit,
) :
OnConnectionListener, OnErrorListener {
private val flutterTexture = textureRegistry.createSurfaceTexture()
val textureId: Long
get() = flutterTexture.id()
private val streamer = CameraRtmpLiveStreamer(
context = context,
initialOnConnectionListener = this,
initialOnErrorListener = this
)
private var _isPreviewing = false
private var _isStreaming = false
val isStreaming: Boolean
get() = _isStreaming
private var _videoConfig: VideoConfig? = null
val videoConfig: VideoConfig
get() = _videoConfig!!
fun setVideoConfig(
videoConfig: VideoConfig,
onSuccess: () -> Unit,
onError: (Exception) -> Unit
) {
if (isStreaming) {
throw UnsupportedOperationException("You have to stop streaming first")
}
onVideoSizeChanged(videoConfig.resolution)
val wasPreviewing = _isPreviewing
if (wasPreviewing) {
stopPreview()
}
streamer.configure(videoConfig)
_videoConfig = videoConfig
if (wasPreviewing) {
startPreview(onSuccess, onError)
} else {
onSuccess()
}
}
private var _audioConfig: AudioConfig? = null
val audioConfig: AudioConfig
get() = _audioConfig!!
fun setAudioConfig(
audioConfig: AudioConfig,
onSuccess: () -> Unit,
onError: (Exception) -> Unit
) {
if (isStreaming) {
throw UnsupportedOperationException("You have to stop streaming first")
}
permissionsManager.requestPermission(
Manifest.permission.RECORD_AUDIO,
onGranted = {
try {
streamer.configure(audioConfig)
_audioConfig = audioConfig
onSuccess()
} catch (e: Exception) {
onError(e)
}
},
onShowPermissionRationale = { _ ->
/**
* Require an AppCompat theme to use MaterialAlertDialogBuilder
*
context.showDialog(
R.string.permission_required,
R.string.record_audio_permission_required_message,
android.R.string.ok,
onPositiveButtonClick = { onRequiredPermissionLastTime() }
) */
onError(SecurityException("Missing permission Manifest.permission.RECORD_AUDIO"))
},
onDenied = {
onError(SecurityException("Missing permission Manifest.permission.RECORD_AUDIO"))
})
}
var isMuted: Boolean
get() = streamer.settings.audio.isMuted
set(value) {
streamer.settings.audio.isMuted = value
}
val camera: String
get() = streamer.camera
fun setCamera(camera: String, onSuccess: () -> Unit, onError: (Exception) -> Unit) {
permissionsManager.requestPermission(
Manifest.permission.CAMERA,
onGranted = {
try {
streamer.camera = camera
onSuccess()
} catch (e: Exception) {
onError(e)
}
},
onShowPermissionRationale = { _ ->
/**
* Require an AppCompat theme to use MaterialAlertDialogBuilder
*
* context.showDialog(
R.string.permission_required,
R.string.camera_permission_required_message,
android.R.string.ok,
onPositiveButtonClick = { onRequiredPermissionLastTime() }
)*/
onError(SecurityException("Missing permission Manifest.permission.CAMERA"))
},
onDenied = {
onError(SecurityException("Missing permission Manifest.permission.CAMERA"))
})
}
fun setCameraId(cameraId: String, onSuccess: () -> Unit, onError: (Exception) -> Unit) {
setCamera(cameraId, onSuccess, onError)
}
val cameraPosition: String
get() = when {
context.isFrontCamera(streamer.camera) -> "front"
context.isBackCamera(streamer.camera) -> "back"
context.isExternalCamera(streamer.camera) -> "other"
else -> throw IllegalArgumentException("Invalid camera position for camera ${streamer.camera}")
}
fun setCameraPosition(position: String, onSuccess: () -> Unit, onError: (Exception) -> Unit) {
val cameraList = when (position) {
"front" -> context.frontCameraList
"back" -> context.backCameraList
"other" -> context.externalCameraList
else -> throw IllegalArgumentException("Invalid camera position: $position")
}
setCamera(cameraList.first(), onSuccess, onError)
}
fun dispose() {
stopStream()
streamer.stopPreview()
flutterTexture.release()
}
fun startStream(url: String) {
runBlocking {
streamer.connect(url)
try {
streamer.startStream()
_isStreaming = true
} catch (e: Exception) {
streamer.disconnect()
onLost("Failed to start stream: ${e.message}")
throw e
}
}
}
fun stopStream() {
val isConnected = streamer.isConnected
runBlocking {
streamer.stopStream()
streamer.disconnect()
if (isConnected) {
onDisconnected()
}
_isStreaming = false
}
}
fun startPreview(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
permissionsManager.requestPermission(
Manifest.permission.CAMERA,
onGranted = {
if (_videoConfig == null) {
onError(IllegalStateException("Video has not been configured!"))
} else {
try {
streamer.startPreview(getSurface(videoConfig.resolution))
_isPreviewing = true
onSuccess()
} catch (e: Exception) {
onError(e)
}
}
},
onShowPermissionRationale = { _ ->
/**
* Require an AppCompat theme to use MaterialAlertDialogBuilder
*
* context.showDialog(
R.string.permission_required,
R.string.camera_permission_required_message,
android.R.string.ok,
onPositiveButtonClick = { onRequiredPermissionLastTime() }
)*/
onError(SecurityException("Missing permission Manifest.permission.CAMERA"))
},
onDenied = {
onError(SecurityException("Missing permission Manifest.permission.CAMERA"))
})
}
fun stopPreview() {
streamer.stopPreview()
_isPreviewing = false
}
private fun getSurface(resolution: Size): Surface {
val surfaceTexture = flutterTexture.surfaceTexture().apply {
setDefaultBufferSize(
resolution.width,
resolution.height
)
}
return Surface(surfaceTexture)
}
override fun onSuccess() {
onConnectionSucceeded()
}
override fun onLost(message: String) {
onDisconnected()
}
override fun onFailed(message: String) {
onConnectionFailed(message)
}
override fun onError(error: StreamPackError) {
_isStreaming = false
onGenericError(error)
}
}
@@ -0,0 +1,341 @@
package video.api.flutter.livestream
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Handler
import android.os.Looper
import android.util.Size
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.view.TextureRegistry
import video.api.flutter.livestream.utils.addTrailingSlashIfNeeded
import video.api.flutter.livestream.utils.toAudioConfig
import video.api.flutter.livestream.utils.toVideoConfig
import kotlin.math.atan
class MethodCallHandlerImpl(
private val context: Context,
messenger: BinaryMessenger,
private val permissionsManager: PermissionsManager,
private val textureRegistry: TextureRegistry
) : MethodChannel.MethodCallHandler {
private val methodChannel = MethodChannel(messenger, METHOD_CHANNEL_NAME)
private val eventChannel = EventChannel(messenger, EVENT_CHANNEL_NAME)
private var eventSink: EventChannel.EventSink? = null
private var flutterView: FlutterLiveStreamView? = null
fun startListening() {
methodChannel.setMethodCallHandler(this)
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink?.endOfStream()
eventSink = null
}
})
}
fun stopListening() {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"create" -> {
try {
flutterView?.dispose()
flutterView = FlutterLiveStreamView(
context,
textureRegistry,
permissionsManager,
{ sendConnected() },
{ sendDisconnected() },
{ sendConnectionFailed(it) },
{ sendError(it) },
{ sendVideoSizeChanged(it) }
)
result.success(mapOf("textureId" to flutterView!!.textureId))
} catch (e: Exception) {
result.error("failed_to_create_live_stream", e.message, null)
}
}
"dispose" -> {
flutterView?.dispose()
flutterView = null
}
"setVideoConfig" -> {
try {
@Suppress("UNCHECKED_CAST")
val videoConfig = (call.arguments as Map<String, Any>).toVideoConfig()
flutterView!!.setVideoConfig(
videoConfig,
{ result.success(null) },
{
result.error(
"failed_to_set_video_config",
it.message,
null
)
})
} catch (e: Exception) {
result.error("failed_to_set_video_config", e.message, null)
}
}
"setAudioConfig" -> {
try {
@Suppress("UNCHECKED_CAST")
val audioConfig = (call.arguments as Map<String, Any>).toAudioConfig()
flutterView!!.setAudioConfig(
audioConfig,
{ result.success(null) },
{
result.error(
"failed_to_set_audio_config",
it.message,
null
)
})
} catch (e: Exception) {
result.error("failed_to_set_audio_config", e.message, null)
}
}
"startPreview" -> {
try {
flutterView!!.startPreview(
{ result.success(null) },
{
result.error(
"failed_to_start_preview",
it.message,
null
)
})
} catch (e: Exception) {
result.error("failed_to_start_preview", e.message, null)
}
}
"stopPreview" -> {
flutterView?.stopPreview()
result.success(null)
}
"startStreaming" -> {
val streamKey = call.argument<String>("streamKey")
val url = call.argument<String>("url")
when {
streamKey == null -> result.error(
"missing_stream_key", "Stream key is missing", null
)
streamKey.isEmpty() -> result.error(
"empty_stream_key", "Stream key is empty", null
)
url == null -> result.error(
"missing_rtmp_url",
"RTMP URL is missing",
null
)
url.isEmpty() -> result.error("empty_rtmp_url", "RTMP URL is empty", null)
else ->
try {
flutterView!!.startStream(url.addTrailingSlashIfNeeded() + streamKey)
result.success(null)
} catch (e: Exception) {
result.error("failed_to_start_stream", e.message, null)
}
}
}
"stopStreaming" -> {
flutterView?.stopStream()
result.success(null)
}
"getIsStreaming" -> result.success(mapOf("isStreaming" to flutterView!!.isStreaming))
"getCameraPosition" -> {
try {
result.success(mapOf("position" to flutterView!!.cameraPosition))
} catch (e: Exception) {
result.error("failed_to_get_camera_position", e.message, null)
}
}
"setCameraPosition" -> {
val cameraPosition = try {
((call.arguments as Map<*, *>)["position"] as String)
} catch (e: Exception) {
result.error("invalid_parameter", "Invalid camera position", e)
return
}
try {
flutterView!!.setCameraPosition(cameraPosition,
{ result.success(null) },
{
result.error(
"failed_to_set_camera_position",
it.message,
null
)
})
} catch (e: Exception) {
result.error("failed_to_set_camera_position", e.message, null)
}
}
"getBackCameras" -> {
try {
result.success(getBackCameras())
} catch (e: Exception) {
result.error("failed_to_get_back_cameras", e.message, null)
}
}
"setCameraId" -> {
val cameraId = try {
((call.arguments as Map<*, *>)["cameraId"] as String)
} catch (e: Exception) {
result.error("invalid_parameter", "Invalid cameraId", e)
return
}
try {
flutterView!!.setCameraId(cameraId,
{ result.success(null) },
{
result.error(
"failed_to_set_camera_id",
it.message,
null
)
})
} catch (e: Exception) {
result.error("failed_to_set_camera_id", e.message, null)
}
}
"getIsMuted" -> {
try {
result.success(mapOf("isMuted" to flutterView!!.isMuted))
} catch (e: Exception) {
result.error("failed_to_get_is_muted", e.message, null)
}
}
"setIsMuted" -> {
val isMuted = try {
((call.arguments as Map<*, *>)["isMuted"] as Boolean)
} catch (e: Exception) {
result.error("invalid_parameter", "Invalid isMuted", e)
return
}
try {
flutterView!!.isMuted = isMuted
result.success(null)
} catch (e: Exception) {
result.error("failed_to_set_is_muted", e.message, null)
}
}
"getVideoSize" -> {
try {
val videoSize = flutterView!!.videoConfig.resolution
result.success(
mapOf(
"width" to videoSize.width.toDouble(),
"height" to videoSize.height.toDouble()
)
)
} catch (e: Exception) {
result.error("failed_to_get_video_size", e.message, null)
}
}
else -> result.notImplemented()
}
}
private fun sendEvent(type: String) {
Handler(Looper.getMainLooper()).post {
eventSink?.success(mapOf("type" to type))
}
}
private fun sendConnected() {
sendEvent("connected")
}
private fun sendDisconnected() {
sendEvent("disconnected")
}
private fun sendConnectionFailed(message: String) {
Handler(Looper.getMainLooper()).post {
eventSink?.success(mapOf("type" to "connectionFailed", "message" to message))
}
}
private fun sendError(error: Exception) {
Handler(Looper.getMainLooper()).post {
eventSink?.error(error::class.java.name, error.message, error)
}
}
private fun sendVideoSizeChanged(resolution: Size) {
Handler(Looper.getMainLooper()).post {
eventSink?.success(
mapOf(
"type" to "videoSizeChanged",
"width" to resolution.width.toDouble(),
"height" to resolution.height.toDouble() // Dart size fields are in double
)
)
}
}
private fun getBackCameras(): List<Map<String, Any>> {
val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
return manager.cameraIdList.mapNotNull { cameraId ->
val characteristics = manager.getCameraCharacteristics(cameraId)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
if (facing != CameraCharacteristics.LENS_FACING_BACK) {
return@mapNotNull null
}
val focalLengths =
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
?: return@mapNotNull null
val sensorSize =
characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)
?: return@mapNotNull null
val minFocalLength = focalLengths.minOrNull() ?: return@mapNotNull null
val horizontalFov =
2.0 * atan((sensorSize.width / (2.0f * minFocalLength)).toDouble())
mapOf(
"cameraId" to cameraId,
"minFocalLength" to minFocalLength.toDouble(),
"sensorWidth" to sensorSize.width.toDouble(),
"sensorHeight" to sensorSize.height.toDouble(),
"horizontalFov" to horizontalFov,
)
}
}
companion object {
private const val METHOD_CHANNEL_NAME = "video.api.livestream/controller"
private const val EVENT_CHANNEL_NAME = "video.api.livestream/events"
}
}
@@ -0,0 +1,164 @@
package video.api.flutter.livestream
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.plugin.common.PluginRegistry
/**
* Check if the app has the given permissions.
* For a single permission or multiple permissions.
*/
class PermissionsManager(
private val context: Context,
) : PluginRegistry.RequestPermissionsResultListener {
private var uniqueRequestCode = 1
// To request permission, we need the activity
var activity: Activity? = null
private val listeners = mutableMapOf<Int, IListener>()
private fun hasPermission(permission: String) =
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
private fun hasAllPermissions(permissions: List<String>) = permissions.all { permission ->
ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
private fun shouldShowRequestPermissionRationale(
activity: Activity,
permissions: List<String>
) =
permissions.filter { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
fun requestPermissions(
permissions: List<String>,
onAllGranted: () -> Unit,
onShowPermissionRationale: (List<String>, () -> Unit) -> Unit,
onAtLeastOnePermissionDenied: () -> Unit
) {
activity?.let {
requestPermissions(it, permissions, object : IListener {
override fun onAllGranted() {
onAllGranted()
}
override fun onShowPermissionRationale(
permissions: List<String>,
onRequiredPermissionLastTime: () -> Unit
) {
onShowPermissionRationale(permissions, onRequiredPermissionLastTime)
}
override fun onAtLeastOnePermissionDenied() {
onAtLeastOnePermissionDenied()
}
})
} ?: throw IllegalStateException("Missing Activity")
}
private fun requestPermissions(
activity: Activity,
permissions: List<String>,
listener: IListener
) {
val currentRequestCode = synchronized(this) {
uniqueRequestCode++
}
listeners[currentRequestCode] = listener
when {
hasAllPermissions(permissions) -> listener.onAllGranted()
shouldShowRequestPermissionRationale(activity, permissions).isNotEmpty() -> {
val missingPermissions = shouldShowRequestPermissionRationale(activity, permissions)
listener.onShowPermissionRationale(missingPermissions) {
ActivityCompat.requestPermissions(
activity,
missingPermissions.toTypedArray(),
currentRequestCode
)
}
}
else -> ActivityCompat.requestPermissions(
activity,
permissions.toTypedArray(),
currentRequestCode
)
}
}
fun requestPermission(
permission: String,
onGranted: () -> Unit,
onShowPermissionRationale: (() -> Unit) -> Unit,
onDenied: () -> Unit
) {
activity?.let {
requestPermissions(it, listOf(permission), object : IListener {
override fun onAllGranted() {
onGranted()
}
override fun onShowPermissionRationale(
permissions: List<String>,
onRequiredPermissionLastTime: () -> Unit
) {
onShowPermissionRationale(onRequiredPermissionLastTime)
}
override fun onAtLeastOnePermissionDenied() {
onDenied()
}
})
} ?: throw IllegalStateException("Missing Activity")
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
): Boolean {
val listener = listeners[requestCode] ?: return false
listeners.remove(requestCode)
if (grantResults.isEmpty()) {
return false
}
grantResults.forEach {
if (it == PackageManager.PERMISSION_GRANTED) {
listener.onGranted(permissions[grantResults.indexOf(it)])
} else {
listener.onDenied(permissions[grantResults.indexOf(it)])
}
}
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
listener.onAllGranted()
} else {
listener.onAtLeastOnePermissionDenied()
}
return listeners.isEmpty()
}
interface IListener {
fun onAllGranted() {}
fun onGranted(permission: String) {}
fun onShowPermissionRationale(
permissions: List<String>,
onRequiredPermissionLastTime: () -> Unit
) {
}
fun onDenied(permission: String) {}
fun onAtLeastOnePermissionDenied() {}
}
}
@@ -0,0 +1,41 @@
package video.api.flutter.livestream.utils
import android.content.Context
import android.content.DialogInterface
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
/**
* Show a dialog with the given title and message.
*/
fun Context.showDialog(
@StringRes title: Int,
@StringRes message: Int = 0,
@StringRes
positiveButtonText: Int = android.R.string.ok,
@StringRes
negativeButtonText: Int = 0,
onPositiveButtonClick: () -> Unit = {},
onNegativeButtonClick: () -> Unit = {}
) {
AlertDialog.Builder(this)
.setTitle(title)
.setMessage(message)
.apply {
if (positiveButtonText != 0) {
setPositiveButton(positiveButtonText) { dialogInterface: DialogInterface, _: Int ->
dialogInterface.dismiss()
onPositiveButtonClick()
}
}
if (negativeButtonText != 0) {
setNegativeButton(negativeButtonText) { dialogInterface: DialogInterface, _: Int ->
dialogInterface.dismiss()
onNegativeButtonClick()
}
}
}
.show()
}
@@ -0,0 +1,52 @@
package video.api.flutter.livestream.utils
import android.util.Size
import io.github.thibaultbee.streampack.data.AudioConfig
import io.github.thibaultbee.streampack.data.VideoConfig
fun Map<String, Any>.toVideoConfig(): VideoConfig {
return VideoConfig(
startBitrate = this["bitrate"] as Int,
resolution = (this["resolution"] as String).toResolution(),
fps = this["fps"] as Int
)
}
fun Map<String, Any>.toAudioConfig(): AudioConfig {
return AudioConfig(
startBitrate = this["bitrate"] as Int,
sampleRate = this["sampleRate"] as Int,
channelConfig = AudioConfig.getChannelConfig(
if (this["channel"] == "stereo") {
2
} else {
1
}
),
enableNoiseSuppressor = this["enableNoiseSuppressor"] as Boolean,
enableEchoCanceler = this["enableEchoCanceler"] as Boolean
)
}
fun String.toResolution(): Size {
return when (this) {
"240p" -> Size(426, 240)
"360p" -> Size(640, 360)
"480p" -> Size(854, 480)
"720p" -> Size(1280, 720)
"1080p" -> Size(1920, 1080)
else -> throw IllegalArgumentException("Unknown resolution: $this")
}
}
/**
* Add a slash at the end of a [String] only if it is missing.
*
* @return the given string with a trailing slash.
*/
fun String.addTrailingSlashIfNeeded(): String {
return if (this.endsWith("/")) this else "$this/"
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="permission_required">Permission required</string>
<string name="record_audio_permission_required_message">You have to grant the record audio permission to stream.</string>
<string name="camera_permission_required_message">You have to grant the camera permission to stream.</string>
</resources>