mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Switch back to vlc
Mpv is BAD at playing hls, we can't use it for kyoo.
This commit is contained in:
@@ -1,890 +0,0 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.TextureView
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.BasePlayer
|
||||
import androidx.media3.common.C.FORMAT_HANDLED
|
||||
import androidx.media3.common.C.INDEX_UNSET
|
||||
import androidx.media3.common.C.SELECTION_FLAG_DEFAULT
|
||||
import androidx.media3.common.C.SELECTION_FLAG_FORCED
|
||||
import androidx.media3.common.C.TIME_UNSET
|
||||
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
||||
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||
import androidx.media3.common.DeviceInfo
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.Format.NO_VALUE
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.TrackGroup
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.Tracks.Group
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.Clock
|
||||
import androidx.media3.common.util.ListenerSet
|
||||
import androidx.media3.common.util.Size
|
||||
import dev.jdtech.mpv.MPVLib
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
private val applicationLooper: Looper = Looper.getMainLooper()
|
||||
private val applicationHandler = Handler(applicationLooper)
|
||||
private val listeners = ListenerSet<Player.Listener>(
|
||||
applicationLooper,
|
||||
Clock.DEFAULT
|
||||
) { listener, flags ->
|
||||
listener.onEvents(this, Player.Events(flags))
|
||||
}
|
||||
|
||||
private fun notifyListeners(eventFlag: Int, callback: (Player.Listener) -> Unit) =
|
||||
notifyListeners(arrayOf(eventFlag), callback)
|
||||
|
||||
private fun notifyListeners(eventFlag: Array<Int>, callback: (Player.Listener) -> Unit) {
|
||||
val notifyAction = {
|
||||
for (event in eventFlag) {
|
||||
listeners.queueEvent(event, callback)
|
||||
}
|
||||
listeners.flushEvents()
|
||||
}
|
||||
if (Looper.myLooper() == applicationLooper) {
|
||||
notifyAction()
|
||||
} else {
|
||||
applicationHandler.post(notifyAction)
|
||||
}
|
||||
}
|
||||
|
||||
internal val mpv = (MPVLib.create(ctx) ?: throw Error("Failed to initialize MPVLib")).apply {
|
||||
setOptionString("vo", "gpu-next")
|
||||
setOptionString("force-window", "no")
|
||||
setOptionString("gpu-context", "android")
|
||||
setOptionString("opengl-es", "yes")
|
||||
setOptionString("hwdec", "mediacodec-copy")
|
||||
setOptionString("profile", "fast")
|
||||
setOptionString("keep-open", "always")
|
||||
|
||||
setOptionString("cache", "yes")
|
||||
setOptionString("cache-pause-initial", "yes")
|
||||
setOptionString("demuxer-max-bytes", "150MiB")
|
||||
setOptionString("demuxer-max-back-bytes", "75MiB")
|
||||
setOptionString("demuxer-readahead-secs", "20")
|
||||
|
||||
setOptionString("save-position-on-quit", "no")
|
||||
setOptionString("ytdl", "no")
|
||||
setOptionString("hr-seek", "no")
|
||||
|
||||
init()
|
||||
addObserver(this@MpvPlayer)
|
||||
observeProperty("core-idle", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
observeProperty("paused-for-cache", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
observeProperty("pause", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
observeProperty("eof-reached", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
observeProperty("duration", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
observeProperty("speed", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
observeProperty("volume", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
observeProperty("vid", MPVLib.MpvFormat.MPV_FORMAT_INT64)
|
||||
observeProperty("aid", MPVLib.MpvFormat.MPV_FORMAT_INT64)
|
||||
observeProperty("sid", MPVLib.MpvFormat.MPV_FORMAT_INT64)
|
||||
observeProperty("width", MPVLib.MpvFormat.MPV_FORMAT_INT64)
|
||||
observeProperty("height", MPVLib.MpvFormat.MPV_FORMAT_INT64)
|
||||
}
|
||||
|
||||
private var mediaItems: List<MediaItem> = emptyList()
|
||||
private var currentMediaItemIndex: Int = INDEX_UNSET
|
||||
private var currentTrackSelectionParameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
|
||||
private var playerError: PlaybackException? = null
|
||||
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
|
||||
private var userInitiatedTransition: Boolean = false
|
||||
|
||||
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
||||
.add(COMMAND_PLAY_PAUSE)
|
||||
.add(COMMAND_PREPARE)
|
||||
.add(COMMAND_STOP)
|
||||
.add(COMMAND_RELEASE)
|
||||
.add(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)
|
||||
.add(COMMAND_SEEK_TO_DEFAULT_POSITION)
|
||||
.add(COMMAND_SEEK_BACK)
|
||||
.add(COMMAND_SEEK_FORWARD)
|
||||
.add(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
|
||||
.add(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
|
||||
.add(COMMAND_SET_SPEED_AND_PITCH)
|
||||
.add(COMMAND_GET_VOLUME)
|
||||
.add(COMMAND_SET_VOLUME)
|
||||
.add(COMMAND_SET_VIDEO_SURFACE)
|
||||
.add(COMMAND_SET_MEDIA_ITEM)
|
||||
.add(COMMAND_GET_CURRENT_MEDIA_ITEM)
|
||||
.add(COMMAND_GET_METADATA)
|
||||
.add(COMMAND_GET_TIMELINE)
|
||||
.add(COMMAND_GET_TRACKS)
|
||||
.add(COMMAND_SET_TRACK_SELECTION_PARAMETERS)
|
||||
.build()
|
||||
|
||||
override fun getApplicationLooper(): Looper = applicationLooper
|
||||
|
||||
override fun addListener(listener: Player.Listener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeListener(listener: Player.Listener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun setMediaItems(mediaItems: List<MediaItem>, resetPosition: Boolean) {
|
||||
setMediaItems(
|
||||
mediaItems,
|
||||
if (resetPosition) 0 else INDEX_UNSET,
|
||||
if (resetPosition) 0L else TIME_UNSET
|
||||
)
|
||||
}
|
||||
|
||||
override fun setMediaItems(
|
||||
mediaItems: List<MediaItem>,
|
||||
startIndex: Int,
|
||||
startPositionMs: Long
|
||||
) {
|
||||
val prev = currentMediaItemIndex
|
||||
val targetIndex = when {
|
||||
mediaItems.isEmpty() -> INDEX_UNSET
|
||||
startIndex in mediaItems.indices -> startIndex
|
||||
else -> 0
|
||||
}
|
||||
|
||||
this.mediaItems = mediaItems
|
||||
currentMediaItemIndex = targetIndex
|
||||
val target = if (targetIndex != INDEX_UNSET) mediaItems[targetIndex] else null
|
||||
playlistMetadata = target?.mediaMetadata ?: MediaMetadata.EMPTY
|
||||
playerError = null
|
||||
|
||||
mpv.command(arrayOf("stop"))
|
||||
|
||||
if (target != null) {
|
||||
val targetMs = if (startPositionMs == TIME_UNSET) 0L else startPositionMs.coerceAtLeast(0L)
|
||||
mpv.setPropertyDouble("start", targetMs / 1000.0)
|
||||
|
||||
val uri = target.localConfiguration?.uri?.toString()
|
||||
if (!uri.isNullOrEmpty()) {
|
||||
mpv.command(arrayOf("loadfile", uri, "replace"))
|
||||
target.localConfiguration?.subtitleConfigurations?.forEach { subtitle ->
|
||||
mpv.command(arrayOf("sub-add", subtitle.uri.toString(), "cached"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val events = arrayListOf(
|
||||
EVENT_TIMELINE_CHANGED,
|
||||
EVENT_MEDIA_METADATA_CHANGED,
|
||||
EVENT_PLAYLIST_METADATA_CHANGED
|
||||
)
|
||||
if (prev != currentMediaItemIndex) {
|
||||
events.add(EVENT_MEDIA_ITEM_TRANSITION)
|
||||
}
|
||||
val transitionReason = if (userInitiatedTransition) {
|
||||
MEDIA_ITEM_TRANSITION_REASON_SEEK
|
||||
} else {
|
||||
MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
|
||||
}
|
||||
userInitiatedTransition = false
|
||||
notifyListeners(events.toTypedArray()) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
||||
it.onMediaMetadataChanged(mediaMetadata)
|
||||
it.onPlaylistMetadataChanged(playlistMetadata)
|
||||
if (prev != currentMediaItemIndex) {
|
||||
it.onMediaItemTransition(
|
||||
currentMediaItem,
|
||||
transitionReason
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addMediaItems(
|
||||
p0: Int,
|
||||
p1: List<MediaItem>
|
||||
) = Unit
|
||||
|
||||
override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) = Unit
|
||||
|
||||
override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List<MediaItem>) {
|
||||
setMediaItems(mediaItems)
|
||||
}
|
||||
|
||||
override fun removeMediaItems(fromIndex: Int, toIndex: Int) {
|
||||
val prev = currentMediaItemIndex
|
||||
mediaItems = emptyList()
|
||||
currentMediaItemIndex = INDEX_UNSET
|
||||
playlistMetadata = MediaMetadata.EMPTY
|
||||
mpv.command(arrayOf("stop"))
|
||||
|
||||
var events = arrayListOf(
|
||||
EVENT_TIMELINE_CHANGED,
|
||||
EVENT_MEDIA_METADATA_CHANGED,
|
||||
EVENT_PLAYLIST_METADATA_CHANGED
|
||||
)
|
||||
if (prev != INDEX_UNSET) {
|
||||
events.add(EVENT_MEDIA_ITEM_TRANSITION)
|
||||
}
|
||||
notifyListeners(events.toTypedArray()) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
||||
it.onMediaMetadataChanged(mediaMetadata)
|
||||
it.onPlaylistMetadataChanged(playlistMetadata)
|
||||
if (prev != INDEX_UNSET) {
|
||||
it.onMediaItemTransition(null, MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAvailableCommands(): Player.Commands = availableCommands
|
||||
|
||||
override fun prepare() = Unit
|
||||
|
||||
override fun getPlaybackState(): Int =
|
||||
when (true) {
|
||||
(currentMediaItemIndex == INDEX_UNSET) -> STATE_IDLE
|
||||
mpv.getPropertyBoolean("paused-for-cache") -> STATE_BUFFERING
|
||||
mpv.getPropertyBoolean("eof-reached") -> STATE_ENDED
|
||||
else -> STATE_READY
|
||||
}
|
||||
|
||||
override fun getPlaybackSuppressionReason(): Int = PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
|
||||
override fun getPlayerError(): PlaybackException? = playerError
|
||||
|
||||
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||
mpv.setPropertyBoolean("pause", !playWhenReady)
|
||||
}
|
||||
|
||||
override fun getPlayWhenReady(): Boolean = !(mpv.getPropertyBoolean("pause") ?: true)
|
||||
|
||||
override fun setRepeatMode(repeatMode: Int) = Unit
|
||||
|
||||
override fun getRepeatMode(): Int = REPEAT_MODE_OFF
|
||||
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) = Unit
|
||||
|
||||
override fun getShuffleModeEnabled(): Boolean = false
|
||||
|
||||
override fun isLoading(): Boolean = mpv.getPropertyBoolean("paused-for-cache") ?: false
|
||||
|
||||
override fun seekTo(
|
||||
mediaItemIndex: Int,
|
||||
positionMs: Long,
|
||||
seekCommand: Int,
|
||||
isRepeatingCurrentItem: Boolean
|
||||
) {
|
||||
val targetIndex = if (mediaItemIndex == INDEX_UNSET) currentMediaItemIndex else mediaItemIndex
|
||||
if (targetIndex == INDEX_UNSET || targetIndex !in mediaItems.indices) return
|
||||
|
||||
if (targetIndex != currentMediaItemIndex) {
|
||||
userInitiatedTransition = true
|
||||
setMediaItems(mediaItems, targetIndex, positionMs)
|
||||
return
|
||||
}
|
||||
|
||||
val targetMs = if (positionMs == TIME_UNSET) 0L else positionMs.coerceAtLeast(0L)
|
||||
mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute"))
|
||||
}
|
||||
|
||||
override fun getSeekBackIncrement(): Long = 5_000L
|
||||
|
||||
override fun getSeekForwardIncrement(): Long = 15_000L
|
||||
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||
mpv.setPropertyDouble("speed", playbackParameters.speed.coerceAtLeast(0f).toDouble())
|
||||
}
|
||||
|
||||
override fun getPlaybackParameters(): PlaybackParameters {
|
||||
return PlaybackParameters(
|
||||
(mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f)
|
||||
)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
mpv.command(arrayOf("stop"))
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
mpv.removeObserver(this)
|
||||
clearVideoSurface()
|
||||
mpv.destroy()
|
||||
}
|
||||
|
||||
override fun getCurrentTracks(): Tracks {
|
||||
val count = mpv.getPropertyInt("track-list/count") ?: 0
|
||||
if (count <= 0) return Tracks.EMPTY
|
||||
|
||||
val selectedVideo = mpv.getPropertyInt("vid")
|
||||
val selectedAudio = mpv.getPropertyInt("aid")
|
||||
val selectedSubtitle = mpv.getPropertyInt("sid")
|
||||
|
||||
val groupedVideoTracks = LinkedHashMap<String, MutableList<Pair<Int, Format>>>()
|
||||
val result = ArrayList<Group>()
|
||||
|
||||
for (i in 0 until count) {
|
||||
val base = "track-list/$i"
|
||||
val type = mpv.getPropertyString("$base/type") ?: continue
|
||||
val id = mpv.getPropertyInt("$base/id") ?: continue
|
||||
val bitrate = mpv.getPropertyInt("$base/hls-bitrate")
|
||||
?: mpv.getPropertyInt("$base/demux-bitrate")
|
||||
val isDefault = mpv.getPropertyBoolean("$base/default") ?: false
|
||||
val isForced = mpv.getPropertyBoolean("$base/forced") ?: false
|
||||
|
||||
val format = Format.Builder()
|
||||
.setId(id.toString())
|
||||
.setLabel(mpv.getPropertyString("$base/title"))
|
||||
.setLanguage(mpv.getPropertyString("$base/lang"))
|
||||
.setCodecs(mpv.getPropertyString("$base/codec"))
|
||||
.setAverageBitrate(bitrate ?: NO_VALUE)
|
||||
.setSelectionFlags(
|
||||
when {
|
||||
isDefault && isForced -> SELECTION_FLAG_DEFAULT or SELECTION_FLAG_FORCED
|
||||
isDefault -> SELECTION_FLAG_DEFAULT
|
||||
isForced -> SELECTION_FLAG_FORCED
|
||||
else -> 0
|
||||
}
|
||||
)
|
||||
.setSampleMimeType(when(type) {
|
||||
"video" -> "video/x-unknown"
|
||||
"audio" -> "audio/x-unknown"
|
||||
"sub" -> "text/x-unknown"
|
||||
else -> null
|
||||
})
|
||||
.apply {
|
||||
if (type == "video") {
|
||||
val width = mpv.getPropertyInt("$base/demux-w")
|
||||
val height = mpv.getPropertyInt("$base/demux-h")
|
||||
if (width != null && width > 0 && height != null && height > 0) {
|
||||
setWidth(width).setHeight(height)
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
|
||||
when (type) {
|
||||
"video" -> {
|
||||
val programIds = mutableListOf<Int>()
|
||||
val programIdsCount = mpv.getPropertyInt("$base/program-ids/count") ?: 0
|
||||
for (programIndex in -1 until programIdsCount) {
|
||||
val programId = mpv.getPropertyInt("$base/program-ids/$programIndex")
|
||||
if (programId != null) {
|
||||
programIds.add(programId)
|
||||
}
|
||||
}
|
||||
programIds.sort()
|
||||
val programKey =
|
||||
if (programIds.isEmpty()) "none" else programIds.joinToString(",")
|
||||
val key =
|
||||
"programs=$programKey|title=${mpv.getPropertyString("$base/title").orEmpty()}|lang=${
|
||||
mpv.getPropertyString(
|
||||
"$base/lang"
|
||||
).orEmpty()}|codec=${mpv.getPropertyString("$base/codec").orEmpty()}"
|
||||
groupedVideoTracks.getOrPut(key) { mutableListOf() }.add(id to format)
|
||||
}
|
||||
|
||||
"audio" -> {
|
||||
val group = TrackGroup("mpv-audio-$id", format)
|
||||
val selected = booleanArrayOf(selectedAudio == id)
|
||||
val support = intArrayOf(FORMAT_HANDLED)
|
||||
result.add(Group(group, false, support, selected))
|
||||
}
|
||||
|
||||
"sub" -> {
|
||||
val group = TrackGroup("mpv-sub-$id", format)
|
||||
val selected = booleanArrayOf(selectedSubtitle == id)
|
||||
val support = intArrayOf(FORMAT_HANDLED)
|
||||
result.add(Group(group, false, support, selected))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var videoGroupIndex = 0
|
||||
for ((_, entries) in groupedVideoTracks) {
|
||||
val formats = entries.map { it.second }.toTypedArray()
|
||||
val group = TrackGroup("mpv-video-$videoGroupIndex", *formats)
|
||||
val selected = BooleanArray(entries.size) { idx ->
|
||||
selectedVideo == entries[idx].first
|
||||
}
|
||||
val support = IntArray(entries.size) { FORMAT_HANDLED }
|
||||
result.add(Group(group, entries.size > 1, support, selected))
|
||||
videoGroupIndex += 1
|
||||
}
|
||||
|
||||
return Tracks(result)
|
||||
}
|
||||
|
||||
override fun getTrackSelectionParameters(): TrackSelectionParameters =
|
||||
currentTrackSelectionParameters
|
||||
|
||||
override fun setTrackSelectionParameters(trackSelectionParameters: TrackSelectionParameters) {
|
||||
currentTrackSelectionParameters = trackSelectionParameters
|
||||
val videoDisabled = trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_VIDEO)
|
||||
val audioDisabled = trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_AUDIO)
|
||||
val textDisabled = trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_TEXT)
|
||||
|
||||
if (videoDisabled) {
|
||||
mpv.setPropertyString("vid", "no")
|
||||
}
|
||||
if (audioDisabled) {
|
||||
mpv.setPropertyString("aid", "no")
|
||||
}
|
||||
if (textDisabled) {
|
||||
mpv.setPropertyString("sid", "no")
|
||||
}
|
||||
|
||||
var hasVideoOverride = false
|
||||
var hasAudioOverride = false
|
||||
var hasTextOverride = false
|
||||
for (override in trackSelectionParameters.overrides.values) {
|
||||
val selectedIndex = override.trackIndices.firstOrNull() ?: continue
|
||||
if (selectedIndex !in 0 until override.mediaTrackGroup.length) continue
|
||||
val trackId =
|
||||
override.mediaTrackGroup.getFormat(selectedIndex).id?.toIntOrNull() ?: continue
|
||||
|
||||
when (override.type) {
|
||||
TRACK_TYPE_VIDEO -> {
|
||||
hasVideoOverride = true
|
||||
mpv.setPropertyInt("vid", trackId)
|
||||
}
|
||||
TRACK_TYPE_AUDIO -> {
|
||||
hasAudioOverride = true
|
||||
mpv.setPropertyInt("aid", trackId)
|
||||
}
|
||||
TRACK_TYPE_TEXT -> {
|
||||
hasTextOverride = true
|
||||
mpv.setPropertyInt("sid", trackId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val tracks = getCurrentTracks()
|
||||
val videoPreference = selectTrackByPreference(
|
||||
tracks, TRACK_TYPE_VIDEO,
|
||||
trackSelectionParameters.preferredVideoLanguages,
|
||||
trackSelectionParameters.preferredVideoLabels,
|
||||
)
|
||||
if (!videoDisabled && !hasVideoOverride) {
|
||||
videoPreference?.let { mpv.setPropertyInt("vid", it) }
|
||||
?: mpv.setPropertyString("vid", "auto")
|
||||
}
|
||||
val audioPreference = selectTrackByPreference(
|
||||
tracks, TRACK_TYPE_AUDIO,
|
||||
trackSelectionParameters.preferredAudioLanguages,
|
||||
trackSelectionParameters.preferredAudioLabels,
|
||||
)
|
||||
if (!audioDisabled && !hasAudioOverride) {
|
||||
audioPreference?.let { mpv.setPropertyInt("aid", it) }
|
||||
?: mpv.setPropertyString("aid", "auto")
|
||||
}
|
||||
val textPreference = selectTrackByPreference(
|
||||
tracks, TRACK_TYPE_TEXT,
|
||||
trackSelectionParameters.preferredTextLanguages,
|
||||
trackSelectionParameters.preferredTextLabels,
|
||||
)
|
||||
if (!textDisabled && !hasTextOverride) {
|
||||
textPreference?.let { mpv.setPropertyInt("sid", it) }
|
||||
?: mpv.setPropertyString("sid", "auto")
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectTrackByPreference(
|
||||
tracks: Tracks,
|
||||
trackType: Int,
|
||||
languages: List<String>,
|
||||
labels: List<String>,
|
||||
): Int? {
|
||||
val preferredLanguages = languages.filter { it.isNotEmpty() }
|
||||
val preferredLabels = labels.filter { it.isNotEmpty() }
|
||||
|
||||
if (preferredLanguages.isEmpty() && preferredLabels.isEmpty()) return null
|
||||
|
||||
data class Candidate(val id: Int, val langIndex: Int, val labelIndex: Int, val hasDefaultFlag: Boolean)
|
||||
|
||||
val candidates = mutableListOf<Candidate>()
|
||||
|
||||
for (group in tracks.groups) {
|
||||
if (group.type != trackType) continue
|
||||
for (i in 0 until group.length) {
|
||||
val format = group.getTrackFormat(i)
|
||||
val id = format.id?.toIntOrNull() ?: continue
|
||||
|
||||
val langIndex = format.language?.let { preferredLanguages.indexOf(it) } ?: -1
|
||||
val labelIndex = format.label?.let { preferredLabels.indexOf(it) } ?: -1
|
||||
|
||||
val matchesLang = preferredLanguages.isEmpty() || langIndex >= 0
|
||||
val matchesLabel = preferredLabels.isEmpty() || labelIndex >= 0
|
||||
|
||||
if (matchesLang && matchesLabel) {
|
||||
candidates.add(
|
||||
Candidate(
|
||||
id,
|
||||
if (langIndex >= 0) langIndex else Int.MAX_VALUE,
|
||||
if (labelIndex >= 0) labelIndex else Int.MAX_VALUE,
|
||||
(format.selectionFlags and SELECTION_FLAG_DEFAULT) != 0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidates.minWithOrNull(
|
||||
compareBy<Candidate> { it.langIndex }
|
||||
.thenBy { it.labelIndex }
|
||||
.thenByDescending { it.hasDefaultFlag }
|
||||
)?.id
|
||||
}
|
||||
|
||||
override fun getMediaMetadata(): MediaMetadata =
|
||||
getCurrentMediaItem()?.mediaMetadata ?: MediaMetadata.EMPTY
|
||||
|
||||
override fun getPlaylistMetadata(): MediaMetadata = playlistMetadata
|
||||
|
||||
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) {
|
||||
playlistMetadata = mediaMetadata
|
||||
}
|
||||
|
||||
override fun getCurrentTimeline(): Timeline {
|
||||
if (mediaItems.isEmpty()) return Timeline.EMPTY
|
||||
return object : Timeline() {
|
||||
override fun getWindowCount(): Int = mediaItems.size
|
||||
|
||||
override fun getWindow(
|
||||
windowIndex: Int,
|
||||
window: Window,
|
||||
defaultPositionProjectionUs: Long
|
||||
): Window {
|
||||
check(windowIndex in mediaItems.indices)
|
||||
val item = mediaItems[windowIndex]
|
||||
val durationUs = if (windowIndex == currentMediaItemIndex) {
|
||||
val dur = duration
|
||||
if (dur == TIME_UNSET) TIME_UNSET else dur * 1000L
|
||||
} else TIME_UNSET
|
||||
return window.set(
|
||||
windowIndex,
|
||||
item,
|
||||
null,
|
||||
TIME_UNSET,
|
||||
TIME_UNSET,
|
||||
TIME_UNSET,
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
0L,
|
||||
durationUs,
|
||||
0,
|
||||
mediaItems.size-1,
|
||||
0L
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPeriodCount(): Int = mediaItems.size
|
||||
|
||||
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
|
||||
check(periodIndex in mediaItems.indices)
|
||||
val durationUs = if (periodIndex == currentMediaItemIndex) {
|
||||
val dur = duration
|
||||
if (dur == TIME_UNSET) TIME_UNSET else dur * 1000L
|
||||
} else TIME_UNSET
|
||||
return period.set(periodIndex, periodIndex, 0, durationUs, 0L)
|
||||
}
|
||||
|
||||
override fun getIndexOfPeriod(uid: Any): Int {
|
||||
return if (uid is Int && uid in mediaItems.indices) uid else INDEX_UNSET
|
||||
}
|
||||
|
||||
override fun getUidOfPeriod(periodIndex: Int): Any {
|
||||
check(periodIndex in mediaItems.indices)
|
||||
return periodIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentPeriodIndex() = if (currentMediaItemIndex == INDEX_UNSET) INDEX_UNSET else currentMediaItemIndex
|
||||
|
||||
override fun getCurrentMediaItemIndex() = currentMediaItemIndex
|
||||
|
||||
override fun getDuration(): Long {
|
||||
val duration = mpv.getPropertyDouble("duration") ?: return TIME_UNSET
|
||||
if (!duration.isFinite() || duration <= 0.0) return TIME_UNSET
|
||||
return (duration * 1000.0).toLong()
|
||||
}
|
||||
|
||||
override fun getCurrentPosition() =
|
||||
((mpv.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
|
||||
|
||||
override fun getBufferedPosition() =
|
||||
((mpv.getPropertyDouble("demuxer-cache-time") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
|
||||
.coerceAtLeast(0L)
|
||||
|
||||
override fun getTotalBufferedDuration() = bufferedPosition
|
||||
|
||||
override fun isPlayingAd(): Boolean = false
|
||||
|
||||
override fun getCurrentAdGroupIndex(): Int = INDEX_UNSET
|
||||
|
||||
override fun getCurrentAdIndexInAdGroup(): Int = INDEX_UNSET
|
||||
|
||||
override fun getMaxSeekToPreviousPosition() = 3_000L
|
||||
|
||||
override fun getContentPosition() = getCurrentPosition()
|
||||
|
||||
override fun getContentBufferedPosition() = getBufferedPosition()
|
||||
|
||||
override fun getAudioAttributes(): AudioAttributes = AudioAttributes.DEFAULT
|
||||
|
||||
override fun setVolume(volume: Float) {
|
||||
mpv.setPropertyDouble("volume", volume.coerceIn(0f, 1f) * 100.0)
|
||||
}
|
||||
|
||||
override fun getVolume(): Float {
|
||||
return ((mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0) / 100.0).toFloat()
|
||||
}
|
||||
|
||||
override fun mute() {
|
||||
mpv.setPropertyBoolean("mute", true)
|
||||
}
|
||||
|
||||
override fun unmute() {
|
||||
mpv.setPropertyBoolean("mute", false)
|
||||
}
|
||||
|
||||
override fun clearVideoSurface() {
|
||||
mpv.setOptionString("vo", "null")
|
||||
mpv.setOptionString("force-window", "no")
|
||||
mpv.detachSurface()
|
||||
}
|
||||
|
||||
override fun clearVideoSurface(surface: Surface?) {
|
||||
clearVideoSurface()
|
||||
}
|
||||
|
||||
override fun setVideoSurface(surface: Surface?) {
|
||||
mpv.attachSurface(surface ?: return clearVideoSurface())
|
||||
mpv.setOptionString("force-window", "yes")
|
||||
mpv.setOptionString("vo", "gpu-next")
|
||||
}
|
||||
|
||||
fun updateSurfaceSize(width: Int, height: Int) {
|
||||
if (width > 0 && height > 0) {
|
||||
mpv.setPropertyString("android-surface-size", "${width}x${height}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
||||
val holder = surfaceHolder ?: return clearVideoSurface()
|
||||
val surface = holder.surface
|
||||
if (surface != null && surface.isValid) {
|
||||
setVideoSurface(surface)
|
||||
val frame = holder.surfaceFrame
|
||||
val width = frame?.width() ?: 0
|
||||
val height = frame?.height() ?: 0
|
||||
updateSurfaceSize(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
||||
clearVideoSurface()
|
||||
}
|
||||
|
||||
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
val view = surfaceView ?: return clearVideoSurface()
|
||||
setVideoSurfaceHolder(view.holder)
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
val view = surfaceView ?: return clearVideoSurface()
|
||||
clearVideoSurfaceHolder(view.holder)
|
||||
}
|
||||
|
||||
override fun setVideoTextureView(textureView: TextureView?) = Unit
|
||||
|
||||
override fun clearVideoTextureView(textureView: TextureView?) = Unit
|
||||
|
||||
override fun getVideoSize(): VideoSize {
|
||||
val width = mpv.getPropertyInt("width") ?: 0
|
||||
val height = mpv.getPropertyInt("height") ?: 0
|
||||
if (width <= 0 || height <= 0) return VideoSize.UNKNOWN
|
||||
return VideoSize(width, height)
|
||||
}
|
||||
|
||||
override fun getSurfaceSize(): Size = Size.UNKNOWN
|
||||
|
||||
override fun getCurrentCues(): CueGroup = CueGroup.EMPTY_TIME_ZERO
|
||||
|
||||
override fun getDeviceInfo(): DeviceInfo = DeviceInfo.UNKNOWN
|
||||
|
||||
override fun getDeviceVolume(): Int = 0
|
||||
|
||||
override fun isDeviceMuted(): Boolean = false
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceVolume(volume: Int) = Unit
|
||||
|
||||
override fun setDeviceVolume(volume: Int, flags: Int) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun increaseDeviceVolume() = Unit
|
||||
|
||||
override fun increaseDeviceVolume(flags: Int) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun decreaseDeviceVolume() = Unit
|
||||
|
||||
override fun decreaseDeviceVolume(flags: Int) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceMuted(muted: Boolean) = Unit
|
||||
|
||||
override fun setDeviceMuted(muted: Boolean, flags: Int) = Unit
|
||||
|
||||
override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) =
|
||||
Unit
|
||||
|
||||
override fun event(eventId: Int) {
|
||||
when (eventId) {
|
||||
MPVLib.MpvEvent.MPV_EVENT_START_FILE ->
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(STATE_BUFFERING)
|
||||
}
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED ->
|
||||
notifyListeners(
|
||||
arrayOf(
|
||||
EVENT_TIMELINE_CHANGED,
|
||||
EVENT_MEDIA_METADATA_CHANGED,
|
||||
EVENT_PLAYBACK_STATE_CHANGED,
|
||||
EVENT_IS_PLAYING_CHANGED,
|
||||
EVENT_TRACKS_CHANGED
|
||||
)
|
||||
) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
|
||||
it.onMediaMetadataChanged(mediaMetadata)
|
||||
it.onPlaybackStateChanged(STATE_READY)
|
||||
it.onIsPlayingChanged(playWhenReady)
|
||||
it.onTracksChanged(getCurrentTracks())
|
||||
}
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_SEEK,
|
||||
MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART -> {
|
||||
val positionMs = getCurrentPosition()
|
||||
val position = Player.PositionInfo(
|
||||
currentMediaItemIndex,
|
||||
getCurrentMediaItemIndex(),
|
||||
getCurrentMediaItem(),
|
||||
currentMediaItemIndex,
|
||||
getCurrentPeriodIndex(),
|
||||
positionMs,
|
||||
positionMs,
|
||||
INDEX_UNSET,
|
||||
INDEX_UNSET
|
||||
)
|
||||
notifyListeners(EVENT_POSITION_DISCONTINUITY) {
|
||||
it.onPositionDiscontinuity(position, position, DISCONTINUITY_REASON_SEEK)
|
||||
}
|
||||
}
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_END_FILE ->
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(STATE_ENDED)
|
||||
}
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_QUEUE_OVERFLOW -> {
|
||||
playerError = PlaybackException(
|
||||
"mpv event queue overflow",
|
||||
null,
|
||||
PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK
|
||||
)
|
||||
notifyListeners(EVENT_PLAYER_ERROR) {
|
||||
it.onPlayerErrorChanged(playerError)
|
||||
it.onPlayerError(
|
||||
playerError ?: PlaybackException(
|
||||
"unknown",
|
||||
null,
|
||||
PlaybackException.ERROR_CODE_UNSPECIFIED
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String) = Unit
|
||||
|
||||
override fun eventProperty(property: String, value: Long) {
|
||||
when (property) {
|
||||
"vid", "aid", "sid" -> {
|
||||
notifyListeners(EVENT_TRACKS_CHANGED) { it.onTracksChanged(getCurrentTracks()) }
|
||||
}
|
||||
|
||||
"width", "height" -> {
|
||||
notifyListeners(EVENT_VIDEO_SIZE_CHANGED) {
|
||||
it.onVideoSizeChanged(videoSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Double) {
|
||||
when (property) {
|
||||
"duration" ->
|
||||
notifyListeners(arrayOf(EVENT_TIMELINE_CHANGED, EVENT_MEDIA_METADATA_CHANGED)) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
|
||||
it.onMediaMetadataChanged(mediaMetadata)
|
||||
}
|
||||
|
||||
"speed" ->
|
||||
notifyListeners(EVENT_PLAYBACK_PARAMETERS_CHANGED) {
|
||||
it.onPlaybackParametersChanged(
|
||||
PlaybackParameters(
|
||||
value.toFloat().coerceAtLeast(0f)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"volume" ->
|
||||
notifyListeners(EVENT_VOLUME_CHANGED) {
|
||||
it.onVolumeChanged((value.coerceIn(0.0, 100.0) / 100.0).toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Boolean) {
|
||||
when (property) {
|
||||
"pause" ->
|
||||
notifyListeners(arrayOf(EVENT_PLAY_WHEN_READY_CHANGED, EVENT_IS_PLAYING_CHANGED)) {
|
||||
it.onPlayWhenReadyChanged(
|
||||
!value,
|
||||
PLAY_WHEN_READY_CHANGE_REASON_REMOTE
|
||||
)
|
||||
it.onIsPlayingChanged(!value)
|
||||
}
|
||||
|
||||
"core-idle", "eof-reached" ->
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(getPlaybackState())
|
||||
}
|
||||
|
||||
"paused-for-cache" -> {
|
||||
notifyListeners(arrayOf(EVENT_IS_LOADING_CHANGED, EVENT_PLAYBACK_STATE_CHANGED)) {
|
||||
it.onIsLoadingChanged(value)
|
||||
it.onPlaybackStateChanged(getPlaybackState())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: String) = Unit
|
||||
|
||||
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
|
||||
val player: Player = runOnMainThreadSync {
|
||||
// ExoPlayer.Builder(ctx).build()
|
||||
MpvPlayer(ctx)
|
||||
VlcPlayer(ctx)
|
||||
}
|
||||
override val eventMap = EventMap(player)
|
||||
|
||||
@@ -103,15 +103,9 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
): MediaItem {
|
||||
// val headers = Bundle().apply {
|
||||
// putStringArrayList(
|
||||
// MpvPlayer.REQUEST_HEADER_NAMES_KEY,
|
||||
// ArrayList(src.headers.keys)
|
||||
// )
|
||||
// putStringArrayList(
|
||||
// MpvPlayer.REQUEST_HEADER_VALUES_KEY,
|
||||
// ArrayList(src.headers.values)
|
||||
// )
|
||||
// source.startTime?.let {
|
||||
// putLong(MpvPlayer.REQUEST_START_MS_KEY, (it.coerceAtLeast(0.0) * 1000.0).toLong())
|
||||
// VlcPlayer.REQUEST_HEADER_NAMES_KEY,
|
||||
// VlcPlayer.REQUEST_HEADER_VALUES_KEY,
|
||||
// putLong(VlcPlayer.REQUEST_START_MS_KEY, (it.coerceAtLeast(0.0) * 1000.0).toLong())
|
||||
// }
|
||||
// }
|
||||
return MediaItem.Builder()
|
||||
|
||||
@@ -0,0 +1,752 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.TextureView
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.BasePlayer
|
||||
import androidx.media3.common.C.FORMAT_HANDLED
|
||||
import androidx.media3.common.C.INDEX_UNSET
|
||||
import androidx.media3.common.C.SELECTION_FLAG_DEFAULT
|
||||
import androidx.media3.common.C.TIME_UNSET
|
||||
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
||||
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||
import androidx.media3.common.DeviceInfo
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.TrackGroup
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.Tracks.Group
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.Clock
|
||||
import androidx.media3.common.util.ListenerSet
|
||||
import androidx.media3.common.util.Size
|
||||
import org.videolan.libvlc.LibVLC
|
||||
import org.videolan.libvlc.Media
|
||||
import org.videolan.libvlc.MediaPlayer
|
||||
import org.videolan.libvlc.interfaces.IMedia
|
||||
import org.videolan.libvlc.interfaces.IVLCVout
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class VlcPlayer(ctx: Context) : BasePlayer(), MediaPlayer.EventListener {
|
||||
private val applicationLooper: Looper = Looper.getMainLooper()
|
||||
private val applicationHandler = Handler(applicationLooper)
|
||||
private val listeners = ListenerSet<Player.Listener>(
|
||||
applicationLooper,
|
||||
Clock.DEFAULT
|
||||
) { listener, flags ->
|
||||
listener.onEvents(this, Player.Events(flags))
|
||||
}
|
||||
|
||||
private fun notifyListeners(eventFlag: Int, callback: (Player.Listener) -> Unit) =
|
||||
notifyListeners(arrayOf(eventFlag), callback)
|
||||
|
||||
private fun notifyListeners(eventFlags: Array<Int>, callback: (Player.Listener) -> Unit) {
|
||||
val flush = {
|
||||
eventFlags.forEach { listeners.queueEvent(it, callback) }
|
||||
listeners.flushEvents()
|
||||
}
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) flush() else applicationHandler.post(flush)
|
||||
}
|
||||
|
||||
private val libVLC = LibVLC(ctx, arrayListOf(
|
||||
"--network-caching=1500",
|
||||
"--file-caching=1500",
|
||||
"--no-save-config",
|
||||
"--no-stats",
|
||||
"--no-osd",
|
||||
"--quiet"
|
||||
))
|
||||
|
||||
private val player = MediaPlayer(libVLC)
|
||||
|
||||
private val vlcVout: IVLCVout = player.vlcVout
|
||||
|
||||
private var mediaItems: List<MediaItem> = emptyList()
|
||||
private var currentMediaItemIndex: Int = INDEX_UNSET
|
||||
private var currentTrackSelectionParameters = TrackSelectionParameters.Builder().build()
|
||||
private var playerError: PlaybackException? = null
|
||||
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
|
||||
private var userInitiatedTransition: Boolean = false
|
||||
private var cachedBufferedPosition: Long = 0L
|
||||
|
||||
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
||||
.add(COMMAND_PLAY_PAUSE)
|
||||
.add(COMMAND_PREPARE)
|
||||
.add(COMMAND_STOP)
|
||||
.add(COMMAND_RELEASE)
|
||||
.add(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)
|
||||
.add(COMMAND_SEEK_TO_DEFAULT_POSITION)
|
||||
.add(COMMAND_SEEK_BACK)
|
||||
.add(COMMAND_SEEK_FORWARD)
|
||||
.add(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
|
||||
.add(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
|
||||
.add(COMMAND_SET_SPEED_AND_PITCH)
|
||||
.add(COMMAND_GET_VOLUME)
|
||||
.add(COMMAND_SET_VOLUME)
|
||||
.add(COMMAND_SET_VIDEO_SURFACE)
|
||||
.add(COMMAND_SET_MEDIA_ITEM)
|
||||
.add(COMMAND_GET_CURRENT_MEDIA_ITEM)
|
||||
.add(COMMAND_GET_METADATA)
|
||||
.add(COMMAND_GET_TIMELINE)
|
||||
.add(COMMAND_GET_TRACKS)
|
||||
.add(COMMAND_SET_TRACK_SELECTION_PARAMETERS)
|
||||
.build()
|
||||
|
||||
init {
|
||||
player.setEventListener(this)
|
||||
}
|
||||
|
||||
override fun onEvent(event: MediaPlayer.Event) {
|
||||
when (event.type) {
|
||||
MediaPlayer.Event.Opening -> {
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(STATE_BUFFERING)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.Playing -> {
|
||||
notifyListeners(
|
||||
arrayOf(
|
||||
EVENT_TIMELINE_CHANGED,
|
||||
EVENT_MEDIA_METADATA_CHANGED,
|
||||
EVENT_PLAYBACK_STATE_CHANGED,
|
||||
EVENT_IS_PLAYING_CHANGED,
|
||||
EVENT_TRACKS_CHANGED
|
||||
)
|
||||
) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
|
||||
it.onMediaMetadataChanged(mediaMetadata)
|
||||
it.onPlaybackStateChanged(STATE_READY)
|
||||
it.onIsPlayingChanged(true)
|
||||
it.onTracksChanged(getCurrentTracks())
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.Paused -> {
|
||||
notifyListeners(
|
||||
arrayOf(EVENT_PLAY_WHEN_READY_CHANGED, EVENT_IS_PLAYING_CHANGED)
|
||||
) {
|
||||
it.onPlayWhenReadyChanged(false, PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
|
||||
it.onIsPlayingChanged(false)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.Stopped -> {
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(STATE_IDLE)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.EndReached -> {
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(STATE_ENDED)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.Buffering -> {
|
||||
cachedBufferedPosition = (event.buffering * getDuration()).toLong()
|
||||
notifyListeners(arrayOf(EVENT_IS_LOADING_CHANGED, EVENT_PLAYBACK_STATE_CHANGED)) {
|
||||
it.onIsLoadingChanged(true)
|
||||
it.onPlaybackStateChanged(STATE_BUFFERING)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.EncounteredError -> {
|
||||
val error = PlaybackException(
|
||||
"VLC playback error",
|
||||
null,
|
||||
PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK
|
||||
)
|
||||
playerError = error
|
||||
notifyListeners(EVENT_PLAYER_ERROR) {
|
||||
it.onPlayerErrorChanged(error)
|
||||
it.onPlayerError(error)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.TimeChanged -> {
|
||||
notifyListeners(EVENT_POSITION_DISCONTINUITY) {
|
||||
val positionMs = getCurrentPosition()
|
||||
val position = Player.PositionInfo(
|
||||
currentMediaItemIndex,
|
||||
currentMediaItemIndex,
|
||||
getCurrentMediaItem(),
|
||||
currentMediaItemIndex,
|
||||
currentMediaItemIndex,
|
||||
positionMs,
|
||||
positionMs,
|
||||
INDEX_UNSET,
|
||||
INDEX_UNSET
|
||||
)
|
||||
it.onPositionDiscontinuity(position, position, DISCONTINUITY_REASON_SEEK)
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.ESAdded,
|
||||
MediaPlayer.Event.ESDeleted,
|
||||
MediaPlayer.Event.ESSelected -> {
|
||||
notifyListeners(EVENT_TRACKS_CHANGED) {
|
||||
it.onTracksChanged(getCurrentTracks())
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.LengthChanged -> {
|
||||
notifyListeners(EVENT_TIMELINE_CHANGED) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getApplicationLooper(): Looper = applicationLooper
|
||||
|
||||
override fun addListener(listener: Player.Listener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeListener(listener: Player.Listener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun setMediaItems(mediaItems: List<MediaItem>, resetPosition: Boolean) {
|
||||
setMediaItems(
|
||||
mediaItems,
|
||||
if (resetPosition) 0 else INDEX_UNSET,
|
||||
if (resetPosition) 0L else TIME_UNSET
|
||||
)
|
||||
}
|
||||
|
||||
override fun setMediaItems(
|
||||
mediaItems: List<MediaItem>,
|
||||
startIndex: Int,
|
||||
startPositionMs: Long
|
||||
) {
|
||||
val prev = currentMediaItemIndex
|
||||
val targetIndex = when {
|
||||
mediaItems.isEmpty() -> INDEX_UNSET
|
||||
startIndex in mediaItems.indices -> startIndex
|
||||
else -> 0
|
||||
}
|
||||
|
||||
this.mediaItems = mediaItems
|
||||
currentMediaItemIndex = targetIndex
|
||||
playlistMetadata = mediaItems.getOrNull(targetIndex)?.mediaMetadata ?: MediaMetadata.EMPTY
|
||||
playerError = null
|
||||
|
||||
player.stop()
|
||||
|
||||
mediaItems.getOrNull(targetIndex)?.let { item ->
|
||||
val uri = item.localConfiguration?.uri?.toString()
|
||||
if (!uri.isNullOrEmpty()) {
|
||||
val media = Media(libVLC, uri.toUri())
|
||||
media.setHWDecoderEnabled(true, false)
|
||||
|
||||
item.localConfiguration?.subtitleConfigurations?.forEach { subtitle ->
|
||||
media.addSlave(
|
||||
IMedia.Slave(IMedia.Slave.Type.Subtitle, 0, subtitle.uri.toString())
|
||||
)
|
||||
}
|
||||
|
||||
val targetMs = startPositionMs.coerceAtLeast(0L).takeIf { it != TIME_UNSET } ?: 0L
|
||||
media.addOption(":start-time=${targetMs / 1000.0}")
|
||||
|
||||
player.setMedia(media)
|
||||
}
|
||||
}
|
||||
|
||||
val events = mutableListOf(
|
||||
EVENT_TIMELINE_CHANGED,
|
||||
EVENT_MEDIA_METADATA_CHANGED,
|
||||
EVENT_PLAYLIST_METADATA_CHANGED
|
||||
)
|
||||
if (prev != currentMediaItemIndex) events += EVENT_MEDIA_ITEM_TRANSITION
|
||||
val reason = if (userInitiatedTransition) MEDIA_ITEM_TRANSITION_REASON_SEEK else MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
|
||||
userInitiatedTransition = false
|
||||
notifyListeners(events.toTypedArray()) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
||||
it.onMediaMetadataChanged(mediaMetadata)
|
||||
it.onPlaylistMetadataChanged(playlistMetadata)
|
||||
if (prev != currentMediaItemIndex) {
|
||||
it.onMediaItemTransition(currentMediaItem, reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addMediaItems(
|
||||
p0: Int,
|
||||
p1: List<MediaItem>
|
||||
) = Unit
|
||||
|
||||
override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) = Unit
|
||||
|
||||
override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List<MediaItem>) {
|
||||
setMediaItems(mediaItems)
|
||||
}
|
||||
|
||||
override fun removeMediaItems(fromIndex: Int, toIndex: Int) {
|
||||
val prev = currentMediaItemIndex
|
||||
mediaItems = emptyList()
|
||||
currentMediaItemIndex = INDEX_UNSET
|
||||
playlistMetadata = MediaMetadata.EMPTY
|
||||
player.stop()
|
||||
|
||||
val events = mutableListOf(
|
||||
EVENT_TIMELINE_CHANGED,
|
||||
EVENT_MEDIA_METADATA_CHANGED,
|
||||
EVENT_PLAYLIST_METADATA_CHANGED
|
||||
)
|
||||
if (prev != INDEX_UNSET) events += EVENT_MEDIA_ITEM_TRANSITION
|
||||
notifyListeners(events.toTypedArray()) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
||||
it.onMediaMetadataChanged(mediaMetadata)
|
||||
it.onPlaylistMetadataChanged(playlistMetadata)
|
||||
if (prev != INDEX_UNSET) {
|
||||
it.onMediaItemTransition(null, MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAvailableCommands(): Player.Commands = availableCommands
|
||||
|
||||
override fun prepare() {
|
||||
player.play()
|
||||
}
|
||||
|
||||
override fun getPlaybackState(): Int =
|
||||
when {
|
||||
currentMediaItemIndex == INDEX_UNSET -> STATE_IDLE
|
||||
player.media == null -> STATE_IDLE
|
||||
player.isPlaying -> STATE_READY
|
||||
player.isSeekable && player.time >= player.length && player.length > 0 -> STATE_ENDED
|
||||
else -> STATE_READY
|
||||
}
|
||||
|
||||
override fun getPlaybackSuppressionReason(): Int = PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
|
||||
override fun getPlayerError(): PlaybackException? = playerError
|
||||
|
||||
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||
if (playWhenReady) player.play() else player.pause()
|
||||
}
|
||||
|
||||
override fun getPlayWhenReady(): Boolean = player.isPlaying
|
||||
|
||||
override fun setRepeatMode(repeatMode: Int) = Unit
|
||||
|
||||
override fun getRepeatMode(): Int = REPEAT_MODE_OFF
|
||||
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) = Unit
|
||||
|
||||
override fun getShuffleModeEnabled(): Boolean = false
|
||||
|
||||
override fun isLoading() = player.media?.state == IMedia.State.Opening
|
||||
|
||||
override fun seekTo(
|
||||
mediaItemIndex: Int,
|
||||
positionMs: Long,
|
||||
seekCommand: Int,
|
||||
isRepeatingCurrentItem: Boolean
|
||||
) {
|
||||
val targetIndex = if (mediaItemIndex == INDEX_UNSET) currentMediaItemIndex else mediaItemIndex
|
||||
if (targetIndex == INDEX_UNSET || targetIndex !in mediaItems.indices) return
|
||||
|
||||
if (targetIndex != currentMediaItemIndex) {
|
||||
userInitiatedTransition = true
|
||||
setMediaItems(mediaItems, targetIndex, positionMs)
|
||||
return
|
||||
}
|
||||
|
||||
player.time = positionMs.coerceAtLeast(0L).takeIf { it != TIME_UNSET } ?: 0L
|
||||
}
|
||||
|
||||
override fun getSeekBackIncrement(): Long = 5_000L
|
||||
|
||||
override fun getSeekForwardIncrement(): Long = 15_000L
|
||||
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||
player.rate = playbackParameters.speed.coerceAtLeast(0f)
|
||||
}
|
||||
|
||||
override fun getPlaybackParameters(): PlaybackParameters =
|
||||
PlaybackParameters(player.rate.coerceAtLeast(0f))
|
||||
|
||||
override fun stop() {
|
||||
player.stop()
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
player.setEventListener(null)
|
||||
player.stop()
|
||||
clearVideoSurface()
|
||||
player.release()
|
||||
libVLC.release()
|
||||
}
|
||||
|
||||
override fun getCurrentTracks(): Tracks {
|
||||
val result = ArrayList<Group>()
|
||||
val selectedVideo = player.videoTrack
|
||||
val selectedAudio = player.audioTrack
|
||||
val selectedSubtitle = player.spuTrack
|
||||
|
||||
val videoTracks = player.videoTracks
|
||||
if (!videoTracks.isNullOrEmpty()) {
|
||||
val videoFormats = videoTracks.map { track ->
|
||||
Format.Builder()
|
||||
.setId(track.id.toString())
|
||||
.setLabel(track.name)
|
||||
.setSampleMimeType("video/x-unknown")
|
||||
.build()
|
||||
}
|
||||
val group = TrackGroup("vlc-video", *videoFormats.toTypedArray())
|
||||
val selected = BooleanArray(videoFormats.size) { idx ->
|
||||
selectedVideo == videoTracks[idx].id
|
||||
}
|
||||
val support = IntArray(videoFormats.size) { FORMAT_HANDLED }
|
||||
result.add(Group(group, videoFormats.size > 1, support, selected))
|
||||
}
|
||||
|
||||
player.audioTracks?.forEach { track ->
|
||||
val format = Format.Builder()
|
||||
.setId(track.id.toString())
|
||||
.setLabel(track.name)
|
||||
.setSampleMimeType("audio/x-unknown")
|
||||
.build()
|
||||
val group = TrackGroup("vlc-audio-${track.id}", format)
|
||||
val selected = booleanArrayOf(selectedAudio == track.id)
|
||||
val support = intArrayOf(FORMAT_HANDLED)
|
||||
result.add(Group(group, false, support, selected))
|
||||
}
|
||||
|
||||
player.spuTracks?.forEach { track ->
|
||||
if (track.id < 0) return@forEach
|
||||
val format = Format.Builder()
|
||||
.setId(track.id.toString())
|
||||
.setLabel(track.name)
|
||||
.setSampleMimeType("text/x-unknown")
|
||||
.build()
|
||||
val group = TrackGroup("vlc-sub-${track.id}", format)
|
||||
val selected = booleanArrayOf(selectedSubtitle == track.id)
|
||||
val support = intArrayOf(FORMAT_HANDLED)
|
||||
result.add(Group(group, false, support, selected))
|
||||
}
|
||||
|
||||
return if (result.isEmpty()) Tracks.EMPTY else Tracks(result)
|
||||
}
|
||||
|
||||
override fun getTrackSelectionParameters(): TrackSelectionParameters =
|
||||
currentTrackSelectionParameters
|
||||
|
||||
override fun setTrackSelectionParameters(trackSelectionParameters: TrackSelectionParameters) {
|
||||
currentTrackSelectionParameters = trackSelectionParameters
|
||||
val videoDisabled = trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_VIDEO)
|
||||
val audioDisabled = trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_AUDIO)
|
||||
val textDisabled = trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_TEXT)
|
||||
|
||||
if (videoDisabled) player.setVideoTrackEnabled(false)
|
||||
if (audioDisabled) player.setAudioTrack(-1)
|
||||
if (textDisabled) player.setSpuTrack(-1)
|
||||
|
||||
val tracks = getCurrentTracks()
|
||||
|
||||
var hasVideoOverride = false
|
||||
var hasAudioOverride = false
|
||||
var hasTextOverride = false
|
||||
|
||||
for (override in trackSelectionParameters.overrides.values) {
|
||||
val selectedIndex = override.trackIndices.firstOrNull() ?: continue
|
||||
if (selectedIndex !in 0 until override.mediaTrackGroup.length) continue
|
||||
val trackId = override.mediaTrackGroup.getFormat(selectedIndex).id?.toIntOrNull() ?: continue
|
||||
|
||||
when (override.type) {
|
||||
TRACK_TYPE_VIDEO -> {
|
||||
hasVideoOverride = true
|
||||
player.setVideoTrackEnabled(true)
|
||||
player.setVideoTrack(trackId)
|
||||
}
|
||||
TRACK_TYPE_AUDIO -> {
|
||||
hasAudioOverride = true
|
||||
player.setAudioTrack(trackId)
|
||||
}
|
||||
TRACK_TYPE_TEXT -> {
|
||||
hasTextOverride = true
|
||||
player.setSpuTrack(trackId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!videoDisabled && !hasVideoOverride) {
|
||||
val videoPreference = selectTrackByPreference(
|
||||
tracks, TRACK_TYPE_VIDEO,
|
||||
trackSelectionParameters.preferredVideoLanguages,
|
||||
trackSelectionParameters.preferredVideoLabels,
|
||||
)
|
||||
player.setVideoTrackEnabled(true)
|
||||
videoPreference?.let { player.setVideoTrack(it) }
|
||||
}
|
||||
if (!audioDisabled && !hasAudioOverride) {
|
||||
val audioPreference = selectTrackByPreference(
|
||||
tracks, TRACK_TYPE_AUDIO,
|
||||
trackSelectionParameters.preferredAudioLanguages,
|
||||
trackSelectionParameters.preferredAudioLabels,
|
||||
)
|
||||
audioPreference?.let { player.setAudioTrack(it) }
|
||||
}
|
||||
if (!textDisabled && !hasTextOverride) {
|
||||
val textPreference = selectTrackByPreference(
|
||||
tracks, TRACK_TYPE_TEXT,
|
||||
trackSelectionParameters.preferredTextLanguages,
|
||||
trackSelectionParameters.preferredTextLabels,
|
||||
)
|
||||
textPreference?.let { player.setSpuTrack(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectTrackByPreference(
|
||||
tracks: Tracks,
|
||||
trackType: Int,
|
||||
languages: List<String>,
|
||||
labels: List<String>,
|
||||
): Int? {
|
||||
val preferredLanguages = languages.filter { it.isNotEmpty() }
|
||||
val preferredLabels = labels.filter { it.isNotEmpty() }
|
||||
if (preferredLanguages.isEmpty() && preferredLabels.isEmpty()) return null
|
||||
|
||||
data class Candidate(val id: Int, val langIndex: Int, val labelIndex: Int, val hasDefaultFlag: Boolean)
|
||||
|
||||
val candidates = tracks.groups
|
||||
.filter { it.type == trackType }
|
||||
.flatMap { group ->
|
||||
(0 until group.length).mapNotNull { i ->
|
||||
val format = group.getTrackFormat(i)
|
||||
val id = format.id?.toIntOrNull() ?: return@mapNotNull null
|
||||
|
||||
val langIndex = format.language?.let { lang ->
|
||||
preferredLanguages.indexOfFirst { it.equals(lang, ignoreCase = true) }
|
||||
} ?: -1
|
||||
val labelIndex = format.label?.let { label ->
|
||||
preferredLabels.indexOfFirst { it.equals(label, ignoreCase = true) }
|
||||
} ?: -1
|
||||
|
||||
val matchesLang = preferredLanguages.isEmpty() || langIndex >= 0
|
||||
val matchesLabel = preferredLabels.isEmpty() || labelIndex >= 0
|
||||
if (matchesLang && matchesLabel) {
|
||||
Candidate(
|
||||
id,
|
||||
if (langIndex >= 0) langIndex else Int.MAX_VALUE,
|
||||
if (labelIndex >= 0) labelIndex else Int.MAX_VALUE,
|
||||
(format.selectionFlags and SELECTION_FLAG_DEFAULT) != 0
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
return candidates.minWithOrNull(
|
||||
compareBy<Candidate> { it.langIndex }
|
||||
.thenBy { it.labelIndex }
|
||||
.thenByDescending { it.hasDefaultFlag }
|
||||
)?.id
|
||||
}
|
||||
|
||||
override fun getMediaMetadata(): MediaMetadata =
|
||||
getCurrentMediaItem()?.mediaMetadata ?: MediaMetadata.EMPTY
|
||||
|
||||
override fun getPlaylistMetadata(): MediaMetadata = playlistMetadata
|
||||
|
||||
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) {
|
||||
playlistMetadata = mediaMetadata
|
||||
}
|
||||
|
||||
override fun getCurrentTimeline(): Timeline {
|
||||
if (mediaItems.isEmpty()) return Timeline.EMPTY
|
||||
return object : Timeline() {
|
||||
private fun durationUsForIndex(index: Int): Long {
|
||||
if (index != currentMediaItemIndex) return TIME_UNSET
|
||||
val dur = duration
|
||||
return if (dur == TIME_UNSET) TIME_UNSET else dur * 1000L
|
||||
}
|
||||
|
||||
override fun getWindowCount(): Int = mediaItems.size
|
||||
|
||||
override fun getWindow(
|
||||
windowIndex: Int,
|
||||
window: Window,
|
||||
defaultPositionProjectionUs: Long
|
||||
): Window {
|
||||
check(windowIndex in mediaItems.indices)
|
||||
return window.set(
|
||||
windowIndex,
|
||||
mediaItems[windowIndex],
|
||||
null,
|
||||
TIME_UNSET,
|
||||
TIME_UNSET,
|
||||
TIME_UNSET,
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
0L,
|
||||
durationUsForIndex(windowIndex),
|
||||
0,
|
||||
mediaItems.size - 1,
|
||||
0L
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPeriodCount(): Int = mediaItems.size
|
||||
|
||||
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
|
||||
check(periodIndex in mediaItems.indices)
|
||||
return period.set(periodIndex, periodIndex, 0, durationUsForIndex(periodIndex), 0L)
|
||||
}
|
||||
|
||||
override fun getIndexOfPeriod(uid: Any): Int {
|
||||
return if (uid is Int && uid in mediaItems.indices) uid else INDEX_UNSET
|
||||
}
|
||||
|
||||
override fun getUidOfPeriod(periodIndex: Int): Any {
|
||||
check(periodIndex in mediaItems.indices)
|
||||
return periodIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentPeriodIndex() = currentMediaItemIndex
|
||||
|
||||
override fun getCurrentMediaItemIndex() = currentMediaItemIndex
|
||||
|
||||
override fun getDuration(): Long = player.length.takeIf { it > 0 } ?: TIME_UNSET
|
||||
|
||||
override fun getCurrentPosition() = player.time.coerceAtLeast(0L)
|
||||
|
||||
override fun getBufferedPosition(): Long {
|
||||
val duration = getDuration()
|
||||
if (duration == TIME_UNSET) return getCurrentPosition()
|
||||
return cachedBufferedPosition.coerceAtMost(duration)
|
||||
}
|
||||
|
||||
override fun getTotalBufferedDuration() = bufferedPosition
|
||||
|
||||
override fun isPlayingAd(): Boolean = false
|
||||
|
||||
override fun getCurrentAdGroupIndex(): Int = INDEX_UNSET
|
||||
|
||||
override fun getCurrentAdIndexInAdGroup(): Int = INDEX_UNSET
|
||||
|
||||
override fun getMaxSeekToPreviousPosition() = 3_000L
|
||||
|
||||
override fun getContentPosition() = getCurrentPosition()
|
||||
|
||||
override fun getContentBufferedPosition() = getBufferedPosition()
|
||||
|
||||
override fun getAudioAttributes(): AudioAttributes = AudioAttributes.DEFAULT
|
||||
|
||||
override fun setVolume(volume: Float) {
|
||||
player.volume = (volume * 100).toInt()
|
||||
}
|
||||
|
||||
override fun getVolume(): Float = player.volume / 100f
|
||||
|
||||
override fun mute() {
|
||||
player.volume = 0
|
||||
}
|
||||
|
||||
override fun unmute() {
|
||||
player.volume = 100
|
||||
}
|
||||
|
||||
override fun clearVideoSurface() {
|
||||
vlcVout.detachViews()
|
||||
}
|
||||
|
||||
override fun clearVideoSurface(surface: Surface?) {
|
||||
clearVideoSurface()
|
||||
}
|
||||
|
||||
override fun setVideoSurface(surface: Surface?) {
|
||||
if (surface == null) {
|
||||
clearVideoSurface()
|
||||
return
|
||||
}
|
||||
vlcVout.setVideoSurface(surface, null)
|
||||
vlcVout.attachViews()
|
||||
}
|
||||
|
||||
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
||||
if (surfaceHolder == null) return clearVideoSurface()
|
||||
val surface = surfaceHolder.surface
|
||||
if (surface != null && surface.isValid && !vlcVout.areViewsAttached()) {
|
||||
vlcVout.setVideoSurface(surface, surfaceHolder)
|
||||
vlcVout.attachViews()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
||||
clearVideoSurface()
|
||||
}
|
||||
|
||||
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
vlcVout.setVideoView(surfaceView ?: return clearVideoSurface())
|
||||
vlcVout.attachViews()
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
clearVideoSurface()
|
||||
}
|
||||
|
||||
override fun setVideoTextureView(textureView: TextureView?) = Unit
|
||||
|
||||
override fun clearVideoTextureView(textureView: TextureView?) = Unit
|
||||
|
||||
override fun getVideoSize(): VideoSize {
|
||||
val videoTrack = player.currentVideoTrack ?: return VideoSize.UNKNOWN
|
||||
if (videoTrack.width <= 0 || videoTrack.height <= 0) return VideoSize.UNKNOWN
|
||||
return VideoSize(videoTrack.width, videoTrack.height)
|
||||
}
|
||||
|
||||
override fun getSurfaceSize(): Size = Size.UNKNOWN
|
||||
|
||||
override fun getCurrentCues(): CueGroup = CueGroup.EMPTY_TIME_ZERO
|
||||
|
||||
override fun getDeviceInfo(): DeviceInfo = DeviceInfo.UNKNOWN
|
||||
|
||||
override fun getDeviceVolume(): Int = 0
|
||||
|
||||
override fun isDeviceMuted(): Boolean = false
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceVolume(volume: Int) = Unit
|
||||
|
||||
override fun setDeviceVolume(volume: Int, flags: Int) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun increaseDeviceVolume() = Unit
|
||||
|
||||
override fun increaseDeviceVolume(flags: Int) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun decreaseDeviceVolume() = Unit
|
||||
|
||||
override fun decreaseDeviceVolume(flags: Int) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceMuted(muted: Boolean) = Unit
|
||||
|
||||
override fun setDeviceMuted(muted: Boolean, flags: Int) = Unit
|
||||
|
||||
override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) =
|
||||
Unit
|
||||
}
|
||||
Reference in New Issue
Block a user