Skip to content

Commit

Permalink
Add initial subtitle implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsvanvelzen committed May 10, 2024
1 parent 54f575d commit 4c95258
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
Expand All @@ -15,6 +16,7 @@ import org.jellyfin.androidtv.ui.ScreensaverViewModel
import org.jellyfin.androidtv.ui.playback.VideoQueueManager
import org.jellyfin.playback.core.PlaybackManager
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.core.ui.PlayerSubtitleView
import org.jellyfin.playback.core.ui.PlayerSurfaceView
import org.jellyfin.sdk.api.client.ApiClient
import org.koin.android.ext.android.inject
Expand Down Expand Up @@ -74,9 +76,15 @@ class PlaybackRewriteFragment : Fragment() {
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = PlayerSurfaceView(requireContext())
view.playbackManager = playbackManager
return view
return FrameLayout(requireContext()).apply {
addView(PlayerSurfaceView(requireContext()).also { view ->
view.playbackManager = playbackManager
})

addView(PlayerSubtitleView(requireContext()).also { view ->
view.playbackManager = playbackManager
})
}
}

override fun onPause() {
Expand Down
40 changes: 32 additions & 8 deletions playback/core/src/main/kotlin/backend/BackendService.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package org.jellyfin.playback.core.backend

import android.view.SurfaceView
import androidx.core.view.doOnDetach
import org.jellyfin.playback.core.mediastream.PlayableMediaStream
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.core.ui.PlayerSubtitleView
import org.jellyfin.playback.core.ui.PlayerSurfaceView

/**
* Service keeping track of the current playback backend and its related surface view.
Expand All @@ -13,34 +14,57 @@ class BackendService {
val backend get() = _backend

private var listeners = mutableListOf<PlayerBackendEventListener>()
private var _surfaceView: SurfaceView? = null
private var _surfaceView: PlayerSurfaceView? = null
private var _subtitleView: PlayerSubtitleView? = null

fun switchBackend(backend: PlayerBackend) {
_backend?.stop()
_backend?.setListener(null)
_backend?.setSurface(null)
_backend?.setSurfaceView(null)
_backend?.setSubtitleView(null)

_backend = backend.apply {
_surfaceView?.let(::setSurface)
_surfaceView?.let(::setSurfaceView)
_subtitleView?.let(::setSubtitleView)
setListener(BackendEventListener())
}
}

fun attachSurfaceView(surfaceView: SurfaceView) {
fun attachSurfaceView(surfaceView: PlayerSurfaceView) {
// Remove existing surface view
if (_surfaceView != null) {
_backend?.setSurface(null)
_backend?.setSurfaceView(null)
}

// Apply new surface view
_surfaceView = surfaceView.apply {
_backend?.setSurface(surfaceView)
_backend?.setSurfaceView(surfaceView)

// Automatically detach
doOnDetach {
if (surfaceView == _surfaceView) {
_surfaceView = null
_backend?.setSurface(null)
_backend?.setSurfaceView(null)
}
}
}
}

fun attachSubtitleView(subtitleView: PlayerSubtitleView) {
// Remove existing surface view
if (_subtitleView != null) {
_backend?.setSubtitleView(null)
}

// Apply new surface view
_subtitleView = subtitleView.apply {
_backend?.setSubtitleView(subtitleView)

// Automatically detach
doOnDetach {
if (subtitleView == _subtitleView) {
_subtitleView = null
_backend?.setSubtitleView(null)
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions playback/core/src/main/kotlin/backend/PlayerBackend.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.jellyfin.playback.core.backend

import android.view.SurfaceView
import org.jellyfin.playback.core.mediastream.MediaStream
import org.jellyfin.playback.core.mediastream.PlayableMediaStream
import org.jellyfin.playback.core.model.PositionInfo
import org.jellyfin.playback.core.support.PlaySupportReport
import org.jellyfin.playback.core.ui.PlayerSubtitleView
import org.jellyfin.playback.core.ui.PlayerSurfaceView
import kotlin.time.Duration

/**
Expand All @@ -16,7 +17,8 @@ interface PlayerBackend {
fun supportsStream(stream: MediaStream): PlaySupportReport

// UI
fun setSurface(surfaceView: SurfaceView?)
fun setSurfaceView(surfaceView: PlayerSurfaceView?)
fun setSubtitleView(surfaceView: PlayerSubtitleView?)

// Data retrieval

Expand Down
28 changes: 28 additions & 0 deletions playback/core/src/main/kotlin/ui/PlayerSubtitleView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.jellyfin.playback.core.ui

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import org.jellyfin.playback.core.PlaybackManager

/**
* A view that is used to display the subtitle output of the playing media.
* The [playbackManager] must be set when the view is initialized.
*/
class PlayerSubtitleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
lateinit var playbackManager: PlaybackManager

override fun onAttachedToWindow() {
super.onAttachedToWindow()

if (!isInEditMode) {
playbackManager.backendService.attachSubtitleView(this)
}
}
}

