refactor: use MediaController instead of ServiceBinder in AudioPlayerFragment

This commit is contained in:
Bnyro 2024-11-17 23:02:50 +01:00
parent 9ab76ba47b
commit f4a8d7b6b1
15 changed files with 147 additions and 288 deletions

View File

@ -56,4 +56,5 @@ object IntentData {
const val isPlayingOffline = "isPlayingOffline"
const val downloadInfo = "downloadInfo"
const val streams = "streams"
const val chapters = "chapters"
}

View File

@ -9,6 +9,10 @@ inline fun <reified T : Parcelable> Bundle.parcelable(key: String?): T? {
return BundleCompat.getParcelable(this, key, T::class.java)
}
inline fun <reified T : Parcelable> Bundle.parcelableList(key: String?): ArrayList<T>? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
}
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
}

View File

@ -1,58 +0,0 @@
package com.github.libretube.extensions
import android.graphics.Bitmap
import android.support.v4.media.MediaMetadataCompat
import androidx.media3.common.C
import androidx.media3.common.MediaMetadata
import androidx.media3.session.MediaConstants
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun MediaMetadata.toMediaMetadataCompat(duration: Long, thumbnail: Bitmap?): MediaMetadataCompat {
val builder = MediaMetadataCompat.Builder()
title?.let {
builder.putText(MediaMetadataCompat.METADATA_KEY_TITLE, it)
builder.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, it)
}
subtitle?.let {
builder.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, it)
}
description?.let {
builder.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, it)
}
artist?.let {
builder.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, it)
}
albumTitle?.let {
builder.putText(MediaMetadataCompat.METADATA_KEY_ALBUM, it)
}
albumArtist?.let {
builder.putText(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, it)
}
recordingYear?.toLong()?.let {
builder.putLong(MediaMetadataCompat.METADATA_KEY_YEAR, it)
}
artworkUri?.toString()?.let {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, it)
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, it)
}
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, thumbnail)
if (duration != C.TIME_UNSET) {
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
}
mediaType?.toLong()?.let {
builder.putLong(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, it)
}
return builder.build()
}

View File

