Реализация управления воспроизведением через Lock Screen и Control Center
Медиаконтролы на экране блокировки — не опциональная фича, а стандарт ожидания для любого медиаплеера. Без них пользователь вынужден разблокировать телефон, найти приложение и нажать паузу вместо того, чтобы сделать это в одно касание прямо на Lock Screen.
iOS: MPNowPlayingInfoCenter и MPRemoteCommandCenter
Два независимых объекта: первый отвечает за метаданные (что показывать), второй — за команды (что делать при нажатии кнопок).
// Метаданные
var nowPlayingInfo: [String: Any] = [:]
nowPlayingInfo[MPMediaItemPropertyTitle] = track.title
nowPlayingInfo[MPMediaItemPropertyArtist] = track.artist
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime().seconds
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = track.duration
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate // 0.0 пауза, 1.0 игра
// Обложка — асинхронно загружаем изображение
let artwork = MPMediaItemArtwork(boundsSize: CGSize(width: 300, height: 300)) { size in
return self.trackArtworkImage ?? UIImage(named: "placeholder")!
}
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
MPNowPlayingInfoPropertyElapsedPlaybackTime — текущая позиция в треке. Если не обновлять при поиске (seek), прогресс-бар на Lock Screen будет расходиться с реальностью. Обновляем после каждого seek и каждые 5–10 секунд через таймер.
// Команды
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { [weak self] _ in
self?.player.play()
return .success
}
commandCenter.pauseCommand.addTarget { [weak self] _ in
self?.player.pause()
return .success
}
commandCenter.nextTrackCommand.addTarget { [weak self] _ in
self?.playNext()
return .success
}
commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
guard let e = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
self?.player.seek(to: CMTime(seconds: e.positionTime, preferredTimescale: 600))
return .success
}
changePlaybackPositionCommand — ползунок на Lock Screen. Без него пользователь не может перемотать, не открывая приложение.
Команды нужно включать/отключать по контексту: если в плейлисте один трек — commandCenter.nextTrackCommand.isEnabled = false.
Android: MediaSession и MediaNotification
val mediaSession = MediaSession.Builder(context, player)
.setCallback(object : MediaSession.Callback {
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo) =
MediaSession.ConnectionResult.accept(
SessionCommands.EMPTY,
Player.Commands.Builder().addAllCommands().build()
)
})
.build()
media3 автоматически создаёт уведомление с медиаконтролами при использовании MediaSessionService. Кастомизация кнопок через DefaultMediaNotificationProvider:
class CustomNotificationProvider(context: Context) : DefaultMediaNotificationProvider(context) {
override fun getMediaButtons(
session: MediaSession, playerCommands: Player.Commands,
customLayout: ImmutableList<CommandButton>, showPauseButton: Boolean
): ImmutableList<CommandButton> {
// Добавляем кнопку "Избранное" рядом с play/pause
return super.getMediaButtons(session, playerCommands, customLayout, showPauseButton)
.toMutableList().apply { add(favoriteButton) }.toImmutableList()
}
}
На Android обложка в уведомлении: MediaMetadata.Builder().setArtworkUri(uri).build() — система сама загружает изображение по URI. Для избежания ANR при загрузке обложки через сеть — подгружаем через Coil или Glide в CoroutineScope(Dispatchers.IO), передаём готовый Bitmap через setArtworkData.
Flutter
audio_service (pub.dev) — стандартный пакет. Создаём AudioHandler, регистрируем в AudioService.init(). Обработчики команд: onPlay, onPause, onSkipToNext, onSeekTo. Метаданные — mediaItem в AudioHandler.
Сроки
Медиаконтролы на iOS — 1 день (весь объём — настройка двух объектов и обновление метаданных). Android с кастомным уведомлением — 1.5 дня. Flutter — 1–2 дня.







