diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04fdee42..ecc275d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,6 +77,9 @@ dependencies { implementation(libs.androidx.leanback) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.ui) + implementation(libs.androidx.media3.session) implementation(libs.androidx.navigation.fragment) implementation(libs.androidx.navigation.ui) implementation(libs.androidx.paging) @@ -87,8 +90,6 @@ dependencies { kapt(libs.androidx.room.compiler) implementation(libs.androidx.room.ktx) implementation(libs.androidx.swiperefreshlayout) - implementation(libs.exoplayer.core) - implementation(libs.exoplayer.ui) implementation(libs.glide) kapt(libs.glide.compiler) implementation(libs.hilt.android) @@ -97,9 +98,9 @@ dependencies { implementation(libs.material) implementation(libs.timber) - // ExoPlayer FFmpeg extension - implementation(files("libs/extension-ffmpeg-release.aar")) - // MPV implementation(files("libs/libmpv.aar")) + + // Media3 FFmpeg decoder + implementation(files("libs/lib-decoder-ffmpeg-release.aar")) } diff --git a/app/libs/extension-ffmpeg-release.aar b/app/libs/extension-ffmpeg-release.aar deleted file mode 100644 index 5553ef8b..00000000 Binary files a/app/libs/extension-ffmpeg-release.aar and /dev/null differ diff --git a/app/libs/lib-decoder-ffmpeg-release.aar b/app/libs/lib-decoder-ffmpeg-release.aar new file mode 100644 index 00000000..519a00e9 Binary files /dev/null and b/app/libs/lib-decoder-ffmpeg-release.aar differ diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 00000000..bf11943b --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/java/dev/jdtech/jellyfin/BasePlayerActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/BasePlayerActivity.kt index ed736323..ab4df781 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/BasePlayerActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/BasePlayerActivity.kt @@ -5,23 +5,39 @@ import android.view.View import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import androidx.core.view.updatePadding -import com.google.android.exoplayer2.trackselection.MappingTrackSelector +import androidx.media3.exoplayer.trackselection.MappingTrackSelector +import androidx.media3.session.MediaSession import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel abstract class BasePlayerActivity : AppCompatActivity() { abstract val viewModel: PlayerActivityViewModel + lateinit var mediaSession: MediaSession + + override fun onStart() { + super.onStart() + + mediaSession = MediaSession.Builder(this, viewModel.player).build() + } + + override fun onResume() { + super.onResume() + + viewModel.player.playWhenReady = viewModel.playWhenReady + hideSystemUI() + } + override fun onPause() { super.onPause() viewModel.playWhenReady = viewModel.player.playWhenReady == true viewModel.player.playWhenReady = false } - override fun onResume() { - super.onResume() - viewModel.player.playWhenReady = viewModel.playWhenReady - hideSystemUI() + override fun onStop() { + super.onStop() + + mediaSession.release() } @Suppress("DEPRECATION") diff --git a/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 17fa3854..66dc59e3 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -8,10 +8,10 @@ import android.view.WindowManager import android.widget.ImageButton import android.widget.TextView import androidx.activity.viewModels +import androidx.media3.common.C +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.TrackSelectionDialogBuilder import androidx.navigation.navArgs -import com.google.android.exoplayer2.C -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.ui.TrackSelectionDialogBuilder import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment diff --git a/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt b/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt index ed81ec6c..f02a24cc 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt @@ -12,32 +12,31 @@ import android.view.SurfaceHolder import android.view.SurfaceView import android.view.TextureView import androidx.core.content.getSystemService -import com.google.android.exoplayer2.BasePlayer -import com.google.android.exoplayer2.C -import com.google.android.exoplayer2.DeviceInfo -import com.google.android.exoplayer2.ExoPlaybackException -import com.google.android.exoplayer2.Format -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.MediaMetadata -import com.google.android.exoplayer2.PlaybackParameters -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.Player.Commands -import com.google.android.exoplayer2.Timeline -import com.google.android.exoplayer2.Tracks -import com.google.android.exoplayer2.audio.AudioAttributes -import com.google.android.exoplayer2.source.TrackGroup -import com.google.android.exoplayer2.text.Cue -import com.google.android.exoplayer2.text.CueGroup -import com.google.android.exoplayer2.trackselection.TrackSelectionParameters -import com.google.android.exoplayer2.util.Clock -import com.google.android.exoplayer2.util.FlagSet -import com.google.android.exoplayer2.util.ListenerSet -import com.google.android.exoplayer2.util.MimeTypes -import com.google.android.exoplayer2.util.Size -import com.google.android.exoplayer2.util.Util -import com.google.android.exoplayer2.video.VideoSize import dev.jdtech.jellyfin.utils.AppPreferences import `is`.xyz.mpv.MPVLib +import androidx.media3.common.AudioAttributes +import androidx.media3.common.BasePlayer +import androidx.media3.common.C +import androidx.media3.common.DeviceInfo +import androidx.media3.common.FlagSet +import androidx.media3.common.Format +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.MimeTypes +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player +import androidx.media3.common.Player.Commands +import androidx.media3.common.Timeline +import androidx.media3.common.TrackGroup +import androidx.media3.common.TrackSelectionParameters +import androidx.media3.common.Tracks +import androidx.media3.common.VideoSize +import androidx.media3.common.text.CueGroup +import androidx.media3.common.util.Clock +import androidx.media3.common.util.ListenerSet +import androidx.media3.common.util.Size +import androidx.media3.common.util.Util +import androidx.media3.exoplayer.ExoPlaybackException import java.io.File import java.io.FileOutputStream import java.util.concurrent.CopyOnWriteArraySet @@ -177,6 +176,7 @@ class MPVPlayer( var currentMpvTracks: List = emptyList() private var initialCommands = mutableListOf>() private var initialSeekTo: Long = 0L + private var trackSelectionParameters: TrackSelectionParameters = TrackSelectionParameters.Builder(context).build() // mpv events override fun eventProperty(property: String) { @@ -386,13 +386,13 @@ class MPVPlayer( } /** - * Populates a [com.google.android.exoplayer2.Timeline.Window] with data for the window at the specified index. + * Populates a [androidx.media3.common.Timeline.Window] with data for the window at the specified index. * * @param windowIndex The index of the window. - * @param window The [com.google.android.exoplayer2.Timeline.Window] to populate. Must not be null. + * @param window The [androidx.media3.common.Timeline.Window] to populate. Must not be null. * @param defaultPositionProjectionUs A duration into the future that the populated window's * default start position should be projected. - * @return The populated [com.google.android.exoplayer2.Timeline.Window], for convenience. + * @return The populated [androidx.media3.common.Timeline.Window], for convenience. */ override fun getWindow( windowIndex: Int, @@ -427,14 +427,14 @@ class MPVPlayer( } /** - * Populates a [com.google.android.exoplayer2.Timeline.Period] with data for the period at the specified index. + * Populates a [androidx.media3.common.Timeline.Period] with data for the period at the specified index. * * @param periodIndex The index of the period. - * @param period The [com.google.android.exoplayer2.Timeline.Period] to populate. Must not be null. - * @param setIds Whether [com.google.android.exoplayer2.Timeline.Period.id] and [com.google.android.exoplayer2.Timeline.Period.uid] should be populated. If false, + * @param period The [androidx.media3.common.Timeline.Period] to populate. Must not be null. + * @param setIds Whether [androidx.media3.common.Timeline.Period.id] and [androidx.media3.common.Timeline.Period.uid] should be populated. If false, * the fields will be set to null. The caller should pass false for efficiency reasons unless * the fields are required. - * @return The populated [com.google.android.exoplayer2.Timeline.Period], for convenience. + * @return The populated [androidx.media3.common.Timeline.Period], for convenience. */ override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period { return period.set( @@ -447,7 +447,7 @@ class MPVPlayer( } /** - * Returns the index of the period identified by its unique [com.google.android.exoplayer2.Timeline.Period.uid], or [ ][C.INDEX_UNSET] if the period is not in the timeline. + * Returns the index of the period identified by its unique [Timeline.Period.uid], or [ ][C.INDEX_UNSET] if the period is not in the timeline. * * @param uid A unique identifier for a period. * @return The index of the period, or [C.INDEX_UNSET] if the period was not found. @@ -557,12 +557,12 @@ class MPVPlayer( * Clears the playlist and adds the specified [MediaItems][MediaItem]. * * @param mediaItems The new [MediaItems][MediaItem]. - * @param startWindowIndex The window index to start playback from. If [com.google.android.exoplayer2.C.INDEX_UNSET] is + * @param startWindowIndex The window index to start playback from. If [C.INDEX_UNSET] is * passed, the current position is not reset. - * @param startPositionMs The position in milliseconds to start playback from. If [ ][com.google.android.exoplayer2.C.TIME_UNSET] is passed, the default position of the given window is used. In any case, if - * `startWindowIndex` is set to [com.google.android.exoplayer2.C.INDEX_UNSET], this parameter is ignored and the + * @param startPositionMs The position in milliseconds to start playback from. If [ ][C.TIME_UNSET] is passed, the default position of the given window is used. In any case, if + * `startWindowIndex` is set to [C.INDEX_UNSET], this parameter is ignored and the * position is not reset at all. - * @throws com.google.android.exoplayer2.IllegalSeekPositionException If the provided `startWindowIndex` is not within the + * @throws androidx.media3.common.IllegalSeekPositionException If the provided `startWindowIndex` is not within the * bounds of the list of media items. */ override fun setMediaItems( @@ -611,10 +611,10 @@ class MPVPlayer( } /** - * Returns the player's currently available [com.google.android.exoplayer2.Player.Commands]. + * Returns the player's currently available [Commands]. * * - * The returned [com.google.android.exoplayer2.Player.Commands] are not updated when available commands change. Use [ ][com.google.android.exoplayer2.Player.Listener.onAvailableCommandsChanged] to get an update when the available commands + * The returned [Commands] are not updated when available commands change. Use [ ][androidx.media3.common.Player.Listener.onAvailableCommandsChanged] to get an update when the available commands * change. * * @@ -625,8 +625,8 @@ class MPVPlayer( * [.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM] and [.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM] * are unavailable if there is no such [MediaItem]. * - * @return The currently available [com.google.android.exoplayer2.Player.Commands]. - * @see com.google.android.exoplayer2.Player.Listener.onAvailableCommandsChanged + * @return The currently available [Commands]. + * @see androidx.media3.common.Player.Listener.onAvailableCommandsChanged */ override fun getAvailableCommands(): Commands { return Commands.Builder() @@ -682,10 +682,10 @@ class MPVPlayer( } /** - * Returns the current [playback state][com.google.android.exoplayer2.Player.State] of the player. + * Returns the current [playback state][androidx.media3.common.Player.State] of the player. * - * @return The current [playback state][com.google.android.exoplayer2.Player.State]. - * @see com.google.android.exoplayer2.Player.Listener.onPlaybackStateChanged + * @return The current [playback state][androidx.media3.common.Player.State]. + * @see androidx.media3.common.Player.Listener.onPlaybackStateChanged */ override fun getPlaybackState(): Int { return playbackState @@ -694,8 +694,8 @@ class MPVPlayer( /** * Returns the reason why playback is suppressed even though [.getPlayWhenReady] is `true`, or [.PLAYBACK_SUPPRESSION_REASON_NONE] if playback is not suppressed. * - * @return The current [playback suppression reason][com.google.android.exoplayer2.Player.PlaybackSuppressionReason]. - * @see com.google.android.exoplayer2.Player.Listener.onPlaybackSuppressionReasonChanged + * @return The current [playback suppression reason][androidx.media3.common.Player.PlaybackSuppressionReason]. + * @see androidx.media3.common.Player.Listener.onPlaybackSuppressionReasonChanged */ override fun getPlaybackSuppressionReason(): Int { return PLAYBACK_SUPPRESSION_REASON_NONE @@ -703,7 +703,7 @@ class MPVPlayer( /** * Returns the error that caused playback to fail. This is the same error that will have been - * reported via [com.google.android.exoplayer2.Player.Listener.onPlayerError] at the time of failure. It + * reported via [androidx.media3.common.Player.Listener.onPlayerError] at the time of failure. It * can be queried using this method until the player is re-prepared. * * @@ -711,7 +711,7 @@ class MPVPlayer( * [.STATE_IDLE]. * * @return The error, or `null`. - * @see com.google.android.exoplayer2.Player.Listener.onPlayerError + * @see androidx.media3.common.Player.Listener.onPlayerError */ override fun getPlayerError(): ExoPlaybackException? { return null @@ -741,14 +741,14 @@ class MPVPlayer( * Whether playback will proceed when [.getPlaybackState] == [.STATE_READY]. * * @return Whether playback will proceed when ready. - * @see com.google.android.exoplayer2.Player.Listener.onPlayWhenReadyChanged + * @see androidx.media3.common.Player.Listener.onPlayWhenReadyChanged */ override fun getPlayWhenReady(): Boolean { return currentPlayWhenReady } /** - * Sets the [com.google.android.exoplayer2.Player.RepeatMode] to be used for playback. + * Sets the [androidx.media3.common.Player.RepeatMode] to be used for playback. * * @param repeatMode The repeat mode. */ @@ -757,10 +757,10 @@ class MPVPlayer( } /** - * Returns the current [com.google.android.exoplayer2.Player.RepeatMode] used for playback. + * Returns the current [androidx.media3.common.Player.RepeatMode] used for playback. * * @return The current repeat mode. - * @see com.google.android.exoplayer2.Player.Listener.onRepeatModeChanged + * @see androidx.media3.common.Player.Listener.onRepeatModeChanged */ override fun getRepeatMode(): Int { return repeatMode @@ -778,7 +778,7 @@ class MPVPlayer( /** * Returns whether shuffling of windows is enabled. * - * @see com.google.android.exoplayer2.Player.Listener.onShuffleModeEnabledChanged + * @see androidx.media3.common.Player.Listener.onShuffleModeEnabledChanged */ override fun getShuffleModeEnabled(): Boolean { return false @@ -788,7 +788,7 @@ class MPVPlayer( * Whether the player is currently loading the source. * * @return Whether the player is currently loading the source. - * @see com.google.android.exoplayer2.Player.Listener.onIsLoadingChanged + * @see androidx.media3.common.Player.Listener.onIsLoadingChanged */ override fun isLoading(): Boolean { return false @@ -798,9 +798,9 @@ class MPVPlayer( * Seeks to a position specified in milliseconds in the specified window. * * @param windowIndex The index of the window. - * @param positionMs The seek position in the specified window, or [com.google.android.exoplayer2.C.TIME_UNSET] to seek to + * @param positionMs The seek position in the specified window, or [C.TIME_UNSET] to seek to * the window's default position. - * @throws com.google.android.exoplayer2.IllegalSeekPositionException If the player has a non-empty timeline and the provided + * @throws androidx.media3.common.IllegalSeekPositionException If the player has a non-empty timeline and the provided * `windowIndex` is not within the bounds of the current timeline. */ override fun seekTo(windowIndex: Int, positionMs: Long) { @@ -866,7 +866,7 @@ class MPVPlayer( * player to the default, which means there is no speed or pitch adjustment. * * - * Playback parameters changes may cause the player to buffer. [ ][com.google.android.exoplayer2.Player.Listener.onPlaybackParametersChanged] will be called whenever the currently + * Playback parameters changes may cause the player to buffer. [ ][androidx.media3.common.Player.Listener.onPlaybackParametersChanged] will be called whenever the currently * active playback parameters change. * * @param playbackParameters The playback parameters. @@ -880,7 +880,7 @@ class MPVPlayer( /** * Returns the currently active playback parameters. * - * @see com.google.android.exoplayer2.Player.Listener.onPlaybackParametersChanged + * @see androidx.media3.common.Player.Listener.onPlaybackParametersChanged */ override fun getPlaybackParameters(): PlaybackParameters { return playbackParameters @@ -914,11 +914,11 @@ class MPVPlayer( } override fun getTrackSelectionParameters(): TrackSelectionParameters { - TODO("Not yet implemented") + return trackSelectionParameters } override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) { - TODO("Not yet implemented") + trackSelectionParameters = parameters } /** @@ -927,15 +927,15 @@ class MPVPlayer( * * * This [MediaMetadata] is a combination of the [MediaItem.mediaMetadata] and the - * static and dynamic metadata sourced from [com.google.android.exoplayer2.Player.Listener.onMediaMetadataChanged] and - * [com.google.android.exoplayer2.metadata.MetadataOutput.onMetadata]. + * static and dynamic metadata sourced from [androidx.media3.common.Player.Listener.onMediaMetadataChanged] and + * [androidx.media3.exoplayer.metadata.MetadataOutput.onMetadata]. */ override fun getMediaMetadata(): MediaMetadata { return MediaMetadata.EMPTY } override fun getPlaylistMetadata(): MediaMetadata { - TODO("Not yet implemented") + return MediaMetadata.EMPTY } override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) { @@ -945,7 +945,7 @@ class MPVPlayer( /** * Returns the current [Timeline]. Never null, but may be empty. * - * @see com.google.android.exoplayer2.Player.Listener.onTimelineChanged + * @see androidx.media3.common.Player.Listener.onTimelineChanged */ override fun getCurrentTimeline(): Timeline { return timeline @@ -961,7 +961,7 @@ class MPVPlayer( } /** - * Returns the duration of the current content window or ad in milliseconds, or [ ][com.google.android.exoplayer2.C.TIME_UNSET] if the duration is not known. + * Returns the duration of the current content window or ad in milliseconds, or [ ][C.TIME_UNSET] if the duration is not known. */ override fun getDuration(): Long { return timeline.getWindow(currentMediaItemIndex, window).durationMs @@ -999,7 +999,7 @@ class MPVPlayer( /** * If [.isPlayingAd] returns true, returns the index of the ad group in the period - * currently being played. Returns [com.google.android.exoplayer2.C.INDEX_UNSET] otherwise. + * currently being played. Returns [C.INDEX_UNSET] otherwise. */ override fun getCurrentAdGroupIndex(): Int { return C.INDEX_UNSET @@ -1007,7 +1007,7 @@ class MPVPlayer( /** * If [.isPlayingAd] returns true, returns the index of the ad in its ad group. Returns - * [com.google.android.exoplayer2.C.INDEX_UNSET] otherwise. + * [C.INDEX_UNSET] otherwise. */ override fun getCurrentAdIndexInAdGroup(): Int { return C.INDEX_UNSET @@ -1051,7 +1051,7 @@ class MPVPlayer( * @return The linear gain applied to all audio channels. */ override fun getVolume(): Float { - TODO("Not yet implemented") + return MPVLib.getPropertyInt("volume") / 100F } /** @@ -1154,7 +1154,7 @@ class MPVPlayer( * The video's width and height are `0` if there is no video or its size has not been * determined yet. * - * @see com.google.android.exoplayer2.Player.Listener.onVideoSizeChanged + * @see androidx.media3.common.Player.Listener.onVideoSizeChanged */ override fun getVideoSize(): VideoSize { return VideoSize.UNKNOWN @@ -1169,14 +1169,14 @@ class MPVPlayer( } } - /** Returns the current [Cues][Cue]. This list may be empty. */ + /** Returns the current [CueGroup]. This list may be empty. */ override fun getCurrentCues(): CueGroup { - TODO("Not yet implemented") + return CueGroup(emptyList(), 0) } /** Gets the device information. */ override fun getDeviceInfo(): DeviceInfo { - TODO("Not yet implemented") + return DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, 0, 100) } /** @@ -1184,7 +1184,7 @@ class MPVPlayer( * * * For devices with [local playback][DeviceInfo.PLAYBACK_TYPE_LOCAL], the volume returned - * by this method varies according to the current [stream type][com.google.android.exoplayer2.C.StreamType]. The stream + * by this method varies according to the current [stream type][C.StreamType]. The stream * type is determined by [AudioAttributes.usage] which can be converted to stream type with * [Util.getStreamTypeForAudioUsage]. * @@ -1193,12 +1193,12 @@ class MPVPlayer( * remote device is returned. */ override fun getDeviceVolume(): Int { - TODO("Not yet implemented") + return MPVLib.getPropertyInt("volume") } /** Gets whether the device is muted or not. */ override fun isDeviceMuted(): Boolean { - TODO("Not yet implemented") + return MPVLib.getPropertyBoolean("mute") } /** diff --git a/app/src/main/java/dev/jdtech/jellyfin/tv/TvPlayerActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/tv/TvPlayerActivity.kt index 7b55e32b..6029e9b5 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/tv/TvPlayerActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/tv/TvPlayerActivity.kt @@ -42,7 +42,7 @@ internal class TvPlayerActivity : BasePlayerActivity() { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) binding.playerView.player = viewModel.player - val playerControls = binding.playerView.findViewById(R.id.tv_player_controls) + val playerControls = binding.playerView.findViewById(R.id.player_controls) configureInsets(playerControls) bind() @@ -51,7 +51,7 @@ internal class TvPlayerActivity : BasePlayerActivity() { } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - return if (!binding.playerView.isControllerVisible) { + return if (!binding.playerView.isControllerFullyVisible) { binding.playerView.showController() true } else { diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/AppPreferences.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/AppPreferences.kt index 850a69d9..9fd2f394 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/AppPreferences.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/AppPreferences.kt @@ -3,8 +3,8 @@ package dev.jdtech.jellyfin.utils import android.content.SharedPreferences import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE import androidx.core.content.edit -import com.google.android.exoplayer2.C.DEFAULT_SEEK_BACK_INCREMENT_MS -import com.google.android.exoplayer2.C.DEFAULT_SEEK_FORWARD_INCREMENT_MS +import androidx.media3.common.C.DEFAULT_SEEK_BACK_INCREMENT_MS +import androidx.media3.common.C.DEFAULT_SEEK_FORWARD_INCREMENT_MS import javax.inject.Inject class AppPreferences @@ -26,6 +26,8 @@ constructor( val dynamicColors = sharedPreferences.getBoolean(Constants.PREF_DYNAMIC_COLORS, true) // Player + val displayExtendedTitle = sharedPreferences.getBoolean(Constants.PREF_DISPLAY_EXTENDED_TITLE, false) + val playerGestures = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES, true) val playerGesturesVB = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_VB, true) val playerGesturesZoom = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_ZOOM, true) diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt index 2281c16f..ed5d4332 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/Constants.kt @@ -9,6 +9,7 @@ object Constants { // pref const val PREF_CURRENT_SERVER = "pref_current_server" + const val PREF_DISPLAY_EXTENDED_TITLE = "pref_player_display_extended_title" const val PREF_PLAYER_GESTURES = "pref_player_gestures" const val PREF_PLAYER_GESTURES_VB = "pref_player_gestures_vb" const val PREF_PLAYER_GESTURES_ZOOM = "pref_player_gestures_zoom" diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt index f141e4ca..ef169a12 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt @@ -10,8 +10,8 @@ import android.view.ScaleGestureDetector import android.view.View import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout -import com.google.android.exoplayer2.ui.StyledPlayerView +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView import dev.jdtech.jellyfin.PlayerActivity import dev.jdtech.jellyfin.mpv.MPVPlayer import kotlin.math.abs @@ -20,7 +20,7 @@ import timber.log.Timber class PlayerGestureHelper( private val appPreferences: AppPreferences, private val activity: PlayerActivity, - private val playerView: StyledPlayerView, + private val playerView: PlayerView, private val audioManager: AudioManager ) { /** diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 2d9538ec..fc18dac2 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -7,14 +7,14 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.preference.PreferenceManager -import com.google.android.exoplayer2.C -import com.google.android.exoplayer2.DefaultRenderersFactory -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.audio.AudioAttributes -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector +import androidx.media3.common.AudioAttributes +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.Player +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.database.DownloadDatabaseDao import dev.jdtech.jellyfin.models.PlayerItem @@ -36,7 +36,7 @@ constructor( application: Application, private val jellyfinRepository: JellyfinRepository, private val downloadDatabase: DownloadDatabaseDao, - appPreferences: AppPreferences, + private val appPreferences: AppPreferences, ) : ViewModel(), Player.Listener { val player: Player @@ -63,8 +63,6 @@ constructor( var playbackSpeed: Float = 1f var disableSubtitle: Boolean = false - private val sp = PreferenceManager.getDefaultSharedPreferences(application) - init { if (appPreferences.playerMpv) { player = MPVPlayer( @@ -126,6 +124,11 @@ constructor( MediaItem.Builder() .setMediaId(item.itemId.toString()) .setUri(streamUrl) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(item.name) + .build() + ) .setSubtitleConfigurations(mediaSubtitles) .build() mediaItems.add(mediaItem) @@ -135,8 +138,7 @@ constructor( } player.setMediaItems(mediaItems, currentMediaItemIndex, items.getOrNull(currentMediaItemIndex)?.playbackPosition ?: C.TIME_UNSET) - val useMpv = sp.getBoolean("mpv_player", false) - if (!useMpv || !playFromDownloads) + if (!appPreferences.playerMpv || !playFromDownloads) player.prepare() // TODO: This line causes a crash when playing from downloads with MPV player.play() pollPosition(player) @@ -196,10 +198,7 @@ constructor( try { for (item in items) { if (item.itemId.toString() == (player.currentMediaItem?.mediaId ?: "")) { - if (sp.getBoolean( - "display_extended_title", - false - ) && item.parentIndexNumber != null && item.indexNumber != null && item.name != null + if (appPreferences.displayExtendedTitle && item.parentIndexNumber != null && item.indexNumber != null && item.name != null ) _currentItemTitle.value = "S${item.parentIndexNumber}:E${item.indexNumber} - ${item.name}" diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 3dd25c14..5e5bcc29 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -5,7 +5,7 @@ import android.net.Uri import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.android.exoplayer2.util.MimeTypes +import androidx.media3.common.MimeTypes import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.database.DownloadDatabaseDao diff --git a/app/src/main/res/layout-television/tv_player_controls.xml b/app/src/main/res/layout-television/exo_player_control_view.xml similarity index 98% rename from app/src/main/res/layout-television/tv_player_controls.xml rename to app/src/main/res/layout-television/exo_player_control_view.xml index 70ab6187..8dcb886c 100644 --- a/app/src/main/res/layout-television/tv_player_controls.xml +++ b/app/src/main/res/layout-television/exo_player_control_view.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/tv_player_controls" + android:id="@+id/player_controls" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/player_background" @@ -66,7 +66,7 @@ app:layout_constraintStart_toEndOf="@id/btn_audio_track" /> - - - - + app:show_buffering="always" /> - + tools:context=".PlayerActivity"> - + app:animation_enabled="false" + app:show_buffering="always" /> diff --git a/app/src/main/res/layout/exo_styled_player_control_view.xml b/app/src/main/res/layout/exo_player_control_view.xml similarity index 99% rename from app/src/main/res/layout/exo_styled_player_control_view.xml rename to app/src/main/res/layout/exo_player_control_view.xml index f2baaef7..33ad0726 100644 --- a/app/src/main/res/layout/exo_styled_player_control_view.xml +++ b/app/src/main/res/layout/exo_player_control_view.xml @@ -212,7 +212,7 @@ - - - + - diff --git a/app/src/main/res/xml/fragment_settings_player.xml b/app/src/main/res/xml/fragment_settings_player.xml index 313cceac..0a3b85c8 100644 --- a/app/src/main/res/xml/fragment_settings_player.xml +++ b/app/src/main/res/xml/fragment_settings_player.xml @@ -1,7 +1,7 @@ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 011d46b8..92c475ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ androidx-constraintlayout = "2.1.4" androidx-core = "1.9.0" androidx-leanback = "1.2.0-alpha02" androidx-lifecycle = "2.5.1" +androidx-media3 = "1.0.0-beta03" androidx-navigation = "2.5.3" androidx-paging = "3.1.1" androidx-preference = "1.2.0" @@ -14,7 +15,6 @@ androidx-recyclerview = "1.2.1" androidx-recyclerview-selection = "1.1.0" androidx-room = "2.4.3" androidx-swiperefreshlayout = "1.1.0" -exoplayer = "2.18.2" glide = "4.14.2" hilt = "2.44.2" jellyfin = "1.3.7" @@ -33,6 +33,9 @@ androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-cor androidx-leanback = { module = "androidx.leanback:leanback", version.ref = "androidx-leanback" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } +androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } +androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" } +androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "androidx-media3" } androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" } androidx-navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" } androidx-paging = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidx-paging" } @@ -43,8 +46,6 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = " androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" } -exoplayer-core = { module = "com.google.android.exoplayer:exoplayer-core", version.ref = "exoplayer" } -exoplayer-ui = { module = "com.google.android.exoplayer:exoplayer-ui", version.ref = "exoplayer" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }