Merge pull request #3779 from Bnyro/media3

Migration to Media3
This commit is contained in:
Bnyro 2023-05-22 14:26:25 +02:00 committed by GitHub
commit f6f2d37584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 227 additions and 158 deletions

View File

@ -103,10 +103,12 @@ dependencies {
implementation libs.material
/* ExoPlayer */
implementation libs.exoplayer
implementation(libs.exoplayer.extension.cronet) { exclude group: 'com.google.android.gms' }
implementation libs.exoplayer.extension.mediasession
implementation libs.exoplayer.dash
implementation libs.androidx.media3.exoplayer
implementation libs.androidx.media3.ui
implementation libs.androidx.media3.exoplayer.hls
implementation libs.androidx.media3.exoplayer.dash
implementation libs.androidx.media3.session
implementation(libs.androidx.media3.datasource.cronet) { exclude group: 'com.google.android.gms' }
/* Retrofit and Kotlinx Serialization */
implementation libs.square.retrofit

View File

@ -6,7 +6,7 @@ import android.os.Build
import android.util.Rational
import androidx.annotation.RequiresApi
import androidx.core.app.RemoteActionCompat
import com.google.android.exoplayer2.video.VideoSize
import androidx.media3.common.VideoSize
class PictureInPictureParamsCompat private constructor(
private val autoEnterEnabled: Boolean,

View File

@ -0,0 +1,31 @@
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
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,
)
setMediaMetadata(
MediaMetadata.Builder()
.setTitle(streams.title)
.setArtist(streams.uploader)
.setArtworkUri(streams.thumbnailUrl.toUri())
.setExtras(extras)
.build()
)
}

View File

@ -1,7 +1,8 @@
package com.github.libretube.extensions
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
inline fun DefaultTrackSelector.updateParameters(
actions: DefaultTrackSelector.Parameters.Builder.() -> Unit,
) = setParameters(buildUponParameters().apply(actions))

View File

