Migration to Media3

This commit is contained in:
Bnyro 2023-05-15 17:21:49 +02:00
parent 4a5dd6fc35
commit 95e65ae7e3
19 changed files with 192 additions and 136 deletions

View File

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

View File

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

View File

@ -1,7 +1,8 @@
package com.github.libretube.extensions 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( inline fun DefaultTrackSelector.updateParameters(
actions: DefaultTrackSelector.Parameters.Builder.() -> Unit, actions: DefaultTrackSelector.Parameters.Builder.() -> Unit,
) = setParameters(buildUponParameters().apply(actions)) ) = setParameters(buildUponParameters().apply(actions))

View File

@ -12,19 +12,19 @@ import androidx.core.app.PendingIntentCompat
import androidx.core.app.RemoteActionCompat import androidx.core.app.RemoteActionCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.IconCompat 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.R
import com.github.libretube.api.obj.PipedStream import com.github.libretube.api.obj.PipedStream
import com.github.libretube.api.obj.Segment import com.github.libretube.api.obj.Segment
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.enums.AudioQuality import com.github.libretube.enums.AudioQuality
import com.github.libretube.enums.PlayerEvent 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.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -75,6 +75,7 @@ object PlayerHelper {
} }
// get the system default caption style // get the system default caption style
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun getCaptionStyle(context: Context): CaptionStyleCompat { fun getCaptionStyle(context: Context): CaptionStyleCompat {
val captioningManager = context.getSystemService<CaptioningManager>()!! val captioningManager = context.getSystemService<CaptioningManager>()!!
return if (!captioningManager.isEnabled) { return if (!captioningManager.isEnabled) {
@ -461,6 +462,7 @@ object PlayerHelper {
/** /**
* Get the load controls for the player (buffering, etc) * Get the load controls for the player (buffering, etc)
*/ */
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun getLoadControl(): LoadControl { fun getLoadControl(): LoadControl {
return DefaultLoadControl.Builder() return DefaultLoadControl.Builder()
// cache the last three minutes // cache the last three minutes
@ -477,6 +479,7 @@ object PlayerHelper {
/** /**
* Load playback parameters such as speed and skip silence * 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 { fun ExoPlayer.loadPlaybackParams(isBackgroundMode: Boolean = false): ExoPlayer {
skipSilenceEnabled = skipSilence skipSilenceEnabled = skipSilence
val speed = if (isBackgroundMode) backgroundSpeed else playbackSpeed 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.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.IntentData 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.helpers.PlayerHelper.loadPlaybackParams
import com.github.libretube.obj.PlayerNotificationData import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.util.NowPlayingNotification 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -75,6 +75,7 @@ class OfflinePlayerService : LifecycleService() {
* @param downloadWithItem The database download to play from * @param downloadWithItem The database download to play from
* @return whether starting the audio player succeeded * @return whether starting the audio player succeeded
*/ */
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun startAudioPlayer(downloadWithItem: DownloadWithItems): Boolean { private fun startAudioPlayer(downloadWithItem: DownloadWithItems): Boolean {
player = ExoPlayer.Builder(this) player = ExoPlayer.Builder(this)
.setUsePlatformDiagnostics(false) .setUsePlatformDiagnostics(false)

View File

@ -12,6 +12,10 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope 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.R
import com.github.libretube.api.JsonHelper import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
@ -31,10 +35,6 @@ import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.PlayerNotificationData import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayingQueue 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -45,6 +45,7 @@ import kotlinx.serialization.encodeToString
/** /**
* Loads the selected videos audio in background mode with a notification area. * Loads the selected videos audio in background mode with a notification area.
*/ */
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class OnlinePlayerService : LifecycleService() { class OnlinePlayerService : LifecycleService() {
/** /**
* VideoId of the video * VideoId of the video

View File

@ -8,6 +8,17 @@ import android.text.format.DateUtils
import android.view.View import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope 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.PictureInPictureCompat
import com.github.libretube.compat.PictureInPictureParamsCompat import com.github.libretube.compat.PictureInPictureParamsCompat
import com.github.libretube.constants.IntentData 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.helpers.WindowHelper
import com.github.libretube.ui.base.BaseActivity import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.models.PlayerViewModel 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class OfflinePlayerActivity : BaseActivity() { class OfflinePlayerActivity : BaseActivity() {
private lateinit var binding: ActivityOfflinePlayerBinding private lateinit var binding: ActivityOfflinePlayerBinding
private lateinit var videoId: String private lateinit var videoId: String
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var playerView: StyledPlayerView private lateinit var playerView: PlayerView
private lateinit var trackSelector: DefaultTrackSelector private lateinit var trackSelector: DefaultTrackSelector
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding private lateinit var playerBinding: ExoStyledPlayerControlViewBinding

View File

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

View File

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

View File

@ -38,6 +38,17 @@ import androidx.fragment.app.activityViewModels
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope 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 androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.CronetHelper import com.github.libretube.api.CronetHelper
@ -99,24 +110,12 @@ import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.TextUtils import com.github.libretube.util.TextUtils
import com.github.libretube.util.TextUtils.toTimeInSeconds 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 com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
@ -124,6 +123,7 @@ import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.abs import kotlin.math.abs
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class PlayerFragment : Fragment(), OnlinePlayerOptions { class PlayerFragment : Fragment(), OnlinePlayerOptions {
private var _binding: FragmentPlayerBinding? = null private var _binding: FragmentPlayerBinding? = null
val binding get() = _binding!! val binding get() = _binding!!

View File

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

View File

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

View File

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

View File

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

View File

@ -12,13 +12,20 @@ import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.DefaultMediaNotificationProvider
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 coil.request.ImageRequest
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
@ -28,13 +35,10 @@ import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.PlayerHelper import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.obj.PlayerNotificationData import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.google.android.exoplayer2.ExoPlayer import com.google.common.collect.ImmutableList
import com.google.android.exoplayer2.Player import com.google.common.util.concurrent.ListenableFuture
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
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class NowPlayingNotification( class NowPlayingNotification(
private val context: Context, private val context: Context,
private val player: ExoPlayer, private val player: ExoPlayer,
@ -47,12 +51,7 @@ class NowPlayingNotification(
/** /**
* The [MediaSessionCompat] for the [notificationData]. * The [MediaSessionCompat] for the [notificationData].
*/ */
private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSession: MediaSession
/**
* The [MediaSessionConnector] to connect with the [mediaSession] and implement it with the [player].
*/
private lateinit var mediaSessionConnector: MediaSessionConnector
/** /**
* The [PlayerNotificationManager] to load the [mediaSession] content on it. * The [PlayerNotificationManager] to load the [mediaSession] content on it.
@ -137,7 +136,7 @@ class NowPlayingNotification(
ImageHelper.imageLoader.enqueue(request) ImageHelper.imageLoader.enqueue(request)
} }
private val customActionReceiver = object : CustomActionReceiver { private val customActionReceiver = object : PlayerNotificationManager.CustomActionReceiver {
override fun createCustomActions( override fun createCustomActions(
context: Context, context: Context,
instanceId: Int, instanceId: Int,
@ -170,69 +169,89 @@ class NowPlayingNotification(
} }
} }
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 intent = Intent(actionName).setPackage(context.packageName)
val pendingIntent = PendingIntentCompat val pendingIntent = PendingIntentCompat
.getBroadcast(context, instanceId, intent, PendingIntent.FLAG_CANCEL_CURRENT, false) .getBroadcast(context, instanceId, intent, PendingIntent.FLAG_CANCEL_CURRENT, false)
return NotificationCompat.Action.Builder(drawableRes, actionName, pendingIntent).build() return NotificationCompat.Action.Builder(drawableRes, actionName, pendingIntent).build()
} }
private fun createMediaSessionAction(@DrawableRes drawableRes: Int, actionName: String): MediaSessionConnector.CustomActionProvider { private fun createMediaSessionAction(
return object : MediaSessionConnector.CustomActionProvider { @DrawableRes drawableRes: Int,
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { actionName: String,
return PlaybackStateCompat.CustomAction.Builder(actionName, actionName, drawableRes).build() ): CommandButton {
} return CommandButton.Builder()
.setDisplayName(actionName)
override fun onCustomAction(player: Player, action: String, extras: Bundle?) { .setSessionCommand(SessionCommand(actionName, bundleOf()))
handlePlayerAction(action) .setIconResId(drawableRes)
} .build()
}
} }
/** /**
* Creates a [MediaSessionCompat] amd a [MediaSessionConnector] for the player * Creates a [MediaSessionCompat] for the player
*/ */
private fun createMediaSession() { private fun createMediaSession() {
if (this::mediaSession.isInitialized) return 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 availablePlayerCommands = connectionResult.availablePlayerCommands.buildUpon()
.remove(Player.COMMAND_SEEK_TO_PREVIOUS)
.build()
return MediaSession.ConnectionResult.accept(
connectionResult.availableSessionCommands,
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)
}
} }
mediaSessionConnector = MediaSessionConnector(mediaSession).apply { mediaSession = MediaSession.Builder(context, player)
setPlayer(player) .setCallback(sessionCallback)
setQueueNavigator(object : TimelineQueueNavigator(mediaSession) { .build()
override fun getMediaDescription( mediaSession.setCustomLayout(getCustomActions())
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 { private fun getCustomActions() = mutableListOf(
return PlaybackStateCompat.ACTION_PLAY_PAUSE createMediaSessionAction(R.drawable.ic_prev_outlined, PREV),
} createMediaSessionAction(R.drawable.ic_next_outlined, NEXT),
}) createMediaSessionAction(R.drawable.ic_rewind_md, REWIND),
setCustomActionProviders( createMediaSessionAction(R.drawable.ic_forward_md, FORWARD),
createMediaSessionAction(R.drawable.ic_prev_outlined, PREV), )
createMediaSessionAction(R.drawable.ic_next_outlined, NEXT),
createMediaSessionAction(R.drawable.ic_rewind_md, REWIND), private fun getMediaDescription(): MediaDescriptionCompat {
createMediaSessionAction(R.drawable.ic_forward_md, FORWARD), 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()
} }
private fun handlePlayerAction(action: String) { private fun handlePlayerAction(action: String) {
@ -244,6 +263,7 @@ class NowPlayingNotification(
) )
} }
} }
PREV -> { PREV -> {
if (PlayingQueue.hasPrev()) { if (PlayingQueue.hasPrev()) {
PlayingQueue.onQueueItemSelected( PlayingQueue.onQueueItemSelected(
@ -251,9 +271,11 @@ class NowPlayingNotification(
) )
} }
} }
REWIND -> { REWIND -> {
player.seekTo(player.currentPosition - PlayerHelper.seekIncrement) player.seekTo(player.currentPosition - PlayerHelper.seekIncrement)
} }
FORWARD -> { FORWARD -> {
player.seekTo(player.currentPosition + PlayerHelper.seekIncrement) player.seekTo(player.currentPosition + PlayerHelper.seekIncrement)
} }
@ -278,6 +300,19 @@ class NowPlayingNotification(
} }
} }
class NotificationProvider(val context: Context) : DefaultMediaNotificationProvider(context) {
override fun getMediaButtons(
session: MediaSession,
playerCommands: Player.Commands,
customLayout: ImmutableList<CommandButton>,
showPauseButton: Boolean,
): ImmutableList<CommandButton> {
val buttons = super.getMediaButtons(session, playerCommands, customLayout, showPauseButton)
buttons.removeFirst()
return buttons
}
}
/** /**
* Initializes the [playerNotification] attached to the [player] and shows it. * Initializes the [playerNotification] attached to the [player] and shows it.
*/ */
@ -291,7 +326,7 @@ class NowPlayingNotification(
.build().apply { .build().apply {
setPlayer(player) setPlayer(player)
setColorized(true) setColorized(true)
setMediaSessionToken(mediaSession.sessionToken) setMediaSessionToken(mediaSession.sessionCompatToken)
setSmallIcon(R.drawable.ic_launcher_lockscreen) setSmallIcon(R.drawable.ic_launcher_lockscreen)
setUseNextAction(false) setUseNextAction(false)
setUsePreviousAction(false) setUsePreviousAction(false)
@ -305,7 +340,6 @@ class NowPlayingNotification(
fun destroySelfAndPlayer() { fun destroySelfAndPlayer() {
playerNotification?.setPlayer(null) playerNotification?.setPlayer(null)
mediaSession.isActive = false
mediaSession.release() mediaSession.release()
player.stop() player.stop()

View File

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

View File

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

View File

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

View File

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