@ -1,37 +1,50 @@
package com.github.libretube.extensions
import android.support.v4.media.MediaMetadataCompat
import androidx.annotation.OptIn
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.UnstableApi
import com.github.libretube.api.obj.Streams
import com.github.libretube.db.obj.Download
import com.github.libretube.constants.IntentData
import com.github.libretube.db.obj.DownloadChapter
import com.github.libretube.db.obj.DownloadWithItems
@OptIn(UnstableApi::class)
fun MediaItem.Builder.setMetadata(streams: Streams) = apply {
val extras = bundleOf(
MediaMetadataCompat.METADATA_KEY_TITLE to streams.title,
MediaMetadataCompat.METADATA_KEY_ARTIST to streams.uploader
MediaMetadataCompat.METADATA_KEY_ARTIST to streams.uploader,
IntentData.chapters to streams.chapters
)
setMediaMetadata(
MediaMetadata.Builder()
.setTitle(streams.title)
.setArtist(streams.uploader)
.setDurationMs(streams.duration.times(1000))
.setArtworkUri(streams.thumbnailUrl.toUri())
.setComposer(streams.uploaderUrl.toID())
.setExtras(extras)
.build()
)
}
fun MediaItem.Builder.setMetadata(download: Download) = apply {
@OptIn(UnstableApi::class)
fun MediaItem.Builder.setMetadata(downloadWithItems: DownloadWithItems) = apply {
val (download, _, chapters) = downloadWithItems
val extras = bundleOf(
MediaMetadataCompat.METADATA_KEY_TITLE to download.title,
MediaMetadataCompat.METADATA_KEY_ARTIST to download.uploader
MediaMetadataCompat.METADATA_KEY_ARTIST to download.uploader,
IntentData.chapters to chapters.map(DownloadChapter::toChapterSegment)
)
setMediaMetadata(
MediaMetadata.Builder()
.setTitle(download.title)
.setArtist(download.uploader)
.setDurationMs(download.duration?.times(1000))
.setArtworkUri(download.thumbnailPath?.toAndroidUri())
.setExtras(extras)
.build()

View File

@ -53,10 +53,11 @@ object BackgroundHelper {
val playerData = PlayerData(videoId, playlistId, channelId, keepQueue, position)
val sessionToken =
SessionToken(context, ComponentName(context, OnlinePlayerService::class.java))
startMediaService(context, sessionToken, bundleOf(IntentData.playerData to playerData))
startMediaService(
context,
OnlinePlayerService::class.java,
bundleOf(IntentData.playerData to playerData)
)
}
/**
@ -98,8 +99,6 @@ object BackgroundHelper {
downloadTab: DownloadTab,
shuffle: Boolean = false
) {
stopBackgroundPlay(context)
// whether the service is started from the MainActivity or NoInternetActivity
val noInternet = ContextHelper.tryUnwrapActivity<NoInternetActivity>(context) != null
@ -110,19 +109,21 @@ object BackgroundHelper {
IntentData.noInternet to noInternet
)
val sessionToken =
SessionToken(context, ComponentName(context, OfflinePlayerService::class.java))
startMediaService(context, sessionToken, arguments)
startMediaService(context, OfflinePlayerService::class.java, arguments)
}
@OptIn(UnstableApi::class)
fun startMediaService(
context: Context,
sessionToken: SessionToken,
serviceClass: Class<*>,
arguments: Bundle,
onController: (MediaController) -> Unit = {}
) {
stopBackgroundPlay(context)
val sessionToken =
SessionToken(context, ComponentName(context, serviceClass))
val controllerFuture =
MediaController.Builder(context, sessionToken).buildAsync()
controllerFuture.addListener({

View File

@ -1,10 +0,0 @@
package com.github.libretube.obj
import java.nio.file.Path
data class PlayerNotificationData(
val title: String? = null,
val uploaderName: String? = null,
val thumbnailUrl: String? = null,
val thumbnailPath: Path? = null
)

View File

@ -1,12 +1,9 @@
package com.github.libretube.services
import android.content.Intent
import android.os.Binder
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.app.ServiceCompat
import androidx.media3.common.C
@ -22,10 +19,9 @@ import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.github.libretube.R
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.enums.PlayerCommand
import com.github.libretube.enums.PlayerEvent
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.extensions.updateParameters
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.util.NowPlayingNotification
@ -49,14 +45,6 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio
val handler = Handler(Looper.getMainLooper())
private val binder = LocalBinder()
/**
* Listener for passing playback state changes to the AudioPlayerFragment
*/
var onStateOrPlayingChanged: ((isPlaying: Boolean) -> Unit)? = null
var onNewVideoStarted: ((streamItem: StreamItem) -> Unit)? = null
private val watchPositionTimer = PauseableTimer(
onTick = ::saveWatchPosition,
delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS
@ -72,27 +60,11 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio
} else {
watchPositionTimer.pause()
}
onStateOrPlayingChanged?.let { it(isPlaying) }
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
onStateOrPlayingChanged?.let { it(exoPlayer?.isPlaying ?: false) }
this@AbstractPlayerService.onPlaybackStateChanged(playbackState)
}
override fun onPlayerError(error: PlaybackException) {
// show a toast on errors
Handler(Looper.getMainLooper()).post {
Toast.makeText(
applicationContext,
error.localizedMessage,
Toast.LENGTH_SHORT
).show()
}
toastFromMainThread(error.localizedMessage)
}
override fun onEvents(player: Player, events: Player.Events) {
@ -291,26 +263,6 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio
onDestroy()
}
abstract fun onPlaybackStateChanged(playbackState: Int)
abstract fun getChapters(): List<ChapterSegment>
fun getCurrentPosition() = exoPlayer?.currentPosition
fun getDuration() = exoPlayer?.duration
fun seekToPosition(position: Long) = exoPlayer?.seekTo(position)
inner class LocalBinder : Binder() {
// Return this instance of [AbstractPlayerService] so clients can call public methods
fun getService(): AbstractPlayerService = this@AbstractPlayerService
}
override fun onBind(intent: Intent?): IBinder {
// attempt to return [MediaLibraryServiceBinder] first if matched
return super.onBind(intent) ?: binder
}
companion object {
private const val START_SERVICE_ACTION = "start_service_action"
private const val RUN_PLAYER_COMMAND_ACTION = "run_player_command_action"

View File

@ -6,10 +6,8 @@ import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.DownloadChapter
import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.db.obj.filterByTab
import com.github.libretube.enums.FileType
@ -46,6 +44,14 @@ open class OfflinePlayerService : AbstractPlayerService() {
private val scope = CoroutineScope(Dispatchers.Main)
private val playerListener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_ENDED && PlayerHelper.isAutoPlayEnabled()) {
playNextVideo(PlayingQueue.getNext() ?: return)
}
}
}
override suspend fun onServiceCreated(args: Bundle) {
downloadTab = args.serializable(IntentData.downloadTab)!!
shuffle = args.getBoolean(IntentData.shuffle, false)
@ -65,6 +71,8 @@ open class OfflinePlayerService : AbstractPlayerService() {
streamItem.url?.toID()?.let { playNextVideo(it) }
}
exoPlayer?.addListener(playerListener)
fillQueue()
}
@ -76,7 +84,6 @@ open class OfflinePlayerService : AbstractPlayerService() {
Database.downloadDao().findById(videoId)
}!!
this.downloadWithItems = downloadWithItems
onNewVideoStarted?.let { it(downloadWithItems.download.toStreamItem()) }
PlayingQueue.updateCurrent(downloadWithItems.download.toStreamItem())
@ -106,7 +113,7 @@ open class OfflinePlayerService : AbstractPlayerService() {
val mediaItem = MediaItem.Builder()
.setUri(audioItem.path.toAndroidUri())
.setMetadata(downloadWithItems.download)
.setMetadata(downloadWithItems)
.build()
exoPlayer?.setMediaItem(mediaItem)
@ -141,14 +148,4 @@ open class OfflinePlayerService : AbstractPlayerService() {
super.onTaskRemoved(rootIntent)
onDestroy()
}
override fun onPlaybackStateChanged(playbackState: Int) {
// automatically go to the next video/audio when the current one ended
if (playbackState == Player.STATE_ENDED && PlayerHelper.isAutoPlayEnabled()) {
playNextVideo(PlayingQueue.getNext() ?: return)
}
}
override fun getChapters(): List<ChapterSegment> =
downloadWithItems?.downloadChapters.orEmpty().map(DownloadChapter::toChapterSegment)
}

View File

@ -8,7 +8,6 @@ import androidx.media3.common.Player
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.StreamsExtractor
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.Streams
import com.github.libretube.constants.IntentData
@ -55,6 +54,32 @@ class OnlinePlayerService : AbstractPlayerService() {
private val scope = CoroutineScope(Dispatchers.IO)
private val playerListener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Player.STATE_ENDED -> {
if (!isTransitioning) playNextVideo()
}
Player.STATE_IDLE -> {
onDestroy()
}
Player.STATE_BUFFERING -> {}
Player.STATE_READY -> {
isTransitioning = false
// save video to watch history when the video starts playing or is being resumed
// waiting for the player to be ready since the video can't be claimed to be watched
// while it did not yet start actually, but did buffer only so far
scope.launch(Dispatchers.IO) {
streams?.let { DatabaseHelper.addToWatchHistory(videoId, it) }
}
}
}
}
}
override suspend fun onServiceCreated(args: Bundle) {
val playerData = args.parcelable<PlayerData>(IntentData.playerData)
if (playerData == null) {
@ -72,6 +97,8 @@ class OnlinePlayerService : AbstractPlayerService() {
PlayingQueue.setOnQueueTapListener { streamItem ->
streamItem.url?.toID()?.let { playNextVideo(it) }
}
exoPlayer?.addListener(playerListener)
}
override suspend fun startPlayback() {
@ -127,8 +154,6 @@ class OnlinePlayerService : AbstractPlayerService() {
}
}
streams?.let { onNewVideoStarted?.invoke(it.toStreamItem(videoId)) }
exoPlayer?.apply {
playWhenReady = PlayerHelper.playAutomatically
prepare()
@ -208,30 +233,4 @@ class OnlinePlayerService : AbstractPlayerService() {
exoPlayer?.checkForSegments(this, sponsorBlockSegments, sponsorBlockConfig)
}
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Player.STATE_ENDED -> {
if (!isTransitioning) playNextVideo()
}
Player.STATE_IDLE -> {
onDestroy()
}
Player.STATE_BUFFERING -> {}
Player.STATE_READY -> {
isTransitioning = false
// save video to watch history when the video starts playing or is being resumed
// waiting for the player to be ready since the video can't be claimed to be watched
// while it did not yet start actually, but did buffer only so far
scope.launch(Dispatchers.IO) {
streams?.let { DatabaseHelper.addToWatchHistory(videoId, it) }
}
}
}
}
override fun getChapters(): List<ChapterSegment> = streams?.chapters.orEmpty()
}

