Audio Playback Capture in Android X
Learn how to integrate the Android Playback Capture API into your app, allowing you to record and play back audio from other apps. By Evana Margain Puig.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Audio Playback Capture in Android X
30 mins
- Getting Started
- What Is Audio Playback Capture API?
- Managing Permissions for Audio Capture
- Requesting Permissions
- Checking for Permissions in Your Activity
- Changing Permissions
- Recording Audio From Other Apps
- Requesting Permission Before a Capturing Session
- Checking Permissions
- Overriding onActivityResult
- Creating MediaCaptureService
- Starting the Service and Getting Notifications
- Triggering the Audio Capture Playback
- Starting the Playback Audio Capture
- Stopping the Audio Capture
- Connecting the Service
- Adding Your Service to the Android Manifest
- Disabling Audio Playback Capture
- Listening to Recorded Audio
- Getting the Files From Memory
- Listening to Your Files
- Where to Go From Here?
Triggering the Audio Capture Playback
When you start a service, it executes onStartCommand
. In this case, you need to override that method to provide what to do when the service starts.
Replace the return statement inside onStartCommand
with:
// 1
return if (intent != null) {
when (intent.action) {
ACTION_START -> {
// 2
mediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, intent.getParcelableExtra(EXTRA_RESULT_DATA)!!) as MediaProjection
// 3
startAudioCapture()
// 4
Service.START_STICKY
}
// 5
ACTION_STOP -> {
stopAudioCapture()
// 6
Service.START_NOT_STICKY
}
// 7
else -> throw IllegalArgumentException("Unexpected action received: ${intent.action}")
}
} else {
// 8
Service.START_NOT_STICKY
}
Now, break down the code above:
- The service checks whether it received an intent to start action.
- You initialize
mediaProjection
to store the information from the audio capture. - You call the method that will do the audio capture.
- This is the item you need to return from
onStartCommand
, which means the service will stay running until something triggers the stop command. - When you trigger
onStartCommand
with the stop action, you execute the stop capture method. - Then you start the service, but this time, with the non-sticky statement because the service doesn’t need to keep running.
- If the action is neither start nor stop, you throw an exception because that’s not expected.
- Finally, if there is no intent, you also start the service with the non-sticky flag, so it will stop after
onStartCommand
finishes.
When you build and run the app at this point, you’ll see no change. That’s because even though you created the service, record fragment isn’t using it yet.
You have two more functions to implement in your service before it works correctly.
Starting the Playback Audio Capture
The first thing you’ll implement in this section is startAudioCapture
. Locate it just below onStartCommand
and add:
// 1
val config = AudioPlaybackCaptureConfiguration.Builder(mediaProjection!!)
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
.build()
// 2
val audioFormat = AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(8000)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.build()
// 3
audioRecord = AudioRecord.Builder()
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(BUFFER_SIZE_IN_BYTES)
.setAudioPlaybackCaptureConfig(config)
.build()
audioRecord!!.startRecording()
// 4
audioCaptureThread = thread(start = true) {
val outputFile = createAudioFile()
Log.d(LOG_TAG, "Created file for capture target: ${outputFile.absolutePath}")
writeAudioToFile(outputFile)
}
Again, this is a large function. Here’s what you’re doing with it:
- You create a constant that handles the audio capture configuration. The three options available for the
Usage
type are:USAGE_GAME
,USAGE_MEDIA
andUSAGE_UNKNOWN
. - Then you set the values for the recording. The ones in this code are standard, but you may want to modify them.
- Here, you set the previous two values into the audio record builder so it can start recording.
- Finally, the output of the recording goes through to two of the functions you already have in the service. One creates an audio file, while the other writes it to the phone memory.
If you want to know more about the usage
attributes, here’s a brief description of each:
- USAGE_MEDIA: For media like music or movie soundtracks.
- USAGE_GAME: For game audio.
- USAGE_UNKNOWN: Use when you don’t know what type of audio you’ll record.
The function you just created also requires some imports:
import android.media.AudioPlaybackCaptureConfiguration
import android.media.AudioAttributes
import android.media.AudioFormat
import kotlin.concurrent.thread
Build and run to verify everything runs correctly. Click the START AUDIO CAPTURE and STOP AUDIO CAPTURE buttons and you’ll still see the toasts. That’s because the service isn’t attached to RecordingFragment
yet.
Stopping the Audio Capture
Great! You can now start the recording and save it to a file in the device. But you still need to be able to stop it — otherwise, it will keep going forever.
To implement this feature, add this code to stopAudioCapture
:
// 1
requireNotNull(mediaProjection) { "Tried to stop audio capture, but there was no ongoing capture in place!" }
// 2
audioCaptureThread.interrupt()
audioCaptureThread.join()
// 3
audioRecord!!.stop()
audioRecord!!.release()
audioRecord = null
// 4
mediaProjection!!.stop()
stopSelf()
To stop the audio capture, you need to handle several things. As with the previous functions, here’s a breakdown of the issue to understand it:
- You need to ensure an audio capture is really taking place.
- Next, you interrupt the audio capture thread. The join is just a method that waits until the thread is fully stopped.
- You stop the audio record and release the memory manually.
- Finally, you stop the media projection and the service itself.
Great, now your service is finally complete! However, you still have to connect the service to the fragment.
Connecting the Service
Now that your service is complete, you need to connect it to the UI.
Locate onActivityResult
in RecordFragment.kt and add the code below inside if (resultCode == Activity.RESULT_OK)
:
val audioCaptureIntent = Intent(requireContext(), MediaCaptureService::class.java).apply {
action = MediaCaptureService.ACTION_START
putExtra(MediaCaptureService.EXTRA_RESULT_DATA, data!!)
}
ContextCompat.startForegroundService(requireContext(), audioCaptureIntent)
setButtonsEnabled(isCapturingAudio = true)
Then, in stopCapturing, you’ll also need to call the stop method:
ContextCompat.startForegroundService(requireContext(), Intent(requireContext(), MediaCaptureService::class.java).apply {
action = MediaCaptureService.ACTION_STOP
})
setButtonsEnabled(isCapturingAudio = false)
You used setButtonsEnabled
in both the start and stop methods above. This method will enable and disable the play buttons.
Next, implement this method:
private fun setButtonsEnabled(isCapturingAudio: Boolean) {
button_start_recording.isEnabled = !isCapturingAudio
button_stop_recording.isEnabled = isCapturingAudio
}
Android Studio will also ask you to import the service. Add it at the top of the file:
import com.raywenderlich.android.cataudio.service.MediaCaptureService
Build and run and… the app still doesn’t work. For now, just verify your app is running and works as it did before. The only notable difference now is that one of the buttons is disabled whenever the other is enabled.
One more thing, services have to be declared in the Manifest just like activities. You’ll do that next:
Adding Your Service to the Android Manifest
Open AndroidManifest.xml and, inside the application tags, add:
<service
android:name=".service.MediaCaptureService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaProjection"
tools:targetApi="q" />
Build and run. It’s finally working! You’ll notice a red icon in the top-right corner of the phone, close to where you find the clock. This icon indicates that casting is taking place.
Now, go to another app and capture Cat Sounds!
Disabling Audio Playback Capture
Something important to consider is that some apps disable Audio Playback Capture. Apps that target Android 28 need to manually opt-in for audio capture for your app to use it.
If you have an app with content that you don’t want others to record, you can use two methods to restrict it:
- Add the following code to AndroidManifest.xml:
android:allowAudioPlaybackCapture="false"
. - If you have specific audio you don’t want other apps to capture, set its capture policy to
AudioManager.setAllowedCapturePolicy(ALLOW_CAPTURE_BY_SYSTEM)
before playing it.