У меня есть AudioMixerRepository, содержащий карту нескольких экземпляров ExoPlayer (по одному для каждого звука) для их одновременного воспроизведения. Когда пользователь выбирает звук, он сразу же начинает воспроизводиться в приложении.
Я управляю жизненным циклом этих треков в своей AudioMixerViewModel:
Код: Выделить всё
/**
* When app goes to background and session is not active, pause all the tracks.
*/
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
if (!uiState.value.isSessionActive) {
audioMixerRepository.pauseAll()
}
}
/**
* When app goes to foreground, resume all the tracks.
*/
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
if (!uiState.value.isSessionActive) {
audioMixerRepository.resumeAll()
}
}
/**
* When viewModel is destroyed, release all the tracks.
*/
override fun onCleared() {
super.onCleared()
ProcessLifecycleOwner.get().lifecycle.removeObserver(this)
if (!uiState.value.isSessionActive) {
audioMixerRepository.releaseAll()
}
}
Чтобы добиться этого и поддерживать активность сеанса в фоновом режиме, я реализовал MediaSessionService. Поскольку фактическое микширование звука (несколько экземпляров ExoPlayer) управляется моим репозиторием, я использую "фиктивный проигрыватель" внутри службы. Этот проигрыватель загружает беззвучную звуковую дорожку исключительно для поддержания состояния MediaSession, запуска службы переднего плана (только при нажатии кнопки «Пуск») и отображения системного уведомления. Я использую ForwardingPlayer для перехвата действий воспроизведения/паузы и направления их в свой репозиторий.
Вот мой MediaSessionService:
Код: Выделить всё
class AudioSessionMediaService : MediaSessionService() {
private var mediaSession: MediaSession? = null
private var dummyPlayer: ExoPlayer? = null
private val audioRepository: AudioMixerRepository by inject()
@OptIn(UnstableApi::class)
override fun onCreate() {
super.onCreate()
// 1. Create a dummy player. Its only purpose is to maintain the state of MediaSession
dummyPlayer = ExoPlayer.Builder(this).build().apply {
volume = 0f
repeatMode = Player.REPEAT_MODE_ALL
val rawResourceId = R.raw.waves1
val soundUri = Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.path(rawResourceId.toString()).build()
val mediaItem = MediaItem.Builder()
.setUri(soundUri)
.build()
setMediaItem(mediaItem)
prepare()
}
// 2. Intercept commands
val forwardingPlayer = object : ForwardingPlayer(dummyPlayer!!) {
override fun getAvailableCommands(): Player.Commands {
return super.getAvailableCommands().buildUpon()
.remove(COMMAND_SEEK_TO_NEXT)
.remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
.remove(COMMAND_SEEK_TO_PREVIOUS)
.remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
.build()
}
override fun getDuration(): Long {
return C.TIME_UNSET
}
override fun play() {
super.play()
audioRepository.fadeInAllSounds()
}
override fun pause() {
super.pause()
audioRepository.fadeOutAndPauseAllSounds()
}
override fun stop() {
super.stop()
audioRepository.fadeOutAndPauseAllSounds()
}
}
val callback = object : MediaSession.Callback {
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: MutableList,
): ListenableFuture {
val resolvedItems = mediaItems.map { item ->
item.buildUpon()
.setMediaId(item.mediaId)
.setUri(item.localConfiguration?.uri)
.setMediaMetadata(item.mediaMetadata)
.build()
}.toMutableList()
return Futures.immediateFuture(resolvedItems)
}
}
// 3. Create session
mediaSession = MediaSession.Builder(this, forwardingPlayer)
.setId("MultitrackAudioSession")
.setCallback(callback)
.build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
mediaSession
override fun onDestroy() {
mediaSession?.run {
player.release()
release()
mediaSession = null
}
super.onDestroy()
}
}
Я управляю подключением к службе с помощью синглтона AudioSessionManager. Я инициализирую MediaController асинхронно при запуске этого синглтона. Когда пользователь нажимает «Пуск» в пользовательском интерфейсе, я использую этот MediaController для отправки нового MediaItem с настраиваемым заголовком и изображением выбранного микса:
Код: Выделить всё
class AudioSessionManager(private val context: Context) {
private var mediaControllerFuture: ListenableFuture? = null
private var mediaController: MediaController? = null
init {
val sessionToken = SessionToken(
context,
ComponentName(context, AudioSessionMediaService::class.java)
)
mediaControllerFuture = MediaController.Builder(context, sessionToken).buildAsync()
mediaControllerFuture?.addListener(
{ mediaController = mediaControllerFuture?.get() },
ContextCompat.getMainExecutor(context)
)
}
fun startSession(mixTitle: String, mixImageUri: Uri) {
val rawResourceId = R.raw.waves2
val dummyUri = Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.path(rawResourceId.toString()).build()
val mediaItem = MediaItem.Builder()
.setMediaId("mix-${System.currentTimeMillis()}") // Trying to force an update with a unique ID
.setUri(dummyUri)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(mixTitle)
.setArtworkUri(mixImageUri)
.build()
)
.build()
mediaController?.let { controller ->
if (controller.mediaItemCount > 0) {
controller.replaceMediaItem(0, mediaItem)
} else {
controller.setMediaItem(mediaItem)
}
controller.prepare()
controller.play()
}
}
}
Я знаю о MediaNotification.Provider, но я не реализовал его, поскольку в официальной документации Android указано, что пользовательские уведомления через поставщика игнорируются в API 33+.
Мои вопросы:
- Как заставить системное уведомление Media3 в API 33+ отражать пользовательские MediaMetadata (заголовок и обложку), отправленные через MediaController?
- Как можно Я запрещаю отображение уведомления при инициализации, чтобы оно появлялось только, когда пользователь нажимает кнопку «Пуск»?
Подробнее здесь: https://stackoverflow.com/questions/798 ... om-sound-m
Мобильная версия