mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-19 20:26:32 -07:00
feat: Android TV support (#503)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
7ab8c015b9
commit
c299492d6d
168 changed files with 12019 additions and 3073 deletions
|
|
@ -0,0 +1,232 @@
|
|||
package nl.jknaapen.fladder.player
|
||||
|
||||
import PlaybackState
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
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.CaptionStyleCompat
|
||||
import androidx.media3.ui.PlayerView
|
||||
import io.github.peerless2012.ass.media.kt.buildWithAssSupport
|
||||
import io.github.peerless2012.ass.media.type.AssRenderType
|
||||
import nl.jknaapen.fladder.messengers.properlySetSubAndAudioTracks
|
||||
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
||||
import nl.jknaapen.fladder.utility.getAudioTracks
|
||||
import nl.jknaapen.fladder.utility.getSubtitleTracks
|
||||
import java.io.File
|
||||
|
||||
val LocalPlayer = compositionLocalOf<ExoPlayer?> { null }
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
internal fun ExoPlayer(
|
||||
controls: @Composable (
|
||||
player: ExoPlayer,
|
||||
) -> Unit,
|
||||
) {
|
||||
val videoHost = VideoPlayerObject
|
||||
val context = LocalContext.current
|
||||
|
||||
var initialized = false
|
||||
|
||||
val extractorsFactory = DefaultExtractorsFactory().apply {
|
||||
val isLowRamDevice = context.getSystemService<ActivityManager>()?.isLowRamDevice == true
|
||||
setTsExtractorTimestampSearchBytes(
|
||||
when (isLowRamDevice) {
|
||||
true -> TsExtractor.TS_PACKET_SIZE * 1800
|
||||
false -> TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES
|
||||
}
|
||||
)
|
||||
setConstantBitrateSeekingEnabled(true)
|
||||
setConstantBitrateSeekingAlwaysEnabled(true)
|
||||
}
|
||||
|
||||
val videoCache = remember { VideoCache.buildCacheDataSourceFactory(context) }
|
||||
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
|
||||
val renderersFactory = DefaultRenderersFactory(context)
|
||||
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
.setEnableDecoderFallback(true)
|
||||
|
||||
val exoPlayer = remember {
|
||||
ExoPlayer.Builder(context, renderersFactory)
|
||||
.setTrackSelector(DefaultTrackSelector(context).apply {
|
||||
setParameters(buildUponParameters().apply {
|
||||
setAudioOffloadPreferences(
|
||||
TrackSelectionParameters.AudioOffloadPreferences.DEFAULT.buildUpon().apply {
|
||||
setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
|
||||
}.build()
|
||||
)
|
||||
setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
|
||||
setTunnelingEnabled(true)
|
||||
})
|
||||
})
|
||||
.setMediaSourceFactory(
|
||||
DefaultMediaSourceFactory(
|
||||
videoCache,
|
||||
extractorsFactory
|
||||
),
|
||||
)
|
||||
.setAudioAttributes(audioAttributes, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setPauseAtEndOfMediaItems(true)
|
||||
.setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT)
|
||||
.buildWithAssSupport(context, AssRenderType.LEGACY)
|
||||
}
|
||||
|
||||
DisposableEffect(exoPlayer) {
|
||||
val listener = object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
videoHost.setPlaybackState(
|
||||
PlaybackState(
|
||||
position = exoPlayer.currentPosition,
|
||||
buffered = exoPlayer.bufferedPosition,
|
||||
duration = exoPlayer.duration,
|
||||
playing = exoPlayer.isPlaying,
|
||||
buffering = playbackState == Player.STATE_BUFFERING,
|
||||
completed = playbackState == Player.STATE_ENDED,
|
||||
failed = playbackState == Player.STATE_IDLE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
super.onEvents(player, events)
|
||||
videoHost.setPlaybackState(
|
||||
PlaybackState(
|
||||
position = exoPlayer.currentPosition,
|
||||
buffered = exoPlayer.bufferedPosition,
|
||||
duration = exoPlayer.duration,
|
||||
playing = exoPlayer.isPlaying,
|
||||
buffering = exoPlayer.playbackState == Player.STATE_BUFFERING,
|
||||
completed = exoPlayer.playbackState == Player.STATE_ENDED,
|
||||
failed = exoPlayer.playbackState == Player.STATE_IDLE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
super.onTracksChanged(tracks)
|
||||
if (!initialized) {
|
||||
initialized = true
|
||||
VideoPlayerObject.implementation.playbackData.value?.let {
|
||||
exoPlayer.properlySetSubAndAudioTracks(it)
|
||||
}
|
||||
VideoPlayerObject.exoSubTracks.value = exoPlayer.getSubtitleTracks()
|
||||
VideoPlayerObject.exoAudioTracks.value = exoPlayer.getAudioTracks()
|
||||
}
|
||||
}
|
||||
}
|
||||
exoPlayer.addListener(listener)
|
||||
onDispose {
|
||||
exoPlayer.removeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
VideoPlayerObject.implementation.init(exoPlayer)
|
||||
onDispose {
|
||||
videoHost.videoPlayerControls?.onStop(callback = {})
|
||||
VideoPlayerObject.implementation.init(null)
|
||||
exoPlayer.release()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(Color.Black)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = {
|
||||
PlayerView(it).apply {
|
||||
player = exoPlayer
|
||||
useController = false
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
subtitleView?.apply {
|
||||
setStyle(
|
||||
CaptionStyleCompat(
|
||||
android.graphics.Color.WHITE,
|
||||
android.graphics.Color.TRANSPARENT,
|
||||
android.graphics.Color.TRANSPARENT,
|
||||
CaptionStyleCompat.EDGE_TYPE_OUTLINE,
|
||||
android.graphics.Color.BLACK,
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
CompositionLocalProvider(LocalPlayer provides exoPlayer) {
|
||||
controls(exoPlayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
object VideoCache {
|
||||
private const val CACHE_SIZE: Long = 150L * 1024L * 1024L // 150 MB
|
||||
|
||||
@Volatile
|
||||
private var cache: SimpleCache? = null
|
||||
|
||||
fun getCache(context: Context): SimpleCache {
|
||||
return cache ?: synchronized(this) {
|
||||
cache ?: SimpleCache(
|
||||
File(context.cacheDir, "video_cache"),
|
||||
LeastRecentlyUsedCacheEvictor(CACHE_SIZE)
|
||||
).also { cache = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun buildCacheDataSourceFactory(context: Context): DataSource.Factory {
|
||||
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
|
||||
val upstreamFactory = DefaultDataSource.Factory(context, httpDataSourceFactory)
|
||||
|
||||
return CacheDataSource.Factory()
|
||||
.setCache(getCache(context))
|
||||
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue