Support Advanced SubStation Alpha direct play
This commit is contained in:
committed by
Niels van Velzen
parent
e3062edc91
commit
aa964647b5
@@ -138,6 +138,7 @@ dependencies {
|
||||
implementation(libs.androidx.media3.exoplayer.hls)
|
||||
implementation(libs.androidx.media3.ui)
|
||||
implementation(libs.jellyfin.androidx.media3.ffmpeg.decoder)
|
||||
implementation(libs.libass.media3)
|
||||
|
||||
// Markdown
|
||||
implementation(libs.bundles.markwon)
|
||||
|
||||
@@ -72,6 +72,7 @@ fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
|
||||
val userPreferences = get<UserPreferences>()
|
||||
val exoPlayerOptions = ExoPlayerOptions(
|
||||
preferFfmpeg = userPreferences[UserPreferences.preferExoPlayerFfmpeg],
|
||||
enableLibass = userPreferences[UserPreferences.assDirectPlay],
|
||||
enableDebugLogging = userPreferences[UserPreferences.debuggingEnabled],
|
||||
baseDataSourceFactory = get<HttpDataSource.Factory>(),
|
||||
)
|
||||
|
||||
@@ -226,6 +226,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
*/
|
||||
var trickPlayEnabled = booleanPreference("trick_play_enabled", false)
|
||||
|
||||
/**
|
||||
* Enable libass.
|
||||
*/
|
||||
var assDirectPlay = booleanPreference("libass_enabled", false)
|
||||
|
||||
/**
|
||||
* Enable PGS subtitle direct-play.
|
||||
*/
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
||||
import androidx.media3.exoplayer.util.EventLogger;
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory;
|
||||
import androidx.media3.extractor.ExtractorsFactory;
|
||||
import androidx.media3.extractor.ts.TsExtractor;
|
||||
import androidx.media3.ui.AspectRatioFrameLayout;
|
||||
import androidx.media3.ui.CaptionStyleCompat;
|
||||
@@ -59,6 +60,13 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.github.peerless2012.ass.media.AssHandler;
|
||||
import io.github.peerless2012.ass.media.AssHandlerConfig;
|
||||
import io.github.peerless2012.ass.media.factory.AssRenderersFactory;
|
||||
import io.github.peerless2012.ass.media.kt.AssPlayerKt;
|
||||
import io.github.peerless2012.ass.media.parser.AssSubtitleParserFactory;
|
||||
import io.github.peerless2012.ass.media.type.AssRenderType;
|
||||
import io.github.peerless2012.ass.media.widget.AssSubtitleView;
|
||||
import timber.log.Timber;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@@ -88,7 +96,10 @@ public class VideoManager {
|
||||
_helper = helper;
|
||||
nightModeEnabled = userPreferences.get(UserPreferences.Companion.getAudioNightMode());
|
||||
|
||||
mExoPlayer = configureExoplayerBuilder(activity).build();
|
||||
boolean assDirectPlay = userPreferences.get(UserPreferences.Companion.getAssDirectPlay());
|
||||
AssHandler assHandler = assDirectPlay ? new AssHandler(AssRenderType.OVERLAY_OPEN_GL, new AssHandlerConfig()) : null;
|
||||
|
||||
mExoPlayer = configureExoplayerBuilder(activity, assHandler).build();
|
||||
|
||||
if (userPreferences.get(UserPreferences.Companion.getDebuggingEnabled())) {
|
||||
mExoPlayer.addAnalyticsListener(new EventLogger());
|
||||
@@ -120,6 +131,11 @@ public class VideoManager {
|
||||
mExoPlayerView.getSubtitleView().setBottomPaddingFraction(userPreferences.get(UserPreferences.Companion.getSubtitlesOffsetPosition()));
|
||||
mExoPlayerView.getSubtitleView().setStyle(subtitleStyle);
|
||||
|
||||
if (assHandler != null) {
|
||||
assHandler.init(mExoPlayer);
|
||||
mExoPlayerView.getSubtitleView().addView(new AssSubtitleView(mActivity, assHandler));
|
||||
}
|
||||
|
||||
mExoPlayer.addListener(new Player.Listener() {
|
||||
@Override
|
||||
public void onPlayerError(@NonNull PlaybackException error) {
|
||||
@@ -198,7 +214,7 @@ public class VideoManager {
|
||||
* @param context The associated context
|
||||
* @return A configured builder for Exoplayer
|
||||
*/
|
||||
private ExoPlayer.Builder configureExoplayerBuilder(Context context) {
|
||||
private ExoPlayer.Builder configureExoplayerBuilder(Context context, AssHandler assHandler) {
|
||||
ExoPlayer.Builder exoPlayerBuilder = new ExoPlayer.Builder(context);
|
||||
DefaultRenderersFactory defaultRendererFactory = new DefaultRenderersFactory(context);
|
||||
defaultRendererFactory.setEnableDecoderFallback(true);
|
||||
@@ -219,8 +235,17 @@ public class VideoManager {
|
||||
extractorsFactory.setConstantBitrateSeekingEnabled(true);
|
||||
extractorsFactory.setConstantBitrateSeekingAlwaysEnabled(true);
|
||||
DefaultDataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, exoPlayerHttpDataSourceFactory);
|
||||
exoPlayerBuilder.setRenderersFactory(defaultRendererFactory);
|
||||
exoPlayerBuilder.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory));
|
||||
if (assHandler != null) {
|
||||
AssSubtitleParserFactory assSubtitleParserFactory = new AssSubtitleParserFactory(assHandler);
|
||||
ExtractorsFactory assExtractorsFactory = AssPlayerKt.withAssMkvSupport(extractorsFactory, assSubtitleParserFactory, assHandler);
|
||||
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory, assExtractorsFactory);
|
||||
mediaSourceFactory.setSubtitleParserFactory(assSubtitleParserFactory);
|
||||
exoPlayerBuilder.setMediaSourceFactory(mediaSourceFactory);
|
||||
exoPlayerBuilder.setRenderersFactory(new AssRenderersFactory(assHandler, defaultRendererFactory));
|
||||
} else {
|
||||
exoPlayerBuilder.setRenderersFactory(defaultRendererFactory);
|
||||
exoPlayerBuilder.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory));
|
||||
}
|
||||
|
||||
exoPlayerBuilder.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
|
||||
@@ -70,6 +70,14 @@ fun SettingsDeveloperScreen() {
|
||||
captionContent = { Text(stringResource(R.string.enable_playback_module_description)) },
|
||||
onClick = { playbackRewriteVideoEnabled = !playbackRewriteVideoEnabled }
|
||||
)
|
||||
|
||||
var assDirectPlay by rememberPreference(userPreferences, UserPreferences.assDirectPlay)
|
||||
ListButton(
|
||||
headingContent = { Text(stringResource(R.string.preference_enable_libass)) },
|
||||
trailingContent = { Checkbox(checked = assDirectPlay) },
|
||||
captionContent = { Text(stringResource(R.string.enable_playback_module_description)) },
|
||||
onClick = { assDirectPlay = !assDirectPlay }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
|
||||
@@ -83,7 +83,7 @@ fun createDeviceProfile(
|
||||
maxBitrate = userPreferences.getMaxBitrate(),
|
||||
isAC3Enabled = userPreferences[UserPreferences.ac3Enabled],
|
||||
downMixAudio = userPreferences[UserPreferences.audioBehaviour] == AudioBehavior.DOWNMIX_TO_STEREO,
|
||||
assDirectPlay = false,
|
||||
assDirectPlay = userPreferences[UserPreferences.assDirectPlay],
|
||||
pgsDirectPlay = userPreferences[UserPreferences.pgsDirectPlay],
|
||||
)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ kotest = "6.1.4"
|
||||
kotlin = "2.3.10"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
kotlinx-serialization = "1.10.0"
|
||||
libass-android = "0.4.0"
|
||||
markwon = "4.6.2"
|
||||
mockk = "1.14.9"
|
||||
slf4j-timber = "0.0.4"
|
||||
@@ -100,6 +101,7 @@ androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls
|
||||
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "androidx-media3" }
|
||||
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" }
|
||||
jellyfin-androidx-media3-ffmpeg-decoder = { module = "org.jellyfin.media3:media3-ffmpeg-decoder", version.ref = "jellyfin-androidx-media" }
|
||||
libass-media3 = { module = "io.github.peerless2012:ass-media", version.ref = "libass-android" }
|
||||
|
||||
# Markwon
|
||||
markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon" }
|
||||
|
||||
@@ -37,6 +37,7 @@ dependencies {
|
||||
implementation(libs.androidx.media3.exoplayer.hls)
|
||||
implementation(libs.jellyfin.androidx.media3.ffmpeg.decoder)
|
||||
implementation(libs.androidx.media3.ui)
|
||||
implementation(libs.libass.media3)
|
||||
|
||||
// Logging
|
||||
implementation(libs.timber)
|
||||
|
||||
@@ -23,6 +23,12 @@ import androidx.media3.exoplayer.util.EventLogger
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||
import androidx.media3.extractor.ts.TsExtractor
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import io.github.peerless2012.ass.media.AssHandler
|
||||
import io.github.peerless2012.ass.media.factory.AssRenderersFactory
|
||||
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 io.github.peerless2012.ass.media.widget.AssSubtitleView
|
||||
import org.jellyfin.playback.core.backend.BasePlayerBackend
|
||||
import org.jellyfin.playback.core.mediastream.MediaStream
|
||||
import org.jellyfin.playback.core.mediastream.PlayableMediaStream
|
||||
@@ -61,6 +67,10 @@ class ExoPlayerBackend(
|
||||
private val timedEventState = TimedEventState()
|
||||
private var lastKnownDuration: Duration? = null
|
||||
|
||||
private val assHandler by lazy {
|
||||
AssHandler(AssRenderType.OVERLAY_OPEN_GL)
|
||||
}
|
||||
|
||||
private val exoPlayer by lazy {
|
||||
val dataSourceFactory = DefaultDataSource.Factory(
|
||||
context,
|
||||
@@ -78,7 +88,14 @@ class ExoPlayerBackend(
|
||||
setConstantBitrateSeekingAlwaysEnabled(true)
|
||||
}
|
||||
|
||||
val mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory)
|
||||
val mediaSourceFactory = if (exoPlayerOptions.enableLibass) {
|
||||
val assSubtitleParserFactory = AssSubtitleParserFactory(assHandler)
|
||||
val assExtractorsFactory = extractorsFactory.withAssMkvSupport(assSubtitleParserFactory, assHandler)
|
||||
DefaultMediaSourceFactory(dataSourceFactory, assExtractorsFactory).apply {
|
||||
setSubtitleParserFactory(assSubtitleParserFactory)
|
||||
}
|
||||
} else DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory)
|
||||
|
||||
val renderersFactory = DefaultRenderersFactory(context).apply {
|
||||
setEnableDecoderFallback(true)
|
||||
setExtensionRendererMode(
|
||||
@@ -87,6 +104,9 @@ class ExoPlayerBackend(
|
||||
false -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||
}
|
||||
)
|
||||
}.let { renderersFactory ->
|
||||
if (exoPlayerOptions.enableLibass) AssRenderersFactory(assHandler, renderersFactory)
|
||||
else renderersFactory
|
||||
}
|
||||
|
||||
ExoPlayer.Builder(context)
|
||||
@@ -110,6 +130,10 @@ class ExoPlayerBackend(
|
||||
if (exoPlayerOptions.enableDebugLogging) {
|
||||
player.addAnalyticsListener(EventLogger())
|
||||
}
|
||||
|
||||
if (exoPlayerOptions.enableLibass) {
|
||||
assHandler.init(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +203,11 @@ class ExoPlayerBackend(
|
||||
override fun setSubtitleView(surfaceView: PlayerSubtitleView?) {
|
||||
if (surfaceView != null) {
|
||||
if (subtitleView == null) {
|
||||
subtitleView = SubtitleView(surfaceView.context)
|
||||
subtitleView = SubtitleView(surfaceView.context).apply {
|
||||
if (exoPlayerOptions.enableLibass) {
|
||||
addView(AssSubtitleView(surfaceView.context, assHandler))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
surfaceView.addView(subtitleView)
|
||||
|
||||
@@ -6,5 +6,6 @@ import androidx.media3.datasource.DefaultHttpDataSource
|
||||
data class ExoPlayerOptions(
|
||||
val preferFfmpeg: Boolean = false,
|
||||
val enableDebugLogging: Boolean = false,
|
||||
val enableLibass: Boolean = false,
|
||||
val baseDataSourceFactory: DataSource.Factory = DefaultHttpDataSource.Factory(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user