diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f9d088555..30cba5243 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -404,13 +404,49 @@
android:name=".services.OnlinePlayerService"
android:enabled="true"
android:exported="false"
- android:foregroundServiceType="mediaPlayback" />
+ android:foregroundServiceType="mediaPlayback">
+
+
+
+
+
+
+ android:foregroundServiceType="mediaPlayback">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
val urlTexts: List
-)
+): Parcelable
diff --git a/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt b/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt
index 663c9692c..0b37fbf69 100644
--- a/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt
@@ -1,11 +1,14 @@
package com.github.libretube.api.obj
+import android.os.Parcelable
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.enums.FileType
+import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import kotlin.io.path.Path
@Serializable
+@Parcelize
data class PipedStream(
var url: String? = null,
val format: String? = null,
@@ -26,7 +29,7 @@ data class PipedStream(
val contentLength: Long = -1,
val audioTrackType: String? = null,
val audioTrackLocale: String? = null
-) {
+): Parcelable {
private fun getQualityString(fileName: String): String {
return "${fileName}_${quality?.replace(" ", "_")}_$format." +
mimeType?.split("/")?.last()
diff --git a/app/src/main/java/com/github/libretube/api/obj/PreviewFrames.kt b/app/src/main/java/com/github/libretube/api/obj/PreviewFrames.kt
index ad4371960..8ad44bac5 100644
--- a/app/src/main/java/com/github/libretube/api/obj/PreviewFrames.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/PreviewFrames.kt
@@ -1,8 +1,11 @@
package com.github.libretube.api.obj
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
+@Parcelize
data class PreviewFrames(
val urls: List,
val frameWidth: Int,
@@ -11,4 +14,4 @@ data class PreviewFrames(
val durationPerFrame: Long,
val framesPerPageX: Int,
val framesPerPageY: Int
-)
+): Parcelable
diff --git a/app/src/main/java/com/github/libretube/api/obj/Streams.kt b/app/src/main/java/com/github/libretube/api/obj/Streams.kt
index d4d50cd17..aa8fd0d16 100644
--- a/app/src/main/java/com/github/libretube/api/obj/Streams.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/Streams.kt
@@ -1,5 +1,6 @@
package com.github.libretube.api.obj
+import android.os.Parcelable
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.enums.FileType
import com.github.libretube.helpers.ProxyHelper
@@ -8,18 +9,22 @@ import com.github.libretube.parcelable.DownloadData
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.io.path.Path
@Serializable
+@Parcelize
data class Streams(
var title: String,
val description: String,
@Serializable(SafeInstantSerializer::class)
@SerialName("uploadDate")
- val uploadTimestamp: Instant?,
+ @IgnoredOnParcel
+ val uploadTimestamp: Instant? = null,
val uploaded: Long? = null,
val uploader: String,
@@ -48,7 +53,8 @@ data class Streams(
val chapters: List = emptyList(),
val uploaderSubscriberCount: Long = 0,
val previewFrames: List = emptyList()
-) {
+): Parcelable {
+ @IgnoredOnParcel
val isLive = livestream || duration <= 0
fun toDownloadItems(downloadData: DownloadData): List {
diff --git a/app/src/main/java/com/github/libretube/api/obj/Subtitle.kt b/app/src/main/java/com/github/libretube/api/obj/Subtitle.kt
index 8e1b349f2..aad87c203 100644
--- a/app/src/main/java/com/github/libretube/api/obj/Subtitle.kt
+++ b/app/src/main/java/com/github/libretube/api/obj/Subtitle.kt
@@ -1,17 +1,20 @@
package com.github.libretube.api.obj
import android.content.Context
+import android.os.Parcelable
import com.github.libretube.R
+import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
+@Parcelize
data class Subtitle(
val url: String? = null,
val mimeType: String? = null,
val name: String? = null,
val code: String? = null,
val autoGenerated: Boolean? = null
-) {
+): Parcelable {
fun getDisplayName(context: Context) = if (autoGenerated != true) {
name!!
} else {
diff --git a/app/src/main/java/com/github/libretube/constants/IntentData.kt b/app/src/main/java/com/github/libretube/constants/IntentData.kt
index 432f03ddc..781a144ed 100644
--- a/app/src/main/java/com/github/libretube/constants/IntentData.kt
+++ b/app/src/main/java/com/github/libretube/constants/IntentData.kt
@@ -55,4 +55,5 @@ object IntentData {
const val noInternet = "noInternet"
const val isPlayingOffline = "isPlayingOffline"
const val downloadInfo = "downloadInfo"
+ const val streams = "streams"
}
diff --git a/app/src/main/java/com/github/libretube/enums/PlayerCommand.kt b/app/src/main/java/com/github/libretube/enums/PlayerCommand.kt
new file mode 100644
index 000000000..fbc95b975
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/enums/PlayerCommand.kt
@@ -0,0 +1,11 @@
+package com.github.libretube.enums
+
+enum class PlayerCommand {
+ START_PLAYBACK,
+ SKIP_SILENCE,
+ SET_VIDEO_TRACK_TYPE_DISABLED,
+ SET_AUDIO_ROLE_FLAGS,
+ SET_RESOLUTION,
+ SET_AUDIO_LANGUAGE,
+ SET_SUBTITLE
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/libretube/extensions/SetMetadata.kt b/app/src/main/java/com/github/libretube/extensions/SetMetadata.kt
index a5b7d387a..98d8e6761 100644
--- a/app/src/main/java/com/github/libretube/extensions/SetMetadata.kt
+++ b/app/src/main/java/com/github/libretube/extensions/SetMetadata.kt
@@ -1,22 +1,15 @@
package com.github.libretube.extensions
-import android.content.res.Resources
-import android.graphics.BitmapFactory
import android.support.v4.media.MediaMetadataCompat
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
-import com.github.libretube.R
import com.github.libretube.api.obj.Streams
+import com.github.libretube.db.obj.Download
fun MediaItem.Builder.setMetadata(streams: Streams) = apply {
- val appIcon = BitmapFactory.decodeResource(
- Resources.getSystem(),
- R.drawable.ic_launcher_monochrome
- )
val extras = bundleOf(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON to appIcon,
MediaMetadataCompat.METADATA_KEY_TITLE to streams.title,
MediaMetadataCompat.METADATA_KEY_ARTIST to streams.uploader
)
@@ -29,3 +22,18 @@ fun MediaItem.Builder.setMetadata(streams: Streams) = apply {
.build()
)
}
+
+fun MediaItem.Builder.setMetadata(download: Download) = apply {
+ val extras = bundleOf(
+ MediaMetadataCompat.METADATA_KEY_TITLE to download.title,
+ MediaMetadataCompat.METADATA_KEY_ARTIST to download.uploader
+ )
+ setMediaMetadata(
+ MediaMetadata.Builder()
+ .setTitle(download.title)
+ .setArtist(download.uploader)
+ .setArtworkUri(download.thumbnailPath?.toAndroidUri())
+ .setExtras(extras)
+ .build()
+ )
+}
diff --git a/app/src/main/java/com/github/libretube/helpers/BackgroundHelper.kt b/app/src/main/java/com/github/libretube/helpers/BackgroundHelper.kt
index 7b4927674..daed87eed 100644
--- a/app/src/main/java/com/github/libretube/helpers/BackgroundHelper.kt
+++ b/app/src/main/java/com/github/libretube/helpers/BackgroundHelper.kt
@@ -1,25 +1,34 @@
package com.github.libretube.helpers
import android.app.ActivityManager
+import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import androidx.core.content.ContextCompat
+import android.os.Bundle
+import androidx.annotation.OptIn
import androidx.core.content.getSystemService
+import androidx.core.os.bundleOf
import androidx.fragment.app.commit
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.session.MediaController
+import androidx.media3.session.SessionToken
import com.github.libretube.constants.IntentData
import com.github.libretube.parcelable.PlayerData
+import com.github.libretube.services.AbstractPlayerService
import com.github.libretube.services.OfflinePlayerService
import com.github.libretube.services.OnlinePlayerService
+import com.github.libretube.services.VideoOfflinePlayerService
+import com.github.libretube.services.VideoOnlinePlayerService
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.activities.NoInternetActivity
import com.github.libretube.ui.fragments.DownloadTab
import com.github.libretube.ui.fragments.PlayerFragment
+import com.google.common.util.concurrent.MoreExecutors
/**
* Helper for starting a new Instance of the [OnlinePlayerService]
*/
object BackgroundHelper {
-
/**
* Start the foreground service [OnlinePlayerService] to play in background. [position]
* is seek to position specified in milliseconds in the current [videoId].
@@ -35,26 +44,31 @@ object BackgroundHelper {
) {
// close the previous video player if open
if (!keepVideoPlayerAlive) {
- val fragmentManager = ContextHelper.unwrapActivity(context).supportFragmentManager
+ val fragmentManager =
+ ContextHelper.unwrapActivity(context).supportFragmentManager
fragmentManager.fragments.firstOrNull { it is PlayerFragment }?.let {
fragmentManager.commit { remove(it) }
}
}
- // create an intent for the background mode service
val playerData = PlayerData(videoId, playlistId, channelId, keepQueue, position)
- val intent = Intent(context, OnlinePlayerService::class.java)
- .putExtra(IntentData.playerData, playerData)
- // start the background mode as foreground service
- ContextCompat.startForegroundService(context, intent)
+ val sessionToken =
+ SessionToken(context, ComponentName(context, OnlinePlayerService::class.java))
+
+ startMediaService(context, sessionToken, bundleOf(IntentData.playerData to playerData))
}
/**
* Stop the [OnlinePlayerService] service if it is running.
*/
fun stopBackgroundPlay(context: Context) {
- arrayOf(OnlinePlayerService::class.java, OfflinePlayerService::class.java).forEach {
+ arrayOf(
+ OnlinePlayerService::class.java,
+ OfflinePlayerService::class.java,
+ VideoOfflinePlayerService::class.java,
+ VideoOnlinePlayerService::class.java
+ ).forEach {
val intent = Intent(context, it)
context.stopService(intent)
}
@@ -78,18 +92,46 @@ object BackgroundHelper {
* @param context the current context
* @param videoId the videoId of the video or null if all available downloads should be shuffled
*/
- fun playOnBackgroundOffline(context: Context, videoId: String?, downloadTab: DownloadTab, shuffle: Boolean = false) {
+ fun playOnBackgroundOffline(
+ context: Context,
+ videoId: String?,
+ downloadTab: DownloadTab,
+ shuffle: Boolean = false
+ ) {
stopBackgroundPlay(context)
// whether the service is started from the MainActivity or NoInternetActivity
val noInternet = ContextHelper.tryUnwrapActivity(context) != null
- val playerIntent = Intent(context, OfflinePlayerService::class.java)
- .putExtra(IntentData.videoId, videoId)
- .putExtra(IntentData.shuffle, shuffle)
- .putExtra(IntentData.downloadTab, downloadTab)
- .putExtra(IntentData.noInternet, noInternet)
+ val arguments = bundleOf(
+ IntentData.videoId to videoId,
+ IntentData.shuffle to shuffle,
+ IntentData.downloadTab to downloadTab,
+ IntentData.noInternet to noInternet
+ )
- ContextCompat.startForegroundService(context, playerIntent)
+ val sessionToken =
+ SessionToken(context, ComponentName(context, OfflinePlayerService::class.java))
+
+ startMediaService(context, sessionToken, arguments)
+ }
+
+ @OptIn(UnstableApi::class)
+ fun startMediaService(
+ context: Context,
+ sessionToken: SessionToken,
+ arguments: Bundle,
+ onController: (MediaController) -> Unit = {}
+ ) {
+ val controllerFuture =
+ MediaController.Builder(context, sessionToken).buildAsync()
+ controllerFuture.addListener({
+ val controller = controllerFuture.get()
+ controller.sendCustomCommand(
+ AbstractPlayerService.startServiceCommand,
+ arguments
+ )
+ onController(controller)
+ }, MoreExecutors.directExecutor())
}
}
diff --git a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt
index f106c81f2..9a1e012c8 100644
--- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt
+++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt
@@ -598,7 +598,7 @@ object PlayerHelper {
* @param segments List of the SponsorBlock segments
* @return If segment found and should skip manually, the end position of the segment in ms, otherwise null
*/
- fun ExoPlayer.checkForSegments(
+ fun Player.checkForSegments(
context: Context,
segments: List,
sponsorBlockConfig: MutableMap
@@ -633,7 +633,7 @@ object PlayerHelper {
return null
}
- fun ExoPlayer.isInSegment(segments: List): Boolean {
+ fun Player.isInSegment(segments: List): Boolean {
return segments.any {
val (start, end) = it.segmentStartAndEnd
val (segmentStart, segmentEnd) = (start * 1000f).toLong() to (end * 1000f).toLong()
@@ -835,7 +835,7 @@ object PlayerHelper {
else -> R.drawable.ic_play
}
- fun saveWatchPosition(player: ExoPlayer, videoId: String) {
+ fun saveWatchPosition(player: Player, videoId: String) {
if (player.duration == C.TIME_UNSET || player.currentPosition in listOf(0L, C.TIME_UNSET)) {
return
}
diff --git a/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt b/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt
index 9402e0247..16b6e0a2c 100644
--- a/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt
+++ b/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt
@@ -1,44 +1,47 @@
package com.github.libretube.services
-import android.content.BroadcastReceiver
-import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
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.NotificationCompat
import androidx.core.app.ServiceCompat
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.LifecycleService
-import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
-import com.github.libretube.LibreTubeApp.Companion.PLAYER_CHANNEL_NAME
+import androidx.media3.session.CommandButton
+import androidx.media3.session.MediaLibraryService
+import androidx.media3.session.MediaLibraryService.MediaLibrarySession
+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.NotificationId
+import com.github.libretube.enums.PlayerCommand
import com.github.libretube.enums.PlayerEvent
-import com.github.libretube.extensions.serializableExtra
import com.github.libretube.extensions.updateParameters
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PauseableTimer
import com.github.libretube.util.PlayingQueue
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@UnstableApi
-abstract class AbstractPlayerService : LifecycleService() {
- var player: ExoPlayer? = null
- var nowPlayingNotification: NowPlayingNotification? = null
+abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySession.Callback {
+ private var mediaLibrarySession: MediaLibrarySession? = null
+ var exoPlayer: ExoPlayer? = null
+
+ private var nowPlayingNotification: NowPlayingNotification? = null
var trackSelector: DefaultTrackSelector? = null
lateinit var videoId: String
@@ -76,7 +79,7 @@ abstract class AbstractPlayerService : LifecycleService() {
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
- onStateOrPlayingChanged?.let { it(player?.isPlaying ?: false) }
+ onStateOrPlayingChanged?.let { it(exoPlayer?.isPlaying ?: false) }
this@AbstractPlayerService.onPlaybackStateChanged(playbackState)
}
@@ -96,103 +99,162 @@ abstract class AbstractPlayerService : LifecycleService() {
super.onEvents(player, events)
if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
- PlayerHelper.setPreferredAudioQuality(this@AbstractPlayerService, player, trackSelector ?: return)
+ PlayerHelper.setPreferredAudioQuality(
+ this@AbstractPlayerService,
+ player,
+ trackSelector ?: return
+ )
}
}
}
- private val playerActionReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val event = intent.serializableExtra(PlayerHelper.CONTROL_TYPE) ?: return
- val player = player ?: return
+ override fun onCustomCommand(
+ session: MediaSession,
+ controller: MediaSession.ControllerInfo,
+ customCommand: SessionCommand,
+ args: Bundle
+ ): ListenableFuture {
+ if (customCommand.customAction == START_SERVICE_ACTION) {
+ PlayingQueue.resetToDefaults()
- if (PlayerHelper.handlePlayerAction(player, event)) return
-
- when (event) {
- PlayerEvent.Next -> {
- PlayingQueue.navigateNext()
- }
- PlayerEvent.Prev -> {
- PlayingQueue.navigatePrev()
- }
- PlayerEvent.Stop -> {
- onDestroy()
- }
- else -> Unit
+ CoroutineScope(Dispatchers.IO).launch {
+ onServiceCreated(args)
+ startPlayback()
}
+
+ return super.onCustomCommand(session, controller, customCommand, args)
+ }
+
+ if (customCommand.customAction == RUN_PLAYER_COMMAND_ACTION) {
+ runPlayerCommand(args)
+
+ return super.onCustomCommand(session, controller, customCommand, args)
+ }
+
+ handlePlayerAction(PlayerEvent.valueOf(customCommand.customAction))
+
+ return super.onCustomCommand(session, controller, customCommand, args)
+ }
+
+ open fun runPlayerCommand(args: Bundle) {
+ when {
+ args.containsKey(PlayerCommand.SKIP_SILENCE.name) ->
+ exoPlayer?.skipSilenceEnabled = args.getBoolean(PlayerCommand.SKIP_SILENCE.name)
+ }
+ }
+
+ private fun handlePlayerAction(event: PlayerEvent) {
+ if (PlayerHelper.handlePlayerAction(exoPlayer ?: return, event)) return
+
+ when (event) {
+ PlayerEvent.Next -> {
+ PlayingQueue.navigateNext()
+ }
+
+ PlayerEvent.Prev -> {
+ PlayingQueue.navigatePrev()
+ }
+
+ PlayerEvent.Stop -> {
+ onDestroy()
+ }
+
+ else -> Unit
}
}
abstract val isOfflinePlayer: Boolean
+ abstract val isAudioOnlyPlayer: Boolean
abstract val intentActivity: Class<*>
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? =
+ mediaLibrarySession
+
override fun onCreate() {
super.onCreate()
- val notification = NotificationCompat.Builder(this, PLAYER_CHANNEL_NAME)
- .setContentTitle(getString(R.string.app_name))
- .setContentText(getString(R.string.playingOnBackground))
- .setSmallIcon(R.drawable.ic_launcher_lockscreen)
- .build()
-
- startForeground(NotificationId.PLAYER_PLAYBACK.id, notification)
-
- ContextCompat.registerReceiver(
+ val notificationProvider = NowPlayingNotification(
this,
- playerActionReceiver,
- IntentFilter(PlayerHelper.getIntentActionName(this)),
- ContextCompat.RECEIVER_NOT_EXPORTED
- )
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- PlayingQueue.resetToDefaults()
-
- lifecycleScope.launch {
- if (intent != null) {
- onServiceCreated(intent)
- createPlayerAndNotification()
- startPlaybackAndUpdateNotification()
- }
- else stopSelf()
- }
-
- return super.onStartCommand(intent, flags, startId)
- }
-
- abstract suspend fun onServiceCreated(intent: Intent)
-
- @OptIn(UnstableApi::class)
- private fun createPlayerAndNotification() {
- val trackSelector = DefaultTrackSelector(this)
- this.trackSelector = trackSelector
-
- trackSelector.updateParameters {
- setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true)
- }
-
- player = PlayerHelper.createPlayer(this, trackSelector, true)
- // prevent android from putting LibreTube to sleep when locked
- player!!.setWakeMode(C.WAKE_MODE_LOCAL)
- player!!.addListener(playerListener)
-
- PlayerHelper.setPreferredCodecs(trackSelector)
-
- nowPlayingNotification = NowPlayingNotification(
- this,
- player!!,
backgroundOnly = true,
offlinePlayer = isOfflinePlayer,
intentActivity = intentActivity
)
+ setMediaNotificationProvider(notificationProvider)
+
+ createPlayerAndMediaSession()
}
- abstract suspend fun startPlaybackAndUpdateNotification()
+ abstract suspend fun onServiceCreated(args: Bundle)
+
+ override fun onConnect(
+ session: MediaSession,
+ controller: MediaSession.ControllerInfo
+ ): MediaSession.ConnectionResult {
+ val connectionResult = super.onConnect(session, controller)
+
+ // Select the button to display.
+ val customLayout = listOf(
+ CommandButton.Builder()
+ .setDisplayName(getString(R.string.rewind))
+ .setSessionCommand(SessionCommand(PlayerEvent.Prev.name, Bundle.EMPTY))
+ .setIconResId(R.drawable.ic_prev_outlined)
+ .build(),
+ CommandButton.Builder()
+ .setDisplayName(getString(R.string.play_next))
+ .setSessionCommand(SessionCommand(PlayerEvent.Next.name, Bundle.EMPTY))
+ .setIconResId(R.drawable.ic_next_outlined)
+ .build(),
+ )
+ val mediaNotificationSessionCommands =
+ connectionResult.availableSessionCommands.buildUpon()
+ .also { builder ->
+ builder.add(startServiceCommand)
+ builder.add(runPlayerActionCommand)
+ customLayout.forEach { commandButton ->
+ commandButton.sessionCommand?.let { builder.add(it) }
+ }
+ }
+ .build()
+
+ val playerCommands = connectionResult.availablePlayerCommands.buildUpon()
+ .remove(Player.COMMAND_SEEK_TO_PREVIOUS)
+ .build()
+
+ return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
+ .setAvailableSessionCommands(mediaNotificationSessionCommands)
+ .setAvailablePlayerCommands(playerCommands)
+ .setCustomLayout(customLayout)
+ .build()
+ }
+
+ @OptIn(UnstableApi::class)
+ private fun createPlayerAndMediaSession() {
+ val trackSelector = DefaultTrackSelector(this)
+ this.trackSelector = trackSelector
+
+ if (isAudioOnlyPlayer) {
+ trackSelector.updateParameters {
+ setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true)
+ }
+ }
+
+ val player = PlayerHelper.createPlayer(this, trackSelector, true)
+ // prevent android from putting LibreTube to sleep when locked
+ player.setWakeMode(if (isOfflinePlayer) C.WAKE_MODE_LOCAL else C.WAKE_MODE_NETWORK)
+ player.addListener(playerListener)
+ this.exoPlayer = player
+
+ PlayerHelper.setPreferredCodecs(trackSelector)
+
+ mediaLibrarySession = MediaLibrarySession.Builder(this, player, this).build()
+ }
+
+ abstract suspend fun startPlayback()
fun saveWatchPosition() {
if (isTransitioning || !PlayerHelper.watchPositionsVideo) return
- player?.let { PlayerHelper.saveWatchPosition(it, videoId) }
+ exoPlayer?.let { PlayerHelper.saveWatchPosition(it, videoId) }
}
override fun onDestroy() {
@@ -200,20 +262,19 @@ abstract class AbstractPlayerService : LifecycleService() {
saveWatchPosition()
- nowPlayingNotification?.destroySelf()
nowPlayingNotification = null
watchPositionTimer.destroy()
handler.removeCallbacksAndMessages(null)
runCatching {
- player?.stop()
- player?.release()
+ exoPlayer?.stop()
+ exoPlayer?.release()
}
- player = null
- runCatching {
- unregisterReceiver(playerActionReceiver)
+ kotlin.runCatching {
+ mediaLibrarySession?.release()
+ mediaLibrarySession = null
}
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
@@ -234,19 +295,27 @@ abstract class AbstractPlayerService : LifecycleService() {
abstract fun getChapters(): List
- fun getCurrentPosition() = player?.currentPosition
+ fun getCurrentPosition() = exoPlayer?.currentPosition
- fun getDuration() = player?.duration
+ fun getDuration() = exoPlayer?.duration
- fun seekToPosition(position: Long) = player?.seekTo(position)
+ 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 {
- super.onBind(intent)
- return binder
+ 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"
+
+ val startServiceCommand = SessionCommand(START_SERVICE_ACTION, Bundle.EMPTY)
+ val runPlayerActionCommand = SessionCommand(RUN_PLAYER_COMMAND_ACTION, Bundle.EMPTY)
}
}
diff --git a/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt b/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt
index 91a752d42..dd7fa3c31 100644
--- a/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt
+++ b/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt
@@ -1,7 +1,8 @@
package com.github.libretube.services
import android.content.Intent
-import androidx.lifecycle.lifecycleScope
+import android.os.Bundle
+import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
@@ -12,15 +13,16 @@ 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
-import com.github.libretube.extensions.serializableExtra
+import com.github.libretube.extensions.serializable
+import com.github.libretube.extensions.setMetadata
import com.github.libretube.extensions.toAndroidUri
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.PlayerHelper
-import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.activities.NoInternetActivity
import com.github.libretube.ui.fragments.DownloadTab
import com.github.libretube.util.PlayingQueue
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -30,9 +32,10 @@ import kotlin.io.path.exists
/**
* A service to play downloaded audio in the background
*/
-@UnstableApi
-class OfflinePlayerService : AbstractPlayerService() {
+@OptIn(UnstableApi::class)
+open class OfflinePlayerService : AbstractPlayerService() {
override val isOfflinePlayer: Boolean = true
+ override val isAudioOnlyPlayer: Boolean = true
private var noInternetService: Boolean = false
override val intentActivity: Class<*>
get() = if (noInternetService) NoInternetActivity::class.java else MainActivity::class.java
@@ -41,17 +44,19 @@ class OfflinePlayerService : AbstractPlayerService() {
private lateinit var downloadTab: DownloadTab
private var shuffle: Boolean = false
- override suspend fun onServiceCreated(intent: Intent) {
- downloadTab = intent.serializableExtra(IntentData.downloadTab)!!
- shuffle = intent.getBooleanExtra(IntentData.shuffle, false)
- noInternetService = intent.getBooleanExtra(IntentData.noInternet, false)
+ private val scope = CoroutineScope(Dispatchers.Main)
+
+ override suspend fun onServiceCreated(args: Bundle) {
+ downloadTab = args.serializable(IntentData.downloadTab)!!
+ shuffle = args.getBoolean(IntentData.shuffle, false)
+ noInternetService = args.getBoolean(IntentData.noInternet, false)
videoId = if (shuffle) {
runBlocking(Dispatchers.IO) {
Database.downloadDao().getRandomVideoIdByFileType(FileType.AUDIO)
}
} else {
- intent.getStringExtra(IntentData.videoId)
+ args.getString(IntentData.videoId)
} ?: return
PlayingQueue.clear()
@@ -66,7 +71,7 @@ class OfflinePlayerService : AbstractPlayerService() {
/**
* Attempt to start an audio player with the given download items
*/
- override suspend fun startPlaybackAndUpdateNotification() {
+ override suspend fun startPlayback() {
val downloadWithItems = withContext(Dispatchers.IO) {
Database.downloadDao().findById(videoId)
}!!
@@ -75,13 +80,20 @@ class OfflinePlayerService : AbstractPlayerService() {
PlayingQueue.updateCurrent(downloadWithItems.download.toStreamItem())
- val notificationData = PlayerNotificationData(
- title = downloadWithItems.download.title,
- uploaderName = downloadWithItems.download.uploader,
- thumbnailPath = downloadWithItems.download.thumbnailPath
- )
- nowPlayingNotification?.updatePlayerNotification(videoId, notificationData)
+ withContext(Dispatchers.Main) {
+ setMediaItem(downloadWithItems)
+ exoPlayer?.playWhenReady = PlayerHelper.playAutomatically
+ exoPlayer?.prepare()
+ if (PlayerHelper.watchPositionsAudio) {
+ PlayerHelper.getStoredWatchPosition(videoId, downloadWithItems.download.duration)?.let {
+ exoPlayer?.seekTo(it)
+ }
+ }
+ }
+ }
+
+ open fun setMediaItem(downloadWithItems: DownloadWithItems) {
val audioItem = downloadWithItems.downloadItems.filter { it.path.exists() }
.firstOrNull { it.type == FileType.AUDIO }
?: // in some rare cases, video files can contain audio
@@ -94,17 +106,10 @@ class OfflinePlayerService : AbstractPlayerService() {
val mediaItem = MediaItem.Builder()
.setUri(audioItem.path.toAndroidUri())
+ .setMetadata(downloadWithItems.download)
.build()
- player?.setMediaItem(mediaItem)
- player?.playWhenReady = PlayerHelper.playAutomatically
- player?.prepare()
-
- if (PlayerHelper.watchPositionsAudio) {
- PlayerHelper.getStoredWatchPosition(videoId, downloadWithItems.download.duration)?.let {
- player?.seekTo(it)
- }
- }
+ exoPlayer?.setMediaItem(mediaItem)
}
private suspend fun fillQueue() {
@@ -124,8 +129,8 @@ class OfflinePlayerService : AbstractPlayerService() {
this.videoId = videoId
- lifecycleScope.launch {
- startPlaybackAndUpdateNotification()
+ scope.launch {
+ startPlayback()
}
}
diff --git a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt
index d2dc1a9ec..f6ddb6460 100644
--- a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt
+++ b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt
@@ -1,8 +1,7 @@
package com.github.libretube.services
-import android.content.Intent
+import android.os.Bundle
import androidx.core.net.toUri
-import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.Player
@@ -14,17 +13,17 @@ import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.Streams
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHelper
-import com.github.libretube.extensions.parcelableExtra
+import com.github.libretube.extensions.parcelable
import com.github.libretube.extensions.setMetadata
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.checkForSegments
import com.github.libretube.helpers.ProxyHelper
-import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.parcelable.PlayerData
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.util.PlayingQueue
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -36,6 +35,7 @@ import kotlinx.serialization.encodeToString
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class OnlinePlayerService : AbstractPlayerService() {
override val isOfflinePlayer: Boolean = false
+ override val isAudioOnlyPlayer: Boolean = true
override val intentActivity: Class<*> = MainActivity::class.java
// PlaylistId/ChannelId for autoplay
@@ -53,8 +53,10 @@ class OnlinePlayerService : AbstractPlayerService() {
private var sponsorBlockSegments = listOf()
private var sponsorBlockConfig = PlayerHelper.getSponsorBlockCategories()
- override suspend fun onServiceCreated(intent: Intent) {
- val playerData = intent.parcelableExtra(IntentData.playerData)
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ override suspend fun onServiceCreated(args: Bundle) {
+ val playerData = args.parcelable(IntentData.playerData)
if (playerData == null) {
stopSelf()
return
@@ -72,7 +74,7 @@ class OnlinePlayerService : AbstractPlayerService() {
}
}
- override suspend fun startPlaybackAndUpdateNotification() {
+ override suspend fun startPlayback() {
val timestamp = startTimestamp ?: 0L
startTimestamp = null
@@ -110,30 +112,24 @@ class OnlinePlayerService : AbstractPlayerService() {
}
private fun playAudio(seekToPosition: Long) {
- lifecycleScope.launch(Dispatchers.IO) {
+ scope.launch {
setMediaItem()
withContext(Dispatchers.Main) {
// seek to the previous position if available
if (seekToPosition != 0L) {
- player?.seekTo(seekToPosition)
+ exoPlayer?.seekTo(seekToPosition)
} else if (PlayerHelper.watchPositionsAudio) {
PlayerHelper.getStoredWatchPosition(videoId, streams?.duration)?.let {
- player?.seekTo(it)
+ exoPlayer?.seekTo(it)
}
}
}
}
- val playerNotificationData = PlayerNotificationData(
- streams?.title,
- streams?.uploader,
- streams?.thumbnailUrl
- )
- nowPlayingNotification?.updatePlayerNotification(videoId, playerNotificationData)
streams?.let { onNewVideoStarted?.invoke(it.toStreamItem(videoId)) }
- player?.apply {
+ exoPlayer?.apply {
playWhenReady = PlayerHelper.playAutomatically
prepare()
}
@@ -146,7 +142,7 @@ class OnlinePlayerService : AbstractPlayerService() {
*/
private fun playNextVideo(nextId: String? = null) {
if (nextId == null && PlayingQueue.repeatMode == Player.REPEAT_MODE_ONE) {
- player?.seekTo(0)
+ exoPlayer?.seekTo(0)
return
}
@@ -161,13 +157,13 @@ class OnlinePlayerService : AbstractPlayerService() {
this.streams = null
this.sponsorBlockSegments = emptyList()
- lifecycleScope.launch {
- startPlaybackAndUpdateNotification()
+ scope.launch {
+ startPlayback()
}
}
/**
- * Sets the [MediaItem] with the [streams] into the [player]
+ * Sets the [MediaItem] with the [streams] into the [exoPlayer]
*/
private suspend fun setMediaItem() {
val streams = streams ?: return
@@ -185,14 +181,14 @@ class OnlinePlayerService : AbstractPlayerService() {
.setMimeType(mimeType)
.setMetadata(streams)
.build()
- withContext(Dispatchers.Main) { player?.setMediaItem(mediaItem) }
+ withContext(Dispatchers.Main) { exoPlayer?.setMediaItem(mediaItem) }
}
/**
* fetch the segments for SponsorBlock
*/
private fun fetchSponsorBlockSegments() {
- lifecycleScope.launch(Dispatchers.IO) {
+ scope.launch(Dispatchers.IO) {
runCatching {
if (sponsorBlockConfig.isEmpty()) return@runCatching
sponsorBlockSegments = RetrofitInstance.api.getSegments(
@@ -210,7 +206,7 @@ class OnlinePlayerService : AbstractPlayerService() {
private fun checkForSegments() {
handler.postDelayed(this::checkForSegments, 100)
- player?.checkForSegments(this, sponsorBlockSegments, sponsorBlockConfig)
+ exoPlayer?.checkForSegments(this, sponsorBlockSegments, sponsorBlockConfig)
}
override fun onPlaybackStateChanged(playbackState: Int) {
@@ -230,7 +226,7 @@ class OnlinePlayerService : AbstractPlayerService() {
// 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
- lifecycleScope.launch(Dispatchers.IO) {
+ scope.launch(Dispatchers.IO) {
streams?.let { DatabaseHelper.addToWatchHistory(videoId, it) }
}
}
diff --git a/app/src/main/java/com/github/libretube/services/VideoOfflinePlayerService.kt b/app/src/main/java/com/github/libretube/services/VideoOfflinePlayerService.kt
new file mode 100644
index 000000000..fb160db58
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/services/VideoOfflinePlayerService.kt
@@ -0,0 +1,85 @@
+package com.github.libretube.services
+
+import androidx.annotation.OptIn
+import androidx.media3.common.C
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaItem.SubtitleConfiguration
+import androidx.media3.common.MimeTypes
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.FileDataSource
+import androidx.media3.exoplayer.source.MergingMediaSource
+import androidx.media3.exoplayer.source.ProgressiveMediaSource
+import androidx.media3.exoplayer.source.SingleSampleMediaSource
+import com.github.libretube.db.obj.DownloadWithItems
+import com.github.libretube.enums.FileType
+import com.github.libretube.extensions.setMetadata
+import com.github.libretube.extensions.toAndroidUri
+import com.github.libretube.extensions.updateParameters
+import kotlin.io.path.exists
+
+@OptIn(UnstableApi::class)
+class VideoOfflinePlayerService: OfflinePlayerService() {
+ override val isAudioOnlyPlayer = false
+
+ override fun setMediaItem(downloadWithItems: DownloadWithItems) {
+ val downloadFiles = downloadWithItems.downloadItems.filter { it.path.exists() }
+
+ val videoUri = downloadFiles.firstOrNull { it.type == FileType.VIDEO }?.path?.toAndroidUri()
+ val audioUri = downloadFiles.firstOrNull { it.type == FileType.AUDIO }?.path?.toAndroidUri()
+ val subtitleInfo = downloadFiles.firstOrNull { it.type == FileType.SUBTITLE }
+
+ val subtitle = subtitleInfo?.let {
+ SubtitleConfiguration.Builder(it.path.toAndroidUri())
+ .setMimeType(MimeTypes.APPLICATION_TTML)
+ .setLanguage(it.language ?: "en")
+ .build()
+ }
+
+ when {
+ videoUri != null && audioUri != null -> {
+ val videoItem = MediaItem.Builder()
+ .setUri(videoUri)
+ .setMetadata(downloadWithItems.download)
+ .setSubtitleConfigurations(listOfNotNull(subtitle))
+ .build()
+
+ val videoSource = ProgressiveMediaSource.Factory(FileDataSource.Factory())
+ .createMediaSource(videoItem)
+
+ val audioSource = ProgressiveMediaSource.Factory(FileDataSource.Factory())
+ .createMediaSource(MediaItem.fromUri(audioUri))
+
+ var mediaSource = MergingMediaSource(audioSource, videoSource)
+ if (subtitle != null) {
+ val subtitleSource = SingleSampleMediaSource.Factory(FileDataSource.Factory())
+ .createMediaSource(subtitle, C.TIME_UNSET)
+
+ mediaSource = MergingMediaSource(mediaSource, subtitleSource)
+ }
+
+ exoPlayer?.setMediaSource(mediaSource)
+ }
+
+ videoUri != null -> exoPlayer?.setMediaItem(
+ MediaItem.Builder()
+ .setUri(videoUri)
+ .setMetadata(downloadWithItems.download)
+ .setSubtitleConfigurations(listOfNotNull(subtitle))
+ .build()
+ )
+
+ audioUri != null -> exoPlayer?.setMediaItem(
+ MediaItem.Builder()
+ .setUri(audioUri)
+ .setMetadata(downloadWithItems.download)
+ .setSubtitleConfigurations(listOfNotNull(subtitle))
+ .build()
+ )
+ }
+
+ trackSelector?.updateParameters {
+ setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
+ setPreferredTextLanguage(subtitle?.language)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/libretube/services/VideoOnlinePlayerService.kt b/app/src/main/java/com/github/libretube/services/VideoOnlinePlayerService.kt
new file mode 100644
index 000000000..f6cfa413d
--- /dev/null
+++ b/app/src/main/java/com/github/libretube/services/VideoOnlinePlayerService.kt
@@ -0,0 +1,179 @@
+package com.github.libretube.services
+
+import android.net.Uri
+import android.os.Bundle
+import androidx.annotation.OptIn
+import androidx.core.net.toUri
+import androidx.media3.common.C
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaItem.SubtitleConfiguration
+import androidx.media3.common.MimeTypes
+import androidx.media3.common.util.UnstableApi
+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
+import com.github.libretube.constants.PreferenceKeys
+import com.github.libretube.enums.PlayerCommand
+import com.github.libretube.extensions.parcelable
+import com.github.libretube.extensions.setMetadata
+import com.github.libretube.extensions.toastFromMainThread
+import com.github.libretube.extensions.updateParameters
+import com.github.libretube.helpers.PlayerHelper
+import com.github.libretube.helpers.PreferenceHelper
+import com.github.libretube.helpers.ProxyHelper
+import com.github.libretube.ui.activities.MainActivity
+import com.github.libretube.util.YoutubeHlsPlaylistParser
+import java.util.concurrent.Executors
+
+@OptIn(UnstableApi::class)
+class VideoOnlinePlayerService : AbstractPlayerService() {
+ override val isOfflinePlayer: Boolean = false
+ override val isAudioOnlyPlayer: Boolean = false
+ override val intentActivity: Class<*> = MainActivity::class.java
+
+ private val cronetDataSourceFactory = CronetDataSource.Factory(
+ CronetHelper.cronetEngine,
+ Executors.newCachedThreadPool()
+ )
+
+ private lateinit var streams: Streams
+
+ override suspend fun onServiceCreated(args: Bundle) {
+ this.streams = args.parcelable(IntentData.streams) ?: return
+
+ startPlayback()
+ }
+
+ override suspend fun startPlayback() = Unit
+
+ override fun runPlayerCommand(args: Bundle) {
+ when {
+ args.containsKey(PlayerCommand.START_PLAYBACK.name) -> setStreamSource()
+ args.containsKey(PlayerCommand.SKIP_SILENCE.name) -> exoPlayer?.skipSilenceEnabled = args.getBoolean(PlayerCommand.SKIP_SILENCE.name)
+ args.containsKey(PlayerCommand.SET_VIDEO_TRACK_TYPE_DISABLED.name) -> trackSelector?.updateParameters {
+ setTrackTypeDisabled(
+ C.TRACK_TYPE_VIDEO,
+ args.getBoolean(PlayerCommand.SET_VIDEO_TRACK_TYPE_DISABLED.name)
+ )
+ }
+ args.containsKey(PlayerCommand.SET_AUDIO_ROLE_FLAGS.name) -> {
+ trackSelector?.updateParameters {
+ setPreferredAudioRoleFlags(args.getInt(PlayerCommand.SET_AUDIO_ROLE_FLAGS.name))
+ }
+ }
+ args.containsKey(PlayerCommand.SET_AUDIO_LANGUAGE.name) -> {
+ trackSelector?.updateParameters {
+ setPreferredAudioLanguage(args.getString(PlayerCommand.SET_AUDIO_LANGUAGE.name))
+ }
+ }
+ args.containsKey(PlayerCommand.SET_RESOLUTION.name) -> {
+ trackSelector?.updateParameters {
+ val resolution = args.getInt(PlayerCommand.SET_RESOLUTION.name)
+ setMinVideoSize(Int.MIN_VALUE, resolution)
+ setMaxVideoSize(Int.MAX_VALUE, resolution)
+ }
+ }
+ args.containsKey(PlayerCommand.SET_SUBTITLE.name) -> {
+ updateCurrentSubtitle(args.parcelable(PlayerCommand.SET_SUBTITLE.name))
+ }
+ }
+ }
+
+ private fun setStreamSource() {
+ if (!this::streams.isInitialized) return
+
+ val (uri, mimeType) = when {
+ // LBRY HLS
+ PreferenceHelper.getBoolean(
+ PreferenceKeys.LBRY_HLS,
+ false
+ ) && streams.videoStreams.any {
+ it.quality.orEmpty().contains("LBRY HLS")
+ } -> {
+ val lbryHlsUrl = streams.videoStreams.first {
+ it.quality!!.contains("LBRY HLS")
+ }.url!!
+ lbryHlsUrl.toUri() to MimeTypes.APPLICATION_M3U8
+ }
+ // DASH
+ !PlayerHelper.useHlsOverDash && streams.videoStreams.isNotEmpty() -> {
+ // only use the dash manifest generated by YT if either it's a livestream or no other source is available
+ val dashUri =
+ if (streams.isLive && streams.dash != null) {
+ ProxyHelper.unwrapStreamUrl(
+ streams.dash!!
+ ).toUri()
+ } else {
+ // skip LBRY urls when checking whether the stream source is usable
+ PlayerHelper.createDashSource(streams, this)
+ }
+
+ dashUri to MimeTypes.APPLICATION_MPD
+ }
+ // HLS
+ streams.hls != null -> {
+ val hlsMediaSourceFactory = HlsMediaSource.Factory(cronetDataSourceFactory)
+ .setPlaylistParserFactory(YoutubeHlsPlaylistParser.Factory())
+
+ val mediaSource = hlsMediaSourceFactory.createMediaSource(
+ createMediaItem(
+ ProxyHelper.unwrapStreamUrl(streams.hls!!).toUri(),
+ MimeTypes.APPLICATION_M3U8
+ )
+ )
+ exoPlayer?.setMediaSource(mediaSource)
+ return
+ }
+ // NO STREAM FOUND
+ else -> {
+ toastFromMainThread(R.string.unknown_error)
+ return
+ }
+ }
+ setMediaSource(uri, mimeType)
+ }
+
+ private fun getSubtitleConfigs(): List = streams.subtitles.map {
+ val roleFlags = getSubtitleRoleFlags(it)
+ SubtitleConfiguration.Builder(it.url!!.toUri())
+ .setRoleFlags(roleFlags)
+ .setLanguage(it.code)
+ .setMimeType(it.mimeType).build()
+ }
+
+ private fun createMediaItem(uri: Uri, mimeType: String) = MediaItem.Builder()
+ .setUri(uri)
+ .setMimeType(mimeType)
+ .setSubtitleConfigurations(getSubtitleConfigs())
+ .setMetadata(streams)
+ .build()
+
+ private fun setMediaSource(uri: Uri, mimeType: String) {
+ val mediaItem = createMediaItem(uri, mimeType)
+ exoPlayer?.setMediaItem(mediaItem)
+ }
+
+ private fun getSubtitleRoleFlags(subtitle: Subtitle?): Int {
+ return if (subtitle?.autoGenerated != true) {
+ C.ROLE_FLAG_CAPTION
+ } else {
+ PlayerHelper.ROLE_FLAG_AUTO_GEN_SUBTITLE
+ }
+ }
+
+ private fun updateCurrentSubtitle(subtitle: Subtitle?) =
+ trackSelector?.updateParameters {
+ val roleFlags = if (subtitle?.code != null) getSubtitleRoleFlags(subtitle) else 0
+ setPreferredTextRoleFlags(roleFlags)
+ setPreferredTextLanguage(subtitle?.code)
+ }
+
+ override fun onPlaybackStateChanged(playbackState: Int) = Unit
+
+ override fun getChapters(): List = emptyList()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt
index 59929c70c..ad2cd86bb 100644
--- a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt
+++ b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt
@@ -1,29 +1,23 @@
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
-import android.content.pm.ActivityInfo
import android.content.res.Configuration
-import android.net.Uri
import android.os.Bundle
import android.text.format.DateUtils
import android.view.KeyEvent
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
+import androidx.core.os.bundleOf
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
-import androidx.media3.common.C
-import androidx.media3.common.MediaItem
-import androidx.media3.common.MediaItem.SubtitleConfiguration
-import androidx.media3.common.MimeTypes
import androidx.media3.common.Player
-import androidx.media3.datasource.FileDataSource
-import androidx.media3.exoplayer.source.MergingMediaSource
-import androidx.media3.exoplayer.source.ProgressiveMediaSource
-import androidx.media3.exoplayer.source.SingleSampleMediaSource
+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
@@ -36,19 +30,16 @@ import com.github.libretube.db.obj.filterByTab
import com.github.libretube.enums.FileType
import com.github.libretube.enums.PlayerEvent
import com.github.libretube.extensions.serializableExtra
-import com.github.libretube.extensions.toAndroidUri
-import com.github.libretube.extensions.updateParameters
+import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.WindowHelper
-import com.github.libretube.obj.PlayerNotificationData
+import com.github.libretube.services.VideoOfflinePlayerService
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.fragments.DownloadTab
import com.github.libretube.ui.interfaces.TimeFrameReceiver
import com.github.libretube.ui.listeners.SeekbarPreviewListener
import com.github.libretube.ui.models.ChaptersViewModel
import com.github.libretube.ui.models.CommonPlayerViewModel
-import com.github.libretube.ui.models.OfflinePlayerViewModel
-import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.OfflineTimeFrameReceiver
import com.github.libretube.util.PauseableTimer
import com.github.libretube.util.PlayingQueue
@@ -61,15 +52,15 @@ import kotlin.io.path.exists
class OfflinePlayerActivity : BaseActivity() {
private lateinit var binding: ActivityOfflinePlayerBinding
private lateinit var videoId: String
+
+ private lateinit var playerController: MediaController
private lateinit var playerView: PlayerView
private var timeFrameReceiver: TimeFrameReceiver? = null
- private var nowPlayingNotification: NowPlayingNotification? = null
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
private val commonPlayerViewModel: CommonPlayerViewModel by viewModels()
- private val viewModel: OfflinePlayerViewModel by viewModels { OfflinePlayerViewModel.Factory }
private val chaptersViewModel: ChaptersViewModel by viewModels()
-
+
private val watchPositionTimer = PauseableTimer(
onTick = this::saveWatchPosition,
delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS
@@ -82,6 +73,13 @@ class OfflinePlayerActivity : BaseActivity() {
playerBinding.duration.text = DateUtils.formatElapsedTime(
player.duration / 1000
)
+
+ if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
+ requestedOrientation = PlayerHelper.getOrientation(
+ playerController.videoSize.width,
+ playerController.videoSize.height
+ )
+ }
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
@@ -110,7 +108,7 @@ class OfflinePlayerActivity : BaseActivity() {
SeekbarPreviewListener(
timeFrameReceiver ?: return,
binding.player.binding,
- viewModel.player.duration
+ playerController.duration
)
)
}
@@ -124,7 +122,7 @@ class OfflinePlayerActivity : BaseActivity() {
private val playerActionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val event = intent.serializableExtra(PlayerHelper.CONTROL_TYPE) ?: return
- if (PlayerHelper.handlePlayerAction(viewModel.player, event)) return
+ if (PlayerHelper.handlePlayerAction(playerController, event)) return
when (event) {
PlayerEvent.Prev -> playNextVideo(PlayingQueue.getPrev() ?: return)
@@ -135,17 +133,23 @@ class OfflinePlayerActivity : BaseActivity() {
}
private val pipParams
- get() = PictureInPictureParamsCompat.Builder()
- .setActions(PlayerHelper.getPiPModeActions(this, viewModel.player.isPlaying))
- .setAutoEnterEnabled(PlayerHelper.pipEnabled && viewModel.player.isPlaying)
- .setAspectRatio(viewModel.player.videoSize)
- .build()
+ get() = run {
+ val isPlaying = ::playerController.isInitialized && playerController.isPlaying
+
+ PictureInPictureParamsCompat.Builder()
+ .setActions(PlayerHelper.getPiPModeActions(this,isPlaying))
+ .setAutoEnterEnabled(PlayerHelper.pipEnabled && isPlaying)
+ .apply {
+ if (isPlaying) {
+ setAspectRatio(playerController.videoSize)
+ }
+ }
+ .build()
+ }
override fun onCreate(savedInstanceState: Bundle?) {
WindowHelper.toggleFullscreen(window, true)
- requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
-
super.onCreate(savedInstanceState)
videoId = intent?.getStringExtra(IntentData.videoId)!!
@@ -160,13 +164,19 @@ class OfflinePlayerActivity : BaseActivity() {
playNextVideo(streamItem.url ?: return@setOnQueueTapListener)
}
- initializePlayer()
- playVideo()
-
- requestedOrientation = PlayerHelper.getOrientation(
- viewModel.player.videoSize.width,
- viewModel.player.videoSize.height
+ 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) {
+ playerController = it
+ playerController.addListener(playerListener)
+ initializePlayerView()
+ }
ContextCompat.registerReceiver(
this,
@@ -188,14 +198,11 @@ class OfflinePlayerActivity : BaseActivity() {
playVideo()
}
- private fun initializePlayer() {
- viewModel.player.setWakeMode(C.WAKE_MODE_LOCAL)
- viewModel.player.addListener(playerListener)
-
+ private fun initializePlayerView() {
playerView = binding.player
playerView.setShowSubtitleButton(true)
playerView.subtitleView?.isVisible = true
- playerView.player = viewModel.player
+ playerView.player = playerController
playerBinding = binding.player.binding
playerBinding.fullscreen.isInvisible = true
@@ -216,13 +223,6 @@ class OfflinePlayerActivity : BaseActivity() {
binding.playerGestureControlsView.binding,
chaptersViewModel
)
-
- nowPlayingNotification = NowPlayingNotification(
- this,
- viewModel.player,
- offlinePlayer = true,
- intentActivity = OfflinePlayerActivity::class.java
- )
}
private fun playVideo() {
@@ -230,7 +230,6 @@ class OfflinePlayerActivity : BaseActivity() {
val (downloadInfo, downloadItems, downloadChapters) = withContext(Dispatchers.IO) {
Database.downloadDao().findById(videoId)
}!!
- PlayingQueue.updateCurrent(downloadInfo.toStreamItem())
val chapters = downloadChapters.map(DownloadChapter::toChapterSegment)
chaptersViewModel.chaptersLiveData.value = chapters
@@ -240,88 +239,15 @@ class OfflinePlayerActivity : BaseActivity() {
playerBinding.exoTitle.text = downloadInfo.title
playerBinding.exoTitle.isVisible = true
- val video = downloadFiles.firstOrNull { it.type == FileType.VIDEO }
- val audio = downloadFiles.firstOrNull { it.type == FileType.AUDIO }
- val subtitle = downloadFiles.firstOrNull { it.type == FileType.SUBTITLE }
-
- val videoUri = video?.path?.toAndroidUri()
- val audioUri = audio?.path?.toAndroidUri()
- val subtitleUri = subtitle?.path?.toAndroidUri()
-
- setMediaSource(videoUri, audioUri, subtitleUri)
-
- viewModel.trackSelector.updateParameters {
- setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
- setPreferredTextLanguage("en")
- }
-
- timeFrameReceiver = video?.path?.let {
+ timeFrameReceiver = downloadFiles.firstOrNull { it.type == FileType.VIDEO }?.path?.let {
OfflineTimeFrameReceiver(this@OfflinePlayerActivity, it)
}
- viewModel.player.playWhenReady = PlayerHelper.playAutomatically
- viewModel.player.prepare()
-
if (PlayerHelper.watchPositionsVideo) {
PlayerHelper.getStoredWatchPosition(videoId, downloadInfo.duration)?.let {
- viewModel.player.seekTo(it)
+ playerController.seekTo(it)
}
}
-
- val data = PlayerNotificationData(
- downloadInfo.title,
- downloadInfo.uploader,
- downloadInfo.thumbnailPath.toString()
- )
- nowPlayingNotification?.updatePlayerNotification(videoId, data)
- }
- }
-
- private fun setMediaSource(videoUri: Uri?, audioUri: Uri?, subtitleUri: Uri?) {
- val subtitle = subtitleUri?.let {
- SubtitleConfiguration.Builder(it)
- .setMimeType(MimeTypes.APPLICATION_TTML)
- .setLanguage("en")
- .build()
- }
-
- when {
- videoUri != null && audioUri != null -> {
- val videoItem = MediaItem.Builder()
- .setUri(videoUri)
- .setSubtitleConfigurations(listOfNotNull(subtitle))
- .build()
-
- val videoSource = ProgressiveMediaSource.Factory(FileDataSource.Factory())
- .createMediaSource(videoItem)
-
- val audioSource = ProgressiveMediaSource.Factory(FileDataSource.Factory())
- .createMediaSource(MediaItem.fromUri(audioUri))
-
- var mediaSource = MergingMediaSource(audioSource, videoSource)
- if (subtitle != null) {
- val subtitleSource = SingleSampleMediaSource.Factory(FileDataSource.Factory())
- .createMediaSource(subtitle, C.TIME_UNSET)
-
- mediaSource = MergingMediaSource(mediaSource, subtitleSource)
- }
-
- viewModel.player.setMediaSource(mediaSource)
- }
-
- videoUri != null -> viewModel.player.setMediaItem(
- MediaItem.Builder()
- .setUri(videoUri)
- .setSubtitleConfigurations(listOfNotNull(subtitle))
- .build()
- )
-
- audioUri != null -> viewModel.player.setMediaItem(
- MediaItem.Builder()
- .setUri(audioUri)
- .setSubtitleConfigurations(listOfNotNull(subtitle))
- .build()
- )
}
}
@@ -336,7 +262,7 @@ class OfflinePlayerActivity : BaseActivity() {
private fun saveWatchPosition() {
if (!PlayerHelper.watchPositionsVideo) return
- PlayerHelper.saveWatchPosition(viewModel.player, videoId)
+ PlayerHelper.saveWatchPosition(playerController, videoId)
}
override fun onResume() {
@@ -349,19 +275,17 @@ class OfflinePlayerActivity : BaseActivity() {
super.onPause()
if (PlayerHelper.pauseOnQuit) {
- viewModel.player.pause()
+ playerController.pause()
}
}
override fun onDestroy() {
saveWatchPosition()
-
- nowPlayingNotification?.destroySelf()
- nowPlayingNotification = null
+
watchPositionTimer.destroy()
runCatching {
- viewModel.player.stop()
+ playerController.stop()
}
runCatching {
@@ -372,7 +296,7 @@ class OfflinePlayerActivity : BaseActivity() {
}
override fun onUserLeaveHint() {
- if (PlayerHelper.pipEnabled && viewModel.player.isPlaying) {
+ if (PlayerHelper.pipEnabled && playerController.isPlaying) {
PictureInPictureCompat.enterPictureInPictureMode(this, pipParams)
}
diff --git a/app/src/main/java/com/github/libretube/ui/fragments/AudioPlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/AudioPlayerFragment.kt
index 8cc3d19eb..4a55a76ea 100644
--- a/app/src/main/java/com/github/libretube/ui/fragments/AudioPlayerFragment.kt
+++ b/app/src/main/java/com/github/libretube/ui/fragments/AudioPlayerFragment.kt
@@ -155,10 +155,10 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
it.text = (PlayerHelper.seekIncrement / 1000).toString()
}
binding.rewindFL.setOnClickListener {
- playerService?.player?.seekBy(-PlayerHelper.seekIncrement)
+ playerService?.exoPlayer?.seekBy(-PlayerHelper.seekIncrement)
}
binding.forwardFL.setOnClickListener {
- playerService?.player?.seekBy(PlayerHelper.seekIncrement)
+ playerService?.exoPlayer?.seekBy(PlayerHelper.seekIncrement)
}
binding.openQueue.setOnClickListener {
@@ -166,7 +166,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
}
binding.playbackOptions.setOnClickListener {
- playerService?.player?.let {
+ playerService?.exoPlayer?.let {
PlaybackOptionsSheet(it)
.show(childFragmentManager)
}
@@ -182,7 +182,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
NavigationHelper.navigateVideo(
context = requireContext(),
videoUrlOrId = PlayingQueue.getCurrent()?.url,
- timestamp = playerService?.player?.currentPosition?.div(1000) ?: 0,
+ timestamp = playerService?.exoPlayer?.currentPosition?.div(1000) ?: 0,
keepQueue = true,
forceVideo = true
)
@@ -192,7 +192,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
ChaptersBottomSheet.SEEK_TO_POSITION_REQUEST_KEY,
viewLifecycleOwner
) { _, bundle ->
- playerService?.player?.seekTo(bundle.getLong(IntentData.currentPosition))
+ playerService?.exoPlayer?.seekTo(bundle.getLong(IntentData.currentPosition))
}
binding.openChapters.setOnClickListener {
@@ -202,7 +202,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
ChaptersBottomSheet()
.apply {
arguments = bundleOf(
- IntentData.duration to playerService.player?.duration?.div(1000)
+ IntentData.duration to playerService.exoPlayer?.duration?.div(1000)
)
}
.show(childFragmentManager)
@@ -218,11 +218,11 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
binding.thumbnail.setOnTouchListener(listener)
binding.playPause.setOnClickListener {
- playerService?.player?.togglePlayPauseState()
+ playerService?.exoPlayer?.togglePlayPauseState()
}
binding.miniPlayerPause.setOnClickListener {
- playerService?.player?.togglePlayPauseState()
+ playerService?.exoPlayer?.togglePlayPauseState()
}
binding.showMore.setOnClickListener {
@@ -381,7 +381,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
}
private fun updatePlayPauseButton() {
- playerService?.player?.let {
+ playerService?.exoPlayer?.let {
val binding = _binding ?: return
val iconRes = PlayerHelper.getPlayPauseActionIcon(it)
@@ -396,8 +396,10 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
isPaused = !isPlaying
}
playerService?.onNewVideoStarted = { streamItem ->
- updateStreamInfo(streamItem)
- _binding?.openChapters?.isVisible = !playerService?.getChapters().isNullOrEmpty()
+ handler.post {
+ updateStreamInfo(streamItem)
+ _binding?.openChapters?.isVisible = !playerService?.getChapters().isNullOrEmpty()
+ }
}
initializeSeekBar()
@@ -422,7 +424,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
}
override fun onSingleTap() {
- playerService?.player?.togglePlayPauseState()
+ playerService?.exoPlayer?.togglePlayPauseState()
}
override fun onLongTap() {
@@ -479,7 +481,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
if (_binding == null) return
handler.postDelayed(this::updateChapterIndex, 100)
- val player = playerService?.player ?: return
+ val player = playerService?.exoPlayer ?: return
val currentIndex =
PlayerHelper.getCurrentChapterIndex(player.currentPosition, chaptersModel.chapters)
diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt
index d28a9ec20..6b2bc09bf 100644
--- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt
+++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt
@@ -4,6 +4,7 @@ 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
@@ -11,7 +12,6 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Bitmap
import android.media.session.PlaybackState
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
@@ -45,16 +45,12 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
-import androidx.media3.common.MediaItem
-import androidx.media3.common.MediaItem.SubtitleConfiguration
-import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
-import androidx.media3.datasource.cronet.CronetDataSource
-import androidx.media3.exoplayer.hls.HlsMediaSource
+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.CronetHelper
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.Streams
@@ -66,17 +62,16 @@ import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentPlayerBinding
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder
+import com.github.libretube.enums.PlayerCommand
import com.github.libretube.enums.PlayerEvent
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.parcelable
import com.github.libretube.extensions.serializableExtra
-import com.github.libretube.extensions.setMetadata
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.extensions.togglePlayPauseState
import com.github.libretube.extensions.updateIfChanged
-import com.github.libretube.extensions.updateParameters
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.DownloadHelper
import com.github.libretube.helpers.ImageHelper
@@ -85,16 +80,16 @@ import com.github.libretube.helpers.NavBarHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.checkForSegments
-import com.github.libretube.helpers.PlayerHelper.getVideoStats
import com.github.libretube.helpers.PlayerHelper.isInSegment
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.helpers.WindowHelper
-import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.obj.ShareData
import com.github.libretube.obj.VideoResolution
import com.github.libretube.parcelable.PlayerData
+import com.github.libretube.services.AbstractPlayerService
+import com.github.libretube.services.VideoOnlinePlayerService
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseActivity
@@ -111,19 +106,15 @@ import com.github.libretube.ui.models.CommonPlayerViewModel
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.CommentsSheet
-import com.github.libretube.ui.sheets.StatsSheet
-import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.OnlineTimeFrameReceiver
import com.github.libretube.util.PauseableTimer
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.TextUtils
import com.github.libretube.util.TextUtils.toTimeInSeconds
-import com.github.libretube.util.YoutubeHlsPlaylistParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
-import java.util.concurrent.Executors
import kotlin.math.abs
import kotlin.math.ceil
@@ -138,9 +129,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
private val playerGestureControlsViewBinding get() = binding.playerGestureControlsView.binding
private val commonPlayerViewModel: CommonPlayerViewModel by activityViewModels()
- private val viewModel: PlayerViewModel by viewModels { PlayerViewModel.Factory }
+ private val viewModel: PlayerViewModel by viewModels()
private val commentsViewModel: CommentsViewModel by activityViewModels()
private val chaptersViewModel: ChaptersViewModel by activityViewModels()
+ private lateinit var playerController: MediaController
// Video information passed by the intent
private lateinit var videoId: String
@@ -161,10 +153,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
// if null, use same quality as fullscreen
private var noFullscreenResolution: Int? = null
- private val cronetDataSourceFactory = CronetDataSource.Factory(
- CronetHelper.cronetEngine,
- Executors.newCachedThreadPool()
- )
+ private var selectedAudioLanguageAndRoleFlags: Pair? = null
private val handler = Handler(Looper.getMainLooper())
@@ -211,7 +200,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
override fun onReceive(context: Context, intent: Intent) {
val event = intent.serializableExtra(PlayerHelper.CONTROL_TYPE) ?: return
- if (PlayerHelper.handlePlayerAction(viewModel.player, event)) return
+ if (PlayerHelper.handlePlayerAction(playerController, event)) return
when (event) {
PlayerEvent.Next -> {
@@ -291,14 +280,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
) {
updatePlayPauseButton()
}
-
- if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
- PlayerHelper.setPreferredAudioQuality(
- requireContext(),
- viewModel.player,
- viewModel.trackSelector
- )
- }
}
override fun onPlaybackStateChanged(playbackState: Int) {
@@ -306,9 +287,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
// set the playback speed to one if having reached the end of a livestream
if (playbackState == Player.STATE_BUFFERING && binding.player.isLive &&
- viewModel.player.duration - viewModel.player.currentPosition < 700
+ playerController.duration - playerController.currentPosition < 700
) {
- viewModel.player.setPlaybackSpeed(1f)
+ playerController.setPlaybackSpeed(1f)
}
// check if video has ended, next video is available and autoplay is enabled/the video is part of a played playlist.
@@ -342,7 +323,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
if (playbackState == Player.STATE_BUFFERING) {
if (bufferingTimeoutTask == null) {
bufferingTimeoutTask = Runnable {
- viewModel.player.pause()
+ playerController.pause()
}
}
@@ -360,7 +341,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
try {
- viewModel.player.play()
+ playerController.play()
} catch (e: Exception) {
e.printStackTrace()
}
@@ -406,6 +387,15 @@ 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()) {
+ playerController = it
+ playerController.addListener(playerListener)
+ }
}
override fun onCreateView(
@@ -431,7 +421,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
playerLayoutOrientation = resources.configuration.orientation
- createExoPlayer()
initializeTransitionLayout()
initializeOnClickActions()
@@ -591,7 +580,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
binding.playImageView.setOnClickListener {
- viewModel.player.togglePlayPauseState()
+ playerController.togglePlayPauseState()
}
activity?.supportFragmentManager
@@ -626,7 +615,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
IntentData.shareObjectType to ShareObjectType.VIDEO,
IntentData.shareData to ShareData(
currentVideo = streams.title,
- currentPosition = viewModel.player.currentPosition / 1000
+ currentPosition = playerController.currentPosition / 1000
)
)
val newShareDialog = ShareDialog()
@@ -653,7 +642,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
binding.relPlayerBackground.setOnClickListener {
// pause the current player
- viewModel.player.pause()
+ playerController.pause()
// start the background mode
playOnBackground()
@@ -712,7 +701,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
PixelCopy.request(surfaceView, bmp, { _ ->
screenshotBitmap = bmp
- val currentPosition = viewModel.player.currentPosition.toFloat() / 1000
+ val currentPosition =
+ playerController.currentPosition.toFloat() / 1000
openScreenshotFile.launch("${streams.title}-${currentPosition}.png")
}, Handler(Looper.getMainLooper()))
}
@@ -743,7 +733,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
BackgroundHelper.playOnBackground(
requireContext(),
videoId,
- viewModel.player.currentPosition,
+ playerController.currentPosition,
playlistId,
channelId,
keepQueue = true,
@@ -756,8 +746,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
private fun updateFullscreenOrientation() {
if (PlayerHelper.autoFullscreenEnabled || !this::streams.isInitialized) return
- val height = streams.videoStreams.firstOrNull()?.height ?: viewModel.player.videoSize.height
- val width = streams.videoStreams.firstOrNull()?.width ?: viewModel.player.videoSize.width
+ val height = streams.videoStreams.firstOrNull()?.height
+ ?: playerController.videoSize.height
+ val width =
+ streams.videoStreams.firstOrNull()?.width ?: playerController.videoSize.width
mainActivity.requestedOrientation = PlayerHelper.getOrientation(width, height)
}
@@ -839,14 +831,16 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
// disable video stream since it's not needed when screen off
if (!isInteractive) {
- viewModel.trackSelector.updateParameters {
- setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true)
- }
+ playerController.sendCustomCommand(
+ AbstractPlayerService.runPlayerActionCommand, bundleOf(
+ PlayerCommand.SET_VIDEO_TRACK_TYPE_DISABLED.name to true
+ )
+ )
}
// pause player if screen off and setting enabled
if (!isInteractive && PlayerHelper.pausePlayerOnScreenOffEnabled) {
- viewModel.player.pause()
+ playerController.pause()
}
// the app was put somewhere in the background - remember to not automatically continue
@@ -864,12 +858,15 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
if (closedVideo) {
closedVideo = false
- viewModel.nowPlayingNotification?.refreshNotification()
}
// re-enable and load video stream
- viewModel.trackSelector.updateParameters {
- setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, false)
+ if (::playerController.isInitialized) {
+ playerController.sendCustomCommand(
+ AbstractPlayerService.runPlayerActionCommand, bundleOf(
+ PlayerCommand.SET_VIDEO_TRACK_TYPE_DISABLED.name to false
+ )
+ )
}
}
@@ -878,13 +875,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
saveWatchPosition()
- viewModel.nowPlayingNotification?.destroySelf()
- viewModel.nowPlayingNotification = null
watchPositionTimer.destroy()
handler.removeCallbacksAndMessages(null)
- viewModel.player.removeListener(playerListener)
- viewModel.player.pause()
+ playerController.removeListener(playerListener)
+ playerController.pause()
if (PlayerHelper.pipEnabled) {
// disable the auto PiP mode for SDK >= 32
@@ -940,17 +935,17 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
// save the watch position if video isn't finished and option enabled
private fun saveWatchPosition() {
if (!isPlayerTransitioning && PlayerHelper.watchPositionsVideo) {
- PlayerHelper.saveWatchPosition(viewModel.player, videoId)
+ PlayerHelper.saveWatchPosition(playerController, videoId)
}
}
private fun checkForSegments() {
- if (!viewModel.player.isPlaying || !PlayerHelper.sponsorBlockEnabled) return
+ if (!playerController.isPlaying || !PlayerHelper.sponsorBlockEnabled) return
handler.postDelayed(this::checkForSegments, 100)
if (!viewModel.sponsorBlockEnabled || viewModel.segments.isEmpty()) return
- viewModel.player.checkForSegments(
+ playerController.checkForSegments(
requireContext(),
viewModel.segments,
viewModel.sponsorBlockConfig
@@ -959,12 +954,13 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
if (commonPlayerViewModel.isMiniPlayerVisible.value == true) return@let
binding.sbSkipBtn.isVisible = true
binding.sbSkipBtn.setOnClickListener {
- viewModel.player.seekTo((segment.segmentStartAndEnd.second * 1000f).toLong())
+ playerController.seekTo((segment.segmentStartAndEnd.second * 1000f).toLong())
segment.skipped = true
}
return
}
- if (!viewModel.player.isInSegment(viewModel.segments)) binding.sbSkipBtn.isGone = true
+ if (!playerController.isInSegment(viewModel.segments)) binding.sbSkipBtn.isGone =
+ true
}
private fun playVideo() {
@@ -983,6 +979,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
this@PlayerFragment.streams = streams!!
+ playerController.sendCustomCommand(
+ AbstractPlayerService.startServiceCommand,
+ bundleOf(IntentData.streams to streams)
+ )
}
val isFirstVideo = PlayingQueue.isEmpty()
@@ -1019,17 +1019,18 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
binding.player.apply {
useController = false
- player = viewModel.player
+ player = playerController
}
initializePlayerView()
// don't continue playback when the fragment is re-created after Android killed it
val wasIntentStopped = requireArguments().getBoolean(IntentData.wasIntentStopped, false)
- viewModel.player.playWhenReady = PlayerHelper.playAutomatically && !wasIntentStopped
+ playerController.playWhenReady =
+ PlayerHelper.playAutomatically && !wasIntentStopped
requireArguments().putBoolean(IntentData.wasIntentStopped, false)
- viewModel.player.prepare()
+ playerController.prepare()
if (binding.playerMotionLayout.progress != 1.0f) {
// show controllers when not in picture in picture mode
@@ -1039,13 +1040,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
binding.player.useController = true
}
}
- // show the player notification
- initializePlayerNotification()
fetchSponsorBlockSegments()
if (streams.category == Streams.categoryMusic) {
- viewModel.player.setPlaybackSpeed(1f)
+ playerController.setPlaybackSpeed(1f)
}
viewModel.isOrientationChangeInProgress = false
@@ -1076,7 +1075,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
*/
private fun playNextVideo(nextId: String? = null) {
if (nextId == null && PlayingQueue.repeatMode == Player.REPEAT_MODE_ONE) {
- viewModel.player.seekTo(0)
+ playerController.seekTo(0)
return
}
@@ -1117,7 +1116,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
viewModel,
commonPlayerViewModel,
viewLifecycleOwner,
- viewModel.trackSelector,
this
)
@@ -1206,7 +1204,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
if (videoId == this.videoId) {
// try finding the time stamp of the url and seek to it if found
uri.getQueryParameter("t")?.toTimeInSeconds()?.let {
- viewModel.player.seekTo(it * 1000)
+ playerController.seekTo(it * 1000)
}
} else {
// YouTube video link without time or not the current video, thus load in player
@@ -1215,7 +1213,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
private fun updatePlayPauseButton() {
- binding.playImageView.setImageResource(PlayerHelper.getPlayPauseActionIcon(viewModel.player))
+ binding.playImageView.setImageResource(PlayerHelper.getPlayPauseActionIcon(playerController))
}
private suspend fun initializeHighlight(highlight: Segment) {
@@ -1234,31 +1232,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
)
}
- private fun getSubtitleConfigs(): List = streams.subtitles.map {
- val roleFlags = getSubtitleRoleFlags(it)
- SubtitleConfiguration.Builder(it.url!!.toUri())
- .setRoleFlags(roleFlags)
- .setLanguage(it.code)
- .setMimeType(it.mimeType).build()
- }
-
- private fun createMediaItem(uri: Uri, mimeType: String) = MediaItem.Builder()
- .setUri(uri)
- .setMimeType(mimeType)
- .setSubtitleConfigurations(getSubtitleConfigs())
- .setMetadata(streams)
- .build()
-
- private fun setMediaSource(uri: Uri, mimeType: String) {
- val mediaItem = createMediaItem(uri, mimeType)
- viewModel.player.setMediaItem(mediaItem)
- }
-
/**
* Get all available player resolutions
*/
private fun getAvailableResolutions(): List {
- val resolutions = viewModel.player.currentTracks.groups.asSequence()
+ val resolutions = playerController.currentTracks.groups.asSequence()
.flatMap { group ->
(0 until group.length).map {
group.getTrackFormat(it).height
@@ -1274,29 +1252,31 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
private fun initStreamSources() {
// use the video's default audio track when starting playback
- viewModel.trackSelector.updateParameters {
- setPreferredAudioRoleFlags(C.ROLE_FLAG_MAIN)
- }
+ playerController.sendCustomCommand(
+ AbstractPlayerService.runPlayerActionCommand, bundleOf(
+ PlayerCommand.SET_AUDIO_ROLE_FLAGS.name to C.ROLE_FLAG_MAIN
+ )
+ )
// set the default subtitle if available
updateCurrentSubtitle(viewModel.currentSubtitle)
// set media source and resolution in the beginning
- lifecycleScope.launch(Dispatchers.IO) {
- setStreamSource()
+ updateResolutionOnFullscreenChange(commonPlayerViewModel.isFullscreen.value == true)
+ playerController.sendCustomCommand(
+ AbstractPlayerService.runPlayerActionCommand,
+ bundleOf(PlayerCommand.START_PLAYBACK.name to true)
+ )
- withContext(Dispatchers.Main) {
- // support for time stamped links
- if (timeStamp != 0L) {
- viewModel.player.seekTo(timeStamp * 1000)
- // delete the time stamp because it already got consumed
- timeStamp = 0L
- } else if (!streams.isLive) {
- // seek to the saved watch position
- PlayerHelper.getStoredWatchPosition(videoId, streams.duration)?.let {
- viewModel.player.seekTo(it)
- }
- }
+ // support for time stamped links
+ if (timeStamp != 0L) {
+ playerController.seekTo(timeStamp * 1000)
+ // delete the time stamp because it already got consumed
+ timeStamp = 0L
+ } else if (!streams.isLive) {
+ // seek to the saved watch position
+ PlayerHelper.getStoredWatchPosition(videoId, streams.duration)?.let {
+ playerController.seekTo(it)
}
}
}
@@ -1308,10 +1288,13 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
resolution
}
- viewModel.trackSelector.updateParameters {
- setMaxVideoSize(Int.MAX_VALUE, transformedResolution)
- setMinVideoSize(Int.MIN_VALUE, transformedResolution)
- }
+ playerController.sendCustomCommand(
+ AbstractPlayerService.runPlayerActionCommand, bundleOf(
+ PlayerCommand.SET_RESOLUTION.name to transformedResolution
+ )
+ )
+
+ binding.player.selectedResolution = resolution
}
private fun updateResolutionOnFullscreenChange(isFullscreen: Boolean) {
@@ -1322,86 +1305,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
}
- private suspend fun setStreamSource() {
- updateResolutionOnFullscreenChange(commonPlayerViewModel.isFullscreen.value == true)
-
- val (uri, mimeType) = when {
- // LBRY HLS
- PreferenceHelper.getBoolean(
- PreferenceKeys.LBRY_HLS,
- false
- ) && streams.videoStreams.any {
- it.quality.orEmpty().contains("LBRY HLS")
- } -> {
- val lbryHlsUrl = streams.videoStreams.first {
- it.quality!!.contains("LBRY HLS")
- }.url!!
- lbryHlsUrl.toUri() to MimeTypes.APPLICATION_M3U8
- }
- // DASH
- !PlayerHelper.useHlsOverDash && streams.videoStreams.isNotEmpty() -> {
- // only use the dash manifest generated by YT if either it's a livestream or no other source is available
- val dashUri =
- if (streams.isLive && streams.dash != null) {
- ProxyHelper.unwrapStreamUrl(
- streams.dash!!
- ).toUri()
- } else {
- // skip LBRY urls when checking whether the stream source is usable
- PlayerHelper.createDashSource(streams, requireContext())
- }
-
- dashUri to MimeTypes.APPLICATION_MPD
- }
- // HLS
- streams.hls != null -> {
- val hlsMediaSourceFactory = HlsMediaSource.Factory(cronetDataSourceFactory)
- .setPlaylistParserFactory(YoutubeHlsPlaylistParser.Factory())
-
- val mediaSource = hlsMediaSourceFactory.createMediaSource(
- createMediaItem(
- ProxyHelper.unwrapStreamUrl(streams.hls!!).toUri(),
- MimeTypes.APPLICATION_M3U8
- )
- )
- withContext(Dispatchers.Main) { viewModel.player.setMediaSource(mediaSource) }
- return
- }
- // NO STREAM FOUND
- else -> {
- context?.toastFromMainDispatcher(R.string.unknown_error)
- return
- }
- }
- withContext(Dispatchers.Main) { setMediaSource(uri, mimeType) }
- }
-
- private fun createExoPlayer() {
- viewModel.player.setWakeMode(C.WAKE_MODE_NETWORK)
- viewModel.player.addListener(playerListener)
-
- // control for the track sources like subtitles and audio source
- PlayerHelper.setPreferredCodecs(viewModel.trackSelector)
- }
-
- /**
- * show the [NowPlayingNotification] for the current video
- */
- private fun initializePlayerNotification() {
- if (viewModel.nowPlayingNotification == null) {
- viewModel.nowPlayingNotification = NowPlayingNotification(
- requireContext(),
- viewModel.player
- )
- }
- val playerNotificationData = PlayerNotificationData(
- streams.title,
- streams.uploader,
- streams.thumbnailUrl
- )
- viewModel.nowPlayingNotification?.updatePlayerNotification(videoId, playerNotificationData)
- }
-
/**
* Use the sensor mode if auto fullscreen is enabled
*/
@@ -1438,24 +1341,15 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
.show(childFragmentManager)
}
- private fun getSubtitleRoleFlags(subtitle: Subtitle?): Int {
- return if (subtitle?.autoGenerated != true) {
- C.ROLE_FLAG_CAPTION
- } else {
- PlayerHelper.ROLE_FLAG_AUTO_GEN_SUBTITLE
- }
- }
-
override fun onQualityClicked() {
// get the available resolutions
val resolutions = getAvailableResolutions()
- val currentQuality = viewModel.trackSelector.parameters.maxVideoHeight
// Dialog for quality selection
BaseBottomSheet()
.setSimpleItems(
resolutions.map(VideoResolution::name),
- preselectedItem = resolutions.firstOrNull { it.resolution == currentQuality }?.name
+ preselectedItem = resolutions.firstOrNull { it.resolution == binding.player.selectedResolution }?.name
) { which ->
val newResolution = resolutions[which].resolution
setPlayerResolution(newResolution, true)
@@ -1473,7 +1367,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
override fun onAudioStreamClicked() {
val context = requireContext()
val audioLanguagesAndRoleFlags = PlayerHelper.getAudioLanguagesAndRoleFlagsFromTrackGroups(
- viewModel.player.currentTracks.groups,
+ playerController.currentTracks.groups,
false
)
val audioLanguages = audioLanguagesAndRoleFlags.map {
@@ -1499,18 +1393,22 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
} else {
baseBottomSheet.setSimpleItems(
audioLanguages,
- preselectedItem = audioLanguagesAndRoleFlags.firstOrNull {
- val format = viewModel.player.audioFormat
- format?.language == it.first && format?.roleFlags == it.second
- }?.let {
+ preselectedItem = selectedAudioLanguageAndRoleFlags?.let {
PlayerHelper.getAudioTrackNameFromFormat(context, it)
},
) { index ->
val selectedAudioFormat = audioLanguagesAndRoleFlags[index]
- viewModel.trackSelector.updateParameters {
- setPreferredAudioLanguage(selectedAudioFormat.first)
- setPreferredAudioRoleFlags(selectedAudioFormat.second)
- }
+ playerController.sendCustomCommand(
+ AbstractPlayerService.runPlayerActionCommand, bundleOf(
+ PlayerCommand.SET_AUDIO_ROLE_FLAGS.name to selectedAudioFormat.second
+ )
+ )
+ playerController.sendCustomCommand(
+ AbstractPlayerService.runPlayerActionCommand, bundleOf(
+ PlayerCommand.SET_AUDIO_LANGUAGE.name to selectedAudioFormat.first
+ )
+ )
+ selectedAudioLanguageAndRoleFlags = selectedAudioFormat
}
}
@@ -1523,10 +1421,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
override fun onStatsClicked() {
if (!this::streams.isInitialized) return
- val videoStats = getVideoStats(viewModel.player, videoId)
- StatsSheet()
- .apply { arguments = bundleOf(IntentData.videoStats to videoStats) }
- .show(childFragmentManager)
+ // TODO: reimplement video stats
+// val videoStats = getVideoStats(playerController, videoId)
+// StatsSheet()
+// .apply { arguments = bundleOf(IntentData.videoStats to videoStats) }
+// .show(childFragmentManager)
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
@@ -1545,8 +1444,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
// close button got clicked in PiP mode
// pause the video and keep the app alive
if (lifecycle.currentState == Lifecycle.State.CREATED) {
- viewModel.player.pause()
- viewModel.nowPlayingNotification?.cancelNotification()
+ playerController.pause()
closedVideo = true
}
@@ -1559,36 +1457,43 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
}
- private fun updateCurrentSubtitle(subtitle: Subtitle?) =
- viewModel.trackSelector.updateParameters {
- val roleFlags = if (subtitle?.code != null) getSubtitleRoleFlags(subtitle) else 0
- setPreferredTextRoleFlags(roleFlags)
- setPreferredTextLanguage(subtitle?.code)
- }
+ private fun updateCurrentSubtitle(subtitle: Subtitle?) {
+ if (!::playerController.isInitialized) return
+
+ playerController.sendCustomCommand(
+ AbstractPlayerService.runPlayerActionCommand, bundleOf(
+ PlayerCommand.SET_SUBTITLE.name to subtitle
+ )
+ )
+ }
fun onUserLeaveHint() {
if (shouldStartPiP()) {
PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams)
} else if (PlayerHelper.pauseOnQuit) {
- viewModel.player.pause()
+ playerController.pause()
}
}
- private val pipParams
- get() = PictureInPictureParamsCompat.Builder()
- .setActions(
- PlayerHelper.getPiPModeActions(
- requireActivity(),
- viewModel.player.isPlaying
+ private val pipParams: PictureInPictureParamsCompat
+ get() = run {
+ val isPlaying = ::playerController.isInitialized && playerController.isPlaying
+
+ PictureInPictureParamsCompat.Builder()
+ .setActions(
+ PlayerHelper.getPiPModeActions(
+ requireActivity(),
+ isPlaying
+ )
)
- )
- .setAutoEnterEnabled(PlayerHelper.pipEnabled && viewModel.player.isPlaying)
- .apply {
- if (viewModel.player.isPlaying) {
- setAspectRatio(viewModel.player.videoSize)
+ .setAutoEnterEnabled(PlayerHelper.pipEnabled && isPlaying)
+ .apply {
+ if (isPlaying) {
+ setAspectRatio(playerController.videoSize)
+ }
}
- }
- .build()
+ .build()
+ }
private fun createSeekbarPreviewListener(): SeekbarPreviewListener {
return SeekbarPreviewListener(
@@ -1606,7 +1511,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
private fun shouldStartPiP(): Boolean {
- return shouldUsePip() && viewModel.player.isPlaying &&
+ return shouldUsePip() && playerController.isPlaying &&
!BackgroundHelper.isBackgroundServiceRunning(requireContext())
}
@@ -1620,7 +1525,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
val orientation = resources.configuration.orientation
if (commonPlayerViewModel.isFullscreen.value != true && orientation != playerLayoutOrientation) {
// remember the current position before recreating the activity
- arguments?.putLong(IntentData.timeStamp, viewModel.player.currentPosition / 1000)
+ arguments?.putLong(
+ IntentData.timeStamp,
+ playerController.currentPosition / 1000
+ )
playerLayoutOrientation = orientation
viewModel.isOrientationChangeInProgress = true
diff --git a/app/src/main/java/com/github/libretube/ui/models/OfflinePlayerViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/OfflinePlayerViewModel.kt
deleted file mode 100644
index 94ef31025..000000000
--- a/app/src/main/java/com/github/libretube/ui/models/OfflinePlayerViewModel.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.github.libretube.ui.models
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
-import androidx.lifecycle.viewmodel.initializer
-import androidx.lifecycle.viewmodel.viewModelFactory
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
-import com.github.libretube.helpers.PlayerHelper
-
-@UnstableApi
-class OfflinePlayerViewModel(
- val player: ExoPlayer,
- val trackSelector: DefaultTrackSelector,
-) : ViewModel() {
-
- companion object {
- val Factory = viewModelFactory {
- initializer {
- val context = this[APPLICATION_KEY]!!
- val trackSelector = DefaultTrackSelector(context)
- OfflinePlayerViewModel(
- player = PlayerHelper.createPlayer(context, trackSelector, false),
- trackSelector = trackSelector,
- )
- }
- }
- }
-
- override fun onCleared() {
- super.onCleared()
- player.release()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/libretube/ui/models/PlayerViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/PlayerViewModel.kt
index df8d38a22..d03e7c252 100644
--- a/app/src/main/java/com/github/libretube/ui/models/PlayerViewModel.kt
+++ b/app/src/main/java/com/github/libretube/ui/models/PlayerViewModel.kt
@@ -2,17 +2,10 @@ package com.github.libretube.ui.models
import android.content.Context
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
-import androidx.lifecycle.viewmodel.initializer
-import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
-import com.github.libretube.R
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.StreamsExtractor
-import com.github.libretube.api.obj.Message
import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.Streams
import com.github.libretube.api.obj.Subtitle
@@ -24,10 +17,7 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
@UnstableApi
-class PlayerViewModel(
- val player: ExoPlayer,
- val trackSelector: DefaultTrackSelector,
-) : ViewModel() {
+class PlayerViewModel : ViewModel() {
// data to remember for recovery on orientation change
private var streamsInfo: Streams? = null
@@ -69,22 +59,4 @@ class PlayerViewModel(
).segments
}
}
-
- companion object {
- val Factory = viewModelFactory {
- initializer {
- val context = this[APPLICATION_KEY]!!
- val trackSelector = DefaultTrackSelector(context)
- PlayerViewModel(
- player = PlayerHelper.createPlayer(context, trackSelector, false),
- trackSelector = trackSelector,
- )
- }
- }
- }
-
- override fun onCleared() {
- super.onCleared()
- player.release()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/libretube/ui/sheets/PlaybackOptionsSheet.kt b/app/src/main/java/com/github/libretube/ui/sheets/PlaybackOptionsSheet.kt
index e5db3fa8c..ed33c7cc6 100644
--- a/app/src/main/java/com/github/libretube/ui/sheets/PlaybackOptionsSheet.kt
+++ b/app/src/main/java/com/github/libretube/ui/sheets/PlaybackOptionsSheet.kt
@@ -4,17 +4,22 @@ import android.os.Bundle
import android.view.LayoutInflater
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
import com.github.libretube.databinding.PlaybackBottomSheetBinding
+import com.github.libretube.enums.PlayerCommand
import com.github.libretube.extensions.round
import com.github.libretube.helpers.PreferenceHelper
+import com.github.libretube.services.AbstractPlayerService
import com.github.libretube.ui.adapters.SliderLabelsAdapter
class PlaybackOptionsSheet(
- private val player: ExoPlayer
+ private val player: Player
) : ExpandedBottomSheet() {
private var _binding: PlaybackBottomSheetBinding? = null
private val binding get() = _binding!!
@@ -32,8 +37,10 @@ class PlaybackOptionsSheet(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = binding
- binding.speedShortcuts.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- binding.pitchShortcuts.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ binding.speedShortcuts.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ binding.pitchShortcuts.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
binding.speedShortcuts.adapter = SliderLabelsAdapter(SUGGESTED_SPEEDS) {
binding.speed.value = it
@@ -57,7 +64,15 @@ class PlaybackOptionsSheet(
}
binding.skipSilence.setOnCheckedChangeListener { _, isChecked ->
- player.skipSilenceEnabled = 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)
+ )
+ }
PreferenceHelper.putBoolean(PreferenceKeys.SKIP_SILENCE, isChecked)
}
}
diff --git a/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt
index f21850eee..fe2015207 100644
--- a/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt
+++ b/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt
@@ -31,7 +31,7 @@ import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.text.Cue
-import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.MediaController
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.CaptionStyleCompat
import androidx.media3.ui.PlayerView
@@ -601,8 +601,8 @@ abstract class CustomExoPlayerView(
}
override fun onPlaybackSpeedClicked() {
- player?.let {
- PlaybackOptionsSheet(it as ExoPlayer).show(supportFragmentManager)
+ (player as? MediaController)?.let {
+ PlaybackOptionsSheet(it).show(supportFragmentManager)
}
}
diff --git a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt
index 5b34c9473..f910dea02 100644
--- a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt
+++ b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt
@@ -14,7 +14,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.trackselection.TrackSelector
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
@@ -29,7 +28,6 @@ import com.github.libretube.ui.dialogs.SubmitSegmentDialog
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
import com.github.libretube.ui.models.CommonPlayerViewModel
import com.github.libretube.ui.models.PlayerViewModel
-import com.github.libretube.ui.sheets.PlayingQueueSheet
import com.github.libretube.util.PlayingQueue
@UnstableApi
@@ -40,7 +38,6 @@ class OnlinePlayerView(
private var playerOptions: OnlinePlayerOptions? = null
private var playerViewModel: PlayerViewModel? = null
private var commonPlayerViewModel: CommonPlayerViewModel? = null
- private var trackSelector: TrackSelector? = null
private var viewLifecycleOwner: LifecycleOwner? = null
private val handler = Handler(Looper.getMainLooper())
@@ -51,6 +48,8 @@ class OnlinePlayerView(
*/
var currentWindow: Window? = null
+ var selectedResolution: Int? = null
+
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun getOptionsMenuItems(): List {
return super.getOptionsMenuItems() +
@@ -72,7 +71,7 @@ class OnlinePlayerView(
BottomSheetItem(
context.getString(R.string.captions),
R.drawable.ic_caption,
- this::getCurrentCaptionLanguage
+ { playerViewModel?.currentSubtitle?.code ?: context.getString(R.string.none) }
) {
playerOptions?.onCaptionsClicked()
},
@@ -89,25 +88,14 @@ class OnlinePlayerView(
private fun getCurrentResolutionSummary(): String {
val currentQuality = player?.videoSize?.height ?: 0
var summary = "${currentQuality}p"
- val trackSelector = trackSelector ?: return summary
- val selectedQuality = trackSelector.parameters.maxVideoHeight
- if (selectedQuality == Int.MAX_VALUE) {
+ if (selectedResolution == null) {
summary += " - ${context.getString(R.string.auto)}"
- } else if (selectedQuality > currentQuality) {
+ } else if ((selectedResolution ?: 0) > currentQuality) {
summary += " - ${context.getString(R.string.resolution_limited)}"
}
return summary
}
- @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
- private fun getCurrentCaptionLanguage(): String {
- return if (trackSelector != null && trackSelector!!.parameters.preferredTextLanguages.isNotEmpty()) {
- trackSelector!!.parameters.preferredTextLanguages[0]
- } else {
- context.getString(R.string.none)
- }
- }
-
private fun getCurrentAudioTrackTitle(): String {
if (player == null) {
return context.getString(R.string.unknown_or_no_audio)
@@ -153,13 +141,11 @@ class OnlinePlayerView(
playerViewModel: PlayerViewModel,
commonPlayerViewModel: CommonPlayerViewModel,
viewLifecycleOwner: LifecycleOwner,
- trackSelector: TrackSelector,
playerOptions: OnlinePlayerOptions
) {
this.playerViewModel = playerViewModel
this.commonPlayerViewModel = commonPlayerViewModel
this.viewLifecycleOwner = viewLifecycleOwner
- this.trackSelector = trackSelector
this.playerOptions = playerOptions
commonPlayerViewModel.isFullscreen.observe(viewLifecycleOwner) { isFullscreen ->
diff --git a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt
index 8355fcc26..75445f0a7 100644
--- a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt
+++ b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt
@@ -1,75 +1,35 @@
package com.github.libretube.util
-import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
-import android.graphics.Bitmap
-import android.os.Build
import android.os.Bundle
-import android.support.v4.media.session.MediaSessionCompat
-import android.support.v4.media.session.PlaybackStateCompat
-import androidx.annotation.DrawableRes
-import androidx.core.app.NotificationCompat
import androidx.core.app.PendingIntentCompat
-import androidx.core.content.getSystemService
-import androidx.media.app.NotificationCompat.MediaStyle
-import androidx.media3.common.MediaMetadata
-import androidx.media3.common.Player
-import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.CommandButton
+import androidx.media3.session.DefaultMediaNotificationProvider
+import androidx.media3.session.MediaNotification
+import androidx.media3.session.MediaSession
import com.github.libretube.LibreTubeApp.Companion.PLAYER_CHANNEL_NAME
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.enums.NotificationId
import com.github.libretube.enums.PlayerEvent
-import com.github.libretube.extensions.toMediaMetadataCompat
-import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.PlayerHelper
-import com.github.libretube.obj.PlayerNotificationData
-import com.github.libretube.services.OnClearFromRecentService
import com.github.libretube.ui.activities.MainActivity
-import java.util.UUID
+import com.google.common.collect.ImmutableList
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class NowPlayingNotification(
private val context: Context,
- private val player: ExoPlayer,
private val backgroundOnly: Boolean = false,
private val offlinePlayer: Boolean = false,
private val intentActivity: Class<*> = MainActivity::class.java
-) {
- private var videoId: String? = null
- private val nManager = context.getSystemService()!!
-
- /**
- * The metadata of the current playing song (thumbnail, title, uploader)
- */
- private var notificationData: PlayerNotificationData? = null
-
- /**
- * The [MediaSessionCompat] for the [notificationData].
- */
- private lateinit var mediaSession: MediaSessionCompat
-
- /**
- * The [NotificationCompat.Builder] to load the [mediaSession] content on it.
- */
- private var notificationBuilder: NotificationCompat.Builder? = null
-
- /**
- * The [Bitmap] which represents the background / thumbnail of the notification
- */
- private var notificationBitmap: Bitmap? = null
-
- private fun loadCurrentLargeIcon() {
- if (DataSaverMode.isEnabled(context)) return
-
- if (notificationBitmap == null) {
- enqueueThumbnailRequest {
- createOrUpdateNotification()
- }
- }
- }
+): MediaNotification.Provider {
+ private val nProvider = DefaultMediaNotificationProvider.Builder(context)
+ .setNotificationId(NotificationId.PLAYER_PLAYBACK.id)
+ .setChannelId(PLAYER_CHANNEL_NAME)
+ .setChannelName(R.string.player_channel_name)
+ .build()
private fun createCurrentContentIntent(): PendingIntent? {
// starts a new MainActivity Intent when the player notification is clicked
@@ -84,194 +44,10 @@ class NowPlayingNotification(
}
}
-
return PendingIntentCompat
.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, false)
}
- private fun createIntent(action: String): PendingIntent? {
- val intent = Intent(action)
- .setPackage(context.packageName)
-
- return PendingIntentCompat
- .getBroadcast(context, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT, false)
- }
-
- private fun enqueueThumbnailRequest(callback: (Bitmap) -> Unit) {
- ImageHelper.getImageWithCallback(
- context,
- notificationData?.thumbnailPath?.toString() ?: notificationData?.thumbnailUrl
- ) {
- notificationBitmap = processBitmap(it)
- callback.invoke(notificationBitmap!!)
- }
- }
-
- private fun processBitmap(bitmap: Bitmap): Bitmap {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- bitmap
- } else {
- ImageHelper.getSquareBitmap(bitmap)
- }
- }
-
- private val legacyNotificationButtons
- get() = listOf(
- createNotificationAction(R.drawable.ic_prev_outlined, PlayerEvent.Prev.name),
- createNotificationAction(
- if (player.isPlaying) R.drawable.ic_pause else R.drawable.ic_play,
- PlayerEvent.PlayPause.name
- ),
- createNotificationAction(R.drawable.ic_next_outlined, PlayerEvent.Next.name),
- createNotificationAction(R.drawable.ic_rewind_md, PlayerEvent.Rewind.name),
- createNotificationAction(R.drawable.ic_forward_md, PlayerEvent.Forward.name)
- )
-
- private fun createNotificationAction(
- drawableRes: Int,
- actionName: String
- ): NotificationCompat.Action {
- return NotificationCompat.Action.Builder(drawableRes, actionName, createIntent(actionName))
- .build()
- }
-
- private fun createMediaSessionAction(
- @DrawableRes drawableRes: Int,
- actionName: String
- ): PlaybackStateCompat.CustomAction {
- return PlaybackStateCompat.CustomAction.Builder(actionName, actionName, drawableRes).build()
- }
-
- /**
- * Creates a [MediaSessionCompat] for the player
- */
- private fun createMediaSession() {
- if (this::mediaSession.isInitialized) return
-
- val sessionCallback = object : MediaSessionCompat.Callback() {
- override fun onRewind() {
- handlePlayerAction(PlayerEvent.Rewind)
- super.onRewind()
- }
-
- override fun onFastForward() {
- handlePlayerAction(PlayerEvent.Forward)
- super.onFastForward()
- }
-
- override fun onPlay() {
- handlePlayerAction(PlayerEvent.PlayPause)
- super.onPlay()
- }
-
- override fun onPause() {
- handlePlayerAction(PlayerEvent.PlayPause)
- super.onPause()
- }
-
- override fun onSkipToNext() {
- handlePlayerAction(PlayerEvent.Next)
- super.onSkipToNext()
- }
-
- override fun onSkipToPrevious() {
- handlePlayerAction(PlayerEvent.Prev)
- super.onSkipToPrevious()
- }
-
- override fun onStop() {
- handlePlayerAction(PlayerEvent.Stop)
- super.onStop()
- }
-
- override fun onSeekTo(pos: Long) {
- player.seekTo(pos)
- super.onSeekTo(pos)
- }
-
- override fun onCustomAction(action: String, extras: Bundle?) {
- runCatching { handlePlayerAction(PlayerEvent.valueOf(action)) }
- super.onCustomAction(action, extras)
- }
- }
-
- mediaSession = MediaSessionCompat(context, UUID.randomUUID().toString())
- mediaSession.setCallback(sessionCallback)
-
- updateSessionMetadata()
- updateSessionPlaybackState()
-
- val playerStateListener = object : Player.Listener {
- override fun onIsPlayingChanged(isPlaying: Boolean) {
- super.onIsPlayingChanged(isPlaying)
- updateSessionPlaybackState(isPlaying = isPlaying)
- }
-
- override fun onIsLoadingChanged(isLoading: Boolean) {
- super.onIsLoadingChanged(isLoading)
-
- if (!isLoading) {
- updateSessionMetadata()
- }
-
- updateSessionPlaybackState(isLoading = isLoading)
- }
-
- override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
- super.onMediaMetadataChanged(mediaMetadata)
- updateSessionMetadata(mediaMetadata)
- }
- }
-
- player.addListener(playerStateListener)
- }
-
- private fun updateSessionMetadata(metadata: MediaMetadata? = null) {
- val data = metadata ?: player.mediaMetadata
- val newMetadata = data.toMediaMetadataCompat(player.duration, notificationBitmap)
- mediaSession.setMetadata(newMetadata)
- }
-
- private fun updateSessionPlaybackState(isPlaying: Boolean? = null, isLoading: Boolean? = null) {
- val loading = isLoading == true || (isPlaying == false && player.isLoading)
-
- val newPlaybackState = when {
- loading -> PlaybackStateCompat.STATE_BUFFERING
- isPlaying ?: player.isPlaying -> PlaybackStateCompat.STATE_PLAYING
- else -> PlaybackStateCompat.STATE_PAUSED
- }
-
- mediaSession.setPlaybackState(createPlaybackState(newPlaybackState))
- }
-
- private fun createPlaybackState(@PlaybackStateCompat.State state: Int): PlaybackStateCompat {
- val stateActions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
- PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
- PlaybackStateCompat.ACTION_REWIND or
- PlaybackStateCompat.ACTION_FAST_FORWARD or
- PlaybackStateCompat.ACTION_PLAY_PAUSE or
- PlaybackStateCompat.ACTION_PAUSE or
- PlaybackStateCompat.ACTION_PLAY or
- PlaybackStateCompat.ACTION_SEEK_TO
-
- return PlaybackStateCompat.Builder()
- .setActions(stateActions)
- .addCustomAction(
- createMediaSessionAction(
- R.drawable.ic_rewind_md,
- PlayerEvent.Rewind.name
- )
- )
- .addCustomAction(
- createMediaSessionAction(
- R.drawable.ic_forward_md,
- PlayerEvent.Forward.name
- )
- )
- .setState(state, player.currentPosition, player.playbackParameters.speed)
- .build()
- }
-
/**
* Forward the action to the responsible notification owner (e.g. PlayerFragment)
*/
@@ -282,79 +58,23 @@ class NowPlayingNotification(
context.sendBroadcast(intent)
}
- /**
- * Updates or creates the [notificationBuilder]
- */
- fun updatePlayerNotification(videoId: String, data: PlayerNotificationData) {
- this.videoId = videoId
- this.notificationData = data
- // reset the thumbnail bitmap in order to become reloaded for the new video
- this.notificationBitmap = null
-
- loadCurrentLargeIcon()
-
- if (notificationBuilder == null) {
- createMediaSession()
- createNotificationBuilder()
- // update the notification each time the player continues playing or pauses
- player.addListener(object : Player.Listener {
- override fun onIsPlayingChanged(isPlaying: Boolean) {
- createOrUpdateNotification()
- super.onIsPlayingChanged(isPlaying)
- }
- })
- context.startService(Intent(context, OnClearFromRecentService::class.java))
- }
-
- createOrUpdateNotification()
+ override fun createNotification(
+ mediaSession: MediaSession,
+ customLayout: ImmutableList,
+ actionFactory: MediaNotification.ActionFactory,
+ onNotificationChangedCallback: MediaNotification.Provider.Callback
+ ): MediaNotification {
+ createCurrentContentIntent()?.let { mediaSession.setSessionActivity(it) }
+ nProvider.setSmallIcon(R.drawable.ic_launcher_lockscreen)
+ return nProvider.createNotification(mediaSession, customLayout, actionFactory, onNotificationChangedCallback)
}
- /**
- * Initializes the [notificationBuilder] attached to the [player] and shows it.
- */
- private fun createNotificationBuilder() {
- notificationBuilder = NotificationCompat.Builder(context, PLAYER_CHANNEL_NAME)
- .setSmallIcon(R.drawable.ic_launcher_lockscreen)
- .setContentIntent(createCurrentContentIntent())
- .setDeleteIntent(createIntent(PlayerEvent.Stop.name))
- .setStyle(
- MediaStyle()
- .setMediaSession(mediaSession.sessionToken)
- .setShowActionsInCompactView(1)
- )
- }
-
- private fun createOrUpdateNotification() {
- if (notificationBuilder == null) return
- val notification = notificationBuilder!!
- .setContentTitle(notificationData?.title)
- .setContentText(notificationData?.uploaderName)
- .setLargeIcon(notificationBitmap)
- .clearActions()
- .apply {
- legacyNotificationButtons.forEach {
- addAction(it)
- }
- }
- .build()
- updateSessionMetadata()
- nManager.notify(NotificationId.PLAYER_PLAYBACK.id, notification)
- }
-
- /**
- * Destroy the [NowPlayingNotification]
- */
- fun destroySelf() {
- if (this::mediaSession.isInitialized) mediaSession.release()
-
- nManager.cancel(NotificationId.PLAYER_PLAYBACK.id)
- }
-
- fun cancelNotification() {
- nManager.cancel(NotificationId.PLAYER_PLAYBACK.id)
- }
-
- fun refreshNotification() {
- createOrUpdateNotification()
+ override fun handleCustomCommand(
+ session: MediaSession,
+ action: String,
+ extras: Bundle
+ ): Boolean {
+ runCatching { handlePlayerAction(PlayerEvent.valueOf(action)) }
+ return true
}
}