Merge pull request #5934 from Bnyro/master

feat: preference to force maximum audio quality
This commit is contained in:
Bnyro 2024-04-23 15:20:45 +02:00 committed by GitHub
commit faecb1dceb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 89 additions and 44 deletions

View File

@ -1,14 +1,15 @@
package com.github.libretube.helpers package com.github.libretube.helpers
import android.app.Activity import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.view.accessibility.CaptioningManager import android.view.accessibility.CaptioningManager
import android.widget.Toast import android.widget.Toast
import androidx.annotation.OptIn
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.app.RemoteActionCompat import androidx.core.app.RemoteActionCompat
@ -19,7 +20,9 @@ import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.PlaybackParameters import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.cronet.CronetDataSource import androidx.media3.datasource.cronet.CronetDataSource
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
@ -45,14 +48,14 @@ import com.github.libretube.extensions.updateParameters
import com.github.libretube.obj.VideoStats import com.github.libretube.obj.VideoStats
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 java.util.Locale
import java.util.concurrent.Executors
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
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 java.util.Locale
import java.util.concurrent.Executors
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
object PlayerHelper { object PlayerHelper {
private const val ACTION_MEDIA_CONTROL = "media_control" private const val ACTION_MEDIA_CONTROL = "media_control"
@ -99,7 +102,7 @@ object PlayerHelper {
/** /**
* Get the system's default captions style * Get the system's default captions style
*/ */
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @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) {
@ -339,15 +342,15 @@ object PlayerHelper {
val playAutomatically: Boolean val playAutomatically: Boolean
get() = PreferenceHelper.getBoolean( get() = PreferenceHelper.getBoolean(
PreferenceKeys.PLAY_AUTOMATICALLY, PreferenceKeys.PLAY_AUTOMATICALLY,
true true
) )
val disablePipedProxy: Boolean val disablePipedProxy: Boolean
get() = PreferenceHelper.getBoolean( get() = PreferenceHelper.getBoolean(
PreferenceKeys.DISABLE_VIDEO_IMAGE_PROXY, PreferenceKeys.DISABLE_VIDEO_IMAGE_PROXY,
false false
) )
fun shouldPlayNextVideo(isPlaylist: Boolean = false): Boolean { fun shouldPlayNextVideo(isPlaylist: Boolean = false): Boolean {
// if there is no next video, it obviously should not be played // if there is no next video, it obviously should not be played
@ -356,11 +359,11 @@ object PlayerHelper {
} }
return autoPlayEnabled || ( return autoPlayEnabled || (
isPlaylist && PreferenceHelper.getBoolean( isPlaylist && PreferenceHelper.getBoolean(
PreferenceKeys.AUTOPLAY_PLAYLISTS, PreferenceKeys.AUTOPLAY_PLAYLISTS,
false false
) )
) )
} }
private val handleAudioFocus private val handleAudioFocus
@ -382,19 +385,44 @@ object PlayerHelper {
.toIntOrNull() .toIntOrNull()
} }
/** @OptIn(UnstableApi::class)
* Apply the preferred audio quality: auto or worst fun setPreferredAudioQuality(
*/ context: Context,
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) player: Player,
fun applyPreferredAudioQuality(context: Context, trackSelector: DefaultTrackSelector) { trackSelector: DefaultTrackSelector
) {
val prefKey = if (NetworkHelper.isNetworkMetered(context)) { val prefKey = if (NetworkHelper.isNetworkMetered(context)) {
PreferenceKeys.PLAYER_AUDIO_QUALITY_MOBILE PreferenceKeys.PLAYER_AUDIO_QUALITY_MOBILE
} else { } else {
PreferenceKeys.PLAYER_AUDIO_QUALITY PreferenceKeys.PLAYER_AUDIO_QUALITY
} }
when (PreferenceHelper.getString(prefKey, "auto")) {
"worst" -> trackSelector.updateParameters { val qualityPref = PreferenceHelper.getString(prefKey, "auto")
setMaxAudioBitrate(1) if (qualityPref == "auto") return
// multiple groups due to different possible audio languages
val audioTrackGroups = player.currentTracks.groups
.filter { it.type == C.TRACK_TYPE_AUDIO }
for (audioTrackGroup in audioTrackGroups) {
// find the best audio bitrate
val streams = (0 until audioTrackGroup.length).map { index ->
index to audioTrackGroup.getTrackFormat(index).bitrate
}
// if no bitrate info is available, fallback to the
// - first stream for lowest quality
// - last stream for highest quality
val streamIndex = if (qualityPref == "best") {
streams.maxByOrNull { it.second }?.takeIf { it.second != -1 }?.first
?: (streams.size - 1)
} else {
streams.minByOrNull { it.second }?.takeIf { it.second != -1 }?.first ?: 0
}
trackSelector.updateParameters {
val override = TrackSelectionOverride(audioTrackGroup.mediaTrackGroup, streamIndex)
setOverrideForType(override)
} }
} }
} }
@ -412,7 +440,8 @@ object PlayerHelper {
val intent = Intent(getIntentActionName(activity)) val intent = Intent(getIntentActionName(activity))
.setPackage(activity.packageName) .setPackage(activity.packageName)
.putExtra(CONTROL_TYPE, event) .putExtra(CONTROL_TYPE, event)
val pendingIntent = PendingIntentCompat.getBroadcast(activity, event.ordinal, intent, 0, false)!! val pendingIntent =
PendingIntentCompat.getBroadcast(activity, event.ordinal, intent, 0, false)!!
val text = activity.getString(title) val text = activity.getString(title)
val icon = IconCompat.createWithResource(activity, id) val icon = IconCompat.createWithResource(activity, id)
@ -468,7 +497,7 @@ object PlayerHelper {
/** /**
* Create a basic player, that is used for all types of playback situations inside the app * Create a basic player, that is used for all types of playback situations inside the app
*/ */
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @OptIn(androidx.media3.common.util.UnstableApi::class)
fun createPlayer( fun createPlayer(
context: Context, context: Context,
trackSelector: DefaultTrackSelector, trackSelector: DefaultTrackSelector,
@ -501,7 +530,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) @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
@ -518,7 +547,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) @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
@ -767,12 +796,12 @@ object PlayerHelper {
*/ */
fun haveAudioTrackRoleFlagSet(@C.RoleFlags roleFlags: Int): Boolean { fun haveAudioTrackRoleFlagSet(@C.RoleFlags roleFlags: Int): Boolean {
return isFlagSet(roleFlags, C.ROLE_FLAG_DESCRIBES_VIDEO) || return isFlagSet(roleFlags, C.ROLE_FLAG_DESCRIBES_VIDEO) ||
isFlagSet(roleFlags, C.ROLE_FLAG_DUB) || isFlagSet(roleFlags, C.ROLE_FLAG_DUB) ||
isFlagSet(roleFlags, C.ROLE_FLAG_MAIN) || isFlagSet(roleFlags, C.ROLE_FLAG_MAIN) ||
isFlagSet(roleFlags, C.ROLE_FLAG_ALTERNATE) isFlagSet(roleFlags, C.ROLE_FLAG_ALTERNATE)
} }
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @OptIn(androidx.media3.common.util.UnstableApi::class)
fun getVideoStats(player: ExoPlayer, videoId: String): VideoStats { fun getVideoStats(player: ExoPlayer, videoId: String): VideoStats {
val videoInfo = "${player.videoFormat?.codecs.orEmpty()} ${ val videoInfo = "${player.videoFormat?.codecs.orEmpty()} ${
TextUtils.formatBitrate( TextUtils.formatBitrate(
@ -812,12 +841,12 @@ object PlayerHelper {
} }
PlayerEvent.Forward -> { PlayerEvent.Forward -> {
player.seekBy(PlayerHelper.seekIncrement) player.seekBy(seekIncrement)
true true
} }
PlayerEvent.Rewind -> { PlayerEvent.Rewind -> {
player.seekBy(-PlayerHelper.seekIncrement) player.seekBy(-seekIncrement)
true true
} }

View File

@ -80,6 +80,7 @@ class OnlinePlayerService : LifecycleService() {
* The [ExoPlayer] player. Followed tutorial [here](https://developer.android.com/codelabs/exoplayer-intro) * The [ExoPlayer] player. Followed tutorial [here](https://developer.android.com/codelabs/exoplayer-intro)
*/ */
var player: ExoPlayer? = null var player: ExoPlayer? = null
private var trackSelector: DefaultTrackSelector? = null
private var isTransitioning = true private var isTransitioning = true
/** /**
@ -163,6 +164,14 @@ class OnlinePlayerService : LifecycleService() {
).show() ).show()
} }
} }
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
PlayerHelper.setPreferredAudioQuality(this@OnlinePlayerService, player, trackSelector ?: return)
}
}
} }
private val playerActionReceiver = object : BroadcastReceiver() { private val playerActionReceiver = object : BroadcastReceiver() {
@ -321,13 +330,12 @@ class OnlinePlayerService : LifecycleService() {
private fun initializePlayer() { private fun initializePlayer() {
if (player != null) return if (player != null) return
val trackSelector = DefaultTrackSelector(this) trackSelector = DefaultTrackSelector(this)
PlayerHelper.applyPreferredAudioQuality(this, trackSelector) trackSelector!!.updateParameters {
trackSelector.updateParameters {
setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true)
} }
player = PlayerHelper.createPlayer(this, trackSelector, true) player = PlayerHelper.createPlayer(this, trackSelector!!, true)
// prevent android from putting LibreTube to sleep when locked // prevent android from putting LibreTube to sleep when locked
player!!.setWakeMode(WAKE_MODE_NETWORK) player!!.setWakeMode(WAKE_MODE_NETWORK)

View File

@ -273,6 +273,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
override fun onEvents(player: Player, events: Player.Events) { override fun onEvents(player: Player, events: Player.Events) {
updateDisplayedDuration() updateDisplayedDuration()
super.onEvents(player, events) super.onEvents(player, events)
if (events.containsAny( if (events.containsAny(
Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED,
Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_IS_PLAYING_CHANGED,
@ -281,6 +282,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
) { ) {
updatePlayPauseButton() updatePlayPauseButton()
} }
if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
PlayerHelper.setPreferredAudioQuality(requireContext(), exoPlayer, trackSelector)
}
} }
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
@ -586,7 +591,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
playOnBackground() playOnBackground()
} }
binding.relPlayerPip.isVisible = PictureInPictureCompat.isPictureInPictureAvailable(requireContext()) binding.relPlayerPip.isVisible =
PictureInPictureCompat.isPictureInPictureAvailable(requireContext())
binding.relPlayerPip.setOnClickListener { binding.relPlayerPip.setOnClickListener {
PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams) PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams)
@ -916,7 +922,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
val videoStream = streams.videoStreams.firstOrNull() val videoStream = streams.videoStreams.firstOrNull()
isShort = PlayingQueue.getCurrent()?.isShort == true || isShort = PlayingQueue.getCurrent()?.isShort == true ||
(videoStream?.height ?: 0) > (videoStream?.width ?: 0) (videoStream?.height ?: 0) > (videoStream?.width ?: 0)
PlayingQueue.setOnQueueTapListener { streamItem -> PlayingQueue.setOnQueueTapListener { streamItem ->
streamItem.url?.toID()?.let { playNextVideo(it) } streamItem.url?.toID()?.let { playNextVideo(it) }
@ -952,7 +958,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
if (binding.playerMotionLayout.progress != 1.0f) { if (binding.playerMotionLayout.progress != 1.0f) {
// show controllers when not in picture in picture mode // show controllers when not in picture in picture mode
val inPipMode = PlayerHelper.pipEnabled && val inPipMode = PlayerHelper.pipEnabled &&
PictureInPictureCompat.isInPictureInPictureMode(requireActivity()) PictureInPictureCompat.isInPictureInPictureMode(requireActivity())
if (!inPipMode) { if (!inPipMode) {
binding.player.useController = true binding.player.useController = true
} }
@ -1349,7 +1355,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
this.setPreferredVideoMimeType(mimeType) this.setPreferredVideoMimeType(mimeType)
} }
} }
PlayerHelper.applyPreferredAudioQuality(requireContext(), trackSelector)
} }
/** /**
@ -1570,7 +1575,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
private fun shouldStartPiP(): Boolean { private fun shouldStartPiP(): Boolean {
return shouldUsePip() && exoPlayer.isPlaying && return shouldUsePip() && exoPlayer.isPlaying &&
!BackgroundHelper.isBackgroundServiceRunning(requireContext()) !BackgroundHelper.isBackgroundServiceRunning(requireContext())
} }
private fun killPlayerFragment() { private fun killPlayerFragment() {

View File

@ -265,11 +265,13 @@
<string-array name="audioQuality"> <string-array name="audioQuality">
<item>@string/auto</item> <item>@string/auto</item>
<item>@string/worst_quality</item> <item>@string/worst_quality</item>
<item>@string/best_quality</item>
</string-array> </string-array>
<string-array name="audioQualityValues"> <string-array name="audioQualityValues">
<item>auto</item> <item>auto</item>
<item>worst</item> <item>worst</item>
<item>best</item>
</string-array> </string-array>
<string-array name="audioQualityBitrates"> <string-array name="audioQualityBitrates">

View File

@ -234,6 +234,7 @@
<string name="playerAudioFormat">Audio format for player</string> <string name="playerAudioFormat">Audio format for player</string>
<string name="playerAudioQuality">Audio quality</string> <string name="playerAudioQuality">Audio quality</string>
<string name="worst_quality">Worst</string> <string name="worst_quality">Worst</string>
<string name="best_quality">Best</string>
<string name="default_subtitle_language">Subtitle language</string> <string name="default_subtitle_language">Subtitle language</string>
<string name="notifications">Notifications</string> <string name="notifications">Notifications</string>
<string name="notify_new_streams">Show notifications for new streams</string> <string name="notify_new_streams">Show notifications for new streams</string>