@ -12,19 +12,19 @@ import androidx.core.app.PendingIntentCompat
import androidx.core.app.RemoteActionCompat
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.IconCompat
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.PlaybackParameters
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.LoadControl
import androidx.media3.ui.CaptionStyleCompat
import com.github.libretube.R
import com.github.libretube.api.obj.PipedStream
import com.github.libretube.api.obj.Segment
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.enums.AudioQuality
import com.github.libretube.enums.PlayerEvent
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.LoadControl
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ui.CaptionStyleCompat
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@ -75,6 +75,7 @@ object PlayerHelper {
}
// get the system default caption style
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun getCaptionStyle(context: Context): CaptionStyleCompat {
val captioningManager = context.getSystemService<CaptioningManager>()!!
return if (!captioningManager.isEnabled) {
@ -461,6 +462,7 @@ object PlayerHelper {
/**
* Get the load controls for the player (buffering, etc)
*/
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun getLoadControl(): LoadControl {
return DefaultLoadControl.Builder()
// cache the last three minutes
@ -477,6 +479,7 @@ object PlayerHelper {
/**
* Load playback parameters such as speed and skip silence
*/
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun ExoPlayer.loadPlaybackParams(isBackgroundMode: Boolean = false): ExoPlayer {
skipSilenceEnabled = skipSilence
val speed = if (isBackgroundMode) backgroundSpeed else playbackSpeed

View File

@ -6,6 +6,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import com.github.libretube.R
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.IntentData
@ -18,8 +20,6 @@ import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.util.NowPlayingNotification
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -75,6 +75,7 @@ class OfflinePlayerService : LifecycleService() {
* @param downloadWithItem The database download to play from
* @return whether starting the audio player succeeded
*/
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun startAudioPlayer(downloadWithItem: DownloadWithItems): Boolean {
player = ExoPlayer.Builder(this)
.setUsePlatformDiagnostics(false)

View File

@ -12,6 +12,10 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import com.github.libretube.R
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance
@ -23,6 +27,7 @@ import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.WatchPosition
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.setMetadata
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.checkForSegments
@ -31,10 +36,6 @@ import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayingQueue
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -45,6 +46,7 @@ import kotlinx.serialization.encodeToString
/**
* Loads the selected videos audio in background mode with a notification area.
*/
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class OnlinePlayerService : LifecycleService() {
/**
* VideoId of the video
@ -293,8 +295,7 @@ class OnlinePlayerService : LifecycleService() {
* Sets the [MediaItem] with the [streams] into the [player]
*/
private fun setMediaItem() {
val streams = streams
streams ?: return
val streams = streams ?: return
val uri = if (streams.audioStreams.isNotEmpty()) {
PlayerHelper.getAudioSource(this, streams.audioStreams)
@ -304,6 +305,7 @@ class OnlinePlayerService : LifecycleService() {
val mediaItem = MediaItem.Builder()
.setUri(ProxyHelper.rewriteUrl(uri))
.setMetadata(streams)
.build()
player?.setMediaItem(mediaItem)
}

View File

@ -8,6 +8,17 @@ import android.text.format.DateUtils
import android.view.View
import androidx.activity.viewModels
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.ExoPlayer
import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.ui.PlayerView
import com.github.libretube.compat.PictureInPictureCompat
import com.github.libretube.compat.PictureInPictureParamsCompat
import com.github.libretube.constants.IntentData
@ -22,26 +33,16 @@ import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
import com.github.libretube.helpers.WindowHelper
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.models.PlayerViewModel
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MediaItem.SubtitleConfiguration
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.FileDataSource
import com.google.android.exoplayer2.util.MimeTypes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class OfflinePlayerActivity : BaseActivity() {
private lateinit var binding: ActivityOfflinePlayerBinding
private lateinit var videoId: String
private lateinit var player: ExoPlayer
private lateinit var playerView: StyledPlayerView
private lateinit var playerView: PlayerView
private lateinit var trackSelector: DefaultTrackSelector
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding

View File

@ -4,13 +4,13 @@ import android.graphics.Color
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.media3.exoplayer.ExoPlayer
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.databinding.ChapterColumnBinding
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.ThemeHelper
import com.github.libretube.ui.viewholders.ChaptersViewHolder
import com.google.android.exoplayer2.ExoPlayer
class ChaptersAdapter(
private val chapters: List<ChapterSegment>,

View File

@ -4,10 +4,10 @@ import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.media3.exoplayer.ExoPlayer
import com.github.libretube.R
import com.github.libretube.databinding.DialogStatsBinding
import com.github.libretube.util.TextUtils
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class StatsDialog(

View File

@ -37,6 +37,17 @@ import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit
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.DefaultDataSource
import androidx.media3.datasource.cronet.CronetDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.CronetHelper
@ -98,17 +109,6 @@ import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.TextUtils
import com.github.libretube.util.TextUtils.toTimeInSeconds
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MediaItem.SubtitleConfiguration
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.cronet.CronetDataSource
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -120,8 +120,10 @@ import retrofit2.HttpException
import java.io.IOException
import java.util.*
import java.util.concurrent.Executors
import com.github.libretube.extensions.setMetadata
import kotlin.math.abs
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class PlayerFragment : Fragment(), OnlinePlayerOptions {
private var _binding: FragmentPlayerBinding? = null
val binding get() = _binding!!
@ -1234,10 +1236,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
private fun setMediaSource(uri: Uri, mimeType: String) {
val mediaItem: MediaItem = MediaItem.Builder()
val mediaItem = MediaItem.Builder()
.setUri(uri)
.setMimeType(mimeType)
.setSubtitleConfigurations(subtitles)
.setMetadata(streams)
.build()
exoPlayer.setMediaItem(mediaItem)
}

View File

@ -7,13 +7,15 @@ import android.view.ViewGroup.MarginLayoutParams
import androidx.core.graphics.drawable.toBitmap
import androidx.core.math.MathUtils
import androidx.core.view.updateLayoutParams
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.TimeBar
import coil.request.ImageRequest
import com.github.libretube.api.obj.PreviewFrames
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.obj.PreviewFrame
import com.google.android.exoplayer2.ui.TimeBar
@UnstableApi
class SeekbarPreviewListener(
private val previewFrames: List<PreviewFrames>,
private val playerBinding: ExoStyledPlayerControlViewBinding,
@ -59,6 +61,8 @@ class SeekbarPreviewListener(
playerBinding.seekbarPreview.alpha = 1f
}
.start()
onScrubEnd.invoke(position)
}
/**

View File

@ -4,12 +4,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.media3.common.PlaybackParameters
import androidx.media3.exoplayer.ExoPlayer
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.PlaybackBottomSheetBinding
import com.github.libretube.extensions.round
import com.github.libretube.helpers.PreferenceHelper
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.PlaybackParameters
class PlaybackOptionsSheet(
private val player: ExoPlayer,

View File

@ -22,6 +22,16 @@ import androidx.core.view.isVisible
import androidx.core.view.marginStart
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.Player
import androidx.media3.common.text.Cue
import androidx.media3.common.util.RepeatModeUtil
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.TrackSelector
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.CaptionStyleCompat
import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView
import androidx.media3.ui.TimeBar
import com.github.libretube.R
import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
@ -44,22 +54,13 @@ import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
import com.github.libretube.util.PlayingQueue
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.text.Cue
import com.google.android.exoplayer2.trackselection.TrackSelector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.ui.SubtitleView
import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.util.RepeatModeUtil
@SuppressLint("ClickableViewAccessibility")
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
internal class CustomExoPlayerView(
context: Context,
attributeSet: AttributeSet? = null,
) : StyledPlayerView(context, attributeSet), PlayerOptions, PlayerGestureOptions {
) : PlayerView(context, attributeSet), PlayerOptions, PlayerGestureOptions {
val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this)
/**
@ -405,7 +406,7 @@ internal class CustomExoPlayerView(
if (isLocked) {
ContextCompat.getColor(
context,
com.google.android.exoplayer2.R.color.exo_black_opacity_60,
androidx.media3.ui.R.color.exo_black_opacity_60,
)
} else {
Color.TRANSPARENT
@ -600,12 +601,12 @@ internal class CustomExoPlayerView(
.show(supportFragmentManager)
}
override fun onConfigurationChanged(newConfig: Configuration?) {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// add a larger bottom margin to the time bar in landscape mode
val offset = when {
playerViewModel?.isFullscreen?.value ?: (newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE) -> 20.dpToPx()
playerViewModel?.isFullscreen?.value ?: (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) -> 20.dpToPx()
else -> 10.dpToPx()
}
@ -620,7 +621,7 @@ internal class CustomExoPlayerView(
if (!hasCutout && binding.topBar.marginStart == 0) return
// add a margin to the top and the bottom bar in landscape mode for notches
val newMargin = when (newConfig?.orientation) {
val newMargin = when (newConfig.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> LANDSCAPE_MARGIN_HORIZONTAL
else -> 0
}

View File

@ -7,17 +7,20 @@ import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import androidx.core.view.marginLeft
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.DefaultTimeBar
import com.github.libretube.api.obj.Segment
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.extensions.dpToPx
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ThemeHelper
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.DefaultTimeBar
import com.google.android.material.R
/**
* TimeBar that can be marked with SponsorBlock Segments
*/
@UnstableApi
class MarkableTimeBar(
context: Context,
attributeSet: AttributeSet? = null,
@ -57,7 +60,7 @@ class MarkableTimeBar(
Paint().apply {
color = ThemeHelper.getThemeColor(
context,
com.google.android.material.R.attr.colorOnSecondary,
R.attr.colorOnSecondary,
)
},
)

View File

@ -6,19 +6,22 @@ import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
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.core.graphics.drawable.toBitmap
import androidx.core.os.bundleOf
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import androidx.media3.ui.PlayerNotificationManager
import coil.request.ImageRequest
import com.github.libretube.R
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
@ -28,13 +31,9 @@ import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.ui.activities.MainActivity
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.ui.PlayerNotificationManager.CustomActionReceiver
import com.google.common.util.concurrent.ListenableFuture
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class NowPlayingNotification(
private val context: Context,
private val player: ExoPlayer,
@ -47,12 +46,7 @@ class NowPlayingNotification(
/**
* The [MediaSessionCompat] for the [notificationData].
*/
private lateinit var mediaSession: MediaSessionCompat
/**
* The [MediaSessionConnector] to connect with the [mediaSession] and implement it with the [player].
*/
private lateinit var mediaSessionConnector: MediaSessionConnector
private lateinit var mediaSession: MediaSession
/**
* The [PlayerNotificationManager] to load the [mediaSession] content on it.
@ -102,10 +96,10 @@ class NowPlayingNotification(
player: Player,
callback: PlayerNotificationManager.BitmapCallback,
): Bitmap? {
if (DataSaverMode.isEnabled(context)) return null
// On Android 13 and up, the metadata is responsible for the thumbnail
if (DataSaverMode.isEnabled(context) ||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return null
if (bitmap == null) enqueueThumbnailRequest(callback)
return bitmap
}
@ -119,7 +113,7 @@ class NowPlayingNotification(
// online image
notificationData?.thumbnailPath?.let { path ->
ImageHelper.getDownloadedImage(context, path)?.let {
bitmap = processThumbnailBitmap(it)
bitmap = ImageHelper.getSquareBitmap(it)
callback.onBitmap(bitmap!!)
}
return
@ -128,7 +122,7 @@ class NowPlayingNotification(
val request = ImageRequest.Builder(context)
.data(notificationData?.thumbnailUrl)
.target {
bitmap = processThumbnailBitmap(it.toBitmap())
bitmap = ImageHelper.getSquareBitmap(it.toBitmap())
callback.onBitmap(bitmap!!)
}
.build()
@ -137,7 +131,7 @@ class NowPlayingNotification(
ImageHelper.imageLoader.enqueue(request)
}
private val customActionReceiver = object : CustomActionReceiver {
private val customActionReceiver = object : PlayerNotificationManager.CustomActionReceiver {
override fun createCustomActions(
context: Context,
instanceId: Int,
@ -159,82 +153,96 @@ class NowPlayingNotification(
}
}
/**
* Returns the bitmap on Android 13+, for everything below scaled down to a square
*/
private fun processThumbnailBitmap(bitmap: Bitmap): Bitmap {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
ImageHelper.getSquareBitmap(bitmap)
} else {
bitmap
}
}
private fun createNotificationAction(drawableRes: Int, actionName: String, instanceId: Int): NotificationCompat.Action {
private fun createNotificationAction(
drawableRes: Int,
actionName: String,
instanceId: Int,
): NotificationCompat.Action {
val intent = Intent(actionName).setPackage(context.packageName)
val pendingIntent = PendingIntentCompat
.getBroadcast(context, instanceId, intent, PendingIntent.FLAG_CANCEL_CURRENT, false)
return NotificationCompat.Action.Builder(drawableRes, actionName, pendingIntent).build()
}
private fun createMediaSessionAction(@DrawableRes drawableRes: Int, actionName: String): MediaSessionConnector.CustomActionProvider {
return object : MediaSessionConnector.CustomActionProvider {
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
return PlaybackStateCompat.CustomAction.Builder(actionName, actionName, drawableRes).build()
}
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
handlePlayerAction(action)
}
}
private fun createMediaSessionAction(
@DrawableRes drawableRes: Int,
actionName: String,
): CommandButton {
return CommandButton.Builder()
.setDisplayName(actionName)
.setSessionCommand(SessionCommand(actionName, bundleOf()))
.setIconResId(drawableRes)
.build()
}
/**
* Creates a [MediaSessionCompat] amd a [MediaSessionConnector] for the player
* Creates a [MediaSessionCompat] for the player
*/
private fun createMediaSession() {
if (this::mediaSession.isInitialized) return
mediaSession = MediaSessionCompat(context, this.javaClass.name).apply {
isActive = true
val sessionCallback = object : MediaSession.Callback {
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo,
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
val availablePlayerCommands = connectionResult.availablePlayerCommands // Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build()
getCustomActions().forEach { button ->
button.sessionCommand?.let { availableSessionCommands.add(it) }
}
session.setAvailableCommands(controller, availableSessionCommands.build(), availablePlayerCommands)
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
availablePlayerCommands,
)
}
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle,
): ListenableFuture<SessionResult> {
handlePlayerAction(customCommand.customAction)
return super.onCustomCommand(session, controller, customCommand, args)
}
override fun onPlayerCommandRequest(
session: MediaSession,
controller: MediaSession.ControllerInfo,
playerCommand: Int
): Int {
if (playerCommand == Player.COMMAND_SEEK_TO_PREVIOUS) {
handlePlayerAction(PREV)
return SessionResult.RESULT_SUCCESS
}
return super.onPlayerCommandRequest(session, controller, playerCommand)
}
override fun onPostConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
) {
session.setCustomLayout(getCustomActions())
}
}
mediaSessionConnector = MediaSessionConnector(mediaSession).apply {
setPlayer(player)
setQueueNavigator(object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(
player: Player,
windowIndex: Int,
): MediaDescriptionCompat {
val appIcon = BitmapFactory.decodeResource(
context.resources,
R.drawable.ic_launcher_monochrome,
)
val extras = bundleOf(
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON to appIcon,
MediaMetadataCompat.METADATA_KEY_TITLE to notificationData?.title,
MediaMetadataCompat.METADATA_KEY_ARTIST to notificationData?.uploaderName,
)
return MediaDescriptionCompat.Builder()
.setTitle(notificationData?.title)
.setSubtitle(notificationData?.uploaderName)
.setIconBitmap(appIcon)
.setExtras(extras)
.build()
}
override fun getSupportedQueueNavigatorActions(player: Player): Long {
return PlaybackStateCompat.ACTION_PLAY_PAUSE
}
})
setCustomActionProviders(
createMediaSessionAction(R.drawable.ic_prev_outlined, PREV),
createMediaSessionAction(R.drawable.ic_next_outlined, NEXT),
createMediaSessionAction(R.drawable.ic_rewind_md, REWIND),
createMediaSessionAction(R.drawable.ic_forward_md, FORWARD),
)
}
mediaSession = MediaSession.Builder(context, player)
.setCallback(sessionCallback)
.build()
mediaSession.setCustomLayout(getCustomActions())
}
private fun getCustomActions() = mutableListOf(
// disabled and overwritten in onPlayerCommandRequest
// createMediaSessionAction(R.drawable.ic_prev_outlined, PREV),
createMediaSessionAction(R.drawable.ic_next_outlined, NEXT),
createMediaSessionAction(R.drawable.ic_rewind_md, REWIND),
createMediaSessionAction(R.drawable.ic_forward_md, FORWARD),
)
private fun handlePlayerAction(action: String) {
when (action) {
NEXT -> {
@ -244,6 +252,7 @@ class NowPlayingNotification(
)
}
}
PREV -> {
if (PlayingQueue.hasPrev()) {
PlayingQueue.onQueueItemSelected(
@ -251,9 +260,11 @@ class NowPlayingNotification(
)
}
}
REWIND -> {
player.seekTo(player.currentPosition - PlayerHelper.seekIncrement)
}
FORWARD -> {
player.seekTo(player.currentPosition + PlayerHelper.seekIncrement)
}
@ -291,10 +302,12 @@ class NowPlayingNotification(
.build().apply {
setPlayer(player)
setColorized(true)
setMediaSessionToken(mediaSession.sessionToken)
setMediaSessionToken(mediaSession.sessionCompatToken)
setSmallIcon(R.drawable.ic_launcher_lockscreen)
setUseNextAction(false)
setUsePreviousAction(false)
setUseRewindAction(false)
setUseFastForwardAction(false)
setUseStopAction(true)
}
}
@ -305,7 +318,6 @@ class NowPlayingNotification(
fun destroySelfAndPlayer() {
playerNotification?.setPlayer(null)
mediaSession.isActive = false
mediaSession.release()
player.stop()

View File

@ -10,6 +10,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
app:controller_layout_id="@layout/exo_styled_player_control_view"
app:show_buffering="when_playing">
<com.github.libretube.ui.views.DoubleTapOverlay

View File

@ -304,6 +304,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
app:controller_layout_id="@layout/exo_styled_player_control_view"
app:layout_constraintBottom_toBottomOf="@id/main_container"
app:layout_constraintStart_toStartOf="@id/main_container"
app:layout_constraintTop_toTopOf="@id/main_container"
@ -349,7 +350,7 @@
app:drawableTint="@android:color/white" />
</com.google.android.material.card.MaterialCardView>
<com.github.libretube.ui.views.AutoplayCountdownView
android:id="@+id/autoplay_countdown"
android:layout_width="match_parent"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">LibreTube</string>
<string name="startpage">Home</string>
<string name="subscriptions">Subscriptions</string>
<string name="library">Library</string>

View File

@ -10,7 +10,6 @@ preference = "1.2.0"
extJunit = "1.1.5"
espresso = "3.5.1"
workRuntime = "2.8.1"
exoplayer = "2.18.6"
retrofit = "2.9.0"
desugaring = "2.0.3"
cronetEmbedded = "108.5359.79"
@ -21,6 +20,7 @@ room = "2.5.1"
kotlinxSerialization = "1.5.1"
kotlinxDatetime = "0.4.0"
kotlinxRetrofit = "1.0.0"
media3 = "1.0.1"
[libraries]
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
@ -34,12 +34,14 @@ androidx-preference = { group = "androidx.preference", name = "preference-ktx",
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "extJunit" }
androidx-test-espressoCore = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
androidx-work-runtime = { group = "androidx.work", name="work-runtime-ktx", version.ref="workRuntime" }
exoplayer = { group = "com.google.android.exoplayer", name = "exoplayer", version.ref = "exoplayer" }
exoplayer-extension-mediasession = { group = "com.google.android.exoplayer", name = "extension-mediasession", version.ref = "exoplayer" }
androidx-media3-exoplayer = { group = "androidx.media3", name="media3-exoplayer", version.ref="media3" }
androidx-media3-exoplayer-hls = { group = "androidx.media3", name="media3-exoplayer-hls", version.ref="media3" }
androidx-media3-exoplayer-dash = { group = "androidx.media3", name="media3-exoplayer-dash", version.ref="media3" }
androidx-media3-datasource-cronet = { group = "androidx.media3", name = "media3-datasource-cronet", version.ref = "media3" }
androidx-media3-session = { group="androidx.media3", name="media3-session", version.ref="media3" }
androidx-media3-ui = { group="androidx.media3", name="media3-ui", version.ref="media3" }
square-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
desugaring = { group = "com.android.tools", name = "desugar_jdk_libs_nio", version.ref = "desugaring" }
exoplayer-extension-cronet = { group = "com.google.android.exoplayer", name = "extension-cronet", version.ref = "exoplayer" }
exoplayer-dash = { group = "com.google.android.exoplayer", name = "exoplayer-dash", version.ref = "exoplayer" }
cronet-embedded = { group = "org.chromium.net", name = "cronet-embedded", version.ref = "cronetEmbedded" }
cronet-okhttp = { group = "com.google.net.cronet", name = "cronet-okhttp", version.ref = "cronetOkHttp" }
coil = { group = "io.coil-kt", name = "coil", version.ref="coil" }