mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-13 13:50:30 +05:30
commit
f6f2d37584
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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" }
|
||||
|
Loading…
Reference in New Issue
Block a user