diff --git a/app/build.gradle b/app/build.gradle index 8be27ab13..61602277a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/com/github/libretube/compat/PictureInPictureParamsCompat.kt b/app/src/main/java/com/github/libretube/compat/PictureInPictureParamsCompat.kt index 7dfb8762e..1a2b3095d 100644 --- a/app/src/main/java/com/github/libretube/compat/PictureInPictureParamsCompat.kt +++ b/app/src/main/java/com/github/libretube/compat/PictureInPictureParamsCompat.kt @@ -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, diff --git a/app/src/main/java/com/github/libretube/extensions/SetMetadata.kt b/app/src/main/java/com/github/libretube/extensions/SetMetadata.kt new file mode 100644 index 000000000..3887eb04f --- /dev/null +++ b/app/src/main/java/com/github/libretube/extensions/SetMetadata.kt @@ -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() + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/extensions/UpdateParameters.kt b/app/src/main/java/com/github/libretube/extensions/UpdateParameters.kt index 96d2b02ca..a04bc9be6 100644 --- a/app/src/main/java/com/github/libretube/extensions/UpdateParameters.kt +++ b/app/src/main/java/com/github/libretube/extensions/UpdateParameters.kt @@ -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)) 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 ef2be931b..757e8b94b 100644 --- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt @@ -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()!! 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 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 9950b7bee..d894b61fb 100644 --- a/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt @@ -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) 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 4f651c216..1d0a90545 100644 --- a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt @@ -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) } 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 8f8a5e11d..bc8bb2754 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 @@ -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 diff --git a/app/src/main/java/com/github/libretube/ui/adapters/ChaptersAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/ChaptersAdapter.kt index d0135793c..d302cdc0c 100644 --- a/app/src/main/java/com/github/libretube/ui/adapters/ChaptersAdapter.kt +++ b/app/src/main/java/com/github/libretube/ui/adapters/ChaptersAdapter.kt @@ -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, diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/StatsDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/StatsDialog.kt index d18e7afc4..30d88a18e 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/StatsDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/StatsDialog.kt @@ -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( 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 dbeede137..06caacb0f 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 @@ -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) } diff --git a/app/src/main/java/com/github/libretube/ui/listeners/SeekbarPreviewListener.kt b/app/src/main/java/com/github/libretube/ui/listeners/SeekbarPreviewListener.kt index 2045c73ee..28a9b661b 100644 --- a/app/src/main/java/com/github/libretube/ui/listeners/SeekbarPreviewListener.kt +++ b/app/src/main/java/com/github/libretube/ui/listeners/SeekbarPreviewListener.kt @@ -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, private val playerBinding: ExoStyledPlayerControlViewBinding, @@ -59,6 +61,8 @@ class SeekbarPreviewListener( playerBinding.seekbarPreview.alpha = 1f } .start() + + onScrubEnd.invoke(position) } /** 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 9e22c3714..7ff8b2de8 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,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, 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 7b13d98ed..a3ebeb61f 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 @@ -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 } diff --git a/app/src/main/java/com/github/libretube/ui/views/MarkableTimeBar.kt b/app/src/main/java/com/github/libretube/ui/views/MarkableTimeBar.kt index 09d26e6e2..62ff1e429 100644 --- a/app/src/main/java/com/github/libretube/ui/views/MarkableTimeBar.kt +++ b/app/src/main/java/com/github/libretube/ui/views/MarkableTimeBar.kt @@ -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, ) }, ) 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 16a36e00c..ddca94adb 100644 --- a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt +++ b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt @@ -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 { + 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() diff --git a/app/src/main/res/layout/activity_offline_player.xml b/app/src/main/res/layout/activity_offline_player.xml index 963c69bf7..9a552814f 100644 --- a/app/src/main/res/layout/activity_offline_player.xml +++ b/app/src/main/res/layout/activity_offline_player.xml @@ -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"> - + + LibreTube Home Subscriptions Library diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7dbbe21eb..d7b33867b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }