Реалізація управління через 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()
}
}
Обложка у сповіщенні: 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 дні.







