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