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.

4.8 (4) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. The service checks whether it received an intent to start action.
  2. You initialize mediaProjection to store the information from the audio capture.
  3. You call the method that will do the audio capture.
  4. This is the item you need to return from onStartCommand, which means the service will stay running until something triggers the stop command.
  5. When you trigger onStartCommand with the stop action, you execute the stop capture method.
  6. Then you start the service, but this time, with the non-sticky statement because the service doesn’t need to keep running.
  7. If the action is neither start nor stop, you throw an exception because that’s not expected.
  8. 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 onStartCommandand 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:

  1. You create a constant that handles the audio capture configuration. The three options available for the Usage type are: USAGE_GAME, USAGE_MEDIA and USAGE_UNKNOWN.
  2. Then you set the values for the recording. The ones in this code are standard, but you may want to modify them.
  3. Here, you set the previous two values into the audio record builder so it can start recording.
  4. 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:

  1. You need to ensure an audio capture is really taking place.
  2. Next, you interrupt the audio capture thread. The join is just a method that waits until the thread is fully stopped.
  3. You stop the audio record and release the memory manually.
  4. 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.

Record Cat Sounds screen with a toast displayed

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.

Audio Recording in progress

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:

  1. Add the following code to AndroidManifest.xml: android:allowAudioPlaybackCapture="false".
  2. 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.