diff --git a/README.md b/README.md index 4d3bc8d6..adefe527 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Home | Library | Movie | Season | Episode - ExoPlayer - Video codecs: H.263, H.264, H.265, VP8, VP9, AV1 - Support depends on Android device - - Audio codecs: Vorbis, Opus, FLAC, ALAC, PCM ยต-law, PCM A-law, MP1, MP2, MP3, AMR-NB, AMR-WB, AAC, AC-3, E-AC-3, DTS, DTS-HD, TrueHD + - Audio codecs: Vorbis, Opus, FLAC, ALAC, PCM, MP3, AMR-NB, AMR-WB, AAC, AC-3, E-AC-3, DTS, DTS-HD, TrueHD - Support provided by ExoPlayer FFmpeg extension - Subtitle codecs: SRT, VTT, SSA/ASS, PGSSUB - SSA/ASS has limited styling support see [this issue](https://github.com/google/ExoPlayer/issues/8435) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a3d4356b..9392d99e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -109,7 +109,7 @@ dependencies { kapt("com.google.dagger:hilt-compiler:$hiltVersion") // ExoPlayer - val exoplayerVersion = "2.17.1" + val exoplayerVersion = "2.18.0" implementation("com.google.android.exoplayer:exoplayer-core:$exoplayerVersion") implementation("com.google.android.exoplayer:exoplayer-ui:$exoplayerVersion") implementation(files("libs/extension-ffmpeg-release.aar")) diff --git a/app/libs/extension-ffmpeg-release.aar b/app/libs/extension-ffmpeg-release.aar index 019feffa..5553ef8b 100644 Binary files a/app/libs/extension-ffmpeg-release.aar and b/app/libs/extension-ffmpeg-release.aar differ diff --git a/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 95520971..5a624cc9 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -103,8 +103,10 @@ class PlayerActivity : BasePlayerActivity() { if (audioRenderer == null) return@setOnClickListener val trackSelectionDialogBuilder = TrackSelectionDialogBuilder( - this, resources.getString(R.string.select_audio_track), - viewModel.trackSelector, audioRenderer + this, + resources.getString(R.string.select_audio_track), + viewModel.player, + C.TRACK_TYPE_AUDIO ) val trackSelectionDialog = trackSelectionDialogBuilder.build() trackSelectionDialog.show() @@ -134,8 +136,10 @@ class PlayerActivity : BasePlayerActivity() { if (subtitleRenderer == null) return@setOnClickListener val trackSelectionDialogBuilder = TrackSelectionDialogBuilder( - this, resources.getString(R.string.select_subtile_track), - viewModel.trackSelector, subtitleRenderer + this, + resources.getString(R.string.select_subtile_track), + viewModel.player, + C.TRACK_TYPE_TEXT ) trackSelectionDialogBuilder.setShowDisableOption(true) diff --git a/app/src/main/java/dev/jdtech/jellyfin/dialogs/TrackSelectionDialogFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/dialogs/TrackSelectionDialogFragment.kt index 14ac0fd5..d789e37e 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/dialogs/TrackSelectionDialogFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/dialogs/TrackSelectionDialogFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import dev.jdtech.jellyfin.R +import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.mpv.TrackType import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel import java.lang.IllegalStateException @@ -14,21 +15,13 @@ class TrackSelectionDialogFragment( private val viewModel: PlayerActivityViewModel ) : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val trackNames: List when (type) { TrackType.AUDIO -> { - trackNames = viewModel.currentAudioTracks.map { track -> - val nameParts: MutableList = mutableListOf() - if (track.title.isNotEmpty()) nameParts.add(track.title) - if (track.lang.isNotEmpty()) nameParts.add(track.lang) - if (track.codec.isNotEmpty()) nameParts.add(track.codec) - nameParts.joinToString(separator = " - ") - } return activity?.let { activity -> val builder = MaterialAlertDialogBuilder(activity) builder.setTitle(getString(R.string.select_audio_track)) .setSingleChoiceItems( - trackNames.toTypedArray(), + getTrackNames(viewModel.currentAudioTracks), viewModel.currentAudioTracks.indexOfFirst { it.selected }) { dialog, which -> viewModel.switchToTrack( TrackType.AUDIO, @@ -40,18 +33,11 @@ class TrackSelectionDialogFragment( } ?: throw IllegalStateException("Activity cannot be null") } TrackType.SUBTITLE -> { - trackNames = viewModel.currentSubtitleTracks.map { track -> - val nameParts: MutableList = mutableListOf() - if (track.title.isNotEmpty()) nameParts.add(track.title) - if (track.lang.isNotEmpty()) nameParts.add(track.lang) - if (track.codec.isNotEmpty()) nameParts.add(track.codec) - nameParts.joinToString(separator = " - ") - } return activity?.let { activity -> val builder = MaterialAlertDialogBuilder(activity) builder.setTitle(getString(R.string.select_subtile_track)) .setSingleChoiceItems( - trackNames.toTypedArray(), + getTrackNames(viewModel.currentSubtitleTracks), viewModel.currentSubtitleTracks.indexOfFirst { if (viewModel.disableSubtitle) it.ffIndex == -1 else it.selected }) { dialog, which -> viewModel.switchToTrack( TrackType.SUBTITLE, @@ -67,4 +53,14 @@ class TrackSelectionDialogFragment( } } } + + private fun getTrackNames(tracks: List): Array { + return tracks.map { track -> + val nameParts: MutableList = mutableListOf() + if (track.title.isNotEmpty()) nameParts.add(track.title) + if (track.lang.isNotEmpty()) nameParts.add(track.lang) + if (track.codec.isNotEmpty()) nameParts.add(track.codec) + nameParts.joinToString(separator = " - ") + }.toTypedArray() + } } \ No newline at end of file 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 163a8671..a08a1d3a 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/mpv/MPVPlayer.kt @@ -17,14 +17,10 @@ import androidx.core.content.getSystemService import com.google.android.exoplayer2.* import com.google.android.exoplayer2.Player.Commands import com.google.android.exoplayer2.audio.AudioAttributes -import com.google.android.exoplayer2.source.MediaSource -import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.TrackGroup -import com.google.android.exoplayer2.source.TrackGroupArray import com.google.android.exoplayer2.text.Cue -import com.google.android.exoplayer2.trackselection.TrackSelectionArray +import com.google.android.exoplayer2.text.CueGroup import com.google.android.exoplayer2.trackselection.TrackSelectionParameters -import com.google.android.exoplayer2.upstream.DataSource import com.google.android.exoplayer2.util.* import com.google.android.exoplayer2.video.VideoSize import kotlinx.parcelize.Parcelize @@ -153,7 +149,7 @@ class MPVPlayer( CopyOnWriteArraySet() // Internal state. - private var internalMediaItems: List? = null + private var internalMediaItems: List = emptyList() @Player.State private var playbackState: Int = Player.STATE_IDLE @@ -161,7 +157,7 @@ class MPVPlayer( @Player.RepeatMode private val repeatMode: Int = REPEAT_MODE_OFF - private var tracksInfo: TracksInfo = TracksInfo.EMPTY + private var tracks: Tracks = Tracks.EMPTY private var playbackParameters: PlaybackParameters = PlaybackParameters.DEFAULT // MPV Custom @@ -170,7 +166,7 @@ class MPVPlayer( private var currentPositionMs: Long? = null private var currentDurationMs: Long? = null private var currentCacheDurationMs: Long? = null - var currentTracks: List = emptyList() + var currentMpvTracks: List = emptyList() private var initialCommands = mutableListOf>() private var initialSeekTo: Long = 0L @@ -183,18 +179,18 @@ class MPVPlayer( handler.post { when (property) { "track-list" -> { - val (tracks, newTracksInfo) = getMPVTracks(value) - tracks.forEach { Log.i("mpv", "${it.ffIndex} ${it.type} ${it.codec}") } - currentTracks = tracks + val (mpvTracks, newTracks) = getMPVTracks(value) + mpvTracks.forEach { Log.i("mpv", "${it.ffIndex} ${it.type} ${it.codec}") } + currentMpvTracks = mpvTracks if (isPlayerReady) { - if (newTracksInfo != tracksInfo) { - tracksInfo = newTracksInfo + if (newTracks != tracks) { + tracks = newTracks listeners.sendEvent(Player.EVENT_TRACKS_CHANGED) { listener -> - listener.onTracksInfoChanged(currentTracksInfo) + listener.onTracksChanged(currentTracks) } } } else { - tracksInfo = newTracksInfo + tracks = newTracks } } } @@ -206,7 +202,7 @@ class MPVPlayer( when (property) { "eof-reached" -> { if (value && isPlayerReady) { - if (currentIndex < (internalMediaItems?.size ?: 0)) { + if (currentIndex < (internalMediaItems.size)) { currentIndex += 1 prepareMediaItem(currentIndex) play() @@ -299,7 +295,7 @@ class MPVPlayer( if (!isPlayerReady) { isPlayerReady = true listeners.sendEvent(Player.EVENT_TRACKS_CHANGED) { listener -> - listener.onTracksInfoChanged(currentTracksInfo) + listener.onTracksChanged(currentTracks) } seekTo(C.TIME_UNSET) if (playWhenReady) { @@ -372,8 +368,8 @@ class MPVPlayer( index: Int ): Boolean { if (index != C.INDEX_UNSET) { - Log.i("mpv", "${currentTracks.size}") - currentTracks.firstOrNull { + Log.i("mpv", "${currentMpvTracks.size}") + currentMpvTracks.firstOrNull { it.type == trackType && (if (isExternal) it.title else "${it.ffIndex}") == "$index" }.let { track -> if (track != null) { @@ -386,7 +382,7 @@ class MPVPlayer( } } } else { - if (currentTracks.indexOfFirst { it.type == trackType && it.selected } != C.INDEX_UNSET) { + if (currentMpvTracks.indexOfFirst { it.type == trackType && it.selected } != C.INDEX_UNSET) { MPVLib.setPropertyString(trackType, "no") } } @@ -399,7 +395,7 @@ class MPVPlayer( * Returns the number of windows in the timeline. */ override fun getWindowCount(): Int { - return internalMediaItems?.size ?: 0 + return internalMediaItems.size } /** @@ -417,7 +413,7 @@ class MPVPlayer( defaultPositionProjectionUs: Long ): Window { val currentMediaItem = - internalMediaItems?.get(windowIndex) ?: MediaItem.Builder().build() + internalMediaItems.getOrNull(windowIndex) ?: MediaItem.Builder().build() return window.set( /* uid= */ windowIndex, /* mediaItem= */ currentMediaItem, @@ -440,7 +436,7 @@ class MPVPlayer( * Returns the number of periods in the timeline. */ override fun getPeriodCount(): Int { - return internalMediaItems?.size ?: 0 + return internalMediaItems.size } /** @@ -678,7 +674,7 @@ class MPVPlayer( currentPositionMs = null currentDurationMs = null currentCacheDurationMs = null - tracksInfo = TracksInfo.EMPTY + tracks = Tracks.EMPTY playbackParameters = PlaybackParameters.DEFAULT initialCommands.clear() //initialSeekTo = 0L @@ -686,7 +682,7 @@ class MPVPlayer( /** Prepares the player. */ override fun prepare() { - internalMediaItems?.forEach { mediaItem -> + internalMediaItems.forEach { mediaItem -> MPVLib.command( arrayOf( "loadfile", @@ -837,7 +833,7 @@ class MPVPlayer( } private fun prepareMediaItem(index: Int) { - internalMediaItems?.get(index)?.let { mediaItem -> + internalMediaItems.getOrNull(index)?.let { mediaItem -> resetInternalState() mediaItem.localConfiguration?.subtitleConfigurations?.forEach { subtitle -> initialCommands.add( @@ -926,33 +922,8 @@ class MPVPlayer( currentIndex = 0 } - /** - * Returns the available track groups. - * - * @see com.google.android.exoplayer2.Player.Listener.onTracksChanged - */ - @Deprecated("Deprecated in Java") - override fun getCurrentTrackGroups(): TrackGroupArray { - return TrackGroupArray.EMPTY - } - - /** - * Returns the current track selections. - * - * - * A concrete implementation may include null elements if it has a fixed number of renderer - * components, wishes to report a TrackSelection for each of them, and has one or more renderer - * components that is not assigned any selected tracks. - * - * @see com.google.android.exoplayer2.Player.Listener.onTracksChanged - */ - @Deprecated("Deprecated in Java") - override fun getCurrentTrackSelections(): TrackSelectionArray { - return TrackSelectionArray() - } - - override fun getCurrentTracksInfo(): TracksInfo { - return tracksInfo + override fun getCurrentTracks(): Tracks { + return tracks } override fun getTrackSelectionParameters(): TrackSelectionParameters { @@ -1203,7 +1174,7 @@ class MPVPlayer( } /** Returns the current [Cues][Cue]. This list may be empty. */ - override fun getCurrentCues(): MutableList { + override fun getCurrentCues(): CueGroup { TODO("Not yet implemented") } @@ -1258,78 +1229,6 @@ class MPVPlayer( throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.") } - /*private class CurrentTrackSelection( - private val currentTrackGroup: TrackGroup, - private val index: Int - ) : TrackSelection { - /** - * Returns an integer specifying the type of the selection, or [.TYPE_UNSET] if not - * specified. - * - * - * Track selection types are specific to individual applications, but should be defined - * starting from [.TYPE_CUSTOM_BASE] to ensure they don't conflict with any types that may - * be added to the library in the future. - */ - override fun getType(): Int { - return TrackSelection.TYPE_UNSET - } - - /** Returns the [TrackGroup] to which the selected tracks belong. */ - override fun getTrackGroup(): TrackGroup { - return currentTrackGroup - } - - /** Returns the number of tracks in the selection. */ - override fun length(): Int { - return if (index != C.INDEX_UNSET) 1 else 0 - } - - /** - * Returns the format of the track at a given index in the selection. - * - * @param index The index in the selection. - * @return The format of the selected track. - */ - override fun getFormat(index: Int): Format { - return currentTrackGroup.getFormat(index) - } - - /** - * Returns the index in the track group of the track at a given index in the selection. - * - * @param index The index in the selection. - * @return The index of the selected track. - */ - override fun getIndexInTrackGroup(index: Int): Int { - return index - } - - /** - * Returns the index in the selection of the track with the specified format. The format is - * located by identity so, for example, `selection.indexOf(selection.getFormat(index)) == - * index` even if multiple selected tracks have formats that contain the same values. - * - * @param format The format. - * @return The index in the selection, or [C.INDEX_UNSET] if the track with the specified - * format is not part of the selection. - */ - override fun indexOf(format: Format): Int { - return currentTrackGroup.indexOf(format) - } - - /** - * Returns the index in the selection of the track with the specified index in the track group. - * - * @param indexInTrackGroup The index in the track group. - * @return The index in the selection, or [C.INDEX_UNSET] if the track with the specified - * index is not part of the selection. - */ - override fun indexOf(indexInTrackGroup: Int): Int { - return indexInTrackGroup - } - }*/ - companion object { /** * Fraction to which audio volume is ducked on loss of audio focus @@ -1449,10 +1348,10 @@ class MPVPlayer( } } - private fun getMPVTracks(trackList: String): Pair, TracksInfo> { - val tracks = mutableListOf() - var tracksInfo = TracksInfo.EMPTY - val trackGroupInfos = mutableListOf() + private fun getMPVTracks(trackList: String): Pair, Tracks> { + val mpvTracks = mutableListOf() + var tracks = Tracks.EMPTY + val trackGroups = mutableListOf() val trackListVideo = mutableListOf() val trackListAudio = mutableListOf() @@ -1475,7 +1374,7 @@ class MPVPlayer( width = null, height = null ) - tracks.add(emptyTrack) + mpvTracks.add(emptyTrack) trackListText.add(emptyTrack.toFormat()) val currentTrackList = JSONArray(trackList) for (index in 0 until currentTrackList.length()) { @@ -1483,21 +1382,21 @@ class MPVPlayer( val currentFormat = currentTrack.toFormat() when (currentTrack.type) { TrackType.VIDEO -> { - tracks.add(currentTrack) + mpvTracks.add(currentTrack) trackListVideo.add(currentFormat) if (currentTrack.selected) { indexCurrentVideo = trackListVideo.indexOf(currentFormat) } } TrackType.AUDIO -> { - tracks.add(currentTrack) + mpvTracks.add(currentTrack) trackListAudio.add(currentFormat) if (currentTrack.selected) { indexCurrentAudio = trackListAudio.indexOf(currentFormat) } } TrackType.SUBTITLE -> { - tracks.add(currentTrack) + mpvTracks.add(currentTrack) trackListText.add(currentFormat) if (currentTrack.selected) { indexCurrentText = trackListText.indexOf(currentFormat) @@ -1507,71 +1406,45 @@ class MPVPlayer( } } if (trackListText.size == 1 && trackListText[0].id == emptyTrack.id.toString()) { - tracks.remove(emptyTrack) + mpvTracks.remove(emptyTrack) trackListText.removeFirst() } if (trackListVideo.isNotEmpty()) { with(TrackGroup(*trackListVideo.toTypedArray())) { - TracksInfo.TrackGroupInfo( + Tracks.Group( this, + true, intArrayOf(C.FORMAT_HANDLED), - C.TRACK_TYPE_VIDEO, BooleanArray(this.length) { it == indexCurrentVideo } ) } } if (trackListAudio.isNotEmpty()) { with(TrackGroup(*trackListAudio.toTypedArray())) { - TracksInfo.TrackGroupInfo( + Tracks.Group( this, + true, IntArray(this.length) { C.FORMAT_HANDLED }, - C.TRACK_TYPE_AUDIO, BooleanArray(this.length) { it == indexCurrentAudio } ) } } if (trackListText.isNotEmpty()) { with(TrackGroup(*trackListText.toTypedArray())) { - TracksInfo.TrackGroupInfo( + Tracks.Group( this, + true, IntArray(this.length) { C.FORMAT_HANDLED }, - C.TRACK_TYPE_TEXT, BooleanArray(this.length) { it == indexCurrentText } ) } } - if (trackGroupInfos.isNotEmpty()) { - tracksInfo = TracksInfo(trackGroupInfos) + if (trackGroups.isNotEmpty()) { + tracks = Tracks(trackGroups) } } catch (e: JSONException) { } - return Pair(tracks, tracksInfo) - } - - /** - * Merges multiple [subtitleSources] into a single [videoSource] - */ - fun mergeMediaSources( - videoSource: MediaSource, - subtitleSources: Array, - dataSource: DataSource.Factory - ): MediaSource { - return when { - subtitleSources.isEmpty() -> videoSource - else -> { - val subtitleConfigurations = mutableListOf() - subtitleSources.forEach { subtitleSource -> - subtitleSource.mediaItem.localConfiguration?.subtitleConfigurations?.forEach { subtitle -> - subtitleConfigurations.add(subtitle) - } - } - ProgressiveMediaSource.Factory(dataSource) - .createMediaSource( - videoSource.mediaItem.buildUpon() - .setSubtitleConfigurations(subtitleConfigurations).build() - ) - } - } + return Pair(mpvTracks, tracks) } } } 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 5eedb5b8..8f300bfa 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -63,8 +63,8 @@ constructor( init { val useMpv = sp.getBoolean("mpv_player", false) - val preferredAudioLanguage = sp.getString("audio_language", null) ?: "" - val preferredSubtitleLanguage = sp.getString("subtitle_language", null) ?: "" + val preferredAudioLanguage = sp.getString("audio_language", "")!! + val preferredSubtitleLanguage = sp.getString("subtitle_language", "")!! if (useMpv) { val preferredLanguages = mapOf( @@ -180,7 +180,7 @@ constructor( } } } - handler.postDelayed(this, 2000) + handler.postDelayed(this, 5000) } } handler.post(runnable) @@ -225,7 +225,7 @@ constructor( currentSubtitleTracks.clear() when (player) { is MPVPlayer -> { - player.currentTracks.forEach { + player.currentMpvTracks.forEach { when (it.type) { TrackType.AUDIO -> { currentAudioTracks.add(it)