mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-17 17:52:23 +03:00
Merge branch 'fix_jukebox' into 'develop'
Fix Jukebox handling Closes #798 See merge request ultrasonic/ultrasonic!813
This commit is contained in:
commit
8a093befbf
@ -64,6 +64,13 @@
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.JukeboxMediaPlayer"
|
||||
android:label="Ultrasonic Jukebox Media Player Service"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
|
||||
<service android:name=".playback.PlaybackService"
|
||||
android:label="@string/common.appname"
|
||||
|
@ -270,7 +270,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
val isVolumeAdjust = isVolumeDown || isVolumeUp
|
||||
val isJukebox = mediaPlayerController.isJukeboxEnabled
|
||||
if (isVolumeAdjust && isJukebox) {
|
||||
mediaPlayerController.adjustJukeboxVolume(isVolumeUp)
|
||||
mediaPlayerController.adjustVolume(isVolumeUp)
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
@ -291,7 +291,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
R.id.menu_exit -> {
|
||||
setResult(Constants.RESULT_CLOSE_ALL)
|
||||
mediaPlayerController.stopJukeboxService()
|
||||
mediaPlayerController.onDestroy()
|
||||
finish()
|
||||
exit()
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package org.moire.ultrasonic.di
|
||||
import org.koin.dsl.module
|
||||
import org.moire.ultrasonic.service.Downloader
|
||||
import org.moire.ultrasonic.service.ExternalStorageMonitor
|
||||
import org.moire.ultrasonic.service.JukeboxMediaPlayer
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
import org.moire.ultrasonic.service.PlaybackStateSerializer
|
||||
@ -12,7 +11,6 @@ import org.moire.ultrasonic.service.PlaybackStateSerializer
|
||||
* This Koin module contains the registration of classes related to the media player
|
||||
*/
|
||||
val mediaPlayerModule = module {
|
||||
single { JukeboxMediaPlayer() }
|
||||
single { MediaPlayerLifecycleSupport() }
|
||||
single { PlaybackStateSerializer() }
|
||||
single { ExternalStorageMonitor() }
|
||||
|
@ -35,10 +35,8 @@ import android.widget.Toast
|
||||
import android.widget.ViewFlipper
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.session.SessionResult
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
@ -363,6 +361,7 @@ class PlayerFragment :
|
||||
// Use launch to ensure running it in the main thread
|
||||
launch {
|
||||
onPlaylistChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ -373,12 +372,6 @@ class PlayerFragment :
|
||||
}
|
||||
}
|
||||
|
||||
mediaPlayerController.controller?.addListener(object : Player.Listener {
|
||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
})
|
||||
|
||||
// Query the Jukebox state in an IO Context
|
||||
ioScope.launch(CommunicationError.getHandler(context)) {
|
||||
try {
|
||||
@ -671,9 +664,7 @@ class PlayerFragment :
|
||||
|
||||
val isStarred = track.starred
|
||||
|
||||
mediaPlayerController.controller?.setRating(
|
||||
HeartRating(!isStarred)
|
||||
)?.let {
|
||||
mediaPlayerController.toggleSongStarred()?.let {
|
||||
Futures.addCallback(
|
||||
it,
|
||||
object : FutureCallback<SessionResult> {
|
||||
@ -882,7 +873,7 @@ class PlayerFragment :
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val pos = viewHolder.bindingAdapterPosition
|
||||
val item = mediaPlayerController.controller?.getMediaItemAt(pos)
|
||||
val item = mediaPlayerController.getMediaItemAt(pos)
|
||||
mediaPlayerController.removeFromPlaylist(pos)
|
||||
|
||||
val songRemoved = String.format(
|
||||
@ -1073,9 +1064,9 @@ class PlayerFragment :
|
||||
private fun seek(forward: Boolean) {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
if (forward) {
|
||||
mediaPlayerController.controller?.seekForward()
|
||||
mediaPlayerController.seekForward()
|
||||
} else {
|
||||
mediaPlayerController.controller?.seekBack()
|
||||
mediaPlayerController.seekBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||
isStarted = false
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
|
||||
// Clear Koin
|
||||
UApp.instance!!.shutdownKoin()
|
||||
}
|
||||
|
||||
private val resolver: ResolvingDataSource.Resolver = ResolvingDataSource.Resolver {
|
||||
|
@ -10,7 +10,6 @@ package org.moire.ultrasonic.service
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
@ -22,10 +21,9 @@ import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.activity.NavigationActivity
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.SimpleServiceBinder
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
@ -116,7 +114,7 @@ class DownloadService : Service() {
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setShowWhen(false)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(getPendingIntentForContent())
|
||||
.setContentIntent(Util.getPendingIntentToShowPlayer(this))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
}
|
||||
|
||||
@ -156,18 +154,6 @@ class DownloadService : Service() {
|
||||
return notificationBuilder.build()
|
||||
}
|
||||
|
||||
private fun getPendingIntentForContent(): PendingIntent {
|
||||
val intent = Intent(this, NavigationActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// needed starting Android 12 (S = 31)
|
||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
intent.putExtra(Constants.INTENT_SHOW_PLAYER, true)
|
||||
return PendingIntent.getActivity(this, 0, intent, flags)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
companion object {
|
||||
|
||||
|
@ -55,7 +55,6 @@ class Downloader(
|
||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
private val mediaController: MediaPlayerController by inject()
|
||||
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
|
||||
|
||||
var started: Boolean = false
|
||||
var shouldStop: Boolean = false
|
||||
@ -163,7 +162,7 @@ class Downloader(
|
||||
return
|
||||
}
|
||||
|
||||
if (jukeboxMediaPlayer.isEnabled || !Util.isNetworkConnected()) {
|
||||
if (JukeboxMediaPlayer.running.get() || !Util.isNetworkConnected()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -6,14 +6,44 @@
|
||||
*/
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_PAUSE
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_PLAY
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
||||
import android.view.KeyEvent.KEYCODE_MEDIA_STOP
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.DeviceInfo
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.session.MediaSession
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
@ -21,62 +51,399 @@ import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.math.roundToInt
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
|
||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||
import org.moire.ultrasonic.playback.MediaNotificationProvider
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.Util.getPendingIntentToShowPlayer
|
||||
import org.moire.ultrasonic.util.Util.sleepQuietly
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import timber.log.Timber
|
||||
|
||||
private const val STATUS_UPDATE_INTERVAL_SECONDS = 5L
|
||||
private const val SEEK_INCREMENT_SECONDS = 5L
|
||||
private const val SEEK_START_AFTER_SECONDS = 5
|
||||
|
||||
/**
|
||||
* Provides an asynchronous interface to the remote jukebox on the Subsonic server.
|
||||
*
|
||||
* TODO: Report warning if queue fills up.
|
||||
* TODO: Create shutdown method?
|
||||
* TODO: Disable repeat.
|
||||
* TODO: Persist RC state?
|
||||
* TODO: Minimize status updates.
|
||||
*/
|
||||
class JukeboxMediaPlayer {
|
||||
@Suppress("TooManyFunctions")
|
||||
class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
private val tasks = TaskQueue()
|
||||
private val executorService = Executors.newSingleThreadScheduledExecutor()
|
||||
private var statusUpdateFuture: ScheduledFuture<*>? = null
|
||||
private val timeOfLastUpdate = AtomicLong()
|
||||
private var jukeboxStatus: JukeboxStatus? = null
|
||||
private var previousJukeboxStatus: JukeboxStatus? = null
|
||||
private var gain = 0.5f
|
||||
private var volumeToast: VolumeToast? = null
|
||||
private val running = AtomicBoolean()
|
||||
private var serviceThread: Thread? = null
|
||||
private var enabled = false
|
||||
|
||||
// TODO: These create circular references, try to refactor
|
||||
private val mediaPlayerControllerLazy = inject<MediaPlayerController>(
|
||||
MediaPlayerController::class.java
|
||||
)
|
||||
private var listeners: MutableList<Player.Listener> = mutableListOf()
|
||||
private val playlist: MutableList<MediaItem> = mutableListOf()
|
||||
private var currentIndex: Int = 0
|
||||
private val notificationProvider = MediaNotificationProvider(applicationContext())
|
||||
private lateinit var mediaSession: MediaSession
|
||||
private lateinit var notificationManagerCompat: NotificationManagerCompat
|
||||
|
||||
fun startJukeboxService() {
|
||||
if (running.get()) {
|
||||
return
|
||||
}
|
||||
@Suppress("MagicNumber")
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (running.get()) return
|
||||
running.set(true)
|
||||
|
||||
tasks.clear()
|
||||
updatePlaylist()
|
||||
stop()
|
||||
|
||||
startFuture?.set(this)
|
||||
|
||||
startProcessTasks()
|
||||
|
||||
notificationManagerCompat = NotificationManagerCompat.from(this)
|
||||
mediaSession = MediaSession.Builder(applicationContext(), this)
|
||||
.setId("jukebox")
|
||||
.setSessionActivity(getPendingIntentToShowPlayer(this))
|
||||
.build()
|
||||
val notification = notificationProvider.createNotification(
|
||||
mediaSession,
|
||||
ImmutableList.of(),
|
||||
JukeboxNotificationActionFactory()
|
||||
) {}
|
||||
|
||||
if (Util.SDK_INT >= 29) {
|
||||
startForeground(
|
||||
notification.notificationId,
|
||||
notification.notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
)
|
||||
} else {
|
||||
startForeground(
|
||||
notification.notificationId, notification.notification
|
||||
)
|
||||
}
|
||||
|
||||
Timber.d("Started Jukebox Service")
|
||||
}
|
||||
|
||||
fun stopJukeboxService() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
if (Intent.ACTION_MEDIA_BUTTON != intent?.action) return START_STICKY
|
||||
|
||||
val extras = intent.extras
|
||||
if ((extras != null) && extras.containsKey(Intent.EXTRA_KEY_EVENT)) {
|
||||
val event = extras.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
||||
when (event?.keyCode) {
|
||||
KEYCODE_MEDIA_PLAY -> play()
|
||||
KEYCODE_MEDIA_PAUSE -> stop()
|
||||
KEYCODE_MEDIA_STOP -> stop()
|
||||
KEYCODE_MEDIA_PLAY_PAUSE -> if (isPlaying) stop() else play()
|
||||
KEYCODE_MEDIA_PREVIOUS -> seekToPrevious()
|
||||
KEYCODE_MEDIA_NEXT -> seekToNext()
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (!running.get()) return
|
||||
running.set(false)
|
||||
|
||||
sleepQuietly(1000)
|
||||
if (serviceThread != null) {
|
||||
serviceThread!!.interrupt()
|
||||
}
|
||||
|
||||
tasks.clear()
|
||||
stop()
|
||||
stopForeground(true)
|
||||
mediaSession.release()
|
||||
|
||||
super.onDestroy()
|
||||
Timber.d("Stopped Jukebox Service")
|
||||
}
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun requestStop() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
val notification = notificationProvider.createNotification(
|
||||
mediaSession,
|
||||
ImmutableList.of(),
|
||||
JukeboxNotificationActionFactory()
|
||||
) {}
|
||||
notificationManagerCompat.notify(notification.notificationId, notification.notification)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val running = AtomicBoolean()
|
||||
private var startFuture: SettableFuture<JukeboxMediaPlayer>? = null
|
||||
|
||||
@JvmStatic
|
||||
fun requestStart(): ListenableFuture<JukeboxMediaPlayer>? {
|
||||
if (running.get()) return null
|
||||
startFuture = SettableFuture.create()
|
||||
val context = applicationContext()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(
|
||||
Intent(context, JukeboxMediaPlayer::class.java)
|
||||
)
|
||||
} else {
|
||||
context.startService(Intent(context, JukeboxMediaPlayer::class.java))
|
||||
}
|
||||
Timber.i("JukeboxMediaPlayer starting...")
|
||||
return startFuture
|
||||
}
|
||||
}
|
||||
|
||||
override fun addListener(listener: Player.Listener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeListener(listener: Player.Listener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun getCurrentMediaItem(): MediaItem? {
|
||||
if (playlist.isEmpty()) return null
|
||||
if (currentIndex < 0 || currentIndex >= playlist.size) return null
|
||||
return playlist[currentIndex]
|
||||
}
|
||||
|
||||
override fun getCurrentMediaItemIndex(): Int {
|
||||
return currentIndex
|
||||
}
|
||||
|
||||
override fun getCurrentPeriodIndex(): Int {
|
||||
return currentIndex
|
||||
}
|
||||
|
||||
override fun getContentPosition(): Long {
|
||||
return currentPosition
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
tasks.remove(Stop::class.java)
|
||||
tasks.remove(Start::class.java)
|
||||
startStatusUpdate()
|
||||
tasks.add(Start())
|
||||
}
|
||||
|
||||
override fun seekTo(positionMs: Long) {
|
||||
seekTo(currentIndex, positionMs)
|
||||
}
|
||||
|
||||
override fun seekTo(mediaItemIndex: Int, positionMs: Long) {
|
||||
tasks.remove(Skip::class.java)
|
||||
tasks.remove(Stop::class.java)
|
||||
tasks.remove(Start::class.java)
|
||||
startStatusUpdate()
|
||||
val positionSeconds = (positionMs / 1000).toInt()
|
||||
if (jukeboxStatus != null) {
|
||||
jukeboxStatus!!.positionSeconds = positionSeconds
|
||||
}
|
||||
tasks.add(Skip(mediaItemIndex, positionSeconds))
|
||||
currentIndex = mediaItemIndex
|
||||
}
|
||||
|
||||
override fun seekBack() {
|
||||
seekTo(0L.coerceAtMost((jukeboxStatus?.positionSeconds ?: 0) - SEEK_INCREMENT_SECONDS))
|
||||
}
|
||||
|
||||
override fun seekForward() {
|
||||
seekTo((jukeboxStatus?.positionSeconds ?: 0) + SEEK_INCREMENT_SECONDS)
|
||||
}
|
||||
|
||||
override fun prepare() {}
|
||||
|
||||
override fun isPlaying(): Boolean {
|
||||
return jukeboxStatus?.isPlaying ?: false
|
||||
}
|
||||
|
||||
override fun getPlaybackState(): Int {
|
||||
return when (jukeboxStatus?.isPlaying) {
|
||||
true -> Player.STATE_READY
|
||||
null, false -> Player.STATE_IDLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAvailableCommands(): Player.Commands {
|
||||
val commandsBuilder = Player.Commands.Builder().addAll(
|
||||
Player.COMMAND_SET_VOLUME,
|
||||
Player.COMMAND_GET_VOLUME
|
||||
)
|
||||
if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP)
|
||||
if (playlist.isNotEmpty()) {
|
||||
commandsBuilder.addAll(
|
||||
Player.COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||
Player.COMMAND_GET_MEDIA_ITEMS_METADATA,
|
||||
Player.COMMAND_PLAY_PAUSE,
|
||||
Player.COMMAND_PREPARE,
|
||||
Player.COMMAND_SEEK_BACK,
|
||||
Player.COMMAND_SEEK_FORWARD,
|
||||
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
|
||||
Player.COMMAND_SEEK_TO_MEDIA_ITEM,
|
||||
)
|
||||
if (currentIndex > 0) commandsBuilder.addAll(
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS,
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
|
||||
)
|
||||
if (currentIndex < playlist.size - 1) commandsBuilder.addAll(
|
||||
Player.COMMAND_SEEK_TO_NEXT,
|
||||
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
|
||||
)
|
||||
}
|
||||
return commandsBuilder.build()
|
||||
}
|
||||
|
||||
override fun isCommandAvailable(command: Int): Boolean {
|
||||
return availableCommands.contains(command)
|
||||
}
|
||||
|
||||
override fun getPlayWhenReady(): Boolean {
|
||||
return isPlaying
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
stop()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
tasks.remove(Stop::class.java)
|
||||
tasks.remove(Start::class.java)
|
||||
stopStatusUpdate()
|
||||
tasks.add(Stop())
|
||||
}
|
||||
|
||||
override fun getCurrentTimeline(): Timeline {
|
||||
return PlaylistTimeline(playlist)
|
||||
}
|
||||
|
||||
override fun getMediaItemCount(): Int {
|
||||
return playlist.size
|
||||
}
|
||||
|
||||
override fun getMediaItemAt(index: Int): MediaItem {
|
||||
if (playlist.size == 0) return MediaItem.EMPTY
|
||||
if (index < 0 || index >= playlist.size) return MediaItem.EMPTY
|
||||
return playlist[index]
|
||||
}
|
||||
|
||||
override fun getShuffleModeEnabled(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
|
||||
|
||||
override fun setVolume(volume: Float) {
|
||||
gain = volume
|
||||
tasks.remove(SetGain::class.java)
|
||||
tasks.add(SetGain(volume))
|
||||
val context = applicationContext()
|
||||
if (volumeToast == null) volumeToast = VolumeToast(context)
|
||||
volumeToast!!.setVolume(volume)
|
||||
}
|
||||
|
||||
override fun getVolume(): Float {
|
||||
return gain
|
||||
}
|
||||
|
||||
override fun getDeviceVolume(): Int {
|
||||
return (gain * 100).toInt()
|
||||
}
|
||||
|
||||
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
|
||||
playlist.addAll(index, mediaItems)
|
||||
updatePlaylist()
|
||||
}
|
||||
|
||||
override fun getBufferedPercentage(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun moveMediaItem(currentIndex: Int, newIndex: Int) {
|
||||
if (playlist.size == 0) return
|
||||
if (currentIndex < 0 || currentIndex >= playlist.size) return
|
||||
if (newIndex < 0 || newIndex >= playlist.size) return
|
||||
|
||||
val insertIndex = if (newIndex < currentIndex) newIndex else newIndex - 1
|
||||
val item = playlist.removeAt(currentIndex)
|
||||
playlist.add(insertIndex, item)
|
||||
updatePlaylist()
|
||||
}
|
||||
|
||||
override fun removeMediaItem(index: Int) {
|
||||
if (playlist.size == 0) return
|
||||
if (index < 0 || index >= playlist.size) return
|
||||
playlist.removeAt(index)
|
||||
updatePlaylist()
|
||||
}
|
||||
|
||||
override fun clearMediaItems() {
|
||||
playlist.clear()
|
||||
currentIndex = 0
|
||||
updatePlaylist()
|
||||
}
|
||||
|
||||
override fun getRepeatMode(): Int {
|
||||
return Player.REPEAT_MODE_OFF
|
||||
}
|
||||
|
||||
override fun setRepeatMode(repeatMode: Int) {}
|
||||
|
||||
override fun getCurrentPosition(): Long {
|
||||
return positionSeconds * 1000L
|
||||
}
|
||||
|
||||
override fun getDuration(): Long {
|
||||
if (playlist.isEmpty()) return 0
|
||||
if (currentIndex < 0 || currentIndex >= playlist.size) return 0
|
||||
|
||||
return (
|
||||
playlist[currentIndex].mediaMetadata.extras?.getInt("duration") ?: 0
|
||||
).toLong() * 1000
|
||||
}
|
||||
|
||||
override fun getContentDuration(): Long {
|
||||
return duration
|
||||
}
|
||||
|
||||
override fun getMediaMetadata(): MediaMetadata {
|
||||
if (playlist.isEmpty()) return MediaMetadata.EMPTY
|
||||
if (currentIndex < 0 || currentIndex >= playlist.size) return MediaMetadata.EMPTY
|
||||
|
||||
return playlist[currentIndex].mediaMetadata
|
||||
}
|
||||
|
||||
override fun seekToNext() {
|
||||
if (currentIndex < 0 || currentIndex >= playlist.size) return
|
||||
currentIndex++
|
||||
seekTo(currentIndex, 0)
|
||||
}
|
||||
|
||||
override fun seekToPrevious() {
|
||||
if ((jukeboxStatus?.positionSeconds ?: 0) > SEEK_START_AFTER_SECONDS) {
|
||||
seekTo(currentIndex, 0)
|
||||
return
|
||||
}
|
||||
if (currentIndex <= 0) return
|
||||
currentIndex--
|
||||
seekTo(currentIndex, 0)
|
||||
}
|
||||
|
||||
private fun startProcessTasks() {
|
||||
serviceThread = object : Thread() {
|
||||
override fun run() {
|
||||
@ -111,6 +478,8 @@ class JukeboxMediaPlayer {
|
||||
|
||||
private fun processTasks() {
|
||||
while (running.get()) {
|
||||
// Sleep a bit to spare processor time if we loop a lot
|
||||
sleepQuietly(10)
|
||||
var task: JukeboxTask? = null
|
||||
try {
|
||||
if (!isOffline()) {
|
||||
@ -122,88 +491,110 @@ class JukeboxMediaPlayer {
|
||||
} catch (x: Throwable) {
|
||||
onError(task, x)
|
||||
}
|
||||
sleepQuietly(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStatusUpdate(jukeboxStatus: JukeboxStatus) {
|
||||
timeOfLastUpdate.set(System.currentTimeMillis())
|
||||
previousJukeboxStatus = this.jukeboxStatus
|
||||
this.jukeboxStatus = jukeboxStatus
|
||||
currentIndex = jukeboxStatus.currentPlayingIndex ?: currentIndex
|
||||
|
||||
if (jukeboxStatus.isPlaying != previousJukeboxStatus?.isPlaying) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlaybackStateChanged(
|
||||
if (jukeboxStatus.isPlaying) Player.STATE_READY else Player.STATE_IDLE
|
||||
)
|
||||
it.onIsPlayingChanged(jukeboxStatus.isPlaying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jukeboxStatus.currentPlayingIndex != previousJukeboxStatus?.currentPlayingIndex) {
|
||||
currentIndex = jukeboxStatus.currentPlayingIndex ?: 0
|
||||
val currentMedia =
|
||||
if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex]
|
||||
else MediaItem.EMPTY
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onMediaItemTransition(
|
||||
currentMedia,
|
||||
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
private fun onError(task: JukeboxTask?, x: Throwable) {
|
||||
if (x is ApiNotSupportedException && task !is Stop) {
|
||||
disableJukeboxOnError(x, R.string.download_jukebox_server_too_old)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlayerError(
|
||||
PlaybackException(
|
||||
"Jukebox server too old",
|
||||
null,
|
||||
R.string.download_jukebox_server_too_old
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (x is OfflineException && task !is Stop) {
|
||||
disableJukeboxOnError(x, R.string.download_jukebox_offline)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlayerError(
|
||||
PlaybackException(
|
||||
"Jukebox offline",
|
||||
null,
|
||||
R.string.download_jukebox_offline
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) {
|
||||
disableJukeboxOnError(x, R.string.download_jukebox_not_authorized)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onPlayerError(
|
||||
PlaybackException(
|
||||
"Jukebox not authorized",
|
||||
null,
|
||||
R.string.download_jukebox_not_authorized
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.e(x, "Failed to process jukebox task")
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableJukeboxOnError(x: Throwable, resourceId: Int) {
|
||||
Timber.w(x.toString())
|
||||
val context = applicationContext()
|
||||
Handler(Looper.getMainLooper()).post { toast(context, resourceId, false) }
|
||||
mediaPlayerControllerLazy.value.isJukeboxEnabled = false
|
||||
}
|
||||
|
||||
fun updatePlaylist() {
|
||||
if (!enabled) return
|
||||
private fun updatePlaylist() {
|
||||
if (!running.get()) return
|
||||
tasks.remove(Skip::class.java)
|
||||
tasks.remove(Stop::class.java)
|
||||
tasks.remove(Start::class.java)
|
||||
val ids: MutableList<String> = ArrayList()
|
||||
for (item in mediaPlayerControllerLazy.value.playlist) {
|
||||
for (item in playlist) {
|
||||
ids.add(item.mediaId)
|
||||
}
|
||||
tasks.add(SetPlaylist(ids))
|
||||
}
|
||||
|
||||
fun skip(index: Int, offsetSeconds: Int) {
|
||||
tasks.remove(Skip::class.java)
|
||||
tasks.remove(Stop::class.java)
|
||||
tasks.remove(Start::class.java)
|
||||
startStatusUpdate()
|
||||
if (jukeboxStatus != null) {
|
||||
jukeboxStatus!!.positionSeconds = offsetSeconds
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach {
|
||||
it.onTimelineChanged(
|
||||
PlaylistTimeline(playlist),
|
||||
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED
|
||||
)
|
||||
}
|
||||
}
|
||||
tasks.add(Skip(index, offsetSeconds))
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
tasks.remove(Stop::class.java)
|
||||
tasks.remove(Start::class.java)
|
||||
stopStatusUpdate()
|
||||
tasks.add(Stop())
|
||||
}
|
||||
|
||||
fun start() {
|
||||
tasks.remove(Stop::class.java)
|
||||
tasks.remove(Start::class.java)
|
||||
startStatusUpdate()
|
||||
tasks.add(Start())
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun adjustVolume(up: Boolean) {
|
||||
val delta = if (up) 0.05f else -0.05f
|
||||
gain += delta
|
||||
gain = gain.coerceAtLeast(0.0f)
|
||||
gain = gain.coerceAtMost(1.0f)
|
||||
tasks.remove(SetGain::class.java)
|
||||
tasks.add(SetGain(gain))
|
||||
val context = applicationContext()
|
||||
if (volumeToast == null) volumeToast = VolumeToast(context)
|
||||
volumeToast!!.setVolume(gain)
|
||||
}
|
||||
|
||||
private val musicService: MusicService
|
||||
get() = getMusicService()
|
||||
|
||||
val positionSeconds: Int
|
||||
private val positionSeconds: Int
|
||||
get() {
|
||||
if (jukeboxStatus == null ||
|
||||
jukeboxStatus!!.positionSeconds == null ||
|
||||
@ -219,20 +610,6 @@ class JukeboxMediaPlayer {
|
||||
return jukeboxStatus!!.positionSeconds!!
|
||||
}
|
||||
|
||||
var isEnabled: Boolean
|
||||
set(enabled) {
|
||||
Timber.d("Jukebox Service setting enabled to %b", enabled)
|
||||
this.enabled = enabled
|
||||
tasks.clear()
|
||||
if (enabled) {
|
||||
updatePlaylist()
|
||||
}
|
||||
stop()
|
||||
}
|
||||
get() {
|
||||
return enabled
|
||||
}
|
||||
|
||||
private class TaskQueue {
|
||||
private val queue = LinkedBlockingQueue<JukeboxTask>()
|
||||
fun add(jukeboxTask: JukeboxTask) {
|
||||
@ -317,6 +694,7 @@ class JukeboxMediaPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private class VolumeToast(context: Context) : Toast(context) {
|
||||
private val progressBar: ProgressBar
|
||||
fun setVolume(volume: Float) {
|
||||
@ -335,7 +713,100 @@ class JukeboxMediaPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STATUS_UPDATE_INTERVAL_SECONDS = 5L
|
||||
// The constants below are necessary so a MediaSession can be built from the Jukebox Service
|
||||
override fun isCurrentMediaItemDynamic(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getTrackSelectionParameters(): TrackSelectionParameters {
|
||||
return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
|
||||
}
|
||||
|
||||
override fun getMaxSeekToPreviousPosition(): Long {
|
||||
return SEEK_START_AFTER_SECONDS * 1000L
|
||||
}
|
||||
|
||||
override fun getSeekBackIncrement(): Long {
|
||||
return SEEK_INCREMENT_SECONDS * 1000L
|
||||
}
|
||||
|
||||
override fun getSeekForwardIncrement(): Long {
|
||||
return SEEK_INCREMENT_SECONDS * 1000L
|
||||
}
|
||||
|
||||
override fun isLoading(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getPlaybackSuppressionReason(): Int {
|
||||
return Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
}
|
||||
|
||||
override fun isDeviceMuted(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getCurrentCues(): CueGroup {
|
||||
return CueGroup.EMPTY
|
||||
}
|
||||
|
||||
override fun getAudioAttributes(): AudioAttributes {
|
||||
return AudioAttributes.DEFAULT
|
||||
}
|
||||
|
||||
override fun getVideoSize(): VideoSize {
|
||||
return VideoSize(0, 0)
|
||||
}
|
||||
|
||||
override fun getContentBufferedPosition(): Long {
|
||||
return bufferedPosition
|
||||
}
|
||||
|
||||
override fun getCurrentLiveOffset(): Long {
|
||||
return C.TIME_UNSET
|
||||
}
|
||||
|
||||
override fun getTotalBufferedDuration(): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun isPlayingAd(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getCurrentAdIndexInAdGroup(): Int {
|
||||
return C.INDEX_UNSET
|
||||
}
|
||||
|
||||
override fun getCurrentAdGroupIndex(): Int {
|
||||
return C.INDEX_UNSET
|
||||
}
|
||||
|
||||
override fun canAdvertiseSession(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getApplicationLooper(): Looper {
|
||||
return applicationContext().mainLooper
|
||||
}
|
||||
|
||||
override fun getPlaylistMetadata(): MediaMetadata {
|
||||
return MediaMetadata.EMPTY
|
||||
}
|
||||
|
||||
override fun getDeviceInfo(): DeviceInfo {
|
||||
return DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 1)
|
||||
}
|
||||
|
||||
override fun getPlayerError(): PlaybackException? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getPlaybackParameters(): PlaybackParameters {
|
||||
return PlaybackParameters(1F, 1F)
|
||||
}
|
||||
|
||||
override fun getBufferedPosition(): Long {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* JukeboxNotificationActionFactory.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.MediaNotification
|
||||
import androidx.media3.session.MediaSession
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
|
||||
/**
|
||||
* This class creates Intents and Actions to be used with the Media Notification
|
||||
* of the Jukebox Service
|
||||
*/
|
||||
class JukeboxNotificationActionFactory : MediaNotification.ActionFactory {
|
||||
override fun createMediaAction(
|
||||
mediaSession: MediaSession,
|
||||
icon: IconCompat,
|
||||
title: CharSequence,
|
||||
command: Int
|
||||
): NotificationCompat.Action {
|
||||
return NotificationCompat.Action(
|
||||
icon, title, createMediaActionPendingIntent(mediaSession, command.toLong())
|
||||
)
|
||||
}
|
||||
|
||||
override fun createCustomAction(
|
||||
mediaSession: MediaSession,
|
||||
icon: IconCompat,
|
||||
title: CharSequence,
|
||||
customAction: String,
|
||||
extras: Bundle
|
||||
): NotificationCompat.Action {
|
||||
return NotificationCompat.Action(
|
||||
icon, title, null
|
||||
)
|
||||
}
|
||||
|
||||
override fun createCustomActionFromCustomCommandButton(
|
||||
mediaSession: MediaSession,
|
||||
customCommandButton: CommandButton
|
||||
): NotificationCompat.Action {
|
||||
return NotificationCompat.Action(null, null, null)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun createMediaActionPendingIntent(
|
||||
mediaSession: MediaSession,
|
||||
command: Long
|
||||
): PendingIntent {
|
||||
val keyCode: Int = toKeyCode(command)
|
||||
val intent = Intent(Intent.ACTION_MEDIA_BUTTON)
|
||||
intent.component = ComponentName(UApp.applicationContext(), JukeboxMediaPlayer::class.java)
|
||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
|
||||
return if (Util.SDK_INT >= 26 && command == Player.COMMAND_PLAY_PAUSE.toLong()) {
|
||||
return PendingIntent.getForegroundService(
|
||||
UApp.applicationContext(), keyCode, intent, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getService(
|
||||
UApp.applicationContext(),
|
||||
keyCode,
|
||||
intent,
|
||||
if (Util.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toKeyCode(action: @Player.Command Long): Int {
|
||||
return when (action.toInt()) {
|
||||
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
|
||||
Player.COMMAND_SEEK_TO_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
||||
Player.COMMAND_STOP -> KeyEvent.KEYCODE_MEDIA_STOP
|
||||
Player.COMMAND_SEEK_FORWARD -> KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
||||
Player.COMMAND_SEEK_BACK -> KeyEvent.KEYCODE_MEDIA_REWIND
|
||||
Player.COMMAND_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||
else -> KeyEvent.KEYCODE_UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
/*
|
||||
* JukeboxUnimplemented.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.app.Service
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.TextureView
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.Tracks
|
||||
|
||||
/**
|
||||
* This class helps to hide the unused (thus unimplemented) functions
|
||||
* of the crowded Player interface, so the JukeboxMediaPlayer class can be a bit clearer.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
abstract class JukeboxUnimplementedFunctions : Service(), Player {
|
||||
|
||||
override fun setMediaItems(mediaItems: MutableList<MediaItem>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setMediaItems(mediaItems: MutableList<MediaItem>, resetPosition: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setMediaItems(
|
||||
mediaItems: MutableList<MediaItem>,
|
||||
startIndex: Int,
|
||||
startPositionMs: Long
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setMediaItem(mediaItem: MediaItem) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setMediaItem(mediaItem: MediaItem, resetPosition: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addMediaItem(mediaItem: MediaItem) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addMediaItem(index: Int, mediaItem: MediaItem) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addMediaItems(mediaItems: MutableList<MediaItem>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun removeMediaItems(fromIndex: Int, toIndex: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun seekToDefaultPosition() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun seekToDefaultPosition(mediaItemIndex: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun hasPrevious(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun hasPreviousWindow(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun hasPreviousMediaItem(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun previous() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun seekToPreviousWindow() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun seekToPreviousMediaItem() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun hasNextWindow(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun hasNextMediaItem(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun next() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun seekToNextWindow() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun seekToNextMediaItem() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setPlaybackSpeed(speed: Float) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun stop(reset: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getCurrentTracks(): Tracks {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getCurrentManifest(): Any? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getCurrentWindowIndex(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getNextWindowIndex(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getNextMediaItemIndex(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getPreviousWindowIndex(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getPreviousMediaItemIndex(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isCurrentWindowDynamic(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isCurrentWindowLive(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isCurrentMediaItemLive(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isCurrentWindowSeekable(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isCurrentMediaItemSeekable(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun clearVideoSurface() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun clearVideoSurface(surface: Surface?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setVideoSurface(surface: Surface?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setVideoTextureView(textureView: TextureView?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun clearVideoTextureView(textureView: TextureView?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setDeviceVolume(volume: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun increaseDeviceVolume() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun decreaseDeviceVolume() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setDeviceMuted(muted: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
@ -9,10 +9,13 @@ package org.moire.ultrasonic.service
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.widget.Toast
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
|
||||
import androidx.media3.common.Player.REPEAT_MODE_OFF
|
||||
@ -22,6 +25,7 @@ import androidx.media3.session.SessionResult
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -40,11 +44,15 @@ import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.MainThreadExecutor
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.util.setPin
|
||||
import org.moire.ultrasonic.util.toMediaItem
|
||||
import org.moire.ultrasonic.util.toTrack
|
||||
import timber.log.Timber
|
||||
|
||||
private const val CONTROLLER_SWITCH_DELAY = 500L
|
||||
private const val VOLUME_DELTA = 0.05f
|
||||
|
||||
/**
|
||||
* The implementation of the Media Player Controller.
|
||||
* This class contains everything that is necessary for the Application UI
|
||||
@ -57,18 +65,15 @@ class MediaPlayerController(
|
||||
private val downloader: Downloader,
|
||||
val context: Context
|
||||
) : KoinComponent {
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private var created = false
|
||||
var suggestedPlaylistName: String? = null
|
||||
var keepScreenOn = false
|
||||
var showVisualization = false
|
||||
private var autoPlayStart = false
|
||||
|
||||
private val scrobbler = Scrobbler()
|
||||
|
||||
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private var mainScope = CoroutineScope(Dispatchers.Main)
|
||||
@ -76,89 +81,89 @@ class MediaPlayerController(
|
||||
private var sessionToken =
|
||||
SessionToken(context, ComponentName(context, PlaybackService::class.java))
|
||||
|
||||
private var mediaControllerFuture = MediaController.Builder(
|
||||
context,
|
||||
sessionToken
|
||||
).buildAsync()
|
||||
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||
|
||||
var controller: MediaController? = null
|
||||
private var controller: Player? = null
|
||||
|
||||
private lateinit var listeners: Player.Listener
|
||||
private var listeners: Player.Listener = object : Player.Listener {
|
||||
|
||||
/*
|
||||
* Log all events
|
||||
*/
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
for (i in 0 until events.size()) {
|
||||
Timber.i("Media3 Event, event type: %s", events[i])
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This will be called everytime the playlist has changed.
|
||||
* We run the event through RxBus in order to throttle them
|
||||
*/
|
||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||
RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack))
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
playerStateChangedHandler()
|
||||
publishPlaybackState()
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
playerStateChangedHandler()
|
||||
publishPlaybackState()
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
clearBookmark()
|
||||
// TRANSITION_REASON_AUTO means that the previous track finished playing and a new one has started.
|
||||
if (reason == MEDIA_ITEM_TRANSITION_REASON_AUTO && cachedMediaItem != null) {
|
||||
scrobbler.scrobble(cachedMediaItem?.toTrack(), true)
|
||||
}
|
||||
cachedMediaItem = mediaItem
|
||||
publishPlaybackState()
|
||||
}
|
||||
|
||||
/*
|
||||
* If the same item is contained in a playlist multiple times directly after each
|
||||
* other, Media3 on emits a PositionDiscontinuity event.
|
||||
* Can be removed if https://github.com/androidx/media/issues/68 is fixed.
|
||||
*/
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
playerStateChangedHandler()
|
||||
publishPlaybackState()
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
Timber.w(error.toString())
|
||||
if (!isJukeboxEnabled) return
|
||||
|
||||
val context = UApp.applicationContext()
|
||||
mainScope.launch {
|
||||
Util.toast(
|
||||
context,
|
||||
error.errorCode,
|
||||
false
|
||||
)
|
||||
}
|
||||
isJukeboxEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private var cachedMediaItem: MediaItem? = null
|
||||
|
||||
fun onCreate(onCreated: () -> Unit) {
|
||||
if (created) return
|
||||
externalStorageMonitor.onCreate { reset() }
|
||||
isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault
|
||||
|
||||
mediaControllerFuture.addListener({
|
||||
controller = mediaControllerFuture.get()
|
||||
|
||||
Timber.i("MediaController Instance received")
|
||||
|
||||
listeners = object : Player.Listener {
|
||||
|
||||
/*
|
||||
* Log all events
|
||||
*/
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
for (i in 0 until events.size()) {
|
||||
Timber.i("Media3 Event, event type: %s", events[i])
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This will be called everytime the playlist has changed.
|
||||
* We run the event through RxBus in order to throttle them
|
||||
*/
|
||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||
RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack))
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
playerStateChangedHandler()
|
||||
publishPlaybackState()
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
playerStateChangedHandler()
|
||||
publishPlaybackState()
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
clearBookmark()
|
||||
// TRANSITION_REASON_AUTO means that the previous track finished playing and a new one has started.
|
||||
if (reason == MEDIA_ITEM_TRANSITION_REASON_AUTO && cachedMediaItem != null) {
|
||||
scrobbler.scrobble(cachedMediaItem?.toTrack(), true)
|
||||
}
|
||||
cachedMediaItem = mediaItem
|
||||
publishPlaybackState()
|
||||
}
|
||||
|
||||
/*
|
||||
* If the same item is contained in a playlist multiple times directly after each
|
||||
* other, Media3 on emits a PositionDiscontinuity event.
|
||||
* Can be removed if https://github.com/androidx/media/issues/68 is fixed.
|
||||
*/
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
playerStateChangedHandler()
|
||||
publishPlaybackState()
|
||||
}
|
||||
}
|
||||
|
||||
controller?.addListener(listeners)
|
||||
|
||||
onCreated()
|
||||
|
||||
Timber.i("MediaPlayerController creation complete")
|
||||
|
||||
// controller?.play()
|
||||
}, MoreExecutors.directExecutor())
|
||||
if (activeServerProvider.getActiveServer().jukeboxByDefault) {
|
||||
switchToJukebox(onCreated)
|
||||
} else {
|
||||
switchToLocalPlayer(onCreated)
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
|
||||
// Update the Jukebox state when the active server has changed
|
||||
@ -200,6 +205,23 @@ class MediaPlayerController(
|
||||
Timber.i("MediaPlayerController started")
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
if (!created) return
|
||||
|
||||
// First stop listening to events
|
||||
rxBusSubscription.dispose()
|
||||
controller?.removeListener(listeners)
|
||||
releaseController()
|
||||
|
||||
// Shutdown the rest
|
||||
val context = UApp.applicationContext()
|
||||
externalStorageMonitor.onDestroy()
|
||||
context.stopService(Intent(context, DownloadService::class.java))
|
||||
downloader.onDestroy()
|
||||
created = false
|
||||
Timber.i("MediaPlayerController destroyed")
|
||||
}
|
||||
|
||||
private fun playerStateChangedHandler() {
|
||||
val currentPlaying = controller?.currentMediaItem?.toTrack() ?: return
|
||||
|
||||
@ -254,22 +276,6 @@ class MediaPlayerController(
|
||||
UltrasonicAppWidgetProvider4X4.instance?.notifyChange(context, song, isPlaying, true)
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
if (!created) return
|
||||
|
||||
// First stop listening to events
|
||||
rxBusSubscription.dispose()
|
||||
controller?.removeListener(listeners)
|
||||
|
||||
// Shutdown the rest
|
||||
val context = UApp.applicationContext()
|
||||
externalStorageMonitor.onDestroy()
|
||||
context.stopService(Intent(context, DownloadService::class.java))
|
||||
downloader.onDestroy()
|
||||
created = false
|
||||
Timber.i("MediaPlayerController destroyed")
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun restore(
|
||||
state: PlaybackState,
|
||||
@ -291,15 +297,7 @@ class MediaPlayerController(
|
||||
isShufflePlayEnabled = state.shufflePlay
|
||||
|
||||
if (state.currentPlayingIndex != -1) {
|
||||
if (jukeboxMediaPlayer.isEnabled) {
|
||||
jukeboxMediaPlayer.skip(
|
||||
state.currentPlayingIndex,
|
||||
state.currentPlayingPosition / 1000
|
||||
)
|
||||
} else {
|
||||
seekTo(state.currentPlayingIndex, state.currentPlayingPosition)
|
||||
}
|
||||
|
||||
seekTo(state.currentPlayingIndex, state.currentPlayingPosition)
|
||||
prepare()
|
||||
|
||||
if (autoPlay) {
|
||||
@ -318,12 +316,8 @@ class MediaPlayerController(
|
||||
|
||||
@Synchronized
|
||||
fun play() {
|
||||
if (jukeboxMediaPlayer.isEnabled) {
|
||||
jukeboxMediaPlayer.start()
|
||||
} else {
|
||||
controller?.prepare()
|
||||
controller?.play()
|
||||
}
|
||||
controller?.prepare()
|
||||
controller?.play()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -359,26 +353,27 @@ class MediaPlayerController(
|
||||
if (controller?.currentTimeline?.isEmpty != false ||
|
||||
index >= controller!!.currentTimeline.windowCount
|
||||
) return
|
||||
|
||||
Timber.i("SeekTo: %s %s", index, position)
|
||||
controller?.seekTo(index, position.toLong())
|
||||
}
|
||||
|
||||
fun seekForward() {
|
||||
controller?.seekForward()
|
||||
}
|
||||
|
||||
fun seekBack() {
|
||||
controller?.seekBack()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun pause() {
|
||||
if (jukeboxMediaPlayer.isEnabled) {
|
||||
jukeboxMediaPlayer.stop()
|
||||
} else {
|
||||
controller?.pause()
|
||||
}
|
||||
controller?.pause()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
if (jukeboxMediaPlayer.isEnabled) {
|
||||
jukeboxMediaPlayer.stop()
|
||||
} else {
|
||||
controller?.stop()
|
||||
}
|
||||
controller?.stop()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -405,8 +400,6 @@ class MediaPlayerController(
|
||||
|
||||
controller?.addMediaItems(insertAt, mediaItems)
|
||||
|
||||
jukeboxMediaPlayer.updatePlaylist()
|
||||
|
||||
if (shuffle) isShufflePlayEnabled = true
|
||||
|
||||
prepare()
|
||||
@ -423,10 +416,6 @@ class MediaPlayerController(
|
||||
downloader.downloadBackground(filteredSongs, save)
|
||||
}
|
||||
|
||||
fun stopJukeboxService() {
|
||||
jukeboxMediaPlayer.stopJukeboxService()
|
||||
}
|
||||
|
||||
@set:Synchronized
|
||||
var isShufflePlayEnabled: Boolean
|
||||
get() = controller?.shuffleModeEnabled == true
|
||||
@ -468,8 +457,6 @@ class MediaPlayerController(
|
||||
listOf(), -1, 0, isShufflePlayEnabled, repeatMode
|
||||
)
|
||||
}
|
||||
|
||||
jukeboxMediaPlayer.updatePlaylist()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -495,10 +482,7 @@ class MediaPlayerController(
|
||||
|
||||
@Synchronized
|
||||
fun removeFromPlaylist(position: Int) {
|
||||
|
||||
controller?.removeMediaItem(position)
|
||||
|
||||
jukeboxMediaPlayer.updatePlaylist()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@ -549,17 +533,17 @@ class MediaPlayerController(
|
||||
@get:Synchronized
|
||||
val playerPosition: Int
|
||||
get() {
|
||||
return if (jukeboxMediaPlayer.isEnabled) {
|
||||
jukeboxMediaPlayer.positionSeconds * 1000
|
||||
} else {
|
||||
controller?.currentPosition?.toInt() ?: 0
|
||||
}
|
||||
return controller?.currentPosition?.toInt() ?: 0
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val playerDuration: Int
|
||||
get() {
|
||||
return controller?.duration?.toInt() ?: return 0
|
||||
// Media3 will only report a duration when the file is prepared
|
||||
val reportedDuration = controller?.duration ?: C.TIME_UNSET
|
||||
if (reportedDuration != C.TIME_UNSET) return reportedDuration.toInt()
|
||||
// If Media3 doesn't know the duration yet, use the duration in the metadata
|
||||
return (currentMediaItem?.mediaMetadata?.extras?.getInt("duration") ?: 0) * 1000
|
||||
}
|
||||
|
||||
val playbackState: Int
|
||||
@ -570,21 +554,103 @@ class MediaPlayerController(
|
||||
|
||||
@set:Synchronized
|
||||
var isJukeboxEnabled: Boolean
|
||||
get() = jukeboxMediaPlayer.isEnabled
|
||||
get() = controller is JukeboxMediaPlayer
|
||||
set(jukeboxEnabled) {
|
||||
jukeboxMediaPlayer.isEnabled = jukeboxEnabled
|
||||
|
||||
if (jukeboxEnabled) {
|
||||
jukeboxMediaPlayer.startJukeboxService()
|
||||
reset()
|
||||
|
||||
// Cancel current downloads
|
||||
downloader.clearActiveDownloads()
|
||||
switchToJukebox {}
|
||||
} else {
|
||||
jukeboxMediaPlayer.stopJukeboxService()
|
||||
switchToLocalPlayer {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchToJukebox(onCreated: () -> Unit) {
|
||||
if (JukeboxMediaPlayer.running.get()) return
|
||||
val currentPlaylist = playlist
|
||||
val currentIndex = controller?.currentMediaItemIndex ?: 0
|
||||
val currentPosition = controller?.currentPosition ?: 0
|
||||
downloader.clearActiveDownloads()
|
||||
controller?.pause()
|
||||
controller?.stop()
|
||||
val oldController = controller
|
||||
controller = null // While we switch, the controller shouldn't be available
|
||||
|
||||
// Stop() won't work if we don't give it time to be processed
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (oldController != null) releaseLocalPlayer(oldController)
|
||||
setupJukebox {
|
||||
controller?.addMediaItems(0, currentPlaylist)
|
||||
controller?.seekTo(currentIndex, currentPosition)
|
||||
onCreated()
|
||||
}
|
||||
}, CONTROLLER_SWITCH_DELAY)
|
||||
}
|
||||
|
||||
private fun switchToLocalPlayer(onCreated: () -> Unit) {
|
||||
val currentPlaylist = playlist
|
||||
val currentIndex = controller?.currentMediaItemIndex ?: 0
|
||||
val currentPosition = controller?.currentPosition ?: 0
|
||||
controller?.stop()
|
||||
val oldController = controller
|
||||
controller = null // While we switch, the controller shouldn't be available
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (oldController != null) releaseJukebox(oldController)
|
||||
setupLocalPlayer {
|
||||
controller?.addMediaItems(0, currentPlaylist)
|
||||
controller?.seekTo(currentIndex, currentPosition)
|
||||
onCreated()
|
||||
}
|
||||
}, CONTROLLER_SWITCH_DELAY)
|
||||
}
|
||||
|
||||
private fun releaseController() {
|
||||
when (controller) {
|
||||
null -> return
|
||||
is JukeboxMediaPlayer -> releaseJukebox(controller)
|
||||
is MediaController -> releaseLocalPlayer(controller)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupLocalPlayer(onCreated: () -> Unit) {
|
||||
mediaControllerFuture = MediaController.Builder(
|
||||
context,
|
||||
sessionToken
|
||||
).buildAsync()
|
||||
|
||||
mediaControllerFuture?.addListener({
|
||||
controller = mediaControllerFuture?.get()
|
||||
|
||||
Timber.i("MediaController Instance received")
|
||||
controller?.addListener(listeners)
|
||||
onCreated()
|
||||
Timber.i("MediaPlayerController creation complete")
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
private fun releaseLocalPlayer(player: Player?) {
|
||||
player?.removeListener(listeners)
|
||||
player?.release()
|
||||
if (mediaControllerFuture != null) MediaController.releaseFuture(mediaControllerFuture!!)
|
||||
Timber.i("MediaPlayerController released")
|
||||
}
|
||||
|
||||
private fun setupJukebox(onCreated: () -> Unit) {
|
||||
val jukeboxFuture = JukeboxMediaPlayer.requestStart()
|
||||
jukeboxFuture?.addListener({
|
||||
controller = jukeboxFuture.get()
|
||||
onCreated()
|
||||
controller?.addListener(listeners)
|
||||
Timber.i("JukeboxService creation complete")
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
private fun releaseJukebox(player: Player?) {
|
||||
val jukebox = player as JukeboxMediaPlayer?
|
||||
jukebox?.removeListener(listeners)
|
||||
jukebox?.requestStop()
|
||||
Timber.i("JukeboxService released")
|
||||
}
|
||||
|
||||
/**
|
||||
* This function calls the music service directly and
|
||||
* therefore can't be called from the main thread
|
||||
@ -600,21 +666,26 @@ class MediaPlayerController(
|
||||
return false
|
||||
}
|
||||
|
||||
fun adjustJukeboxVolume(up: Boolean) {
|
||||
jukeboxMediaPlayer.adjustVolume(up)
|
||||
fun adjustVolume(up: Boolean) {
|
||||
val delta = if (up) VOLUME_DELTA else -VOLUME_DELTA
|
||||
var gain = controller?.volume ?: return
|
||||
gain += delta
|
||||
gain = gain.coerceAtLeast(0.0f)
|
||||
gain = gain.coerceAtMost(1.0f)
|
||||
controller?.volume = gain
|
||||
}
|
||||
|
||||
fun setVolume(volume: Float) {
|
||||
controller?.volume = volume
|
||||
}
|
||||
|
||||
fun toggleSongStarred() {
|
||||
if (currentMediaItem == null) return
|
||||
fun toggleSongStarred(): ListenableFuture<SessionResult>? {
|
||||
if (currentMediaItem == null) return null
|
||||
val song = currentMediaItem!!.toTrack()
|
||||
|
||||
controller?.setRating(
|
||||
return (controller as? MediaController)?.setRating(
|
||||
HeartRating(!song.starred)
|
||||
).let {
|
||||
)?.let {
|
||||
Futures.addCallback(
|
||||
it,
|
||||
object : FutureCallback<SessionResult> {
|
||||
@ -635,6 +706,7 @@ class MediaPlayerController(
|
||||
},
|
||||
MainThreadExecutor()
|
||||
)
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
@ -672,6 +744,10 @@ class MediaPlayerController(
|
||||
return getPlayList(false)
|
||||
}
|
||||
|
||||
fun getMediaItemAt(index: Int): MediaItem? {
|
||||
return controller?.getMediaItemAt(index)
|
||||
}
|
||||
|
||||
val playlistInPlayOrder: List<MediaItem>
|
||||
get() {
|
||||
return getPlayList(controller?.shuffleModeEnabled ?: false)
|
||||
|
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* PlaylistTimeline.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.util.Assertions
|
||||
import androidx.media3.common.util.Util
|
||||
import com.google.common.collect.ImmutableList
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* This class wraps a simple playlist provided as List<MediaItem>
|
||||
* to be usable as a Media3 Timeline.
|
||||
*/
|
||||
class PlaylistTimeline @JvmOverloads constructor(
|
||||
mediaItems: List<MediaItem>,
|
||||
shuffledIndices: IntArray = createUnshuffledIndices(
|
||||
mediaItems.size
|
||||
)
|
||||
) :
|
||||
Timeline() {
|
||||
private val mediaItems: ImmutableList<MediaItem>
|
||||
private val shuffledIndices: IntArray
|
||||
private val indicesInShuffled: IntArray
|
||||
override fun getWindowCount(): Int {
|
||||
return mediaItems.size
|
||||
}
|
||||
|
||||
override fun getWindow(
|
||||
windowIndex: Int,
|
||||
window: Window,
|
||||
defaultPositionProjectionUs: Long
|
||||
): Window {
|
||||
window[
|
||||
0, mediaItems[windowIndex], null, 0, 0, 0, true, false, null, 0, Util.msToUs(
|
||||
DEFAULT_DURATION_MS
|
||||
), windowIndex, windowIndex
|
||||
] =
|
||||
0
|
||||
window.isPlaceholder = false
|
||||
return window
|
||||
}
|
||||
|
||||
override fun getNextWindowIndex(
|
||||
windowIndex: Int,
|
||||
repeatMode: @Player.RepeatMode Int,
|
||||
shuffleModeEnabled: Boolean
|
||||
): Int {
|
||||
if (repeatMode == Player.REPEAT_MODE_ONE) {
|
||||
return windowIndex
|
||||
}
|
||||
if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) {
|
||||
return if (repeatMode == Player.REPEAT_MODE_ALL) getFirstWindowIndex(shuffleModeEnabled)
|
||||
else C.INDEX_UNSET
|
||||
}
|
||||
return if (shuffleModeEnabled) shuffledIndices[indicesInShuffled[windowIndex] + 1]
|
||||
else windowIndex + 1
|
||||
}
|
||||
|
||||
override fun getPreviousWindowIndex(
|
||||
windowIndex: Int,
|
||||
repeatMode: @Player.RepeatMode Int,
|
||||
shuffleModeEnabled: Boolean
|
||||
): Int {
|
||||
if (repeatMode == Player.REPEAT_MODE_ONE) {
|
||||
return windowIndex
|
||||
}
|
||||
if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) {
|
||||
return if (repeatMode == Player.REPEAT_MODE_ALL) getLastWindowIndex(shuffleModeEnabled)
|
||||
else C.INDEX_UNSET
|
||||
}
|
||||
return if (shuffleModeEnabled) shuffledIndices[indicesInShuffled[windowIndex] - 1]
|
||||
else windowIndex - 1
|
||||
}
|
||||
|
||||
override fun getLastWindowIndex(shuffleModeEnabled: Boolean): Int {
|
||||
if (isEmpty) {
|
||||
return C.INDEX_UNSET
|
||||
}
|
||||
return if (shuffleModeEnabled) shuffledIndices[windowCount - 1] else windowCount - 1
|
||||
}
|
||||
|
||||
override fun getFirstWindowIndex(shuffleModeEnabled: Boolean): Int {
|
||||
if (isEmpty) {
|
||||
return C.INDEX_UNSET
|
||||
}
|
||||
return if (shuffleModeEnabled) shuffledIndices[0] else 0
|
||||
}
|
||||
|
||||
override fun getPeriodCount(): Int {
|
||||
return windowCount
|
||||
}
|
||||
|
||||
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
|
||||
period[null, null, periodIndex, Util.msToUs(DEFAULT_DURATION_MS)] =
|
||||
0
|
||||
return period
|
||||
}
|
||||
|
||||
override fun getIndexOfPeriod(uid: Any): Int {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getUidOfPeriod(periodIndex: Int): Any {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_DURATION_MS: Long = 100
|
||||
private fun createUnshuffledIndices(length: Int): IntArray {
|
||||
val indices = IntArray(length)
|
||||
for (i in 0 until length) {
|
||||
indices[i] = i
|
||||
}
|
||||
return indices
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Assertions.checkState(mediaItems.size == shuffledIndices.size)
|
||||
this.mediaItems = ImmutableList.copyOf(mediaItems)
|
||||
this.shuffledIndices = Arrays.copyOf(shuffledIndices, shuffledIndices.size)
|
||||
indicesInShuffled = IntArray(shuffledIndices.size)
|
||||
for (i in shuffledIndices.indices) {
|
||||
indicesInShuffled[shuffledIndices[i]] = i
|
||||
}
|
||||
}
|
||||
}
|
@ -9,8 +9,10 @@ package org.moire.ultrasonic.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
@ -40,6 +42,7 @@ import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.activity.NavigationActivity
|
||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||
import org.moire.ultrasonic.domain.Bookmark
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
@ -652,6 +655,18 @@ object Util {
|
||||
)
|
||||
}
|
||||
|
||||
fun getPendingIntentToShowPlayer(context: Context): PendingIntent {
|
||||
val intent = Intent(context, NavigationActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// needed starting Android 12 (S = 31)
|
||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
intent.putExtra(Constants.INTENT_SHOW_PLAYER, true)
|
||||
return PendingIntent.getActivity(context, 0, intent, flags)
|
||||
}
|
||||
|
||||
private val connectivityManager: ConnectivityManager
|
||||
get() = appContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user