feat: auth screen with auto-redirect, sync favorites/history with backend
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
package com.radiola.service
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioDeviceCallback
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.ForwardingPlayer
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Metadata
|
||||
import androidx.media3.common.Player
|
||||
import android.util.Log
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.extractor.metadata.icy.IcyInfo
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -22,7 +32,45 @@ class PlayerController @Inject constructor(
|
||||
private val _currentStationPrefix = MutableStateFlow<String?>(null)
|
||||
val currentStationPrefix: StateFlow<String?> = _currentStationPrefix
|
||||
|
||||
val exoPlayer: ExoPlayer = ExoPlayer.Builder(context)
|
||||
private val _icyTitle = MutableStateFlow<String?>(null)
|
||||
val icyTitle: StateFlow<String?> = _icyTitle.asStateFlow()
|
||||
|
||||
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
var onSkipToNext: (() -> Unit)? = null
|
||||
var onSkipToPrevious: (() -> Unit)? = null
|
||||
|
||||
private val audioDeviceCallback = object : AudioDeviceCallback() {
|
||||
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
|
||||
val addedPlayback = addedDevices?.any { it.isPlaybackDevice() } == true
|
||||
if (addedPlayback && _currentStationPrefix.value != null && !exoPlayer.isPlaying) {
|
||||
Log.d("PlayerController", "Playback device connected → resume")
|
||||
exoPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
|
||||
val removedPlayback = removedDevices?.any { it.isPlaybackDevice() } == true
|
||||
if (removedPlayback && exoPlayer.isPlaying) {
|
||||
Log.d("PlayerController", "Playback device removed → pause")
|
||||
exoPlayer.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AudioDeviceInfo.isPlaybackDevice(): Boolean {
|
||||
return isSink && (
|
||||
type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
|
||||
type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES ||
|
||||
type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
|
||||
type == AudioDeviceInfo.TYPE_USB_HEADSET ||
|
||||
type == AudioDeviceInfo.TYPE_USB_DEVICE ||
|
||||
type == AudioDeviceInfo.TYPE_BLE_HEADSET ||
|
||||
type == AudioDeviceInfo.TYPE_BLE_SPEAKER
|
||||
)
|
||||
}
|
||||
|
||||
private val exoPlayer: ExoPlayer = ExoPlayer.Builder(context)
|
||||
.setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
@@ -37,27 +85,106 @@ class PlayerController @Inject constructor(
|
||||
override fun onIsPlayingChanged(playing: Boolean) {
|
||||
_isPlaying.value = playing
|
||||
}
|
||||
|
||||
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
|
||||
val title = mediaMetadata.title?.toString()
|
||||
if (!title.isNullOrBlank()) {
|
||||
Log.d("PlayerController", "MediaMetadata title: $title")
|
||||
_icyTitle.value = title
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMetadata(metadata: Metadata) {
|
||||
Log.d("PlayerController", "onMetadata called, length=${metadata.length()}")
|
||||
for (i in 0 until metadata.length()) {
|
||||
val entry = metadata.get(i)
|
||||
Log.d("PlayerController", "Metadata entry[$i]: ${entry::class.java.simpleName}")
|
||||
when (entry) {
|
||||
is IcyInfo -> {
|
||||
Log.d("PlayerController", "IcyInfo title='${entry.title}', url='${entry.url}', raw='${entry.rawMetadata}'")
|
||||
entry.title?.let {
|
||||
if (it.isNotBlank()) {
|
||||
_icyTitle.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun play(url: String, stationPrefix: String) {
|
||||
val mediaItem = MediaItem.fromUri(url)
|
||||
val player: Player = object : ForwardingPlayer(exoPlayer) {
|
||||
override fun getAvailableCommands(): Player.Commands {
|
||||
return super.getAvailableCommands()
|
||||
.buildUpon()
|
||||
.add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
|
||||
.add(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun seekToNextMediaItem() {
|
||||
onSkipToNext?.invoke() ?: super.seekToNextMediaItem()
|
||||
}
|
||||
|
||||
override fun seekToPreviousMediaItem() {
|
||||
onSkipToPrevious?.invoke() ?: super.seekToPreviousMediaItem()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null)
|
||||
}
|
||||
|
||||
fun play(url: String, stationPrefix: String, stationName: String) {
|
||||
Log.d("PlayerController", "play() called with url=$url prefix=$stationPrefix")
|
||||
_icyTitle.value = null
|
||||
val mediaItem = MediaItem.Builder()
|
||||
.setUri(url)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setTitle(stationName)
|
||||
.setArtist("")
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
exoPlayer.setMediaItem(mediaItem)
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.play()
|
||||
_currentStationPrefix.value = stationPrefix
|
||||
}
|
||||
|
||||
fun updateMetadata(song: String, artist: String, coverUrl: String, stationName: String) {
|
||||
val currentMediaItem = exoPlayer.currentMediaItem ?: return
|
||||
val artworkUri = coverUrl.takeIf { it.isNotBlank() }?.let { Uri.parse(it) }
|
||||
val updatedMediaItem = currentMediaItem.buildUpon()
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setTitle(song)
|
||||
.setArtist("$artist • $stationName")
|
||||
.setAlbumTitle(stationName)
|
||||
.setArtworkUri(artworkUri)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
exoPlayer.replaceMediaItem(0, updatedMediaItem)
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
exoPlayer.pause()
|
||||
}
|
||||
|
||||
fun play() {
|
||||
exoPlayer.play()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
exoPlayer.stop()
|
||||
_currentStationPrefix.value = null
|
||||
}
|
||||
|
||||
fun release() {
|
||||
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
|
||||
exoPlayer.release()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user