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 9ad6733c3c..3d5a987129 100644 --- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt @@ -1,14 +1,15 @@ package com.github.libretube.helpers import android.app.Activity -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.net.Uri import android.util.Base64 +import android.util.Log import android.view.accessibility.CaptioningManager import android.widget.Toast +import androidx.annotation.OptIn import androidx.annotation.StringRes import androidx.core.app.PendingIntentCompat import androidx.core.app.RemoteActionCompat @@ -19,7 +20,9 @@ import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player +import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks +import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.cronet.CronetDataSource 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.util.PlayingQueue 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.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.util.Locale +import java.util.concurrent.Executors +import kotlin.math.absoluteValue +import kotlin.math.roundToInt object PlayerHelper { private const val ACTION_MEDIA_CONTROL = "media_control" @@ -99,7 +102,7 @@ object PlayerHelper { /** * 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 { val captioningManager = context.getSystemService()!! return if (!captioningManager.isEnabled) { @@ -339,15 +342,15 @@ object PlayerHelper { val playAutomatically: Boolean get() = PreferenceHelper.getBoolean( - PreferenceKeys.PLAY_AUTOMATICALLY, - true - ) + PreferenceKeys.PLAY_AUTOMATICALLY, + true + ) val disablePipedProxy: Boolean get() = PreferenceHelper.getBoolean( - PreferenceKeys.DISABLE_VIDEO_IMAGE_PROXY, - false - ) + PreferenceKeys.DISABLE_VIDEO_IMAGE_PROXY, + false + ) fun shouldPlayNextVideo(isPlaylist: Boolean = false): Boolean { // if there is no next video, it obviously should not be played @@ -356,11 +359,11 @@ object PlayerHelper { } return autoPlayEnabled || ( - isPlaylist && PreferenceHelper.getBoolean( - PreferenceKeys.AUTOPLAY_PLAYLISTS, - false - ) - ) + isPlaylist && PreferenceHelper.getBoolean( + PreferenceKeys.AUTOPLAY_PLAYLISTS, + false + ) + ) } private val handleAudioFocus @@ -382,19 +385,44 @@ object PlayerHelper { .toIntOrNull() } - /** - * Apply the preferred audio quality: auto or worst - */ - @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) - fun applyPreferredAudioQuality(context: Context, trackSelector: DefaultTrackSelector) { + @OptIn(UnstableApi::class) + fun setPreferredAudioQuality( + context: Context, + player: Player, + trackSelector: DefaultTrackSelector + ) { val prefKey = if (NetworkHelper.isNetworkMetered(context)) { PreferenceKeys.PLAYER_AUDIO_QUALITY_MOBILE } else { PreferenceKeys.PLAYER_AUDIO_QUALITY } - when (PreferenceHelper.getString(prefKey, "auto")) { - "worst" -> trackSelector.updateParameters { - setMaxAudioBitrate(1) + + val qualityPref = PreferenceHelper.getString(prefKey, "auto") + 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)) .setPackage(activity.packageName) .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 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 */ - @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) + @OptIn(androidx.media3.common.util.UnstableApi::class) fun createPlayer( context: Context, trackSelector: DefaultTrackSelector, @@ -501,7 +530,7 @@ object PlayerHelper { /** * 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 { return DefaultLoadControl.Builder() // cache the last three minutes @@ -518,7 +547,7 @@ object PlayerHelper { /** * 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 { skipSilenceEnabled = skipSilence val speed = if (isBackgroundMode) backgroundSpeed else playbackSpeed @@ -767,12 +796,12 @@ object PlayerHelper { */ fun haveAudioTrackRoleFlagSet(@C.RoleFlags roleFlags: Int): Boolean { return isFlagSet(roleFlags, C.ROLE_FLAG_DESCRIBES_VIDEO) || - isFlagSet(roleFlags, C.ROLE_FLAG_DUB) || - isFlagSet(roleFlags, C.ROLE_FLAG_MAIN) || - isFlagSet(roleFlags, C.ROLE_FLAG_ALTERNATE) + isFlagSet(roleFlags, C.ROLE_FLAG_DUB) || + isFlagSet(roleFlags, C.ROLE_FLAG_MAIN) || + 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 { val videoInfo = "${player.videoFormat?.codecs.orEmpty()} ${ TextUtils.formatBitrate( @@ -812,12 +841,12 @@ object PlayerHelper { } PlayerEvent.Forward -> { - player.seekBy(PlayerHelper.seekIncrement) + player.seekBy(seekIncrement) true } PlayerEvent.Rewind -> { - player.seekBy(-PlayerHelper.seekIncrement) + player.seekBy(-seekIncrement) true } 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 f87baf260a..a024b1e04b 100644 --- a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt @@ -80,6 +80,7 @@ class OnlinePlayerService : LifecycleService() { * The [ExoPlayer] player. Followed tutorial [here](https://developer.android.com/codelabs/exoplayer-intro) */ var player: ExoPlayer? = null + private var trackSelector: DefaultTrackSelector? = null private var isTransitioning = true /** @@ -163,6 +164,14 @@ class OnlinePlayerService : LifecycleService() { ).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() { @@ -321,13 +330,12 @@ class OnlinePlayerService : LifecycleService() { private fun initializePlayer() { if (player != null) return - val trackSelector = DefaultTrackSelector(this) - PlayerHelper.applyPreferredAudioQuality(this, trackSelector) - trackSelector.updateParameters { + trackSelector = DefaultTrackSelector(this) + trackSelector!!.updateParameters { 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 player!!.setWakeMode(WAKE_MODE_NETWORK) 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 a0b52ade8a..22a094468c 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 @@ -273,6 +273,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { override fun onEvents(player: Player, events: Player.Events) { updateDisplayedDuration() super.onEvents(player, events) + if (events.containsAny( Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_IS_PLAYING_CHANGED, @@ -281,6 +282,10 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { ) { updatePlayPauseButton() } + + if (events.contains(Player.EVENT_TRACKS_CHANGED)) { + PlayerHelper.setPreferredAudioQuality(requireContext(), exoPlayer, trackSelector) + } } override fun onPlaybackStateChanged(playbackState: Int) { @@ -586,7 +591,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { playOnBackground() } - binding.relPlayerPip.isVisible = PictureInPictureCompat.isPictureInPictureAvailable(requireContext()) + binding.relPlayerPip.isVisible = + PictureInPictureCompat.isPictureInPictureAvailable(requireContext()) binding.relPlayerPip.setOnClickListener { PictureInPictureCompat.enterPictureInPictureMode(requireActivity(), pipParams) @@ -916,7 +922,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { val videoStream = streams.videoStreams.firstOrNull() isShort = PlayingQueue.getCurrent()?.isShort == true || - (videoStream?.height ?: 0) > (videoStream?.width ?: 0) + (videoStream?.height ?: 0) > (videoStream?.width ?: 0) PlayingQueue.setOnQueueTapListener { streamItem -> streamItem.url?.toID()?.let { playNextVideo(it) } @@ -952,7 +958,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { if (binding.playerMotionLayout.progress != 1.0f) { // show controllers when not in picture in picture mode val inPipMode = PlayerHelper.pipEnabled && - PictureInPictureCompat.isInPictureInPictureMode(requireActivity()) + PictureInPictureCompat.isInPictureInPictureMode(requireActivity()) if (!inPipMode) { binding.player.useController = true } @@ -1349,7 +1355,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { this.setPreferredVideoMimeType(mimeType) } } - PlayerHelper.applyPreferredAudioQuality(requireContext(), trackSelector) } /** @@ -1570,7 +1575,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { private fun shouldStartPiP(): Boolean { return shouldUsePip() && exoPlayer.isPlaying && - !BackgroundHelper.isBackgroundServiceRunning(requireContext()) + !BackgroundHelper.isBackgroundServiceRunning(requireContext()) } private fun killPlayerFragment() { diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index e549dba591..b4401bd31a 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -265,11 +265,13 @@ @string/auto @string/worst_quality + @string/best_quality auto worst + best diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38c10e747b..0d1bc7541b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -234,6 +234,7 @@ Audio format for player Audio quality Worst + Best Subtitle language Notifications Show notifications for new streams