mirror of
https://github.com/Michatec/Radio.git
synced 2026-04-01 07:56:27 +02:00
575 lines
22 KiB
Kotlin
575 lines
22 KiB
Kotlin
/*
|
|
* PlayerService.kt
|
|
* Implements the PlayerService class
|
|
* PlayerService is Radio's foreground service that plays radio station audio
|
|
*
|
|
* This file is part of
|
|
* TRANSISTOR - Radio App for Android
|
|
*
|
|
* Copyright (c) 2015-22 - Y20K.org
|
|
* Licensed under the MIT-License
|
|
* http://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
|
|
package com.michatec.radio
|
|
|
|
import android.app.PendingIntent
|
|
import android.app.TaskStackBuilder
|
|
import android.content.*
|
|
import android.media.audiofx.AudioEffect
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.CountDownTimer
|
|
import android.util.Log
|
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
import androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT
|
|
import androidx.media3.common.*
|
|
import androidx.media3.common.util.UnstableApi
|
|
import androidx.media3.datasource.HttpDataSource
|
|
import androidx.media3.exoplayer.DefaultLoadControl
|
|
import androidx.media3.exoplayer.ExoPlayer
|
|
import androidx.media3.exoplayer.analytics.AnalyticsListener
|
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
import androidx.media3.exoplayer.upstream.DefaultAllocator
|
|
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
|
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
|
import androidx.media3.session.*
|
|
import com.google.common.collect.ImmutableList
|
|
import com.google.common.util.concurrent.Futures
|
|
import com.google.common.util.concurrent.ListenableFuture
|
|
import com.michatec.radio.core.Collection
|
|
import com.michatec.radio.helpers.AudioHelper
|
|
import com.michatec.radio.helpers.CollectionHelper
|
|
import com.michatec.radio.helpers.FileHelper
|
|
import com.michatec.radio.helpers.PreferencesHelper
|
|
import kotlinx.coroutines.*
|
|
import kotlinx.coroutines.Dispatchers.Main
|
|
import java.util.*
|
|
|
|
|
|
/*
|
|
* PlayerService class
|
|
*/
|
|
@UnstableApi
|
|
class PlayerService : MediaLibraryService() {
|
|
|
|
/* Define log tag */
|
|
private val TAG: String = PlayerService::class.java.simpleName
|
|
|
|
/* Main class variables */
|
|
private lateinit var player: Player
|
|
private lateinit var mediaLibrarySession: MediaLibrarySession
|
|
private lateinit var sleepTimer: CountDownTimer
|
|
var sleepTimerTimeRemaining: Long = 0L
|
|
private var sleepTimerEndTime: Long = 0L
|
|
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
|
|
private var collection: Collection = Collection()
|
|
private lateinit var metadataHistory: MutableList<String>
|
|
private var bufferSizeMultiplier: Int = PreferencesHelper.loadBufferSizeMultiplier()
|
|
private var playbackRestartCounter: Int = 0
|
|
private var playLastStation: Boolean = false
|
|
private var manuallyCancelledSleepTimer = false
|
|
|
|
|
|
/* Overrides onCreate from Service */
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
// load collection
|
|
collection = FileHelper.readCollection(this)
|
|
// create and register collection changed receiver
|
|
LocalBroadcastManager.getInstance(application).registerReceiver(
|
|
collectionChangedReceiver,
|
|
IntentFilter(Keys.ACTION_COLLECTION_CHANGED)
|
|
)
|
|
// initialize player and session
|
|
initializePlayer()
|
|
initializeSession()
|
|
val notificationProvider: DefaultMediaNotificationProvider = CustomNotificationProvider()
|
|
notificationProvider.setSmallIcon(R.drawable.ic_notification_app_icon_white_24dp)
|
|
setMediaNotificationProvider(notificationProvider)
|
|
// fetch the metadata history
|
|
metadataHistory = PreferencesHelper.loadMetadataHistory()
|
|
}
|
|
|
|
|
|
/* Overrides onDestroy from Service */
|
|
override fun onDestroy() {
|
|
// player.removeAnalyticsListener(analyticsListener)
|
|
player.removeListener(playerListener)
|
|
player.release()
|
|
mediaLibrarySession.release()
|
|
super.onDestroy()
|
|
}
|
|
|
|
|
|
/* Overrides onTaskRemoved from Service */
|
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
if (!player.playWhenReady) {
|
|
stopSelf()
|
|
}
|
|
}
|
|
|
|
|
|
/* Overrides onGetSession from MediaSessionService */
|
|
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession {
|
|
return mediaLibrarySession
|
|
}
|
|
|
|
|
|
/* Initializes the ExoPlayer */
|
|
private fun initializePlayer() {
|
|
val exoPlayer: ExoPlayer = ExoPlayer.Builder(this).apply {
|
|
setAudioAttributes(AudioAttributes.DEFAULT, true)
|
|
setHandleAudioBecomingNoisy(true)
|
|
setLoadControl(createDefaultLoadControl(bufferSizeMultiplier))
|
|
setMediaSourceFactory(
|
|
DefaultMediaSourceFactory(this@PlayerService).setLoadErrorHandlingPolicy(
|
|
loadErrorHandlingPolicy
|
|
)
|
|
)
|
|
}.build()
|
|
exoPlayer.addAnalyticsListener(analyticsListener)
|
|
exoPlayer.addListener(playerListener)
|
|
|
|
// manually add seek to next and seek to previous since headphones issue them and they are translated to next and previous station
|
|
player = object : ForwardingPlayer(exoPlayer) {
|
|
override fun getAvailableCommands(): Player.Commands {
|
|
return super.getAvailableCommands().buildUpon().add(COMMAND_SEEK_TO_NEXT)
|
|
.add(COMMAND_SEEK_TO_PREVIOUS).build()
|
|
}
|
|
|
|
override fun isCommandAvailable(command: Int): Boolean {
|
|
return availableCommands.contains(command)
|
|
}
|
|
|
|
override fun getDuration(): Long {
|
|
return C.TIME_UNSET // this will hide progress bar for HLS stations in the notification
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Initializes the MediaSession */
|
|
private fun initializeSession() {
|
|
val intent = Intent(this, MainActivity::class.java)
|
|
val pendingIntent = TaskStackBuilder.create(this).run {
|
|
addNextIntent(intent)
|
|
getPendingIntent(0, if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_CANCEL_CURRENT)
|
|
}
|
|
|
|
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback).apply {
|
|
setSessionActivity(pendingIntent)
|
|
}.build()
|
|
}
|
|
|
|
|
|
/* Creates a LoadControl - increase buffer size by given factor */
|
|
private fun createDefaultLoadControl(factor: Int): DefaultLoadControl {
|
|
val builder = DefaultLoadControl.Builder()
|
|
builder.setAllocator(DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE))
|
|
builder.setBufferDurationsMs(
|
|
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * factor,
|
|
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * factor,
|
|
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS * factor,
|
|
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS * factor
|
|
)
|
|
return builder.build()
|
|
}
|
|
|
|
|
|
/* Starts sleep timer / adds default duration to running sleeptimer */
|
|
private fun startSleepTimer(selectedTimeMillis: Long) {
|
|
// stop running timer
|
|
if (sleepTimerTimeRemaining > 0L && this::sleepTimer.isInitialized) {
|
|
sleepTimer.cancel()
|
|
}
|
|
|
|
// set the end time of the sleep timer
|
|
sleepTimerEndTime = System.currentTimeMillis() + selectedTimeMillis
|
|
|
|
// initialize timer
|
|
sleepTimer = object : CountDownTimer(selectedTimeMillis, 1000) {
|
|
override fun onFinish() {
|
|
Log.v(TAG, "Sleep timer finished. Sweet dreams.")
|
|
sleepTimerTimeRemaining = 0L
|
|
player.stop()
|
|
}
|
|
|
|
override fun onTick(millisUntilFinished: Long) {
|
|
sleepTimerTimeRemaining = millisUntilFinished
|
|
}
|
|
}
|
|
// start timer
|
|
sleepTimer.start()
|
|
// store timer state
|
|
PreferencesHelper.saveSleepTimerRunning(isRunning = true)
|
|
}
|
|
|
|
|
|
/* Cancels sleep timer */
|
|
private fun cancelSleepTimer() {
|
|
if (this::sleepTimer.isInitialized) {
|
|
if (manuallyCancelledSleepTimer) {
|
|
sleepTimerTimeRemaining = 0L
|
|
sleepTimer.cancel()
|
|
}
|
|
manuallyCancelledSleepTimer = false
|
|
}
|
|
// store timer state
|
|
PreferencesHelper.saveSleepTimerRunning(isRunning = false)
|
|
}
|
|
|
|
|
|
/* Function to cancel the timer manually */
|
|
fun manuallyCancelSleepTimer() {
|
|
manuallyCancelledSleepTimer = true
|
|
cancelSleepTimer()
|
|
}
|
|
|
|
|
|
/* Updates metadata */
|
|
private fun updateMetadata(metadata: String = String()) {
|
|
// get metadata string
|
|
val metadataString: String = metadata.ifEmpty {
|
|
player.currentMediaItem?.mediaMetadata?.artist.toString()
|
|
}
|
|
// remove duplicates
|
|
if (metadataHistory.contains(metadataString)) {
|
|
metadataHistory.removeAll { it == metadataString }
|
|
}
|
|
// append metadata to metadata history
|
|
metadataHistory.add(metadataString)
|
|
// trim metadata list
|
|
if (metadataHistory.size > Keys.DEFAULT_SIZE_OF_METADATA_HISTORY) {
|
|
metadataHistory.removeAt(0)
|
|
}
|
|
// save history
|
|
PreferencesHelper.saveMetadataHistory(metadataHistory)
|
|
}
|
|
|
|
|
|
/* Reads collection of stations from storage using GSON */
|
|
private fun loadCollection(context: Context) {
|
|
Log.v(TAG, "Loading collection of stations from storage")
|
|
CoroutineScope(Main).launch {
|
|
// load collection on background thread
|
|
val deferred: Deferred<Collection> =
|
|
async(Dispatchers.Default) { FileHelper.readCollectionSuspended(context) }
|
|
// wait for result and update collection
|
|
collection = deferred.await()
|
|
// // special case: trigger metadata view update for stations that have no metadata
|
|
// if (player.isPlaying && station.name == getCurrentMetadata()) {
|
|
// station = CollectionHelper.getStation(collection, station.uuid)
|
|
// updateMetadata(null)
|
|
// }
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Custom MediaSession Callback that handles player commands
|
|
*/
|
|
private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
|
|
|
|
override fun onAddMediaItems(
|
|
mediaSession: MediaSession,
|
|
controller: MediaSession.ControllerInfo,
|
|
mediaItems: MutableList<MediaItem>
|
|
): ListenableFuture<List<MediaItem>> {
|
|
val updatedMediaItems: List<MediaItem> =
|
|
mediaItems.map { mediaItem ->
|
|
CollectionHelper.getItem(this@PlayerService, collection, mediaItem.mediaId)
|
|
}
|
|
return Futures.immediateFuture(updatedMediaItems)
|
|
}
|
|
|
|
|
|
override fun onConnect(
|
|
session: MediaSession,
|
|
controller: MediaSession.ControllerInfo
|
|
): MediaSession.ConnectionResult {
|
|
// add custom commands
|
|
val connectionResult: MediaSession.ConnectionResult = super.onConnect(session, controller)
|
|
val builder: SessionCommands.Builder = connectionResult.availableSessionCommands.buildUpon()
|
|
builder.add(SessionCommand(Keys.CMD_START_SLEEP_TIMER, Bundle.EMPTY))
|
|
builder.add(SessionCommand(Keys.CMD_CANCEL_SLEEP_TIMER, Bundle.EMPTY))
|
|
builder.add(SessionCommand(Keys.CMD_REQUEST_SLEEP_TIMER_REMAINING, Bundle.EMPTY))
|
|
builder.add(SessionCommand(Keys.CMD_REQUEST_METADATA_HISTORY, Bundle.EMPTY))
|
|
return MediaSession.ConnectionResult.accept(builder.build(), connectionResult.availablePlayerCommands)
|
|
}
|
|
|
|
override fun onSubscribe(
|
|
session: MediaLibrarySession,
|
|
browser: MediaSession.ControllerInfo,
|
|
parentId: String,
|
|
params: LibraryParams?
|
|
): ListenableFuture<LibraryResult<Void>> {
|
|
val children: List<MediaItem> = CollectionHelper.getChildren(this@PlayerService, collection)
|
|
session.notifyChildrenChanged(browser, parentId, children.size, params)
|
|
return Futures.immediateFuture(LibraryResult.ofVoid())
|
|
}
|
|
|
|
override fun onGetChildren(
|
|
session: MediaLibrarySession,
|
|
browser: MediaSession.ControllerInfo,
|
|
parentId: String,
|
|
page: Int,
|
|
pageSize: Int,
|
|
params: LibraryParams?
|
|
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
|
val children: List<MediaItem> = CollectionHelper.getChildren(this@PlayerService, collection)
|
|
return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
|
|
}
|
|
|
|
override fun onGetLibraryRoot(
|
|
session: MediaLibrarySession,
|
|
browser: MediaSession.ControllerInfo,
|
|
params: LibraryParams?
|
|
): ListenableFuture<LibraryResult<MediaItem>> {
|
|
return if (params?.extras?.containsKey(EXTRA_RECENT) == true) {
|
|
// special case: system requested media resumption via EXTRA_RECENT
|
|
playLastStation = true
|
|
Futures.immediateFuture(LibraryResult.ofItem(CollectionHelper.getRecent(this@PlayerService, collection), params))
|
|
} else {
|
|
Futures.immediateFuture(LibraryResult.ofItem(CollectionHelper.getRootItem(), params))
|
|
}
|
|
}
|
|
|
|
override fun onGetItem(
|
|
session: MediaLibrarySession,
|
|
browser: MediaSession.ControllerInfo,
|
|
mediaId: String
|
|
): ListenableFuture<LibraryResult<MediaItem>> {
|
|
val item: MediaItem = CollectionHelper.getItem(this@PlayerService, collection, mediaId)
|
|
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params = */ null))
|
|
}
|
|
|
|
override fun onCustomCommand(
|
|
session: MediaSession,
|
|
controller: MediaSession.ControllerInfo,
|
|
customCommand: SessionCommand,
|
|
args: Bundle
|
|
): ListenableFuture<SessionResult> {
|
|
when (customCommand.customAction) {
|
|
Keys.CMD_START_SLEEP_TIMER -> {
|
|
val selectedTimeMillis = args.getLong(Keys.SLEEP_TIMER_DURATION)
|
|
startSleepTimer(selectedTimeMillis)
|
|
}
|
|
Keys.CMD_CANCEL_SLEEP_TIMER -> {
|
|
manuallyCancelSleepTimer()
|
|
}
|
|
Keys.CMD_REQUEST_SLEEP_TIMER_REMAINING -> {
|
|
val resultBundle = Bundle()
|
|
resultBundle.putLong(Keys.EXTRA_SLEEP_TIMER_REMAINING, sleepTimerTimeRemaining)
|
|
return Futures.immediateFuture(
|
|
SessionResult(
|
|
SessionResult.RESULT_SUCCESS,
|
|
resultBundle
|
|
)
|
|
)
|
|
}
|
|
Keys.CMD_REQUEST_METADATA_HISTORY -> {
|
|
val resultBundle = Bundle()
|
|
resultBundle.putStringArrayList(
|
|
Keys.EXTRA_METADATA_HISTORY,
|
|
ArrayList(metadataHistory)
|
|
)
|
|
return Futures.immediateFuture(
|
|
SessionResult(
|
|
SessionResult.RESULT_SUCCESS,
|
|
resultBundle
|
|
)
|
|
)
|
|
}
|
|
}
|
|
return super.onCustomCommand(session, controller, customCommand, args)
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
* NotificationProvider to customize Notification actions
|
|
*/
|
|
private inner class CustomNotificationProvider :
|
|
DefaultMediaNotificationProvider(this@PlayerService) {
|
|
override fun getMediaButtons(
|
|
session: MediaSession,
|
|
playerCommands: Player.Commands,
|
|
customLayout: ImmutableList<CommandButton>,
|
|
showPauseButton: Boolean
|
|
): ImmutableList<CommandButton> {
|
|
val seekToPreviousCommandButton = CommandButton.Builder()
|
|
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
|
|
.setIconResId(R.drawable.ic_notification_skip_to_previous_36dp)
|
|
.setEnabled(true)
|
|
.build()
|
|
val playCommandButton = CommandButton.Builder()
|
|
.setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
|
|
.setIconResId(if (player.isPlaying) R.drawable.ic_notification_stop_36dp else R.drawable.ic_notification_play_36dp)
|
|
.setEnabled(true)
|
|
.build()
|
|
val seekToNextCommandButton = CommandButton.Builder()
|
|
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
|
|
.setIconResId(R.drawable.ic_notification_skip_to_next_36dp)
|
|
.setEnabled(true)
|
|
.build()
|
|
val commandButtons: MutableList<CommandButton> = mutableListOf(
|
|
seekToPreviousCommandButton,
|
|
playCommandButton,
|
|
seekToNextCommandButton
|
|
)
|
|
return ImmutableList.copyOf(commandButtons)
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Player.Listener: Called when one or more player states changed.
|
|
*/
|
|
private var playerListener: Player.Listener = object : Player.Listener {
|
|
|
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
super.onIsPlayingChanged(isPlaying)
|
|
// store state of playback
|
|
val currentMediaId: String = player.currentMediaItem?.mediaId ?: String()
|
|
PreferencesHelper.saveIsPlaying(isPlaying)
|
|
PreferencesHelper.saveCurrentStationId(currentMediaId)
|
|
// reset restart counter
|
|
playbackRestartCounter = 0
|
|
// save collection and player state
|
|
|
|
collection = CollectionHelper.savePlaybackState(
|
|
this@PlayerService,
|
|
collection,
|
|
currentMediaId,
|
|
isPlaying
|
|
)
|
|
//updatePlayerState(station, playbackState)
|
|
|
|
if (isPlaying) {
|
|
// playback is active
|
|
} else {
|
|
// cancel sleep timer
|
|
cancelSleepTimer()
|
|
// reset metadata
|
|
updateMetadata()
|
|
|
|
// playback is not active
|
|
// Not playing because playback is paused, ended, suppressed, or the player
|
|
// is buffering, stopped or failed. Check player.getPlayWhenReady,
|
|
// player.getPlaybackState, player.getPlaybackSuppressionReason and
|
|
// player.getPlaybackError for details.
|
|
when (player.playbackState) {
|
|
// player is able to immediately play from its current position
|
|
Player.STATE_READY -> {
|
|
stopSelf()
|
|
}
|
|
// buffering - data needs to be loaded
|
|
Player.STATE_BUFFERING -> {
|
|
stopSelf()
|
|
}
|
|
// player finished playing all media
|
|
Player.STATE_ENDED -> {
|
|
stopSelf()
|
|
}
|
|
// initial state or player is stopped or playback failed
|
|
Player.STATE_IDLE -> {
|
|
stopSelf()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
|
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
|
if (!playWhenReady) {
|
|
when (reason) {
|
|
Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM -> {
|
|
stopSelf()
|
|
}
|
|
else -> {
|
|
stopSelf()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
override fun onPlayerError(error: PlaybackException) {
|
|
super.onPlayerError(error)
|
|
Log.d(TAG, "PlayerError occurred: ${error.errorCodeName}")
|
|
if (error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED || error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT) {
|
|
player.prepare()
|
|
player.play()
|
|
}
|
|
}
|
|
|
|
|
|
override fun onMetadata(metadata: Metadata) {
|
|
super.onMetadata(metadata)
|
|
updateMetadata(AudioHelper.getMetadataString(metadata))
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
* Custom LoadErrorHandlingPolicy that network drop outs
|
|
*/
|
|
private val loadErrorHandlingPolicy: DefaultLoadErrorHandlingPolicy = object: DefaultLoadErrorHandlingPolicy() {
|
|
override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long {
|
|
// try to reconnect every 5 seconds - up to 20 times
|
|
if (loadErrorInfo.errorCount <= Keys.DEFAULT_MAX_RECONNECTION_COUNT && loadErrorInfo.exception is HttpDataSource.HttpDataSourceException) {
|
|
return Keys.RECONNECTION_WAIT_INTERVAL
|
|
// } else {
|
|
// CoroutineScope(Main).launch {
|
|
// player.stop()
|
|
// }
|
|
}
|
|
return C.TIME_UNSET
|
|
}
|
|
|
|
override fun getMinimumLoadableRetryCount(dataType: Int): Int {
|
|
return Int.MAX_VALUE
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Custom receiver that handles Keys.ACTION_COLLECTION_CHANGED
|
|
*/
|
|
private val collectionChangedReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
|
override fun onReceive(context: Context, intent: Intent) {
|
|
if (intent.hasExtra(Keys.EXTRA_COLLECTION_MODIFICATION_DATE)) {
|
|
val date = Date(intent.getLongExtra(Keys.EXTRA_COLLECTION_MODIFICATION_DATE, 0L))
|
|
|
|
if (date.after(collection.modificationDate)) {
|
|
Log.v(TAG, "PlayerService - reload collection after broadcast received.")
|
|
loadCollection(context)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Custom AnalyticsListener that enables AudioFX equalizer integration
|
|
*/
|
|
private val analyticsListener = object : AnalyticsListener {
|
|
override fun onAudioSessionIdChanged(
|
|
eventTime: AnalyticsListener.EventTime,
|
|
audioSessionId: Int
|
|
) {
|
|
super.onAudioSessionIdChanged(eventTime, audioSessionId)
|
|
// integrate with system equalizer (AudioFX)
|
|
val intent = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
|
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
|
|
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
|
|
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
|
sendBroadcast(intent)
|
|
}
|
|
}
|
|
}
|