Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Listening to changes of current audio output #2080

Open
Enjot opened this issue Jan 27, 2025 · 4 comments
Open

Listening to changes of current audio output #2080

Enjot opened this issue Jan 27, 2025 · 4 comments
Assignees

Comments

@Enjot
Copy link

Enjot commented Jan 27, 2025

Use case description

I’m developing an app that plays audio for users in a museum (like a virtual tour guide). The app needs to detect the current audio output device so it can automatically adjust the maximum volume. Specifically:

  • Speaker mode: If the user is listening through the phone’s built-in speaker, the app lowers the volume (similar to a voice call), so it doesn’t disturb other visitors.
  • Headphones (wired or wireless): The volume can be restored to normal or maximum.

Proposed solution

Add an API that allows apps to listen to changes of current audio output (e.g., speaker ↔ headphones ↔ Bluetooth).
This would enable apps to accurately respond in real time and adjust their audio behavior accordingly.
Users can change it here:
Image

Alternatives considered

  • Manually detecting routing changes with current Android APIs (e.g., AudioManager, AudioDeviceCallback, MediaRouter or silent AudioTrack with AudioRouting) has proven unreliable or inconsistent when the user switches audio outputs manually without disconnecting headphones.
  • AudioBecomingNoisy doesn't trigger when the user switches audio outputs manually and works only in one direction (from headphones to speaker).
  • Using C.USAGE_VOICE_COMMUNICATION can mimic call-like audio, but it causes problems:
    • Google Assistant can’t stop playback while listening for voice.
    • Incoming/outgoing phone calls may not automatically pause the playback.
    • Other media apps might fail to stop or duck this playback.

Given these limitations, an official, reliable listener/callback for audio output routing changes in Media3/ExoPlayer would greatly simplify the development of apps with dynamic audio output requirements.

@Enjot
Copy link
Author

Enjot commented Jan 27, 2025

I got a working solution. I leave it here.

I found there is an AndroidX version of MediaRouter

implementation("androidx.mediarouter:mediarouter:1.7.0")

I created a custom type for my solution:

sealed interface AudioOutputType {
    data object Headphones : AudioOutputType
    data object Speaker : AudioOutputType
    data object Unknown : AudioOutputType
}

Solution:

class AudioRouteMonitor(private val context: Context) {

    private val mediaRouter: MediaRouter by lazy { MediaRouter.getInstance(context) }
    private var callback: ((AudioOutputType) -> Unit)? = null

    private val selector = MediaRouteSelector.Builder()
        .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
        .build()

    private val mediaRouterCallback = object : MediaRouter.Callback() {
        override fun onRouteSelected(router: MediaRouter, route: RouteInfo, reason: Int) {
            handleRouteChange(route)
        }
    }

    fun startMonitoring(outputChangeCallback: (AudioOutputType) -> Unit) {
        callback = outputChangeCallback
        mediaRouter.addCallback(selector, mediaRouterCallback,
            MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN)
        handleRouteChange(mediaRouter.selectedRoute)
    }

    fun stopMonitoring() {
        mediaRouter.removeCallback(mediaRouterCallback)
        callback = null
    }

    private fun handleRouteChange(route: RouteInfo) {
        val outputType = when {
            isSpeakerRoute(route) -> AudioOutputType.Speaker
            isHeadphonesRoute(route) -> AudioOutputType.Headphones
            else -> AudioOutputType.Unknown
        }
        Timber.d("Active audio route changed to: $outputType")
        callback?.invoke(outputType)
    }

    private fun isSpeakerRoute(route: RouteInfo): Boolean {
        if (route.isDefault) return true
        return listOf(
            RouteInfo.DEVICE_TYPE_BUILTIN_SPEAKER,
            RouteInfo.DEVICE_TYPE_REMOTE_SPEAKER,
            RouteInfo.DEVICE_TYPE_SMARTPHONE
        )
            .contains(route.deviceType)
    }

    private fun isHeadphonesRoute(route: RouteInfo): Boolean {
        return listOf(
            RouteInfo.DEVICE_TYPE_BLUETOOTH_A2DP,
            RouteInfo.DEVICE_TYPE_WIRED_HEADPHONES,
            RouteInfo.DEVICE_TYPE_WIRED_HEADSET,
            RouteInfo.DEVICE_TYPE_USB_HEADSET,
            RouteInfo.DEVICE_TYPE_BLE_HEADSET,
        )
            .contains(route.deviceType)
    }
}

I call this class in my MediaSessionService lifecycle methods, according to documentation: MediaRouter overview | Android media | Android Developers

@microkatz microkatz self-assigned this Jan 28, 2025
@microkatz
Copy link
Contributor

@Enjot

Glad that you were able to create a working solution using the MediaRouter APIs!

Closing the thread as completed. Please feel free to create a new issue if you have additional questions.

@tonihei
Copy link
Collaborator

tonihei commented Jan 28, 2025

For future readers: The approach with MediaRouter isn't necessarily reliable in all situations. The audio track created by ExoPlayer is routed according to its audio attributes and manual overrides (e.g. by using ExoPlayer.setPreferredAudioDevice). The only source of truth for the real audio track is the routedDevice field in the audioCapabilitiesReceiver of DefaultAudioSink, which is not currently exposed to users of ExoPlayer. Re-opening the issue to mark it as a feature request to expose this information.

@tonihei tonihei reopened this Jan 28, 2025
@Tolriq
Copy link
Contributor

Tolriq commented Jan 28, 2025

@tonihei That probably explains #787 and #1397 and a few others.

I do not think it's currently logged anywhere? Anyway to at least have that as a first step?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants