background
Recently, such a thing needs to be done. A service to complete the recording function of multiple apps, roughly has the following logic
- The service is integrated into each end in the form of lib
- When the main app exists, all other apps use the main app's recording service
- When the main app does not exist, other apps use their own recording service
- There is priority, apps with high priority have absolute recording permissions, no matter whether other apps are recording, they must be paused, and priority is given to requests for high-priority apps.
- Supports two recording solutions: AudioRecord and MediaRecorder
Why is it so designed?
- The bottom layer of the Android system has restrictions on recording, and only one process can use the recording function at a time
- Business needs, all transactions guarantee the recording function of the main App
- In order to better manage the recording status, and the communication between multiple apps
Architecture diagram design
Architecture
App layer
Contains all the company's need to integrate recording services, no explanation is needed here
Manager layer
This layer is responsible for the management of the Service layer, including: service binding, unbinding, registration callback, start recording, stop recording, check recording status, check service running status, etc. ### Service layer core logic layer, through the realization of AIDL, To meet cross-process communication, and provide the actual recording function.
Directory at a glance
Looking at the distribution of the code directory, combined with the architecture diagram, we come to implement a set of logic from the bottom to the upper layer
IRecorder interface definition
public interface IRecorder {
String startRecording(RecorderConfig recorderConfig);
void stopRecording();
RecorderState state();
boolean isRecording();
}
IRecorder interface implementation
class JLMediaRecorder : IRecorder {
private var mMediaRecorder: MediaRecorder? = null
private var mState = RecorderState.IDLE
@Synchronized
override fun startRecording(recorderConfig: RecorderConfig): String {
try {
mMediaRecorder = MediaRecorder()
mMediaRecorder?.setAudioSource(recorderConfig.audioSource)
when (recorderConfig.recorderOutFormat) {
RecorderOutFormat.MPEG_4 -> {
mMediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
mMediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
}
RecorderOutFormat.AMR_WB -> {
mMediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.AMR_WB)
mMediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_WB)
}
else -> {
mMediaRecorder?.reset()
mMediaRecorder?.release()
mMediaRecorder = null
return "MediaRecorder 不支持 AudioFormat.PCM"
}
}
} catch (e: IllegalStateException) {
mMediaRecorder?.reset()
mMediaRecorder?.release()
mMediaRecorder = null
return "Error initializing media recorder 初始化失败";
}
return try {
val file = recorderConfig.recorderFile
file.parentFile.mkdirs()
file.createNewFile()
val outputPath: String = file.absolutePath
mMediaRecorder?.setOutputFile(outputPath)
mMediaRecorder?.prepare()
mMediaRecorder?.start()
mState = RecorderState.RECORDING
""
} catch (e: Exception) {
mMediaRecorder?.reset()
mMediaRecorder?.release()
mMediaRecorder = null
recorderConfig.recorderFile.delete()
e.toString()
}
}
override fun isRecording(): Boolean {
return mState == RecorderState.RECORDING
}
@Synchronized
override fun stopRecording() {
try {
if (mState == RecorderState.RECORDING) {
mMediaRecorder?.stop()
mMediaRecorder?.reset()
mMediaRecorder?.release()
}
} catch (e: java.lang.IllegalStateException) {
e.printStackTrace()
}
mMediaRecorder = null
mState = RecorderState.IDLE
}
override fun state(): RecorderState {
return mState
}
}
It is important to note here that @Synchronized
because of the disorder of the state when multiple processes are called at the same time, it is safe to add it.
AIDL interface definition
interface IRecorderService {
void startRecording(in RecorderConfig recorderConfig);
void stopRecording(in RecorderConfig recorderConfig);
boolean isRecording(in RecorderConfig recorderConfig);
RecorderResult getActiveRecording();
void registerCallback(IRecorderCallBack callBack);
void unregisterCallback(IRecorderCallBack callBack);
}
important point:
- Custom parameters need to implement the Parcelable interface
- If you need to call back, it is also the AIDL interface definition
AIDL interface callback definition
interface IRecorderCallBack {
void onStart(in RecorderResult result);
void onStop(in RecorderResult result);
void onException(String error,in RecorderResult result);
}
RecorderService implementation
Next is the core of the function, cross-process service
class RecorderService : Service() {
private var iRecorder: IRecorder? = null
private var currentRecorderResult: RecorderResult = RecorderResult()
private var currentWeight: Int = -1
private val remoteCallbackList: RemoteCallbackList<IRecorderCallBack> = RemoteCallbackList()
private val mBinder: IRecorderService.Stub = object : IRecorderService.Stub() {
override fun startRecording(recorderConfig: RecorderConfig) {
startRecordingInternal(recorderConfig)
}
override fun stopRecording(recorderConfig: RecorderConfig) {
if (recorderConfig.recorderId == currentRecorderResult.recorderId)
stopRecordingInternal()
else {
notifyCallBack {
it.onException(
"Cannot stop the current recording because the recorderId is not the same as the current recording",
currentRecorderResult
)
}
}
}
override fun getActiveRecording(): RecorderResult? {
return currentRecorderResult
}
override fun isRecording(recorderConfig: RecorderConfig?): Boolean {
return if (recorderConfig?.recorderId == currentRecorderResult.recorderId)
iRecorder?.isRecording ?: false
else false
}
override fun registerCallback(callBack: IRecorderCallBack) {
remoteCallbackList.register(callBack)
}
override fun unregisterCallback(callBack: IRecorderCallBack) {
remoteCallbackList.unregister(callBack)
}
}
override fun onBind(intent: Intent?): IBinder? {
return mBinder
}
@Synchronized
private fun startRecordingInternal(recorderConfig: RecorderConfig) {
val willStartRecorderResult =
RecorderResultBuilder.aRecorderResult().withRecorderFile(recorderConfig.recorderFile)
.withRecorderId(recorderConfig.recorderId).build()
if (ContextCompat.checkSelfPermission(
this@RecorderService,
android.Manifest.permission.RECORD_AUDIO
)
!= PackageManager.PERMISSION_GRANTED
) {
logD("Record audio permission not granted, can't record")
notifyCallBack {
it.onException(
"Record audio permission not granted, can't record",
willStartRecorderResult
)
}
return
}
if (ContextCompat.checkSelfPermission(
this@RecorderService,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
!= PackageManager.PERMISSION_GRANTED
) {
logD("External storage permission not granted, can't save recorded")
notifyCallBack {
it.onException(
"External storage permission not granted, can't save recorded",
willStartRecorderResult
)
}
return
}
if (isRecording()) {
val weight = recorderConfig.weight
if (weight < currentWeight) {
logD("Recording with weight greater than in recording")
notifyCallBack {
it.onException(
"Recording with weight greater than in recording",
willStartRecorderResult
)
}
return
}
if (weight > currentWeight) {
//只要权重大于当前权重,立即停止当前。
stopRecordingInternal()
}
if (weight == currentWeight) {
if (recorderConfig.recorderId == currentRecorderResult.recorderId) {
notifyCallBack {
it.onException(
"The same recording cannot be started repeatedly",
willStartRecorderResult
)
}
return
} else {
stopRecordingInternal()
}
}
startRecorder(recorderConfig, willStartRecorderResult)
} else {
startRecorder(recorderConfig, willStartRecorderResult)
}
}
private fun startRecorder(
recorderConfig: RecorderConfig,
willStartRecorderResult: RecorderResult
) {
logD("startRecording result ${willStartRecorderResult.toString()}")
iRecorder = when (recorderConfig.recorderOutFormat) {
RecorderOutFormat.MPEG_4, RecorderOutFormat.AMR_WB -> {
JLMediaRecorder()
}
RecorderOutFormat.PCM -> {
JLAudioRecorder()
}
}
val result = iRecorder?.startRecording(recorderConfig)
if (!result.isNullOrEmpty()) {
logD("startRecording result $result")
notifyCallBack {
it.onException(result, willStartRecorderResult)
}
} else {
currentWeight = recorderConfig.weight
notifyCallBack {
it.onStart(willStartRecorderResult)
}
currentRecorderResult = willStartRecorderResult
}
}
private fun isRecording(): Boolean {
return iRecorder?.isRecording ?: false
}
@Synchronized
private fun stopRecordingInternal() {
logD("stopRecordingInternal")
iRecorder?.stopRecording()
currentWeight = -1
iRecorder = null
MediaScannerConnection.scanFile(
this,
arrayOf(currentRecorderResult.recorderFile?.absolutePath),
null,
null
)
notifyCallBack {
it.onStop(currentRecorderResult)
}
}
private fun notifyCallBack(done: (IRecorderCallBack) -> Unit) {
val size = remoteCallbackList.beginBroadcast()
logD("recorded notifyCallBack size $size")
(0 until size).forEach {
done(remoteCallbackList.getBroadcastItem(it))
}
remoteCallbackList.finishBroadcast()
}
}
Points to note here:
Because it is a cross-process service, when starting recording, it may be that multiple apps are started at the same time, or it may be that while one app is recording, another app calls the stop function, so here is the maintenance of the current currentRecorderResult
object A currentWeight
field is also very important. This field is mainly a matter of maintaining priority. As long as there is an instruction higher than the current priority, the recording service will be operated according to the new instruction. notifyCallBack
Call the AIDL callback at the appropriate time to notify the App to do the corresponding operation.
RecorderManager implementation
step 1
Service registration, here starts according to the package name of the main App, all apps are started in this way
fun initialize(context: Context?, serviceConnectState: ((Boolean) -> Unit)? = null) {
mApplicationContext = context?.applicationContext
if (!isServiceConnected) {
this.mServiceConnectState = serviceConnectState
val serviceIntent = Intent()
serviceIntent.`package` = "com.julive.recorder"
serviceIntent.action = "com.julive.audio.service"
val isCanBind = mApplicationContext?.bindService(
serviceIntent,
mConnection,
Context.BIND_AUTO_CREATE
) ?: false
if (!isCanBind) {
logE("isCanBind:$isCanBind")
this.mServiceConnectState?.invoke(false)
bindSelfService()
}
}
}
isCanBind
Yes false
, that is, the main app is not found, this time you need to start your own service
private fun bindSelfService() {
val serviceIntent = Intent(mApplicationContext, RecorderService::class.java)
val isSelfBind =
mApplicationContext?.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)
logE("isSelfBind:$isSelfBind")
}
- step 2
After successful connection
private val mConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
mRecorderService = IRecorderService.Stub.asInterface(service)
mRecorderService?.asBinder()?.linkToDeath(deathRecipient, 0)
isServiceConnected = true
mServiceConnectState?.invoke(true)
}
override fun onServiceDisconnected(name: ComponentName) {
isServiceConnected = false
mRecorderService = null
logE("onServiceDisconnected:name=$name")
}
}
Then you can use mRecorderService
to operate AIDL interfaces, the final call RecorderService
to achieve
//启动
fun startRecording(recorderConfig: RecorderConfig?) {
if (recorderConfig != null)
mRecorderService?.startRecording(recorderConfig)
}
//暂停
fun stopRecording(recorderConfig: RecorderConfig?) {
if (recorderConfig != null)
mRecorderService?.stopRecording(recorderConfig)
}
//是否录音中
fun isRecording(recorderConfig: RecorderConfig?): Boolean {
return mRecorderService?.isRecording(recorderConfig) ?: false
}
Such a complete set of cross-process communication is completed, and there are few code comments. After the code display of this process, you should be able to understand the overall call process. If you do not understand, please feel free to leave a message.
to sum up
Through the past two days, I have a deeper understanding of the cross-process data processing of the AIDL recording service. There are several difficult to deal with in the state maintenance of the recording, and the maintenance of priority. It is actually easy to handle these two points. No more, there is a question message area to communicate.
Welcome to exchange: git source code: https://github.com/ibaozi-cn/Multi-Process-Audio-Recorder
Byte Beat Android Interview Question Set (including answer analysis)