View File

@ -39,7 +39,7 @@ class VideoOfflinePlayerService: OfflinePlayerService() {
videoUri != null && audioUri != null -> {
val videoItem = MediaItem.Builder()
.setUri(videoUri)
.setMetadata(downloadWithItems.download)
.setMetadata(downloadWithItems)
.setSubtitleConfigurations(listOfNotNull(subtitle))
.build()
@ -63,7 +63,7 @@ class VideoOfflinePlayerService: OfflinePlayerService() {
videoUri != null -> exoPlayer?.setMediaItem(
MediaItem.Builder()
.setUri(videoUri)
.setMetadata(downloadWithItems.download)
.setMetadata(downloadWithItems)
.setSubtitleConfigurations(listOfNotNull(subtitle))
.build()
)
@ -71,7 +71,7 @@ class VideoOfflinePlayerService: OfflinePlayerService() {
audioUri != null -> exoPlayer?.setMediaItem(
MediaItem.Builder()
.setUri(audioUri)
.setMetadata(downloadWithItems.download)
.setMetadata(downloadWithItems)
.setSubtitleConfigurations(listOfNotNull(subtitle))
.build()
)

View File

@ -13,7 +13,6 @@ import androidx.media3.datasource.cronet.CronetDataSource
import androidx.media3.exoplayer.hls.HlsMediaSource
import com.github.libretube.R
import com.github.libretube.api.CronetHelper
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.Streams
import com.github.libretube.api.obj.Subtitle
import com.github.libretube.constants.IntentData
@ -172,8 +171,4 @@ class VideoOnlinePlayerService : AbstractPlayerService() {
setPreferredTextRoleFlags(roleFlags)
setPreferredTextLanguage(subtitle?.code)
}
override fun onPlaybackStateChanged(playbackState: Int) = Unit
override fun getChapters(): List<ChapterSegment> = emptyList()
}

View File

@ -1,7 +1,6 @@
package com.github.libretube.ui.activities
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
@ -17,7 +16,6 @@ import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.media3.ui.PlayerView
import com.github.libretube.compat.PictureInPictureCompat
import com.github.libretube.compat.PictureInPictureParamsCompat
@ -60,7 +58,7 @@ class OfflinePlayerActivity : BaseActivity() {
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
private val commonPlayerViewModel: CommonPlayerViewModel by viewModels()
private val chaptersViewModel: ChaptersViewModel by viewModels()
private val watchPositionTimer = PauseableTimer(
onTick = this::saveWatchPosition,
delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS
@ -137,7 +135,7 @@ class OfflinePlayerActivity : BaseActivity() {
val isPlaying = ::playerController.isInitialized && playerController.isPlaying
PictureInPictureParamsCompat.Builder()
.setActions(PlayerHelper.getPiPModeActions(this,isPlaying))
.setActions(PlayerHelper.getPiPModeActions(this, isPlaying))
.setAutoEnterEnabled(PlayerHelper.pipEnabled && isPlaying)
.apply {
if (isPlaying) {
@ -164,15 +162,11 @@ class OfflinePlayerActivity : BaseActivity() {
playNextVideo(streamItem.url ?: return@setOnQueueTapListener)
}
val sessionToken = SessionToken(
this,
ComponentName(this, VideoOfflinePlayerService::class.java)
)
val arguments = bundleOf(
IntentData.downloadTab to DownloadTab.VIDEO,
IntentData.videoId to videoId
)
BackgroundHelper.startMediaService(this, sessionToken, arguments) {
BackgroundHelper.startMediaService(this, VideoOfflinePlayerService::class.java, arguments) {
playerController = it
playerController.addListener(playerListener)
initializePlayerView()
@ -281,7 +275,7 @@ class OfflinePlayerActivity : BaseActivity() {
override fun onDestroy() {
saveWatchPosition()
watchPositionTimer.destroy()
runCatching {

View File

@ -1,13 +1,9 @@
package com.github.libretube.ui.fragments
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.text.format.DateUtils
import android.view.LayoutInflater
@ -22,14 +18,18 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.FragmentAudioPlayerBinding
import com.github.libretube.extensions.normalize
import com.github.libretube.extensions.parcelableList
import com.github.libretube.extensions.seekBy
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.serializable
import com.github.libretube.extensions.togglePlayPauseState
import com.github.libretube.extensions.updateIfChanged
import com.github.libretube.helpers.AudioHelper
@ -40,7 +40,6 @@ import com.github.libretube.helpers.NavBarHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.services.AbstractPlayerService
import com.github.libretube.services.OfflinePlayerService
import com.github.libretube.services.OnlinePlayerService
import com.github.libretube.ui.activities.MainActivity
@ -77,31 +76,23 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
private var handler = Handler(Looper.getMainLooper())
private var isPaused = !PlayerHelper.playAutomatically
private var playerService: AbstractPlayerService? = null
/** Defines callbacks for service binding, passed to bindService() */
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
val binder = service as AbstractPlayerService.LocalBinder
playerService = binder.getService()
handleServiceConnection()
}
override fun onServiceDisconnected(arg0: ComponentName) {}
}
private var isOffline: Boolean = false
private var playerController: MediaController? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
audioHelper = AudioHelper(requireContext())
val isOffline = requireArguments().getBoolean(IntentData.offlinePlayer)
isOffline = requireArguments().getBoolean(IntentData.offlinePlayer)
val serviceClass =
if (isOffline) OfflinePlayerService::class.java else OnlinePlayerService::class.java
Intent(activity, serviceClass).also { intent ->
activity.bindService(intent, connection, 0)
BackgroundHelper.startMediaService(
requireContext(),
if (isOffline) OfflinePlayerService::class.java else OnlinePlayerService::class.java,
bundleOf()
) {
playerController = it
handleServiceConnection()
}
}
@ -155,10 +146,10 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
it.text = (PlayerHelper.seekIncrement / 1000).toString()
}
binding.rewindFL.setOnClickListener {
playerService?.exoPlayer?.seekBy(-PlayerHelper.seekIncrement)
playerController?.seekBy(-PlayerHelper.seekIncrement)
}
binding.forwardFL.setOnClickListener {
playerService?.exoPlayer?.seekBy(PlayerHelper.seekIncrement)
playerController?.seekBy(PlayerHelper.seekIncrement)
}
binding.openQueue.setOnClickListener {
@ -166,7 +157,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
}
binding.playbackOptions.setOnClickListener {
playerService?.exoPlayer?.let {
playerController?.let {
PlaybackOptionsSheet(it)
.show(childFragmentManager)
}
@ -182,7 +173,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
NavigationHelper.navigateVideo(
context = requireContext(),
videoUrlOrId = PlayingQueue.getCurrent()?.url,
timestamp = playerService?.exoPlayer?.currentPosition?.div(1000) ?: 0,
timestamp = playerController?.currentPosition?.div(1000) ?: 0,
keepQueue = true,
forceVideo = true
)
@ -192,24 +183,24 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
ChaptersBottomSheet.SEEK_TO_POSITION_REQUEST_KEY,
viewLifecycleOwner
) { _, bundle ->
playerService?.exoPlayer?.seekTo(bundle.getLong(IntentData.currentPosition))
playerController?.seekTo(bundle.getLong(IntentData.currentPosition))
}
binding.openChapters.setOnClickListener {
val playerService = playerService ?: return@setOnClickListener
chaptersModel.chaptersLiveData.value = playerService.getChapters()
chaptersModel.chaptersLiveData.value =
playerController?.mediaMetadata?.extras?.serializable(IntentData.chapters)
ChaptersBottomSheet()
.apply {
arguments = bundleOf(
IntentData.duration to playerService.exoPlayer?.duration?.div(1000)
IntentData.duration to playerController?.duration?.div(1000)
)
}
.show(childFragmentManager)
}
binding.miniPlayerClose.setOnClickListener {
activity.unbindService(connection)
playerController?.release()
BackgroundHelper.stopBackgroundPlay(requireContext())
killFragment()
}
@ -218,20 +209,17 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
binding.thumbnail.setOnTouchListener(listener)
binding.playPause.setOnClickListener {
playerService?.exoPlayer?.togglePlayPauseState()
playerController?.togglePlayPauseState()
}
binding.miniPlayerPause.setOnClickListener {
playerService?.exoPlayer?.togglePlayPauseState()
playerController?.togglePlayPauseState()
}
binding.showMore.setOnClickListener {
onLongTap()
}
// load the stream info into the UI
updateStreamInfo()
// update the currently shown volume
binding.volumeProgressBar.let { bar ->
bar.progress = audioHelper.getVolumeWithScale(bar.max)
@ -297,20 +285,19 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
/**
* Load the information from a new stream into the UI
*/
private fun updateStreamInfo(stream: StreamItem? = null) {
private fun updateStreamInfo(metadata: MediaMetadata) {
val binding = _binding ?: return
val current = stream ?: PlayingQueue.getCurrent() ?: return
binding.title.text = metadata.title
binding.miniPlayerTitle.text = metadata.title
binding.title.text = current.title
binding.miniPlayerTitle.text = current.title
binding.uploader.text = current.uploaderName
binding.uploader.text = metadata.artist
binding.uploader.setOnClickListener {
NavigationHelper.navigateChannel(requireContext(), current.uploaderUrl?.toID())
val uploaderId = metadata.composer?.toString() ?: return@setOnClickListener
NavigationHelper.navigateChannel(requireContext(), uploaderId)
}
current.thumbnail?.let { updateThumbnailAsync(it) }
metadata.artworkUri?.let { updateThumbnailAsync(it.toString()) }
initializeSeekBar()
}
@ -344,7 +331,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
private fun initializeSeekBar() {
binding.timeBar.addOnChangeListener { _, value, fromUser ->
if (fromUser) playerService?.seekToPosition(value.toLong() * 1000)
if (fromUser) playerController?.seekTo(value.toLong() * 1000)
}
updateSeekBar()
}
@ -354,7 +341,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
*/
private fun updateSeekBar() {
val binding = _binding ?: return
val duration = playerService?.getDuration()?.takeIf { it > 0 } ?: let {
val duration = playerController?.duration?.takeIf { it > 0 } ?: let {
// if there's no duration available, clear everything
binding.timeBar.value = 0f
binding.duration.text = ""
@ -362,7 +349,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
handler.postDelayed(this::updateSeekBar, 100)
return
}
val currentPosition = playerService?.getCurrentPosition()?.toFloat() ?: 0f
val currentPosition = playerController?.currentPosition?.toFloat() ?: 0f
// set the text for the indicators
binding.duration.text = DateUtils.formatElapsedTime(duration / 1000)
@ -381,7 +368,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
}
private fun updatePlayPauseButton() {
playerService?.exoPlayer?.let {
playerController?.let {
val binding = _binding ?: return
val iconRes = PlayerHelper.getPlayPauseActionIcon(it)
@ -391,19 +378,25 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
}
private fun handleServiceConnection() {
playerService?.onStateOrPlayingChanged = { isPlaying ->
updatePlayPauseButton()
isPaused = !isPlaying
}
playerService?.onNewVideoStarted = { streamItem ->
handler.post {
updateStreamInfo(streamItem)
_binding?.openChapters?.isVisible = !playerService?.getChapters().isNullOrEmpty()
playerController?.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
updatePlayPauseButton()
isPaused = !isPlaying
}
}
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
super.onMediaMetadataChanged(mediaMetadata)
updateStreamInfo(mediaMetadata)
val chapters: List<ChapterSegment>? = mediaMetadata.extras?.parcelableList(IntentData.chapters)
_binding?.openChapters?.isVisible = !chapters.isNullOrEmpty()
}
})
initializeSeekBar()
if (playerService is OfflinePlayerService) {
if (isOffline) {
binding.openVideo.isGone = true
}
}
@ -413,18 +406,8 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
_binding = null
}
override fun onDestroy() {
// unregister all listeners and the connected [playerService]
playerService?.onStateOrPlayingChanged = null
runCatching {
activity.unbindService(connection)
}
super.onDestroy()
}
override fun onSingleTap() {
playerService?.exoPlayer?.togglePlayPauseState()
playerController?.togglePlayPauseState()
}
override fun onLongTap() {
@ -481,10 +464,11 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
if (_binding == null) return
handler.postDelayed(this::updateChapterIndex, 100)
val player = playerService?.exoPlayer ?: return
val currentIndex =
PlayerHelper.getCurrentChapterIndex(player.currentPosition, chaptersModel.chapters)
PlayerHelper.getCurrentChapterIndex(
playerController?.currentPosition ?: return,
chaptersModel.chapters
)
chaptersModel.currentChapterIndex.updateIfChanged(currentIndex ?: return)
}
}

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
@ -48,7 +47,6 @@ import androidx.media3.common.C
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.obj.ChapterSegment
@ -389,11 +387,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
fullscreenResolution = PlayerHelper.getDefaultResolution(requireContext(), true)
noFullscreenResolution = PlayerHelper.getDefaultResolution(requireContext(), false)
val sessionToken = SessionToken(
requireContext(),
ComponentName(requireContext(), VideoOnlinePlayerService::class.java)
)
BackgroundHelper.startMediaService(requireContext(), sessionToken, bundleOf()) {
BackgroundHelper.startMediaService(requireContext(), VideoOnlinePlayerService::class.java, bundleOf()) {
playerController = it
playerController.addListener(playerListener)
}

View File

@ -6,8 +6,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaController
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.constants.PreferenceKeys
@ -19,7 +17,7 @@ import com.github.libretube.services.AbstractPlayerService
import com.github.libretube.ui.adapters.SliderLabelsAdapter
class PlaybackOptionsSheet(
private val player: Player
private val player: MediaController
) : ExpandedBottomSheet() {
private var _binding: PlaybackBottomSheetBinding? = null
private val binding get() = _binding!!
@ -64,15 +62,10 @@ class PlaybackOptionsSheet(
}
binding.skipSilence.setOnCheckedChangeListener { _, isChecked ->
// TODO: unify the skip silence handling
if (player is ExoPlayer) {
player.skipSilenceEnabled = isChecked
} else if (player is MediaController) {
player.sendCustomCommand(
AbstractPlayerService.runPlayerActionCommand,
bundleOf(PlayerCommand.SKIP_SILENCE.name to isChecked)
)
}
player.sendCustomCommand(
AbstractPlayerService.runPlayerActionCommand,
bundleOf(PlayerCommand.SKIP_SILENCE.name to isChecked)
)
PreferenceHelper.putBoolean(PreferenceKeys.SKIP_SILENCE, isChecked)
}
}