-
-
Notifications
You must be signed in to change notification settings - Fork 859
Recording audio while other app use the microphone
The guide show you how to allow your app to record audio from the microphone while other app use it. This way you can use microphone at same time that other apps.
To allow this feature you will need 2 requirements:
- Accessibility permissions
- Android 10+
Remember that Accessibility permissions is a really sensitive permission and Google request you to explain the reason to use it clearly. Otherwise, Google can reject you the app in the Play store.
In manifest:
<service android:name="com.pedro.streamer.screen.ScreenService"
android:foregroundServiceType="mediaProjection|microphone|camera"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service" />
</service>
Create accessibility_service XML (in description you should use a text to explain the use of this permission):
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_name"
android:packageNames="com.pedro.streamer"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:settingsActivity="com.pedro.streamer.screen.ScreenService"
/>
fun checkAccessibilityPermissionsOn(context: Context): Boolean {
try {
val accessibilityEnabled = Settings.Secure.getInt(
context.applicationContext.contentResolver,
Settings.Secure.ACCESSIBILITY_ENABLED
)
return accessibilityEnabled == 0
} catch (_: Settings.SettingNotFoundException) {
return false
}
}
fun gotoAccessibilitySettings(context: Context) {
val settingsIntent = Intent(
Settings.ACTION_ACCESSIBILITY_SETTINGS
)
if (context !is Activity) {
settingsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
context.startActivity(settingsIntent)
} catch (_: ActivityNotFoundException) { }
}
Now, we need create a class to stream using this feature. Remember use MediaRecorder.AudioSource.MIC as audioSource ( no tested with others sources you can test it yourself. This is a code adapted from the app example in this repository. You can use this code as example:
/*
* Copyright (C) 2024 pedroSG94.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pedro.streamer.screen
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.media.MediaRecorder
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import com.pedro.common.ConnectChecker
import com.pedro.encoder.input.sources.audio.AudioSource
import com.pedro.encoder.input.sources.audio.InternalAudioSource
import com.pedro.encoder.input.sources.audio.MicrophoneSource
import com.pedro.encoder.input.sources.audio.MixAudioSource
import com.pedro.encoder.input.sources.video.NoVideoSource
import com.pedro.encoder.input.sources.video.ScreenSource
import com.pedro.library.base.recording.RecordController
import com.pedro.library.generic.GenericStream
import com.pedro.streamer.R
import com.pedro.streamer.utils.PathUtils
import com.pedro.streamer.utils.toast
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Basic Screen service streaming implementation
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
class ScreenService: AccessibilityService(), ConnectChecker {
companion object {
private const val TAG = "DisplayService"
private const val CHANNEL_ID = "DisplayStreamChannel"
const val NOTIFY_ID = 123456
var INSTANCE: ScreenService? = null
}
private var notificationManager: NotificationManager? = null
private lateinit var genericStream: GenericStream
private var mediaProjection: MediaProjection? = null
private val mediaProjectionManager: MediaProjectionManager by lazy {
applicationContext.getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
}
private var callback: ConnectChecker? = null
private val width = 640
private val height = 480
private val vBitrate = 1200 * 1000
private var rotation = 90 //0 for landscape or 90 for portrait
private val sampleRate = 32000
private val isStereo = true
private val aBitrate = 128 * 1000
private var prepared = false
private var recordPath = ""
private var selectedAudioSource: Int = R.id.audio_source_microphone
private val source = MediaRecorder.AudioSource.MIC
override fun onCreate() {
super.onCreate()
Log.i(TAG, "RTP Display service create")
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_HIGH)
notificationManager?.createNotificationChannel(channel)
}
genericStream = GenericStream(baseContext, this, NoVideoSource(), MicrophoneSource(audioSource = source)).apply {
//This is important to keep a constant fps because media projection only produce fps if the screen change
getGlInterface().setForceRender(true, 15)
}
prepared = try {
genericStream.prepareVideo(width, height, vBitrate, rotation = rotation) &&
genericStream.prepareAudio(sampleRate, isStereo, aBitrate,
echoCanceler = true,
noiseSuppressor = true
)
} catch (e: IllegalArgumentException) {
false
}
if (prepared) INSTANCE = this
else toast("Invalid audio or video parameters, prepare failed")
}
override fun onServiceConnected() {
val info = AccessibilityServiceInfo()
info.eventTypes = AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED
info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
info.notificationTimeout = 100
info.packageNames = null
serviceInfo = info
}
private fun keepAliveTrick() {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setSilent(true)
.setOngoing(false)
.build()
startForeground(1, notification)
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
override fun onInterrupt() {}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "RTP Display service started")
return START_STICKY
}
fun sendIntent(): Intent {
return mediaProjectionManager.createScreenCaptureIntent()
}
fun isStreaming(): Boolean {
return genericStream.isStreaming
}
fun isRecording(): Boolean {
return genericStream.isRecording
}
fun stopStream() {
if (genericStream.isStreaming) {
genericStream.stopStream()
notificationManager?.cancel(NOTIFY_ID)
}
}
fun setCallback(connectChecker: ConnectChecker?) {
callback = connectChecker
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "RTP Display service destroy")
stopStream()
INSTANCE = null
//release stream and media projection properly
genericStream.release()
mediaProjection?.stop()
mediaProjection = null
}
fun prepareStream(resultCode: Int, data: Intent): Boolean {
keepAliveTrick()
stopStream()
mediaProjection?.stop()
val mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data) ?: throw IllegalStateException("get MediaProjection failed")
this.mediaProjection = mediaProjection
val screenSource = ScreenSource(applicationContext, mediaProjection)
return try {
genericStream.changeVideoSource(screenSource)
toggleAudioSource(selectedAudioSource)
true
} catch (ignored: IllegalArgumentException) {
false
}
}
fun getCurrentAudioSource(): AudioSource = genericStream.audioSource
fun toggleAudioSource(itemId: Int) {
when (itemId) {
R.id.audio_source_microphone -> {
selectedAudioSource = R.id.audio_source_microphone
if (genericStream.audioSource is MicrophoneSource) return
genericStream.changeAudioSource(MicrophoneSource(audioSource = source))
}
R.id.audio_source_internal -> {
selectedAudioSource = R.id.audio_source_internal
if (genericStream.audioSource is InternalAudioSource) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mediaProjection?.let { genericStream.changeAudioSource(InternalAudioSource(it)) }
} else {
throw IllegalArgumentException("You need min API 29+")
}
}
R.id.audio_source_mix -> {
selectedAudioSource = R.id.audio_source_mix
if (genericStream.audioSource is MixAudioSource) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mediaProjection?.let { genericStream.changeAudioSource(MixAudioSource(it)) }
} else {
throw IllegalArgumentException("You need min API 29+")
}
}
}
}
fun toggleRecord(state: (RecordController.Status) -> Unit) {
if (!genericStream.isRecording) {
val folder = PathUtils.getRecordPath()
if (!folder.exists()) folder.mkdir()
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
recordPath = "${folder.absolutePath}/${sdf.format(Date())}.mp4"
genericStream.startRecord(recordPath) { status ->
if (status == RecordController.Status.RECORDING) {
state(RecordController.Status.RECORDING)
}
}
state(RecordController.Status.STARTED)
} else {
genericStream.stopRecord()
state(RecordController.Status.STOPPED)
PathUtils.updateGallery(this, recordPath)
}
}
fun startStream(endpoint: String) {
if (!genericStream.isStreaming) genericStream.startStream(endpoint)
}
override fun onConnectionStarted(url: String) {
callback?.onConnectionStarted(url)
}
override fun onConnectionSuccess() {
callback?.onConnectionSuccess()
}
override fun onNewBitrate(bitrate: Long) {
callback?.onNewBitrate(bitrate)
}
override fun onConnectionFailed(reason: String) {
callback?.onConnectionFailed(reason)
}
override fun onDisconnect() {
callback?.onDisconnect()
}
override fun onAuthError() {
callback?.onAuthError()
}
override fun onAuthSuccess() {
callback?.onAuthSuccess()
}
}