mirror of
https://github.com/jellyfin/jellyfin-android.git
synced 2026-09-02 21:04:10 +03:00
Add ass/ssa subtitle support (#2136)
* Add ass/ssa subtitle support Create renderers factory using constructor Init libass when exoPlayerDirectPlayAss is true Replace MediaSource.Factory singleton with factory Fix linter errors * Update SSA/ASS direct play description
This commit is contained in:
@@ -172,6 +172,7 @@ dependencies {
|
||||
implementation(libs.bundles.androidx.media3)
|
||||
proprietaryImplementation(libs.androidx.media3.cast)
|
||||
proprietaryImplementation(libs.bundles.playservices)
|
||||
implementation(libs.libass.media)
|
||||
|
||||
// Room
|
||||
implementation(libs.bundles.androidx.room)
|
||||
|
||||
@@ -14,15 +14,16 @@ import androidx.media3.datasource.cache.Cache
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.exoplayer.source.SingleSampleMediaSource
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||
import androidx.media3.extractor.ts.TsExtractor
|
||||
import androidx.work.WorkManager
|
||||
import coil3.ImageLoader
|
||||
import io.github.peerless2012.ass.media.AssHandler
|
||||
import io.github.peerless2012.ass.media.kt.withAssMkvSupport
|
||||
import io.github.peerless2012.ass.media.parser.AssSubtitleParserFactory
|
||||
import io.github.peerless2012.ass.media.type.AssRenderType
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.mobile.MainViewModel
|
||||
@@ -68,6 +69,7 @@ val applicationModule = module {
|
||||
single { RemoteVolumeProvider(get()) }
|
||||
single(named(PLAYER_EVENT_CHANNEL)) { Channel<PlayerEvent>() }
|
||||
factory { WorkManager.getInstance(get()) }
|
||||
single { AssHandler(AssRenderType.OVERLAY_OPEN_GL) }
|
||||
|
||||
// Controllers
|
||||
single { ApiClientController(get(), get(), get(), get(), get()) }
|
||||
@@ -155,7 +157,7 @@ val applicationModule = module {
|
||||
}
|
||||
}
|
||||
|
||||
single<MediaSource.Factory> {
|
||||
factory<MediaSource.Factory> {
|
||||
val context: Context = get()
|
||||
val extractorsFactory = DefaultExtractorsFactory().apply {
|
||||
// https://github.com/google/ExoPlayer/issues/8571
|
||||
@@ -166,7 +168,17 @@ val applicationModule = module {
|
||||
},
|
||||
)
|
||||
}
|
||||
DefaultMediaSourceFactory(get<CacheDataSource.Factory>(), extractorsFactory)
|
||||
|
||||
val appPreferences: AppPreferences = get()
|
||||
if (appPreferences.exoPlayerDirectPlayAss) {
|
||||
val assHandler: AssHandler = get()
|
||||
val assSubtitleParserFactory = AssSubtitleParserFactory(assHandler)
|
||||
val assExtractorsFactory = extractorsFactory.withAssMkvSupport(assSubtitleParserFactory, assHandler)
|
||||
DefaultMediaSourceFactory(get<CacheDataSource.Factory>(), assExtractorsFactory)
|
||||
.setSubtitleParserFactory(assSubtitleParserFactory)
|
||||
} else {
|
||||
DefaultMediaSourceFactory(get<CacheDataSource.Factory>(), extractorsFactory)
|
||||
}
|
||||
}
|
||||
|
||||
single(createdAtStart = true) { StorageManager(get(), get()) }
|
||||
|
||||
@@ -27,6 +27,8 @@ import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.media3.exoplayer.util.EventLogger
|
||||
import io.github.peerless2012.ass.media.AssHandler
|
||||
import io.github.peerless2012.ass.media.factory.AssRenderersFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -239,6 +241,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application),
|
||||
.build()
|
||||
else -> DefaultLoadControl()
|
||||
}
|
||||
val appPreferences: AppPreferences = get()
|
||||
val renderersFactory = DefaultRenderersFactory(getApplication()).apply {
|
||||
setEnableDecoderFallback(true) // Fallback only works if initialization fails, not decoding at playback time
|
||||
val rendererMode = when {
|
||||
@@ -272,7 +275,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application),
|
||||
|
||||
filteredDecoderList
|
||||
}
|
||||
}
|
||||
}.let { if (appPreferences.exoPlayerDirectPlayAss) AssRenderersFactory(get(), it) else it }
|
||||
_player.value = ExoPlayer.Builder(getApplication(), renderersFactory, get()).apply {
|
||||
setUsePlatformDiagnostics(false)
|
||||
setTrackSelector(trackSelector)
|
||||
@@ -282,6 +285,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application),
|
||||
}.build().apply {
|
||||
addListener(this@PlayerViewModel)
|
||||
applyDefaultAudioAttributes(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
if (appPreferences.exoPlayerDirectPlayAss) get<AssHandler>().init(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.ui.PlayerView
|
||||
import io.github.peerless2012.ass.media.AssHandler
|
||||
import io.github.peerless2012.ass.media.widget.AssSubtitleView
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.mobile.R
|
||||
@@ -59,6 +61,7 @@ import androidx.media3.ui.R as Media3R
|
||||
@Suppress("TooManyFunctions")
|
||||
class PlayerFragment : Fragment(), BackPressInterceptor {
|
||||
private val appPreferences: AppPreferences by inject()
|
||||
private val assHandler: AssHandler by inject()
|
||||
private val viewModel: PlayerViewModel by viewModels()
|
||||
private var _playerBinding: FragmentPlayerBinding? = null
|
||||
private val playerBinding: FragmentPlayerBinding get() = _playerBinding!!
|
||||
@@ -209,6 +212,12 @@ class PlayerFragment : Fragment(), BackPressInterceptor {
|
||||
// Disable controller animations
|
||||
playerView.setControllerAnimationEnabled(false)
|
||||
|
||||
if (appPreferences.exoPlayerDirectPlayAss) {
|
||||
playerView.subtitleView?.apply {
|
||||
addView(AssSubtitleView(this.context, assHandler))
|
||||
}
|
||||
}
|
||||
|
||||
playerLockScreenHelper = PlayerLockScreenHelper(this, playerBinding, orientationListener)
|
||||
playerGestureHelper = PlayerGestureHelper(this, playerBinding, playerLockScreenHelper)
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ class SettingsFragment : Fragment(), BackPressInterceptor {
|
||||
}
|
||||
directPlayAssPreference = checkBox(Constants.PREF_EXOPLAYER_DIRECT_PLAY_ASS) {
|
||||
titleRes = R.string.pref_exoplayer_direct_play_ass
|
||||
summaryRes = R.string.pref_exoplayer_direct_play_ass_summary
|
||||
summaryRes = R.string.pref_exoplayer_direct_play_ass_description
|
||||
enabled = appPreferences.videoPlayerType == VideoPlayerType.EXO_PLAYER
|
||||
}
|
||||
val networkBufferOptions = listOf(
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
<string name="pref_exoplayer_allow_horizontal_gesture">Horizontal gestures</string>
|
||||
<string name="pref_exoplayer_allow_horizontal_gesture_summary">Allow horizontal gesture control for video seeking</string>
|
||||
<string name="pref_exoplayer_direct_play_ass">Allow SSA/ASS subtitles in direct play</string>
|
||||
<string name="pref_exoplayer_direct_play_ass_summary">Prevent transcoding and show subtitles with basic styling only. Advanced subtitle styling will not be available if enabled.</string>
|
||||
<string name="pref_exoplayer_direct_play_ass_description">Use experimental client-side subtitle rendering, may not always work</string>
|
||||
<string name="pref_exoplayer_network_buffer">Playback buffer size</string>
|
||||
<string name="network_buffer_auto">Auto</string>
|
||||
<string name="network_buffer_auto_description">Default buffer, suitable for most playback scenarios</string>
|
||||
|
||||
Reference in New Issue
Block a user