android update,open and close service

This commit is contained in:
csf
2022-02-02 17:25:56 +08:00
parent 2e73f29ce9
commit 1af3f3f28d
10 changed files with 1032 additions and 343 deletions

View File

@@ -5,6 +5,8 @@ import android.accessibilityservice.GestureDescription
import android.content.Context
import android.graphics.Path
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.RequiresApi
@@ -12,6 +14,12 @@ import kotlin.concurrent.thread
class InputService : AccessibilityService() {
companion object{
var ctx:InputService? = null
fun isOpen():Boolean{
return ctx!=null
}
}
private val logTag = "input service"
private var leftIsDown = false
private var mPath = Path()
@@ -21,28 +29,26 @@ class InputService : AccessibilityService() {
@RequiresApi(Build.VERSION_CODES.N)
fun rustMouseInput(mask: Int, _x: Int, _y: Int) {
Log.w(logTag, "got mouse input:x:$_x ,y:$_y ,mask:$mask ")
// TODO 临时倍数
// TODO 按键抬起按下时候 x y 都是0
if ( !(mask == 9 || mask == 10) ) {
mouseX = _x * SCALE
mouseY = _y * SCALE
if (!(mask == 9 || mask == 10)) {
mouseX = _x * INFO.scale
mouseY = _y * INFO.scale
}
// left button down ,was up
if (mask == 9){
if (mask == 9) {
leftIsDown = true
startGesture(mouseX,mouseY)
startGesture(mouseX, mouseY)
}
// left down ,was down
if (mask == 9){
continueGesture(mouseX,mouseY)
if (mask == 9) {
continueGesture(mouseX, mouseY)
}
// left up ,was down
if (mask == 10){
if (mask == 10) {
leftIsDown = false
endGesture(mouseX, mouseY)
}
@@ -57,6 +63,7 @@ class InputService : AccessibilityService() {
private fun continueGesture(x: Int, y: Int) {
mPath.lineTo(x.toFloat(), y.toFloat())
}
@RequiresApi(Build.VERSION_CODES.N)
private fun endGesture(x: Int, y: Int) {
mPath.lineTo(x.toFloat(), y.toFloat())
@@ -81,19 +88,20 @@ class InputService : AccessibilityService() {
}, null)
}
external fun init(ctx: Context)
private external fun init(ctx: Context)
init {
System.loadLibrary("rustdesk")
}
private val LOG_TAG = "INPUT_LOG"
@RequiresApi(Build.VERSION_CODES.O)
override fun onServiceConnected() {
super.onServiceConnected()
Log.d(LOG_TAG,"onServiceConnected!")
ctx = this
Log.d(logTag, "onServiceConnected!")
init(this)
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
// TODO("Not yet implemented")
}

View File

@@ -1,7 +1,7 @@
package com.carriez.flutter_hbb
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Build
@@ -9,126 +9,220 @@ import android.provider.Settings
import android.util.DisplayMetrics
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationManagerCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
const val MAX_SIZE = 1400
const val NOTIFY_TYPE_LOGIN_REQ = "NOTIFY_TYPE_LOGIN_REQ"
const val MEDIA_REQUEST_CODE = 42
const val INPUT_REQUEST_CODE = 43
class MainActivity : FlutterActivity() {
companion object {
lateinit var flutterMethodChannel: MethodChannel
}
private val channelTag = "mChannel"
private val logTag = "mMainActivity"
private var mediaProjectionResultIntent: Intent? = null
private val requestCode = 1
init {
System.loadLibrary("rustdesk")
}
external fun rustSetInfo(username: String, hostname: String, width: Int, height: Int)
private external fun init(context: Context)
private external fun close()
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) // 必要 否则无法正确初始化flutter
updateMachineInfo()
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
channelTag
).setMethodCallHandler { call, result ->
when (call.method) {
"getPer" -> {
Log.d(channelTag, "event from flutter,getPer")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getMediaProjection()
}
result.success(true)
fun rustSetByName(name: String, arg1: String, arg2: String) {
when (name) {
"try_start_without_auth" -> {
// to UI
Log.d(logTag, "from rust:got try_start_without_auth")
activity.runOnUiThread {
flutterMethodChannel.invokeMethod(name, mapOf("peerID" to arg1, "name" to arg2))
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
}
"startSer" -> {
mStarService()
result.success(true)
val notification = createNormalNotification(
this,
"请求控制",
"来自$arg1:$arg2 请求连接",
NOTIFY_TYPE_LOGIN_REQ
)
with(NotificationManagerCompat.from(this)) {
notify(12, notification)
}
"stopSer" -> {
mStopService()
result.success(true)
}
"checkInput" -> {
checkInput()
result.success(true)
}
else -> {}
Log.d(logTag, "kotlin invokeMethod try_start_without_auth,done")
}
"start_capture" -> {
Log.d(logTag, "from rust:start_capture")
activity.runOnUiThread {
flutterMethodChannel.invokeMethod(name, mapOf("peerID" to arg1, "name" to arg2))
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
}
// 1.开始捕捉音视频 2.通知栏
startCapture()
val notification = createNormalNotification(
this,
"开始共享屏幕",
"From:$arg2:$arg1",
NOTIFY_TYPE_START_CAPTURE
)
with(NotificationManagerCompat.from(this)) {
notify(13, notification)
}
}
"stop_capture" -> {
Log.d(logTag, "from rust:stop_capture")
stopCapture()
activity.runOnUiThread {
flutterMethodChannel.invokeMethod(name, null)
Log.d(logTag, "activity.runOnUiThread invokeMethod try_start_without_auth,done")
}
}
else -> {}
}
}
override fun onDestroy() {
Log.e(logTag, "onDestroy")
close()
stopCapture()
stopMainService()
stopService(Intent(this, MainService::class.java))
stopService(Intent(this, InputService::class.java))
super.onDestroy()
}
@RequiresApi(Build.VERSION_CODES.M)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) // 必要 否则无法正确初始化flutter
checkPermissions(this)
updateMachineInfo()
flutterMethodChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
channelTag
).apply {
setMethodCallHandler { call, result ->
when (call.method) {
"init_service" -> {
Log.d(logTag, "event from flutter,getPer")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getMediaProjection()
}
result.success(true)
}
"start_capture" -> {
startCapture()
result.success(true)
}
"stop_service" -> {
stopMainService()
result.success(true)
}
"check_input" -> {
checkInput()
result.success(true)
}
"check_video_permission" -> {
val res = MainService.checkMediaPermission()
result.success(res)
}
else -> {}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun getMediaProjection() {
val mMediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val mIntent = mMediaProjectionManager.createScreenCaptureIntent()
startActivityForResult(mIntent, requestCode)
startActivityForResult(mIntent, MEDIA_REQUEST_CODE)
}
private fun mStarService() {
// 实际逻辑是开始监听服务 在成功获取到mediaProjection就开始
private fun initService() {
if (mediaProjectionResultIntent == null) {
Log.w(channelTag, "mediaProjectionResultIntent is null")
Log.w(logTag, "initService fail,mediaProjectionResultIntent is null")
return
}
Log.d(channelTag, "Start a service")
Log.d(logTag, "Init service")
// call init service to rust
init(this)
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = START_SERVICE
serviceIntent.action = INIT_SERVICE
serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent)
// TEST api < O
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
launchMainService(serviceIntent)
}
private fun mStopService() {
Log.d(channelTag, "Stop service")
private fun startCapture() {
if (mediaProjectionResultIntent == null) {
Log.w(logTag, "startCapture fail,mediaProjectionResultIntent is null")
return
}
Log.d(logTag, "Start Capture")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = START_CAPTURE
serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent)
launchMainService(serviceIntent)
}
private fun stopCapture() {
Log.d(logTag, "Stop Capture")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = STOP_CAPTURE
launchMainService(serviceIntent)
}
// TODO 关闭逻辑
private fun stopMainService() {
Log.d(logTag, "Stop service")
val serviceIntent = Intent(this, MainService::class.java)
serviceIntent.action = STOP_SERVICE
launchMainService(serviceIntent)
}
private fun launchMainService(intent: Intent) {
// TEST api < O
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
startForegroundService(intent)
} else {
startService(serviceIntent)
startService(intent)
}
}
private fun checkInput() {
AlertDialog.Builder(this)
.setCancelable(false)
.setTitle("检查Input服务")
.setMessage("请开启相关服务")
.setPositiveButton("Yes") { dialog, which ->
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
if (intent.resolveActivity(packageManager) != null) startActivityForResult(
intent,
11
) else AlertDialog.Builder(this)
.setTitle("错误")
.setMessage("无法启动服务")
.show()
}
.setNegativeButton("No") { dialog, which -> }
.show()
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
}
override fun onResume() {
super.onResume()
val inputPer = InputService.isOpen()
Log.d(logTag,"onResume inputPer:$inputPer")
activity.runOnUiThread {
flutterMethodChannel.invokeMethod("on_permission_changed",mapOf("name" to "input", "value" to inputPer.toString()))
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK && data != null) {
Log.d(channelTag, "got mediaProjectionResultIntent ok")
if (requestCode == MEDIA_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
Log.d(logTag, "got mediaProjectionResultIntent ok")
mediaProjectionResultIntent = data
initService()
}
}
private fun updateMachineInfo() {
// 屏幕尺寸 控制最长边不超过1400 超过则减半直到1400 并储存缩放比例 实际发送给手机端的尺寸为缩小后的尺寸
// 屏幕尺寸 控制最长边不超过1400 超过则减半并储存缩放比例 实际发送给手机端的尺寸为缩小后的尺寸
// input控制时再通过缩放比例恢复原始尺寸进行path入参
val dm = DisplayMetrics()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -146,18 +240,20 @@ class MainActivity : FlutterActivity() {
var h = dm.heightPixels
var scale = 1
if (w != 0 && h != 0) {
if (w > MAX_SIZE || h > MAX_SIZE) {
if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) {
scale = 2
w /= scale
h /= scale
}
Log.d(logTag, "Real size - width:$w,height:$h")
FIXED_WIDTH = 540
FIXED_HEIGHT = 1140
SCALE = scale
INFO.screenWidth = w
INFO.screenHeight = h
INFO.scale = scale
INFO.username = "test"
INFO.hostname = "hostname"
// TODO username hostname
rustSetInfo("csf", "Android", FIXED_WIDTH, FIXED_HEIGHT)
} else {
Log.e(logTag, "Got Screen Size Fail!")
}

View File

@@ -1,156 +1,292 @@
/**
* video_service and audio_service
*/
package com.carriez.flutter_hbb
import android.annotation.SuppressLint
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.PixelFormat
import android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
import android.hardware.display.VirtualDisplay
import android.media.*
import android.media.AudioRecord.READ_BLOCKING
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.util.DisplayMetrics
import android.os.Looper
import android.util.Log
import android.view.Surface
import android.view.WindowManager
import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_MIN
import java.nio.ByteBuffer
import java.util.concurrent.Executors
import kotlin.concurrent.thread
const val EXTRA_MP_DATA = "mp_intent"
const val START_SERVICE = "start_service"
const val INIT_SERVICE = "init_service"
const val START_CAPTURE = "start_capture"
const val STOP_CAPTURE = "stop_capture"
const val STOP_SERVICE = "stop_service"
const val NOTIFY_TYPE_START_CAPTURE = "NOTIFY_TYPE_START_CAPTURE"
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
var FIXED_WIDTH = 0 // 编码器有上限
var FIXED_HEIGHT = 0
var SCALE = 1 // RealScreenWidth = screenWidth * scale
const val M_KEY_BIT_RATE = 1024_000
const val M_KEY_FRAME_RATE = 30
// video const
const val MAX_SCREEN_SIZE = 1200 // 内置编码器有上限 且实际使用中不需要过高的分辨率
const val VIDEO_KEY_BIT_RATE = 1024_000
const val VIDEO_KEY_FRAME_RATE = 30
// audio const
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
const val AUDIO_SAMPLE_RATE = 48000
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
class MainService : Service() {
fun rustGetRaw(): ByteArray {
return rawByteArray!!
companion object {
private var mediaProjection: MediaProjection? = null
fun checkMediaPermission(): Boolean {
val value = mediaProjection != null
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel.invokeMethod(
"on_permission_changed",
mapOf("name" to "media", "value" to value.toString())
)
}
return value
}
}
external fun init(ctx: Context)
init {
System.loadLibrary("rustdesk")
}
private val logTag = "LOG_SERVICE"
private var mMediaProjection: MediaProjection? = null
private var surface: Surface? = null
private val singleThread = Executors.newSingleThreadExecutor()
private var mEncoder: MediaCodec? = null
private var rawByteArray: ByteArray? = null
override fun onBind(intent: Intent): IBinder? {
return null
// rust call jvm
fun rustGetVideoRaw(): ByteArray {
return if (videoData != null) {
videoData!!
} else {
videoZeroData
}
}
fun rustGetAudioRaw(): FloatArray {
return if (isNewData && audioData != null) {
isNewData = false
audioData!!
} else {
audioZeroData
}
}
fun rustGetAudioRawLen(): Int {
return if (isNewData && audioData != null && audioData!!.isNotEmpty()) {
audioData!!.size
} else 0
}
fun rustGetByName(name: String): String {
return when (name) {
"screen_size" -> "${INFO.screenWidth}:${INFO.screenHeight}"
else -> ""
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun rustSetByName(name: String, arg1: String, arg2: String) {
when (name) {
else -> {}
}
}
// jvm call rust
private external fun init(ctx: Context)
private external fun sendVp9(data: ByteArray)
private val logTag = "LOG_SERVICE"
private val useVP9 = false
// video
private var surface: Surface? = null
private val sendVP9Thread = Executors.newSingleThreadExecutor()
private var videoEncoder: MediaCodec? = null
private var videoData: ByteArray? = null
private var imageReader: ImageReader? =
null // * 注意 这里要成为成员变量,防止被回收 https://www.cnblogs.com/yongdaimi/p/11004560.html
private val videoZeroData = ByteArray(32)
private var virtualDisplay: VirtualDisplay? = null
// audio
private var audioRecorder: AudioRecord? = null
private var audioData: FloatArray? = null
private var minBufferSize = 0
private var isNewData = false
private val audioZeroData: FloatArray = FloatArray(32) // 必须是32位 如果只有8位进行ffi传输时会出错
private var audioRecordStat = false
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("whichService", "this service:${Thread.currentThread()}")
init(this) // 注册到rust
if (intent?.action == START_SERVICE) {
Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}")
createNotification()
val mMediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
mMediaProjection = intent.getParcelableExtra<Intent>(EXTRA_MP_DATA)?.let {
mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
}
Log.d(logTag, "获取mMediaProjection成功$mMediaProjection")
if (testSupport()) {
startRecorder()
} else {
Toast.makeText(this, "此设备不支持:$MIME_TYPE", Toast.LENGTH_SHORT).show()
stopSelf(startId)
}
} else if (intent?.action == STOP_SERVICE) {
mEncoder?.let {
try {
Log.d(logTag, "正在释放encoder")
it.signalEndOfInputStream()
it.stop()
it.release()
} catch (e: Exception) {
null
when (intent?.action) {
INIT_SERVICE -> {
Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}")
createForegroundNotification(this)
val mMediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
intent.getParcelableExtra<Intent>(EXTRA_MP_DATA)?.let {
mediaProjection =
mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
Log.d(logTag, "获取mMediaProjection成功$mediaProjection")
checkMediaPermission()
init(this)
} ?: let {
Log.d(logTag, "获取mMediaProjection失败")
}
}
stopSelf()
START_CAPTURE -> {
startCapture()
}
STOP_CAPTURE -> {
stopCapture()
}
STOP_SERVICE -> {
stopCapture()
mediaProjection = null
checkMediaPermission()
stopSelf()
}
}
return super.onStartCommand(intent, flags, startId)
}
lateinit var mImageReader: ImageReader // * 注意 这里要成为成员变量,防止被回收 https://www.cnblogs.com/yongdaimi/p/11004560.html
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun startCapture(): Boolean {
if (testVP9Support()) { // testVP9Support一直返回true 暂时只使用原始数据
startVideoRecorder()
} else {
Toast.makeText(this, "此设备不支持:$MIME_TYPE", Toast.LENGTH_SHORT).show()
return false
}
// 音频只支持安卓10以及以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startAudioRecorder()
}
checkMediaPermission()
return true
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun stopCapture() {
virtualDisplay?.release()
imageReader?.close()
videoEncoder?.let {
it.signalEndOfInputStream()
it.stop()
it.release()
}
audioRecorder?.startRecording()
audioRecordStat = false
// audioRecorder 如果无法重新创建 保留服务的情况不要释放
// audioRecorder?.stop()
// mediaProjection?.stop()
virtualDisplay = null
imageReader = null
videoEncoder = null
videoData = null
// audioRecorder = null
// audioData = null
}
@SuppressLint("WrongConstant")
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun startRecorder() {
Log.d(logTag, "startRecorder")
mMediaProjection?.let { mp ->
// 使用原始数据
mImageReader =
ImageReader.newInstance(FIXED_WIDTH, FIXED_HEIGHT, PixelFormat.RGBA_8888, 2) // 至少是2
mImageReader.setOnImageAvailableListener({ imageReader: ImageReader ->
// Log.d(logTag, "on image")
try {
imageReader.acquireLatestImage().use { image ->
if (image == null) return@setOnImageAvailableListener
val planes = image.planes
val buffer = planes[0].buffer
buffer.rewind()
// 这里注意 处理不当会引发OOM
if (rawByteArray == null) {
rawByteArray = ByteArray(buffer.capacity())
buffer.get(rawByteArray!!)
} else {
buffer.get(rawByteArray!!)
}
}
} catch (ignored: java.lang.Exception) {
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
imageReader.discardFreeBuffers()
}
}, null)
mp.createVirtualDisplay(
"rustdesk test",
FIXED_WIDTH, FIXED_HEIGHT, 200, VIRTUAL_DISPLAY_FLAG_PUBLIC,
mImageReader.surface, null, null
)
// 使用内置编码器
// createMediaCodec()
// mEncoder?.let {
// surface = it.createInputSurface()
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// surface!!.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
// }
// it.setCallback(cb)
// it.start()
// mp.createVirtualDisplay(
// "rustdesk test",
// FIXED_WIDTH, FIXED_HEIGHT, 200, VIRTUAL_DISPLAY_FLAG_PUBLIC,
// surface, null, null
// )
// }
private fun startVideoRecorder() {
Log.d(logTag, "startVideoRecorder")
mediaProjection?.let { mp ->
if (useVP9) {
startVP9VideoRecorder(mp)
} else {
startRawVideoRecorder(mp)
}
} ?: let {
Log.d(logTag, "startRecorder fail,mMediaProjection is null")
}
}
@SuppressLint("WrongConstant")
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun startRawVideoRecorder(mp: MediaProjection) {
Log.d(logTag, "startRawVideoRecorder,screen info:$INFO")
// 使用原始数据
imageReader =
ImageReader.newInstance(
INFO.screenWidth,
INFO.screenHeight,
PixelFormat.RGBA_8888,
2 // maxImages 至少是2
).apply {
// 奇怪的现象必须从MainActivity调用 无法从MainService中调用 会阻塞在这个函数
setOnImageAvailableListener({ imageReader: ImageReader ->
try {
imageReader.acquireLatestImage().use { image ->
if (image == null) return@setOnImageAvailableListener
val planes = image.planes
val buffer = planes[0].buffer
buffer.rewind()
// Be careful about OOM!
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) {
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
imageReader.discardFreeBuffers()
}
}, null)
}
Log.d(logTag, "ImageReader.setOnImageAvailableListener done")
virtualDisplay = mp.createVirtualDisplay(
"RustDesk",
INFO.screenWidth, INFO.screenHeight, 200, VIRTUAL_DISPLAY_FLAG_PUBLIC,
imageReader?.surface, null, null
)
Log.d(logTag, "startRawVideoRecorder done")
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun startVP9VideoRecorder(mp: MediaProjection) {
//使用内置编码器
createMediaCodec()
videoEncoder?.let {
surface = it.createInputSurface()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
surface!!.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
}
it.setCallback(cb)
it.start()
virtualDisplay = mp.createVirtualDisplay(
"rustdesk test",
INFO.screenWidth, INFO.screenHeight, 200, VIRTUAL_DISPLAY_FLAG_PUBLIC,
surface, null, null
)
}
}
private val cb: MediaCodec.Callback = @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
object : MediaCodec.Callback() {
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
@@ -162,7 +298,7 @@ class MainService : Service() {
info: MediaCodec.BufferInfo
) {
codec.getOutputBuffer(index)?.let { buf ->
singleThread.execute {
sendVP9Thread.execute {
// TODO 优化内存使用方式
val byteArray = ByteArray(buf.limit())
buf.get(byteArray)
@@ -177,75 +313,92 @@ class MainService : Service() {
}
}
external fun sendRaw(buf: ByteBuffer)
external fun sendVp9(data: ByteArray)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun testSupport(): Boolean {
val res = MediaCodecList(MediaCodecList.ALL_CODECS)
.findEncoderForFormat(
MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_VP9,
FIXED_WIDTH,
FIXED_HEIGHT
)
)
return res?.let {
true
} ?: let {
false
}
}
private fun createMediaCodec() {
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
val mFormat = MediaFormat.createVideoFormat(MIME_TYPE, FIXED_WIDTH, FIXED_HEIGHT)
mFormat.setInteger(MediaFormat.KEY_BIT_RATE, M_KEY_BIT_RATE)
mFormat.setInteger(MediaFormat.KEY_FRAME_RATE, M_KEY_FRAME_RATE) // codec的帧率设置无效
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
val mFormat = MediaFormat.createVideoFormat(MIME_TYPE, INFO.screenWidth, INFO.screenHeight)
mFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_KEY_BIT_RATE)
mFormat.setInteger(MediaFormat.KEY_FRAME_RATE, VIDEO_KEY_FRAME_RATE) // codec的帧率设置无效
mFormat.setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
)
mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
try {
mEncoder!!.configure(mFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
videoEncoder!!.configure(mFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
} catch (e: Exception) {
Log.e(logTag, "mEncoder.configure fail!")
}
}
private fun createNotification() {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel("my_service", "My Background Service")
} else {
""
@RequiresApi(Build.VERSION_CODES.M)
private fun startAudioRecorder() {
checkAudioRecorder()
if (audioData != null && audioRecorder != null && minBufferSize != 0) {
audioRecorder!!.startRecording()
audioRecordStat = true
thread {
while (audioRecordStat) {
val res = audioRecorder!!.read(audioData!!, 0, minBufferSize, READ_BLOCKING)
// 录制float 需要使用对应的read float[] 函数
if (res != AudioRecord.ERROR_INVALID_OPERATION) {
isNewData = true
}
}
Log.d(logTag, "Exit audio thread")
}
val notification: Notification = NotificationCompat.Builder(this, channelId)
.setOngoing(true)
.setContentTitle("Hello")
.setPriority(PRIORITY_MIN)
.setContentText("TEST TEST")
.build()
startForeground(11, notification)
} else {
Log.d(logTag, "startAudioRecorder fail")
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String {
val chan = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_NONE
@RequiresApi(Build.VERSION_CODES.M)
private fun checkAudioRecorder() {
if (audioData != null && audioRecorder != null && minBufferSize != 0) {
return
}
minBufferSize = 2 * AudioRecord.getMinBufferSize(
AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL_MASK,
AUDIO_ENCODING
)
chan.lightColor = Color.BLUE
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
if (minBufferSize == 0) {
Log.d(logTag, "get min buffer size fail!")
return
}
audioData = FloatArray(minBufferSize)
Log.d(logTag, "init audioData len:${audioData!!.size}")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mediaProjection?.let {
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
.addMatchingUsage(AudioAttributes.USAGE_GAME)
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
audioRecorder = AudioRecord.Builder()
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AUDIO_ENCODING)
.setSampleRate(AUDIO_SAMPLE_RATE)
.setChannelMask(AUDIO_CHANNEL_MASK).build()
)
.setAudioPlaybackCaptureConfig(apcc)
.setBufferSizeInBytes(minBufferSize).build()
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
return
}
}
Log.d(logTag, "createAudioRecorder fail")
}
override fun onDestroy() {
Log.d(logTag, "service stop:${Thread.currentThread()}")
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show()
}
override fun onBind(intent: Intent): IBinder? {
return null
}
}

View File

@@ -0,0 +1,129 @@
package com.carriez.flutter_hbb
import android.Manifest
import android.app.*
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.Icon
import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import java.util.*
val INFO = Info("","",0,0)
data class Info(var username:String, var hostname:String, var screenWidth:Int, var screenHeight:Int,
var scale:Int = 1)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun testVP9Support(): Boolean {
return true // 函数内部永远返回true 暂时只使用原始数据
val res = MediaCodecList(MediaCodecList.ALL_CODECS)
.findEncoderForFormat(
MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_VP9,
INFO.screenWidth,
INFO.screenWidth
)
)
return res!=null
}
fun createForegroundNotification(ctx:Service) {
// 设置通知渠道 android8开始引入 老版本会被忽略 这个东西的作用相当于为通知分类 给用户选择通知消息的种类
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "RustDeskForeground"
val channelName = "RustDesk屏幕分享服务状态"
val channel = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Share your Android Screen with RustDeskService"
}
channel.lightColor = Color.BLUE
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val service = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(channel)
channelId
} else {
""
}
val notification: Notification = NotificationCompat.Builder(ctx, channelId)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
ctx.startForeground(11, notification)
}
fun createNormalNotification(ctx: Context,title:String,text:String,type:String): Notification {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "RustDeskNormal"
val channelName = "RustDesk通知消息"
val channel = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Share your Android Screen with RustDeskService"
}
channel.lightColor = Color.BLUE
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(channel)
channelId
} else {
""
}
val intent = Intent(ctx,MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
action =Intent.ACTION_MAIN // 不设置会造成每次都重新启动一个新的Activity
addCategory(Intent.CATEGORY_LAUNCHER)
putExtra("type",type)
}
val pendingIntent = PendingIntent.getActivity(ctx,0,intent,FLAG_UPDATE_CURRENT)
return NotificationCompat.Builder(ctx, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setPriority(NotificationCompat.PRIORITY_HIGH) // 这里如果设置为低则不显示
.setContentText(text)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
}
const val MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
fun checkPermissions(context: Context) {
val permissions: MutableList<String> = LinkedList()
addPermission(context,permissions, Manifest.permission.WRITE_EXTERNAL_STORAGE)
addPermission(context,permissions, Manifest.permission.RECORD_AUDIO)
addPermission(context,permissions, Manifest.permission.INTERNET)
addPermission(context,permissions, Manifest.permission.READ_PHONE_STATE)
if (permissions.isNotEmpty()) {
ActivityCompat.requestPermissions(
context as Activity, permissions.toTypedArray(),
MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
)
}
}
private fun addPermission(context:Context,permissionList: MutableList<String>, permission: String) {
if (ContextCompat.checkSelfPermission(
context,
permission
) !== PackageManager.PERMISSION_GRANTED
) {
permissionList.add(permission)
}
}