2 changes: 1 addition & 1 deletion playback/core/src/main/kotlin/ui/PlayerSurfaceView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class PlayerSurfaceView @JvmOverloads constructor(
super.onAttachedToWindow()

if (!isInEditMode) {
playbackManager.backendService.attachSurfaceView(surface)
playbackManager.backendService.attachSurfaceView(this)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions playback/exoplayer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies {
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.exoplayer.hls)
implementation(libs.jellyfin.androidx.media3.ffmpeg.decoder)
implementation(libs.androidx.media3.ui)

// Logging
implementation(libs.timber)
Expand Down
25 changes: 22 additions & 3 deletions playback/exoplayer/src/main/kotlin/ExoPlayerBackend.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.jellyfin.playback.exoplayer

import android.app.ActivityManager
import android.content.Context
import android.view.SurfaceView
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.core.content.getSystemService
import androidx.media3.common.C
Expand All @@ -11,19 +11,23 @@ import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.VideoSize
import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.ts.TsExtractor
import androidx.media3.ui.SubtitleView
import org.jellyfin.playback.core.backend.BasePlayerBackend
import org.jellyfin.playback.core.mediastream.MediaStream
import org.jellyfin.playback.core.mediastream.PlayableMediaStream
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.core.model.PositionInfo
import org.jellyfin.playback.core.support.PlaySupportReport
import org.jellyfin.playback.core.ui.PlayerSubtitleView
import org.jellyfin.playback.core.ui.PlayerSurfaceView
import org.jellyfin.playback.exoplayer.support.getPlaySupportReport
import org.jellyfin.playback.exoplayer.support.toFormat
import timber.log.Timber
Expand All @@ -41,6 +45,7 @@ class ExoPlayerBackend(
}

private var currentStream: PlayableMediaStream? = null
private var subtitleView: SubtitleView? = null

private val exoPlayer by lazy {
ExoPlayer.Builder(context)
Expand Down Expand Up @@ -89,6 +94,10 @@ class ExoPlayerBackend(
listener?.onVideoSizeChange(size.width, size.height)
}

override fun onCues(cueGroup: CueGroup) {
subtitleView?.setCues(cueGroup.cues)
}

override fun onPlaybackStateChanged(playbackState: Int) {
onIsPlayingChanged(exoPlayer.isPlaying)
}
Expand All @@ -104,8 +113,18 @@ class ExoPlayerBackend(
stream: MediaStream
): PlaySupportReport = exoPlayer.getPlaySupportReport(stream.toFormat())

override fun setSurface(surfaceView: SurfaceView?) {
exoPlayer.setVideoSurfaceView(surfaceView)
override fun setSurfaceView(surfaceView: PlayerSurfaceView?) {
exoPlayer.setVideoSurfaceView(surfaceView?.surface)
}

override fun setSubtitleView(surfaceView: PlayerSubtitleView?) {
if (surfaceView != null) {
if (subtitleView == null) subtitleView = SubtitleView(surfaceView.context)
surfaceView.addView(subtitleView)
} else {
(subtitleView?.parent as? ViewGroup)?.removeView(subtitleView)
subtitleView = null
}
}

override fun prepareStream(stream: PlayableMediaStream) {
Expand Down

0 comments on commit 4c95258

Please sign in to comment.