mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-12-12 02:09:12 +08:00
improve android server performance
This commit is contained in:
parent
1f4610a3d0
commit
d054da3404
@ -17,7 +17,6 @@ import android.graphics.PixelFormat
|
|||||||
import android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
|
import android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
|
||||||
import android.hardware.display.VirtualDisplay
|
import android.hardware.display.VirtualDisplay
|
||||||
import android.media.*
|
import android.media.*
|
||||||
import android.media.AudioRecord.READ_BLOCKING
|
|
||||||
import android.media.projection.MediaProjection
|
import android.media.projection.MediaProjection
|
||||||
import android.media.projection.MediaProjectionManager
|
import android.media.projection.MediaProjectionManager
|
||||||
import android.os.*
|
import android.os.*
|
||||||
@ -33,6 +32,7 @@ import java.util.concurrent.Executors
|
|||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
const val EXTRA_MP_DATA = "mp_intent"
|
const val EXTRA_MP_DATA = "mp_intent"
|
||||||
const val INIT_SERVICE = "init_service"
|
const val INIT_SERVICE = "init_service"
|
||||||
@ -65,33 +65,6 @@ class MainService : Service() {
|
|||||||
System.loadLibrary("rustdesk")
|
System.loadLibrary("rustdesk")
|
||||||
}
|
}
|
||||||
|
|
||||||
// rust call jvm
|
|
||||||
@Keep
|
|
||||||
fun rustGetVideoRaw(): ByteArray {
|
|
||||||
return if (videoData != null) {
|
|
||||||
videoData!!
|
|
||||||
} else {
|
|
||||||
videoZeroData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
fun rustGetAudioRaw(): FloatArray {
|
|
||||||
return if (isNewData && audioData != null) {
|
|
||||||
isNewData = false
|
|
||||||
audioData!!
|
|
||||||
} else {
|
|
||||||
audioZeroData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
fun rustGetAudioRawLen(): Int {
|
|
||||||
return if (isNewData && audioData != null && audioData!!.isNotEmpty()) {
|
|
||||||
audioData!!.size
|
|
||||||
} else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
fun rustGetByName(name: String): String {
|
fun rustGetByName(name: String): String {
|
||||||
return when (name) {
|
return when (name) {
|
||||||
@ -109,13 +82,13 @@ class MainService : Service() {
|
|||||||
val id = jsonObject["id"] as Int
|
val id = jsonObject["id"] as Int
|
||||||
val username = jsonObject["name"] as String
|
val username = jsonObject["name"] as String
|
||||||
val peerId = jsonObject["peer_id"] as String
|
val peerId = jsonObject["peer_id"] as String
|
||||||
val type = if (jsonObject["is_file_transfer"] as Boolean){
|
val type = if (jsonObject["is_file_transfer"] as Boolean) {
|
||||||
translate("File Connection")
|
translate("File Connection")
|
||||||
}else{
|
} else {
|
||||||
translate("Screen Connection")
|
translate("Screen Connection")
|
||||||
}
|
}
|
||||||
loginRequestNotification(id,type,username,peerId)
|
loginRequestNotification(id, type, username, peerId)
|
||||||
}catch (e:JSONException){
|
} catch (e: JSONException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,16 +100,16 @@ class MainService : Service() {
|
|||||||
val username = jsonObject["name"] as String
|
val username = jsonObject["name"] as String
|
||||||
val peerId = jsonObject["peer_id"] as String
|
val peerId = jsonObject["peer_id"] as String
|
||||||
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
|
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
|
||||||
val type = if (isFileTransfer){
|
val type = if (isFileTransfer) {
|
||||||
translate("File Connection")
|
translate("File Connection")
|
||||||
}else{
|
} else {
|
||||||
translate("Screen Connection")
|
translate("Screen Connection")
|
||||||
}
|
}
|
||||||
if(!isFileTransfer && !isStart){
|
if (!isFileTransfer && !isStart) {
|
||||||
startCapture()
|
startCapture()
|
||||||
}
|
}
|
||||||
onClientAuthorizedNotification(id,type,username,peerId)
|
onClientAuthorizedNotification(id, type, username, peerId)
|
||||||
}catch (e:JSONException){
|
} catch (e: JSONException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,19 +118,22 @@ class MainService : Service() {
|
|||||||
Log.d(logTag, "from rust:stop_capture")
|
Log.d(logTag, "from rust:stop_capture")
|
||||||
stopCapture()
|
stopCapture()
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// jvm call rust
|
// jvm call rust
|
||||||
private external fun init(ctx: Context)
|
private external fun init(ctx: Context)
|
||||||
private external fun startServer()
|
private external fun startServer()
|
||||||
private external fun translateLocale(localeName:String,input: String) : String
|
private external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||||
|
private external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||||
|
private external fun translateLocale(localeName: String, input: String): String
|
||||||
// private external fun sendVp9(data: ByteArray)
|
// private external fun sendVp9(data: ByteArray)
|
||||||
|
|
||||||
private fun translate(input:String):String{
|
private fun translate(input: String): String {
|
||||||
Log.d(logTag,"translate:$LOCAL_NAME")
|
Log.d(logTag, "translate:$LOCAL_NAME")
|
||||||
return translateLocale(LOCAL_NAME,input)
|
return translateLocale(LOCAL_NAME, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logTag = "LOG_SERVICE"
|
private val logTag = "LOG_SERVICE"
|
||||||
@ -170,21 +146,18 @@ class MainService : Service() {
|
|||||||
val isStart: Boolean
|
val isStart: Boolean
|
||||||
get() = _isStart
|
get() = _isStart
|
||||||
|
|
||||||
|
// video
|
||||||
private var mediaProjection: MediaProjection? = null
|
private var mediaProjection: MediaProjection? = null
|
||||||
private var surface: Surface? = null
|
private var surface: Surface? = null
|
||||||
private val sendVP9Thread = Executors.newSingleThreadExecutor()
|
private val sendVP9Thread = Executors.newSingleThreadExecutor()
|
||||||
private var videoEncoder: MediaCodec? = null
|
private var videoEncoder: MediaCodec? = null
|
||||||
private var videoData: ByteArray? = null
|
|
||||||
private var imageReader: ImageReader? = null
|
private var imageReader: ImageReader? = null
|
||||||
private val videoZeroData = ByteArray(32)
|
|
||||||
private var virtualDisplay: VirtualDisplay? = null
|
private var virtualDisplay: VirtualDisplay? = null
|
||||||
|
|
||||||
// audio
|
// audio
|
||||||
private var audioRecorder: AudioRecord? = null
|
private var audioRecorder: AudioRecord? = null
|
||||||
private var audioData: FloatArray? = null
|
private var audioReader: AudioReader? = null
|
||||||
private var minBufferSize = 0
|
private var minBufferSize = 0
|
||||||
private var isNewData = false
|
|
||||||
private val audioZeroData: FloatArray = FloatArray(32)
|
|
||||||
private var audioRecordStat = false
|
private var audioRecordStat = false
|
||||||
|
|
||||||
// notification
|
// notification
|
||||||
@ -239,13 +212,13 @@ class MainService : Service() {
|
|||||||
// TODO
|
// TODO
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
Log.d(logTag,"ImageReader.newInstance:INFO:$INFO")
|
Log.d(logTag, "ImageReader.newInstance:INFO:$INFO")
|
||||||
imageReader =
|
imageReader =
|
||||||
ImageReader.newInstance(
|
ImageReader.newInstance(
|
||||||
INFO.screenWidth,
|
INFO.screenWidth,
|
||||||
INFO.screenHeight,
|
INFO.screenHeight,
|
||||||
PixelFormat.RGBA_8888,
|
PixelFormat.RGBA_8888,
|
||||||
2
|
4
|
||||||
).apply {
|
).apply {
|
||||||
setOnImageAvailableListener({ imageReader: ImageReader ->
|
setOnImageAvailableListener({ imageReader: ImageReader ->
|
||||||
try {
|
try {
|
||||||
@ -254,20 +227,10 @@ class MainService : Service() {
|
|||||||
val planes = image.planes
|
val planes = image.planes
|
||||||
val buffer = planes[0].buffer
|
val buffer = planes[0].buffer
|
||||||
buffer.rewind()
|
buffer.rewind()
|
||||||
// Be careful about OOM!
|
onVideoFrameUpdate(buffer)
|
||||||
if (videoData == null) {
|
|
||||||
videoData = ByteArray(buffer.capacity())
|
|
||||||
buffer.get(videoData!!)
|
|
||||||
Log.d(logTag, "init video ${videoData!!.size}")
|
|
||||||
} else {
|
|
||||||
buffer.get(videoData!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (ignored: java.lang.Exception) {
|
} catch (ignored: java.lang.Exception) {
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
imageReader.discardFreeBuffers()
|
|
||||||
}
|
|
||||||
}, null)
|
}, null)
|
||||||
}
|
}
|
||||||
Log.d(logTag, "ImageReader.setOnImageAvailableListener done")
|
Log.d(logTag, "ImageReader.setOnImageAvailableListener done")
|
||||||
@ -276,7 +239,7 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startCapture(): Boolean {
|
fun startCapture(): Boolean {
|
||||||
if (isStart){
|
if (isStart) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (mediaProjection == null) {
|
if (mediaProjection == null) {
|
||||||
@ -311,7 +274,6 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
virtualDisplay = null
|
virtualDisplay = null
|
||||||
videoEncoder = null
|
videoEncoder = null
|
||||||
videoData = null
|
|
||||||
|
|
||||||
// release audio
|
// release audio
|
||||||
audioRecordStat = false
|
audioRecordStat = false
|
||||||
@ -330,7 +292,7 @@ class MainService : Service() {
|
|||||||
|
|
||||||
mediaProjection = null
|
mediaProjection = null
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
stopService(Intent(this,InputService::class.java)) // close input service maybe not work
|
stopService(Intent(this, InputService::class.java)) // close input service maybe not work
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
@ -348,7 +310,7 @@ class MainService : Service() {
|
|||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
private fun startRawVideoRecorder(mp: MediaProjection) {
|
private fun startRawVideoRecorder(mp: MediaProjection) {
|
||||||
Log.d(logTag, "startRawVideoRecorder,screen info:$INFO")
|
Log.d(logTag, "startRawVideoRecorder,screen info:$INFO")
|
||||||
if(surface==null){
|
if (surface == null) {
|
||||||
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -370,7 +332,7 @@ class MainService : Service() {
|
|||||||
it.setCallback(cb)
|
it.setCallback(cb)
|
||||||
it.start()
|
it.start()
|
||||||
virtualDisplay = mp.createVirtualDisplay(
|
virtualDisplay = mp.createVirtualDisplay(
|
||||||
"rustdesk test",
|
"RustDeskVD",
|
||||||
INFO.screenWidth, INFO.screenHeight, 200, VIRTUAL_DISPLAY_FLAG_PUBLIC,
|
INFO.screenWidth, INFO.screenHeight, 200, VIRTUAL_DISPLAY_FLAG_PUBLIC,
|
||||||
surface, null, null
|
surface, null, null
|
||||||
)
|
)
|
||||||
@ -423,14 +385,13 @@ class MainService : Service() {
|
|||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun startAudioRecorder() {
|
private fun startAudioRecorder() {
|
||||||
checkAudioRecorder()
|
checkAudioRecorder()
|
||||||
if (audioData != null && audioRecorder != null && minBufferSize != 0) {
|
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
||||||
audioRecorder!!.startRecording()
|
audioRecorder!!.startRecording()
|
||||||
audioRecordStat = true
|
audioRecordStat = true
|
||||||
thread {
|
thread {
|
||||||
while (audioRecordStat) {
|
while (audioRecordStat) {
|
||||||
val res = audioRecorder!!.read(audioData!!, 0, minBufferSize, READ_BLOCKING)
|
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||||
if (res != AudioRecord.ERROR_INVALID_OPERATION) {
|
onAudioFrameUpdate(it)
|
||||||
isNewData = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(logTag, "Exit audio thread")
|
Log.d(logTag, "Exit audio thread")
|
||||||
@ -442,10 +403,11 @@ class MainService : Service() {
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun checkAudioRecorder() {
|
private fun checkAudioRecorder() {
|
||||||
if (audioData != null && audioRecorder != null && minBufferSize != 0) {
|
if (audioRecorder != null && audioRecorder != null && minBufferSize != 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
minBufferSize = 2 * AudioRecord.getMinBufferSize(
|
// read f32 to byte , length * 4
|
||||||
|
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||||
AUDIO_SAMPLE_RATE,
|
AUDIO_SAMPLE_RATE,
|
||||||
AUDIO_CHANNEL_MASK,
|
AUDIO_CHANNEL_MASK,
|
||||||
AUDIO_ENCODING
|
AUDIO_ENCODING
|
||||||
@ -454,8 +416,8 @@ class MainService : Service() {
|
|||||||
Log.d(logTag, "get min buffer size fail!")
|
Log.d(logTag, "get min buffer size fail!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
audioData = FloatArray(minBufferSize)
|
audioReader = AudioReader(minBufferSize, 4)
|
||||||
Log.d(logTag, "init audioData len:${audioData!!.size}")
|
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
mediaProjection?.let {
|
mediaProjection?.let {
|
||||||
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
||||||
@ -536,7 +498,12 @@ class MainService : Service() {
|
|||||||
startForeground(DEFAULT_NOTIFY_ID, notification)
|
startForeground(DEFAULT_NOTIFY_ID, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loginRequestNotification(clientID:Int, type: String, username: String, peerId: String) {
|
private fun loginRequestNotification(
|
||||||
|
clientID: Int,
|
||||||
|
type: String,
|
||||||
|
username: String,
|
||||||
|
peerId: String
|
||||||
|
) {
|
||||||
cancelNotification(clientID)
|
cancelNotification(clientID)
|
||||||
val notification = notificationBuilder
|
val notification = notificationBuilder
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
@ -550,7 +517,12 @@ class MainService : Service() {
|
|||||||
notificationManager.notify(getClientNotifyID(clientID), notification)
|
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onClientAuthorizedNotification(clientID: Int, type: String, username: String, peerId: String) {
|
private fun onClientAuthorizedNotification(
|
||||||
|
clientID: Int,
|
||||||
|
type: String,
|
||||||
|
username: String,
|
||||||
|
peerId: String
|
||||||
|
) {
|
||||||
cancelNotification(clientID)
|
cancelNotification(clientID)
|
||||||
val notification = notificationBuilder
|
val notification = notificationBuilder
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
@ -561,11 +533,11 @@ class MainService : Service() {
|
|||||||
notificationManager.notify(getClientNotifyID(clientID), notification)
|
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getClientNotifyID(clientID:Int):Int{
|
private fun getClientNotifyID(clientID: Int): Int {
|
||||||
return clientID + NOTIFY_ID_OFFSET
|
return clientID + NOTIFY_ID_OFFSET
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelNotification(clientID:Int){
|
fun cancelNotification(clientID: Int) {
|
||||||
notificationManager.cancel(getClientNotifyID(clientID))
|
notificationManager.cancel(getClientNotifyID(clientID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ package com.carriez.flutter_hbb
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.AudioRecord
|
||||||
|
import android.media.AudioRecord.READ_BLOCKING
|
||||||
import android.media.MediaCodecList
|
import android.media.MediaCodecList
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@ -11,6 +13,7 @@ import android.util.Log
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.hjq.permissions.Permission
|
import com.hjq.permissions.Permission
|
||||||
import com.hjq.permissions.XXPermissions
|
import com.hjq.permissions.XXPermissions
|
||||||
|
import java.nio.ByteBuffer
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@SuppressLint("ConstantLocale")
|
@SuppressLint("ConstantLocale")
|
||||||
@ -37,7 +40,7 @@ fun testVP9Support(): Boolean {
|
|||||||
return res != null
|
return res != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestPermission(context: Context,type: String){
|
fun requestPermission(context: Context, type: String) {
|
||||||
val permission = when (type) {
|
val permission = when (type) {
|
||||||
"audio" -> {
|
"audio" -> {
|
||||||
Permission.RECORD_AUDIO
|
Permission.RECORD_AUDIO
|
||||||
@ -64,7 +67,7 @@ fun requestPermission(context: Context,type: String){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkPermission(context: Context,type: String): Boolean {
|
fun checkPermission(context: Context, type: String): Boolean {
|
||||||
val permission = when (type) {
|
val permission = when (type) {
|
||||||
"audio" -> {
|
"audio" -> {
|
||||||
Permission.RECORD_AUDIO
|
Permission.RECORD_AUDIO
|
||||||
@ -76,5 +79,41 @@ fun checkPermission(context: Context,type: String): Boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return XXPermissions.isGranted(context,permission)
|
return XXPermissions.isGranted(context, permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioReader(val bufSize: Int, private val maxFrames: Int) {
|
||||||
|
private var currentPos = 0
|
||||||
|
private val bufferPool: Array<ByteBuffer>
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (maxFrames < 0 || maxFrames > 32) {
|
||||||
|
throw Exception("Out of bounds")
|
||||||
|
}
|
||||||
|
if (bufSize <= 0) {
|
||||||
|
throw Exception("Wrong bufSize")
|
||||||
|
}
|
||||||
|
bufferPool = Array(maxFrames) {
|
||||||
|
ByteBuffer.allocateDirect(bufSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun next() {
|
||||||
|
currentPos++
|
||||||
|
if (currentPos >= maxFrames) {
|
||||||
|
currentPos = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun readSync(audioRecord: AudioRecord): ByteBuffer? {
|
||||||
|
val buffer = bufferPool[currentPos]
|
||||||
|
val res = audioRecord.read(buffer, bufSize, READ_BLOCKING)
|
||||||
|
return if (res > 0) {
|
||||||
|
next()
|
||||||
|
buffer
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">RustDesk</string>
|
<string name="app_name">RustDesk</string>
|
||||||
<string name="accessibility_service_description">测试服务 输入服务</string>
|
<string name="accessibility_service_description">Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established</string>
|
||||||
</resources>
|
</resources>
|
@ -41,14 +41,14 @@ class ServerModel with ChangeNotifier {
|
|||||||
/**
|
/**
|
||||||
* 1. check android permission
|
* 1. check android permission
|
||||||
* 2. check config
|
* 2. check config
|
||||||
* audio true by default (if permission on)
|
* audio true by default (if permission on) (false default < Android 10)
|
||||||
* file true by default (if permission on)
|
* file true by default (if permission on)
|
||||||
* input false by default (it need turning on manually everytime)
|
* input false by default (it need turning on manually everytime)
|
||||||
*/
|
*/
|
||||||
await Future.delayed(Duration(seconds: 1));
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
|
||||||
// audio
|
// audio
|
||||||
if(!await PermissionManager.check("audio")){
|
if(androidVersion<30 || !await PermissionManager.check("audio")){
|
||||||
_audioOk = false;
|
_audioOk = false;
|
||||||
FFI.setByName('option', jsonEncode(
|
FFI.setByName('option', jsonEncode(
|
||||||
Map()
|
Map()
|
||||||
|
Loading…
Reference in New Issue
Block a user