删除未使用的相机和记录类来重构记录功能;更新pubspec中的依赖项。Yaml为本地插件路径;简化MainActivity和相关类,以提高性能和可维护性。
This commit is contained in:
@@ -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>
|
||||
+54
@@ -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
|
||||
}
|
||||
}
|
||||
+274
@@ -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)
|
||||
}
|
||||
}
|
||||
+341
@@ -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"
|
||||
}
|
||||
}
|
||||
+164
@@ -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() {}
|
||||
}
|
||||
}
|
||||
+41
@@ -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()
|
||||
}
|
||||
|
||||
+52
@@ -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>
|
||||
Reference in New Issue
Block a user