Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/playground.yml
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ jobs:
nasm \
yasm \
ninja-build \
openjdk-11-jdk-headless \
openjdk-17-jdk-headless \
pkg-config \
tree \
wget
Expand Down Expand Up @@ -365,9 +365,9 @@ jobs:
- name: Build rustdesk
shell: bash
env:
JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: |
export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
case ${{ matrix.job.target }} in
Expand Down
7 changes: 5 additions & 2 deletions flutter/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ protobuf {
}

android {
compileSdkVersion 34
namespace "com.carriez.flutter_hbb"
compileSdkVersion 36
sourceSets {
main.java.srcDirs += 'src/main/kotlin'

Expand All @@ -91,6 +92,7 @@ android {
}

compileOptions {
coreLibraryDesugaringEnabled true
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
Expand All @@ -99,7 +101,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.carriez.flutter_hbb"
minSdkVersion 22
targetSdkVersion 33
targetSdkVersion 36
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand Down Expand Up @@ -128,6 +130,7 @@ flutter {
}

dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
implementation "androidx.media:media:1.6.0"
implementation 'com.github.getActivity:XXPermissions:18.5'
Expand Down
9 changes: 8 additions & 1 deletion flutter/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
Expand Down Expand Up @@ -89,7 +91,12 @@
<service
android:name=".MainService"
android:enabled="true"
android:foregroundServiceType="mediaProjection" />
android:exported="false"
android:foregroundServiceType="specialUse|mediaProjection|microphone">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="@string/foreground_service_special_use_subtype" />
</service>

<service
android:name=".FloatingWindowService"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,33 @@ const val AUDIO_SAMPLE_RATE = 48000
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO

class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
companion object {
private const val LOG_TAG = "LOG_AUDIO_RECORD_HANDLE"
private const val NO_ACTIVE_PUBLISHERS = 0
private var activeAudioFramePublishers = NO_ACTIVE_PUBLISHERS

@Synchronized
private fun acquireAudioFramePublisher() {
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
FFI.setFrameRawEnable("audio", true)
}
activeAudioFramePublishers++
}

@Synchronized
private fun releaseAudioFramePublisher() {
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
Log.e(LOG_TAG, "No active audio frame publisher to release")
return
}
activeAudioFramePublishers--
if (activeAudioFramePublishers == NO_ACTIVE_PUBLISHERS) {
FFI.setFrameRawEnable("audio", false)
}
}
}

private val logTag = LOG_TAG

private var audioRecorder: AudioRecord? = null
private var audioReader: AudioReader? = null
Expand Down Expand Up @@ -79,48 +105,94 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
return
}
// read f32 to byte , length * 4
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
val bufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL_MASK,
AUDIO_ENCODING
)
if (minBufferSize == 0) {
if (bufferSize <= 0) {
Log.d(logTag, "get min buffer size fail!")
return
}
audioReader = AudioReader(minBufferSize, 4)
audioReader = AudioReader(bufferSize, 4)
minBufferSize = bufferSize
Log.d(logTag, "init audioData len:$minBufferSize")
}

private fun releaseRecorder(recorder: AudioRecord) {
try {
recorder.release()
} finally {
if (audioRecorder === recorder) {
audioRecorder = null
}
}
}

private fun captureAudio(reader: AudioReader, recorder: AudioRecord) {
try {
while (audioRecordStat) {
reader.readSync(recorder)?.let {
FFI.onAudioFrameUpdate(it)
}
}
} finally {
minBufferSize = 0
try {
releaseRecorder(recorder)
} finally {
releaseAudioFramePublisher()
Log.d(logTag, "Exit audio thread")
}
}
}

@RequiresApi(Build.VERSION_CODES.M)
fun startAudioRecorder() {
checkAudioReader()
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
fun startAudioRecorder(): Boolean {
val recorder = audioRecorder
if (recorder == null) {
Log.d(logTag, "startAudioRecorder fail")
return false
}
var audioFramePublisherAcquired = false
return try {
checkAudioReader()
val reader = audioReader
if (reader == null || minBufferSize == 0) {
releaseRecorder(recorder)
Log.d(logTag, "startAudioRecorder fail")
return false
}
recorder.startRecording()
if (recorder.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
throw IllegalStateException("AudioRecord failed to enter recording state")
}
audioRecordStat = true
val captureThread = thread(start = false) { captureAudio(reader, recorder) }
acquireAudioFramePublisher()
audioFramePublisherAcquired = true
audioThread = captureThread
captureThread.start()
true
} catch (error: Exception) {
audioRecordStat = false
audioThread = null
Log.e(logTag, "startAudioRecorder fail", error)
try {
FFI.setFrameRawEnable("audio", true)
audioRecorder!!.startRecording()
audioRecordStat = true
audioThread = thread {
while (audioRecordStat) {
audioReader!!.readSync(audioRecorder!!)?.let {
FFI.onAudioFrameUpdate(it)
}
}
// let's release here rather than onDestroy to avoid threading issue
audioRecorder?.release()
audioRecorder = null
minBufferSize = 0
FFI.setFrameRawEnable("audio", false)
Log.d(logTag, "Exit audio thread")
releaseRecorder(recorder)
} finally {
if (audioFramePublisherAcquired) {
releaseAudioFramePublisher()
}
} catch (e: Exception) {
Log.d(logTag, "startAudioRecorder fail:$e")
}
} else {
Log.d(logTag, "startAudioRecorder fail")
false
}
}

fun isVoiceCallActive(): Boolean {
return audioRecorder?.audioSource == MediaRecorder.AudioSource.VOICE_COMMUNICATION
}

fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
if (!isSupportVoiceCall()) {
return false
Expand All @@ -137,11 +209,9 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
if (!isSupportVoiceCall()) {
return true
}
if (isVideoStart()) {
switchOutVoiceCall(mediaProjection)
}
val switched = !isVideoStart() || switchOutVoiceCall(mediaProjection)
tryReleaseAudio()
return true
return switched
}

@RequiresApi(Build.VERSION_CODES.M)
Expand All @@ -159,8 +229,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
Log.e(logTag, "createAudioRecorder fail")
return false
}
startAudioRecorder()
return true
return startAudioRecorder()
}

@RequiresApi(Build.VERSION_CODES.M)
Expand All @@ -177,8 +246,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
Log.e(logTag, "createAudioRecorder fail")
return false
}
startAudioRecorder()
return true
return startAudioRecorder()
}

fun tryReleaseAudio() {
Expand Down
Loading
Loading