mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 16:30:31 +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 isPlayingOffline = "isPlayingOffline"
|
||||||
const val downloadInfo = "downloadInfo"
|
const val downloadInfo = "downloadInfo"
|
||||||
const val streams = "streams"
|
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)
|
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>? {
|
inline fun <reified T : Parcelable> Bundle.parcelableArrayList(key: String?): ArrayList<T>? {
|
||||||
return BundleCompat.getParcelableArrayList(this, key, T::class.java)
|
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
|
package com.github.libretube.extensions
|
||||||
|
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.github.libretube.api.obj.Streams
|
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 {
|
fun MediaItem.Builder.setMetadata(streams: Streams) = apply {
|
||||||
val extras = bundleOf(
|
val extras = bundleOf(
|
||||||
MediaMetadataCompat.METADATA_KEY_TITLE to streams.title,
|
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(
|
setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata.Builder()
|
||||||
.setTitle(streams.title)
|
.setTitle(streams.title)
|
||||||
.setArtist(streams.uploader)
|
.setArtist(streams.uploader)
|
||||||
|
.setDurationMs(streams.duration.times(1000))
|
||||||
.setArtworkUri(streams.thumbnailUrl.toUri())
|
.setArtworkUri(streams.thumbnailUrl.toUri())
|
||||||
|
.setComposer(streams.uploaderUrl.toID())
|
||||||
.setExtras(extras)
|
.setExtras(extras)
|
||||||
.build()
|
.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(
|
val extras = bundleOf(
|
||||||
MediaMetadataCompat.METADATA_KEY_TITLE to download.title,
|
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(
|
setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata.Builder()
|
||||||
.setTitle(download.title)
|
.setTitle(download.title)
|
||||||
.setArtist(download.uploader)
|
.setArtist(download.uploader)
|
||||||
|
.setDurationMs(download.duration?.times(1000))
|
||||||
.setArtworkUri(download.thumbnailPath?.toAndroidUri())
|
.setArtworkUri(download.thumbnailPath?.toAndroidUri())
|
||||||
.setExtras(extras)
|
.setExtras(extras)
|
||||||
.build()
|
.build()
|
||||||
|
@ -53,10 +53,11 @@ object BackgroundHelper {
|
|||||||
|
|
||||||
val playerData = PlayerData(videoId, playlistId, channelId, keepQueue, position)
|
val playerData = PlayerData(videoId, playlistId, channelId, keepQueue, position)
|
||||||
|
|
||||||
val sessionToken =
|
startMediaService(
|
||||||
SessionToken(context, ComponentName(context, OnlinePlayerService::class.java))
|
context,
|
||||||
|
OnlinePlayerService::class.java,
|
||||||
startMediaService(context, sessionToken, bundleOf(IntentData.playerData to playerData))
|
bundleOf(IntentData.playerData to playerData)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,8 +99,6 @@ object BackgroundHelper {
|
|||||||
downloadTab: DownloadTab,
|
downloadTab: DownloadTab,
|
||||||
shuffle: Boolean = false
|
shuffle: Boolean = false
|
||||||
) {
|
) {
|
||||||
stopBackgroundPlay(context)
|
|
||||||
|
|
||||||
// whether the service is started from the MainActivity or NoInternetActivity
|
// whether the service is started from the MainActivity or NoInternetActivity
|
||||||
val noInternet = ContextHelper.tryUnwrapActivity<NoInternetActivity>(context) != null
|
val noInternet = ContextHelper.tryUnwrapActivity<NoInternetActivity>(context) != null
|
||||||
|
|
||||||
@ -110,19 +109,21 @@ object BackgroundHelper {
|
|||||||
IntentData.noInternet to noInternet
|
IntentData.noInternet to noInternet
|
||||||
)
|
)
|
||||||
|
|
||||||
val sessionToken =
|
startMediaService(context, OfflinePlayerService::class.java, arguments)
|
||||||
SessionToken(context, ComponentName(context, OfflinePlayerService::class.java))
|
|
||||||
|
|
||||||
startMediaService(context, sessionToken, arguments)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
fun startMediaService(
|
fun startMediaService(
|
||||||
context: Context,
|
context: Context,
|
||||||
sessionToken: SessionToken,
|
serviceClass: Class<*>,
|
||||||
arguments: Bundle,
|
arguments: Bundle,
|
||||||
onController: (MediaController) -> Unit = {}
|
onController: (MediaController) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
stopBackgroundPlay(context)
|
||||||
|
|
||||||
|
val sessionToken =
|
||||||
|
SessionToken(context, ComponentName(context, serviceClass))
|
||||||
|
|
||||||
val controllerFuture =
|
val controllerFuture =
|
||||||
MediaController.Builder(context, sessionToken).buildAsync()
|
MediaController.Builder(context, sessionToken).buildAsync()
|
||||||
controllerFuture.addListener({
|
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
|
package com.github.libretube.services
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Binder
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
@ -22,10 +19,9 @@ import androidx.media3.session.MediaSession
|
|||||||
import androidx.media3.session.SessionCommand
|
import androidx.media3.session.SessionCommand
|
||||||
import androidx.media3.session.SessionResult
|
import androidx.media3.session.SessionResult
|
||||||
import com.github.libretube.R
|
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.PlayerCommand
|
||||||
import com.github.libretube.enums.PlayerEvent
|
import com.github.libretube.enums.PlayerEvent
|
||||||
|
import com.github.libretube.extensions.toastFromMainThread
|
||||||
import com.github.libretube.extensions.updateParameters
|
import com.github.libretube.extensions.updateParameters
|
||||||
import com.github.libretube.helpers.PlayerHelper
|
import com.github.libretube.helpers.PlayerHelper
|
||||||
import com.github.libretube.util.NowPlayingNotification
|
import com.github.libretube.util.NowPlayingNotification
|
||||||
@ -49,14 +45,6 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio
|
|||||||
|
|
||||||
val handler = Handler(Looper.getMainLooper())
|
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(
|
private val watchPositionTimer = PauseableTimer(
|
||||||
onTick = ::saveWatchPosition,
|
onTick = ::saveWatchPosition,
|
||||||
delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS
|
delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS
|
||||||
@ -72,27 +60,11 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio
|
|||||||
} else {
|
} else {
|
||||||
watchPositionTimer.pause()
|
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) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
// show a toast on errors
|
// show a toast on errors
|
||||||
Handler(Looper.getMainLooper()).post {
|
toastFromMainThread(error.localizedMessage)
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
error.localizedMessage,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
@ -291,26 +263,6 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio
|
|||||||
onDestroy()
|
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 {
|
companion object {
|
||||||
private const val START_SERVICE_ACTION = "start_service_action"
|
private const val START_SERVICE_ACTION = "start_service_action"
|
||||||
private const val RUN_PLAYER_COMMAND_ACTION = "run_player_command_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.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.github.libretube.api.obj.ChapterSegment
|
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.db.DatabaseHolder.Database
|
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.DownloadWithItems
|
||||||
import com.github.libretube.db.obj.filterByTab
|
import com.github.libretube.db.obj.filterByTab
|
||||||
import com.github.libretube.enums.FileType
|
import com.github.libretube.enums.FileType
|
||||||
@ -46,6 +44,14 @@ open class OfflinePlayerService : AbstractPlayerService() {
|
|||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Main)
|
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) {
|
override suspend fun onServiceCreated(args: Bundle) {
|
||||||
downloadTab = args.serializable(IntentData.downloadTab)!!
|
downloadTab = args.serializable(IntentData.downloadTab)!!
|
||||||
shuffle = args.getBoolean(IntentData.shuffle, false)
|
shuffle = args.getBoolean(IntentData.shuffle, false)
|
||||||
@ -65,6 +71,8 @@ open class OfflinePlayerService : AbstractPlayerService() {
|
|||||||
streamItem.url?.toID()?.let { playNextVideo(it) }
|
streamItem.url?.toID()?.let { playNextVideo(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exoPlayer?.addListener(playerListener)
|
||||||
|
|
||||||
fillQueue()
|
fillQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +84,6 @@ open class OfflinePlayerService : AbstractPlayerService() {
|
|||||||
Database.downloadDao().findById(videoId)
|
Database.downloadDao().findById(videoId)
|
||||||
}!!
|
}!!
|
||||||
this.downloadWithItems = downloadWithItems
|
this.downloadWithItems = downloadWithItems
|
||||||
onNewVideoStarted?.let { it(downloadWithItems.download.toStreamItem()) }
|
|
||||||
|
|
||||||
PlayingQueue.updateCurrent(downloadWithItems.download.toStreamItem())
|
PlayingQueue.updateCurrent(downloadWithItems.download.toStreamItem())
|
||||||
|
|
||||||
@ -106,7 +113,7 @@ open class OfflinePlayerService : AbstractPlayerService() {
|
|||||||
|
|
||||||
val mediaItem = MediaItem.Builder()
|
val mediaItem = MediaItem.Builder()
|
||||||
.setUri(audioItem.path.toAndroidUri())
|
.setUri(audioItem.path.toAndroidUri())
|
||||||
.setMetadata(downloadWithItems.download)
|
.setMetadata(downloadWithItems)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
exoPlayer?.setMediaItem(mediaItem)
|
exoPlayer?.setMediaItem(mediaItem)
|
||||||
@ -141,14 +148,4 @@ open class OfflinePlayerService : AbstractPlayerService() {
|
|||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
onDestroy()
|
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.JsonHelper
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.api.StreamsExtractor
|
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.Segment
|
||||||
import com.github.libretube.api.obj.Streams
|
import com.github.libretube.api.obj.Streams
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
@ -55,6 +54,32 @@ class OnlinePlayerService : AbstractPlayerService() {
|
|||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
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) {
|
override suspend fun onServiceCreated(args: Bundle) {
|
||||||
val playerData = args.parcelable<PlayerData>(IntentData.playerData)
|
val playerData = args.parcelable<PlayerData>(IntentData.playerData)
|
||||||
if (playerData == null) {
|
if (playerData == null) {
|
||||||
@ -72,6 +97,8 @@ class OnlinePlayerService : AbstractPlayerService() {
|
|||||||
PlayingQueue.setOnQueueTapListener { streamItem ->
|
PlayingQueue.setOnQueueTapListener { streamItem ->
|
||||||
streamItem.url?.toID()?.let { playNextVideo(it) }
|
streamItem.url?.toID()?.let { playNextVideo(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exoPlayer?.addListener(playerListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startPlayback() {
|
override suspend fun startPlayback() {
|
||||||
@ -127,8 +154,6 @@ class OnlinePlayerService : AbstractPlayerService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
streams?.let { onNewVideoStarted?.invoke(it.toStreamItem(videoId)) }
|
|
||||||
|
|
||||||
exoPlayer?.apply {
|
exoPlayer?.apply {
|
||||||
playWhenReady = PlayerHelper.playAutomatically
|
playWhenReady = PlayerHelper.playAutomatically
|
||||||
prepare()
|
prepare()
|
||||||
@ -208,30 +233,4 @@ class OnlinePlayerService : AbstractPlayerService() {
|
|||||||
|
|
||||||
exoPlayer?.checkForSegments(this, sponsorBlockSegments, sponsorBlockConfig)
|
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 -> {
|
videoUri != null && audioUri != null -> {
|
||||||
val videoItem = MediaItem.Builder()
|
val videoItem = MediaItem.Builder()
|
||||||
.setUri(videoUri)
|
.setUri(videoUri)
|
||||||
.setMetadata(downloadWithItems.download)
|
.setMetadata(downloadWithItems)
|
||||||
.setSubtitleConfigurations(listOfNotNull(subtitle))
|
.setSubtitleConfigurations(listOfNotNull(subtitle))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ class VideoOfflinePlayerService: OfflinePlayerService() {
|
|||||||
videoUri != null -> exoPlayer?.setMediaItem(
|
videoUri != null -> exoPlayer?.setMediaItem(
|
||||||
MediaItem.Builder()
|
MediaItem.Builder()
|
||||||
.setUri(videoUri)
|
.setUri(videoUri)
|
||||||
.setMetadata(downloadWithItems.download)
|
.setMetadata(downloadWithItems)
|
||||||
.setSubtitleConfigurations(listOfNotNull(subtitle))
|
.setSubtitleConfigurations(listOfNotNull(subtitle))
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@ -71,7 +71,7 @@ class VideoOfflinePlayerService: OfflinePlayerService() {
|
|||||||
audioUri != null -> exoPlayer?.setMediaItem(
|
audioUri != null -> exoPlayer?.setMediaItem(
|
||||||
MediaItem.Builder()
|
MediaItem.Builder()
|
||||||
.setUri(audioUri)
|
.setUri(audioUri)
|
||||||
.setMetadata(downloadWithItems.download)
|
.setMetadata(downloadWithItems)
|
||||||
.setSubtitleConfigurations(listOfNotNull(subtitle))
|
.setSubtitleConfigurations(listOfNotNull(subtitle))
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
@ -13,7 +13,6 @@ import androidx.media3.datasource.cronet.CronetDataSource
|
|||||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.CronetHelper
|
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.Streams
|
||||||
import com.github.libretube.api.obj.Subtitle
|
import com.github.libretube.api.obj.Subtitle
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
@ -172,8 +171,4 @@ class VideoOnlinePlayerService : AbstractPlayerService() {
|
|||||||
setPreferredTextRoleFlags(roleFlags)
|
setPreferredTextRoleFlags(roleFlags)
|
||||||
setPreferredTextLanguage(subtitle?.code)
|
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
|
package com.github.libretube.ui.activities
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
@ -17,7 +16,6 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.media3.session.SessionToken
|
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import com.github.libretube.compat.PictureInPictureCompat
|
import com.github.libretube.compat.PictureInPictureCompat
|
||||||
import com.github.libretube.compat.PictureInPictureParamsCompat
|
import com.github.libretube.compat.PictureInPictureParamsCompat
|
||||||
@ -60,7 +58,7 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
|
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
|
||||||
private val commonPlayerViewModel: CommonPlayerViewModel by viewModels()
|
private val commonPlayerViewModel: CommonPlayerViewModel by viewModels()
|
||||||
private val chaptersViewModel: ChaptersViewModel by viewModels()
|
private val chaptersViewModel: ChaptersViewModel by viewModels()
|
||||||
|
|
||||||
private val watchPositionTimer = PauseableTimer(
|
private val watchPositionTimer = PauseableTimer(
|
||||||
onTick = this::saveWatchPosition,
|
onTick = this::saveWatchPosition,
|
||||||
delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS
|
delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS
|
||||||
@ -137,7 +135,7 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
val isPlaying = ::playerController.isInitialized && playerController.isPlaying
|
val isPlaying = ::playerController.isInitialized && playerController.isPlaying
|
||||||
|
|
||||||
PictureInPictureParamsCompat.Builder()
|
PictureInPictureParamsCompat.Builder()
|
||||||
.setActions(PlayerHelper.getPiPModeActions(this,isPlaying))
|
.setActions(PlayerHelper.getPiPModeActions(this, isPlaying))
|
||||||
.setAutoEnterEnabled(PlayerHelper.pipEnabled && isPlaying)
|
.setAutoEnterEnabled(PlayerHelper.pipEnabled && isPlaying)
|
||||||
.apply {
|
.apply {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
@ -164,15 +162,11 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
playNextVideo(streamItem.url ?: return@setOnQueueTapListener)
|
playNextVideo(streamItem.url ?: return@setOnQueueTapListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sessionToken = SessionToken(
|
|
||||||
this,
|
|
||||||
ComponentName(this, VideoOfflinePlayerService::class.java)
|
|
||||||
)
|
|
||||||
val arguments = bundleOf(
|
val arguments = bundleOf(
|
||||||
IntentData.downloadTab to DownloadTab.VIDEO,
|
IntentData.downloadTab to DownloadTab.VIDEO,
|
||||||
IntentData.videoId to videoId
|
IntentData.videoId to videoId
|
||||||
)
|
)
|
||||||
BackgroundHelper.startMediaService(this, sessionToken, arguments) {
|
BackgroundHelper.startMediaService(this, VideoOfflinePlayerService::class.java, arguments) {
|
||||||
playerController = it
|
playerController = it
|
||||||
playerController.addListener(playerListener)
|
playerController.addListener(playerListener)
|
||||||
initializePlayerView()
|
initializePlayerView()
|
||||||
@ -281,7 +275,7 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
saveWatchPosition()
|
saveWatchPosition()
|
||||||
|
|
||||||
watchPositionTimer.destroy()
|
watchPositionTimer.destroy()
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
package com.github.libretube.ui.fragments
|
package com.github.libretube.ui.fragments
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@ -22,14 +18,18 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.session.MediaController
|
||||||
import com.github.libretube.R
|
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.constants.IntentData
|
||||||
import com.github.libretube.databinding.FragmentAudioPlayerBinding
|
import com.github.libretube.databinding.FragmentAudioPlayerBinding
|
||||||
import com.github.libretube.extensions.normalize
|
import com.github.libretube.extensions.normalize
|
||||||
|
import com.github.libretube.extensions.parcelableList
|
||||||
import com.github.libretube.extensions.seekBy
|
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.togglePlayPauseState
|
||||||
import com.github.libretube.extensions.updateIfChanged
|
import com.github.libretube.extensions.updateIfChanged
|
||||||
import com.github.libretube.helpers.AudioHelper
|
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.NavigationHelper
|
||||||
import com.github.libretube.helpers.PlayerHelper
|
import com.github.libretube.helpers.PlayerHelper
|
||||||
import com.github.libretube.helpers.ThemeHelper
|
import com.github.libretube.helpers.ThemeHelper
|
||||||
import com.github.libretube.services.AbstractPlayerService
|
|
||||||
import com.github.libretube.services.OfflinePlayerService
|
import com.github.libretube.services.OfflinePlayerService
|
||||||
import com.github.libretube.services.OnlinePlayerService
|
import com.github.libretube.services.OnlinePlayerService
|
||||||
import com.github.libretube.ui.activities.MainActivity
|
import com.github.libretube.ui.activities.MainActivity
|
||||||
@ -77,31 +76,23 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
private var handler = Handler(Looper.getMainLooper())
|
private var handler = Handler(Looper.getMainLooper())
|
||||||
private var isPaused = !PlayerHelper.playAutomatically
|
private var isPaused = !PlayerHelper.playAutomatically
|
||||||
|
|
||||||
private var playerService: AbstractPlayerService? = null
|
private var isOffline: Boolean = false
|
||||||
|
private var playerController: MediaController? = 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) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
audioHelper = AudioHelper(requireContext())
|
audioHelper = AudioHelper(requireContext())
|
||||||
|
|
||||||
val isOffline = requireArguments().getBoolean(IntentData.offlinePlayer)
|
isOffline = requireArguments().getBoolean(IntentData.offlinePlayer)
|
||||||
|
|
||||||
val serviceClass =
|
BackgroundHelper.startMediaService(
|
||||||
if (isOffline) OfflinePlayerService::class.java else OnlinePlayerService::class.java
|
requireContext(),
|
||||||
Intent(activity, serviceClass).also { intent ->
|
if (isOffline) OfflinePlayerService::class.java else OnlinePlayerService::class.java,
|
||||||
activity.bindService(intent, connection, 0)
|
bundleOf()
|
||||||
|
) {
|
||||||
|
playerController = it
|
||||||
|
handleServiceConnection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,10 +146,10 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
it.text = (PlayerHelper.seekIncrement / 1000).toString()
|
it.text = (PlayerHelper.seekIncrement / 1000).toString()
|
||||||
}
|
}
|
||||||
binding.rewindFL.setOnClickListener {
|
binding.rewindFL.setOnClickListener {
|
||||||
playerService?.exoPlayer?.seekBy(-PlayerHelper.seekIncrement)
|
playerController?.seekBy(-PlayerHelper.seekIncrement)
|
||||||
}
|
}
|
||||||
binding.forwardFL.setOnClickListener {
|
binding.forwardFL.setOnClickListener {
|
||||||
playerService?.exoPlayer?.seekBy(PlayerHelper.seekIncrement)
|
playerController?.seekBy(PlayerHelper.seekIncrement)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.openQueue.setOnClickListener {
|
binding.openQueue.setOnClickListener {
|
||||||
@ -166,7 +157,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackOptions.setOnClickListener {
|
binding.playbackOptions.setOnClickListener {
|
||||||
playerService?.exoPlayer?.let {
|
playerController?.let {
|
||||||
PlaybackOptionsSheet(it)
|
PlaybackOptionsSheet(it)
|
||||||
.show(childFragmentManager)
|
.show(childFragmentManager)
|
||||||
}
|
}
|
||||||
@ -182,7 +173,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
NavigationHelper.navigateVideo(
|
NavigationHelper.navigateVideo(
|
||||||
context = requireContext(),
|
context = requireContext(),
|
||||||
videoUrlOrId = PlayingQueue.getCurrent()?.url,
|
videoUrlOrId = PlayingQueue.getCurrent()?.url,
|
||||||
timestamp = playerService?.exoPlayer?.currentPosition?.div(1000) ?: 0,
|
timestamp = playerController?.currentPosition?.div(1000) ?: 0,
|
||||||
keepQueue = true,
|
keepQueue = true,
|
||||||
forceVideo = true
|
forceVideo = true
|
||||||
)
|
)
|
||||||
@ -192,24 +183,24 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
ChaptersBottomSheet.SEEK_TO_POSITION_REQUEST_KEY,
|
ChaptersBottomSheet.SEEK_TO_POSITION_REQUEST_KEY,
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
) { _, bundle ->
|
) { _, bundle ->
|
||||||
playerService?.exoPlayer?.seekTo(bundle.getLong(IntentData.currentPosition))
|
playerController?.seekTo(bundle.getLong(IntentData.currentPosition))
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.openChapters.setOnClickListener {
|
binding.openChapters.setOnClickListener {
|
||||||
val playerService = playerService ?: return@setOnClickListener
|
chaptersModel.chaptersLiveData.value =
|
||||||
chaptersModel.chaptersLiveData.value = playerService.getChapters()
|
playerController?.mediaMetadata?.extras?.serializable(IntentData.chapters)
|
||||||
|
|
||||||
ChaptersBottomSheet()
|
ChaptersBottomSheet()
|
||||||
.apply {
|
.apply {
|
||||||
arguments = bundleOf(
|
arguments = bundleOf(
|
||||||
IntentData.duration to playerService.exoPlayer?.duration?.div(1000)
|
IntentData.duration to playerController?.duration?.div(1000)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.show(childFragmentManager)
|
.show(childFragmentManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.miniPlayerClose.setOnClickListener {
|
binding.miniPlayerClose.setOnClickListener {
|
||||||
activity.unbindService(connection)
|
playerController?.release()
|
||||||
BackgroundHelper.stopBackgroundPlay(requireContext())
|
BackgroundHelper.stopBackgroundPlay(requireContext())
|
||||||
killFragment()
|
killFragment()
|
||||||
}
|
}
|
||||||
@ -218,20 +209,17 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
binding.thumbnail.setOnTouchListener(listener)
|
binding.thumbnail.setOnTouchListener(listener)
|
||||||
|
|
||||||
binding.playPause.setOnClickListener {
|
binding.playPause.setOnClickListener {
|
||||||
playerService?.exoPlayer?.togglePlayPauseState()
|
playerController?.togglePlayPauseState()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.miniPlayerPause.setOnClickListener {
|
binding.miniPlayerPause.setOnClickListener {
|
||||||
playerService?.exoPlayer?.togglePlayPauseState()
|
playerController?.togglePlayPauseState()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.showMore.setOnClickListener {
|
binding.showMore.setOnClickListener {
|
||||||
onLongTap()
|
onLongTap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// load the stream info into the UI
|
|
||||||
updateStreamInfo()
|
|
||||||
|
|
||||||
// update the currently shown volume
|
// update the currently shown volume
|
||||||
binding.volumeProgressBar.let { bar ->
|
binding.volumeProgressBar.let { bar ->
|
||||||
bar.progress = audioHelper.getVolumeWithScale(bar.max)
|
bar.progress = audioHelper.getVolumeWithScale(bar.max)
|
||||||
@ -297,20 +285,19 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
/**
|
/**
|
||||||
* Load the information from a new stream into the UI
|
* 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 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.uploader.text = metadata.artist
|
||||||
binding.miniPlayerTitle.text = current.title
|
|
||||||
|
|
||||||
binding.uploader.text = current.uploaderName
|
|
||||||
binding.uploader.setOnClickListener {
|
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()
|
initializeSeekBar()
|
||||||
}
|
}
|
||||||
@ -344,7 +331,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
|
|
||||||
private fun initializeSeekBar() {
|
private fun initializeSeekBar() {
|
||||||
binding.timeBar.addOnChangeListener { _, value, fromUser ->
|
binding.timeBar.addOnChangeListener { _, value, fromUser ->
|
||||||
if (fromUser) playerService?.seekToPosition(value.toLong() * 1000)
|
if (fromUser) playerController?.seekTo(value.toLong() * 1000)
|
||||||
}
|
}
|
||||||
updateSeekBar()
|
updateSeekBar()
|
||||||
}
|
}
|
||||||
@ -354,7 +341,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
*/
|
*/
|
||||||
private fun updateSeekBar() {
|
private fun updateSeekBar() {
|
||||||
val binding = _binding ?: return
|
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
|
// if there's no duration available, clear everything
|
||||||
binding.timeBar.value = 0f
|
binding.timeBar.value = 0f
|
||||||
binding.duration.text = ""
|
binding.duration.text = ""
|
||||||
@ -362,7 +349,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
handler.postDelayed(this::updateSeekBar, 100)
|
handler.postDelayed(this::updateSeekBar, 100)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val currentPosition = playerService?.getCurrentPosition()?.toFloat() ?: 0f
|
val currentPosition = playerController?.currentPosition?.toFloat() ?: 0f
|
||||||
|
|
||||||
// set the text for the indicators
|
// set the text for the indicators
|
||||||
binding.duration.text = DateUtils.formatElapsedTime(duration / 1000)
|
binding.duration.text = DateUtils.formatElapsedTime(duration / 1000)
|
||||||
@ -381,7 +368,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayPauseButton() {
|
private fun updatePlayPauseButton() {
|
||||||
playerService?.exoPlayer?.let {
|
playerController?.let {
|
||||||
val binding = _binding ?: return
|
val binding = _binding ?: return
|
||||||
|
|
||||||
val iconRes = PlayerHelper.getPlayPauseActionIcon(it)
|
val iconRes = PlayerHelper.getPlayPauseActionIcon(it)
|
||||||
@ -391,19 +378,25 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleServiceConnection() {
|
private fun handleServiceConnection() {
|
||||||
playerService?.onStateOrPlayingChanged = { isPlaying ->
|
playerController?.addListener(object : Player.Listener {
|
||||||
updatePlayPauseButton()
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
isPaused = !isPlaying
|
super.onIsPlayingChanged(isPlaying)
|
||||||
}
|
|
||||||
playerService?.onNewVideoStarted = { streamItem ->
|
updatePlayPauseButton()
|
||||||
handler.post {
|
isPaused = !isPlaying
|
||||||
updateStreamInfo(streamItem)
|
|
||||||
_binding?.openChapters?.isVisible = !playerService?.getChapters().isNullOrEmpty()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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()
|
initializeSeekBar()
|
||||||
|
|
||||||
if (playerService is OfflinePlayerService) {
|
if (isOffline) {
|
||||||
binding.openVideo.isGone = true
|
binding.openVideo.isGone = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -413,18 +406,8 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
// unregister all listeners and the connected [playerService]
|
|
||||||
playerService?.onStateOrPlayingChanged = null
|
|
||||||
runCatching {
|
|
||||||
activity.unbindService(connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSingleTap() {
|
override fun onSingleTap() {
|
||||||
playerService?.exoPlayer?.togglePlayPauseState()
|
playerController?.togglePlayPauseState()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongTap() {
|
override fun onLongTap() {
|
||||||
@ -481,10 +464,11 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
if (_binding == null) return
|
if (_binding == null) return
|
||||||
handler.postDelayed(this::updateChapterIndex, 100)
|
handler.postDelayed(this::updateChapterIndex, 100)
|
||||||
|
|
||||||
val player = playerService?.exoPlayer ?: return
|
|
||||||
|
|
||||||
val currentIndex =
|
val currentIndex =
|
||||||
PlayerHelper.getCurrentChapterIndex(player.currentPosition, chaptersModel.chapters)
|
PlayerHelper.getCurrentChapterIndex(
|
||||||
|
playerController?.currentPosition ?: return,
|
||||||
|
chaptersModel.chapters
|
||||||
|
)
|
||||||
chaptersModel.currentChapterIndex.updateIfChanged(currentIndex ?: return)
|
chaptersModel.currentChapterIndex.updateIfChanged(currentIndex ?: return)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
@ -48,7 +47,6 @@ import androidx.media3.common.C
|
|||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.media3.session.SessionToken
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.obj.ChapterSegment
|
import com.github.libretube.api.obj.ChapterSegment
|
||||||
@ -389,11 +387,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
fullscreenResolution = PlayerHelper.getDefaultResolution(requireContext(), true)
|
fullscreenResolution = PlayerHelper.getDefaultResolution(requireContext(), true)
|
||||||
noFullscreenResolution = PlayerHelper.getDefaultResolution(requireContext(), false)
|
noFullscreenResolution = PlayerHelper.getDefaultResolution(requireContext(), false)
|
||||||
|
|
||||||
val sessionToken = SessionToken(
|
BackgroundHelper.startMediaService(requireContext(), VideoOnlinePlayerService::class.java, bundleOf()) {
|
||||||
requireContext(),
|
|
||||||
ComponentName(requireContext(), VideoOnlinePlayerService::class.java)
|
|
||||||
)
|
|
||||||
BackgroundHelper.startMediaService(requireContext(), sessionToken, bundleOf()) {
|
|
||||||
playerController = it
|
playerController = it
|
||||||
playerController.addListener(playerListener)
|
playerController.addListener(playerListener)
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.media3.common.PlaybackParameters
|
import androidx.media3.common.PlaybackParameters
|
||||||
import androidx.media3.common.Player
|
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
@ -19,7 +17,7 @@ import com.github.libretube.services.AbstractPlayerService
|
|||||||
import com.github.libretube.ui.adapters.SliderLabelsAdapter
|
import com.github.libretube.ui.adapters.SliderLabelsAdapter
|
||||||
|
|
||||||
class PlaybackOptionsSheet(
|
class PlaybackOptionsSheet(
|
||||||
private val player: Player
|
private val player: MediaController
|
||||||
) : ExpandedBottomSheet() {
|
) : ExpandedBottomSheet() {
|
||||||
private var _binding: PlaybackBottomSheetBinding? = null
|
private var _binding: PlaybackBottomSheetBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
@ -64,15 +62,10 @@ class PlaybackOptionsSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.skipSilence.setOnCheckedChangeListener { _, isChecked ->
|
binding.skipSilence.setOnCheckedChangeListener { _, isChecked ->
|
||||||
// TODO: unify the skip silence handling
|
player.sendCustomCommand(
|
||||||
if (player is ExoPlayer) {
|
AbstractPlayerService.runPlayerActionCommand,
|
||||||
player.skipSilenceEnabled = isChecked
|
bundleOf(PlayerCommand.SKIP_SILENCE.name to isChecked)
|
||||||
} else if (player is MediaController) {
|
)
|
||||||
player.sendCustomCommand(
|
|
||||||
AbstractPlayerService.runPlayerActionCommand,
|
|
||||||
bundleOf(PlayerCommand.SKIP_SILENCE.name to isChecked)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
PreferenceHelper.putBoolean(PreferenceKeys.SKIP_SILENCE, isChecked)
|
PreferenceHelper.putBoolean(PreferenceKeys.SKIP_SILENCE, isChecked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user