mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 08:20:32 +05:30
refactor: use MediaController instead of ServiceBinder in AudioPlayerFragment
This commit is contained in:
parent
9ab76ba47b
commit
f4a8d7b6b1
@ -56,4 +56,5 @@ object IntentData {
|
||||
const val isPlayingOffline = "isPlayingOffline"
|
||||
const val downloadInfo = "downloadInfo"
|
||||
const val streams = "streams"
|
||||
const val chapters = "chapters"
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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()
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
)
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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()
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user