feat(android): support chromecast

This commit is contained in:
2026-07-26 19:27:41 +02:00
parent 6423ed0bc5
commit 06efcbf1aa
32 changed files with 902 additions and 57 deletions
+4
View File
@@ -149,6 +149,10 @@ dependencies {
implementation("androidx.media3:media3-ui:1.4.1")
implementation 'androidx.media3:media3-exoplayer:1.10.0'
implementation("androidx.media3:media3-exoplayer-hls:1.10.0")
implementation("androidx.media3:media3-cast:1.10.0")
implementation("com.google.android.gms:play-services-cast-framework:21.5.0")
implementation("androidx.mediarouter:mediarouter:1.7.0")
}
if (isNewArchitectureEnabled()) {
@@ -18,6 +18,7 @@ import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import com.margelo.nitro.omni.BoolProperty
import com.margelo.nitro.omni.CastStatus
import com.margelo.nitro.omni.HybridOmniEventMapSpec
import com.margelo.nitro.omni.NumberProperty
import com.margelo.nitro.omni.PlayerStatus
@@ -25,7 +26,7 @@ import com.margelo.nitro.omni.Rendition
import com.margelo.nitro.omni.Track
@SuppressLint("UnsafeOptInUsageError")
class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Listener {
class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
private val onPrevListeners = mutableSetOf<() -> Unit>()
private val onNextListeners = mutableSetOf<() -> Unit>()
private val onEndListeners = mutableSetOf<() -> Unit>()
@@ -38,13 +39,57 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
private val stateListeners = mutableMapOf<NumberProperty, MutableSet<(Double) -> Unit>>()
private val stateBoolListeners = mutableMapOf<BoolProperty, MutableSet<(Boolean) -> Unit>>()
private val playerStatusListeners = mutableSetOf<(PlayerStatus) -> Unit>()
private val castStatusListeners = mutableSetOf<(CastStatus) -> Unit>()
private var lastMediaItemIndex = 0
private var lastRendition: Rendition? = null
private var lastIsAutoQuality: Boolean? = null
init {
player.addListener(this)
// swapped on cast start/end
private var _player: Player? = null
var player: Player
get() = _player!!
set(value) {
if (_player === value) return
_player?.removeListener(this)
_player = value
value.addListener(this)
lastMediaItemIndex = value.currentMediaItemIndex
lastRendition = null
lastIsAutoQuality = null
// immediatly send all the state
val status = when (value.playbackState) {
STATE_BUFFERING -> PlayerStatus.LOADING
STATE_READY -> PlayerStatus.READYTOPLAY
else -> PlayerStatus.IDLE
}
playerStatusListeners.forEach { it(status) }
stateBoolListeners[BoolProperty.ISPLAYING]?.forEach { it(value.isPlaying) }
stateBoolListeners[BoolProperty.MUTED]?.forEach { it(value.volume <= 0f) }
stateListeners[NumberProperty.VOLUME]?.forEach { it(value.volume.toDouble()) }
stateListeners[NumberProperty.PLAYBACKRATE]?.forEach {
it(value.playbackParameters.speed.toDouble())
}
stateListeners[NumberProperty.CURRENTTIME]?.forEach {
it((value.currentPosition.toDouble() / 1000.0).coerceAtLeast(0.0))
}
stateListeners[NumberProperty.BUFFERED]?.forEach {
it((value.totalBufferedDuration.toDouble() / 1000.0).coerceAtLeast(0.0))
}
stateListeners[NumberProperty.DURATION]?.forEach {
val duration = value.duration
it(
if (duration == C.TIME_UNSET) 0.0 else (duration.toDouble() / 1000.0).coerceAtLeast(
0.0
)
)
}
onTracksChanged(value.currentTracks)
}
fun emitCastStatus(status: CastStatus) {
castStatusListeners.forEach { it(status) }
}
private fun selectedTrack(trackType: Int): Track? {
@@ -83,6 +128,7 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
}
} else null
}
else -> (0 until group.length).firstOrNull { group.isTrackSelected(it) }
} ?: return null
@@ -121,6 +167,7 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
onEndListeners.forEach { it() }
PlayerStatus.IDLE
}
else -> PlayerStatus.IDLE
}
playerStatusListeners.forEach { it(state) }
@@ -213,7 +260,11 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
}
stateListeners[NumberProperty.DURATION]?.forEach {
val duration = player.duration
it(if (duration == C.TIME_UNSET) 0.0 else (duration.toDouble() / 1000.0).coerceAtLeast(0.0))
it(
if (duration == C.TIME_UNSET) 0.0 else (duration.toDouble() / 1000.0).coerceAtLeast(
0.0
)
)
}
}
@@ -241,6 +292,14 @@ class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Li
playerStatusListeners.remove(cb)
}
override fun addCastStatusListener(cb: (value: CastStatus) -> Unit) {
castStatusListeners.add(cb)
}
override fun removeCastStatusListener(cb: (value: CastStatus) -> Unit) {
castStatusListeners.remove(cb)
}
override fun addOnEndListener(cb: () -> Unit) {
onEndListeners.add(cb)
}
@@ -0,0 +1,20 @@
package dev.zoriya.omni
import android.content.Context
import com.google.android.gms.cast.CastMediaControlIntent
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.SessionProvider
class OmniCastOptionsProvider : OptionsProvider {
override fun getCastOptions(context: Context): CastOptions {
val appId = OmniPlayer.receiverApplicationId
?: CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID
return CastOptions.Builder()
.setReceiverApplicationId(appId)
.build()
}
override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? = null
}
@@ -0,0 +1,74 @@
package dev.zoriya.omni
import android.annotation.SuppressLint
import androidx.media3.cast.CastTrackSelector
import androidx.media3.common.C
import androidx.media3.common.TrackGroup
import androidx.media3.common.util.Util
import com.google.android.gms.cast.MediaTrack
import com.google.common.collect.ImmutableSet
@SuppressLint("UnsafeOptInUsageError")
class OmniCastTrackSelector : CastTrackSelector() {
override fun evaluate(request: CastTrackSelectorRequest): CastTrackSelectorResult {
val params = request.trackSelectionParameters
val selections = LinkedHashSet(request.currentlySelectedTrackGroups)
selectType(
selections,
request,
castType = MediaTrack.TYPE_AUDIO,
languages = params.preferredAudioLanguages,
labels = params.preferredAudioLabels,
disabled = C.TRACK_TYPE_AUDIO in params.disabledTrackTypes,
selectDefault = true,
)
selectType(
selections,
request,
castType = MediaTrack.TYPE_TEXT,
languages = params.preferredTextLanguages,
labels = params.preferredTextLabels,
disabled = C.TRACK_TYPE_TEXT in params.disabledTrackTypes,
selectDefault = false,
)
return request.buildResultUpon()
.setSelections(ImmutableSet.copyOf(selections))
.build()
}
private fun selectType(
selections: MutableSet<TrackGroup>,
request: CastTrackSelectorRequest,
castType: Int,
languages: List<String>,
labels: List<String>,
disabled: Boolean,
selectDefault: Boolean,
) {
val tracks = request.mediaTracks
val groups = request.trackGroupList
val indices = tracks.indices.filter { tracks[it].type == castType }
if (indices.isEmpty()) return
val previous = indices.filter { groups[it] in selections }
indices.forEach { selections.remove(groups[it]) }
if (disabled) return
val match = indices.firstOrNull { i ->
val name: String? = tracks[i].name
val language: String? = tracks[i].language
(labels.isNotEmpty() && name != null && name in labels) ||
(languages.isNotEmpty() && language != null &&
Util.normalizeLanguageCode(language) in languages)
}
val chosen = when {
match != null -> match
previous.isNotEmpty() -> previous.first()
selectDefault -> indices.first()
else -> null
}
chosen?.let { selections.add(groups[it]) }
}
}
@@ -0,0 +1,95 @@
package dev.zoriya.omni
import android.annotation.SuppressLint
import androidx.core.net.toUri
import androidx.media3.cast.MediaItemConverter
import androidx.media3.common.MediaItem
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.MediaTrack
import com.google.android.gms.common.images.WebImage
import org.json.JSONObject
import com.google.android.gms.cast.MediaMetadata as CastMediaMetadata
@SuppressLint("UnsafeOptInUsageError")
class OmniMediaItemConverter : MediaItemConverter {
override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem {
val info = mediaQueueItem.media
val uri = info?.contentUrl ?: info?.contentId ?: ""
val builder = MediaItem.Builder()
.setUri(uri)
.setMediaId(info?.contentId ?: uri)
info?.contentType?.let { builder.setMimeType(it) }
info?.metadata?.let { md ->
builder.setMediaMetadata(
androidx.media3.common.MediaMetadata.Builder()
.setTitle(md.getString(CastMediaMetadata.KEY_TITLE))
.setArtist(md.getString(CastMediaMetadata.KEY_ARTIST))
.setAlbumTitle(md.getString(CastMediaMetadata.KEY_ALBUM_TITLE))
.build()
)
}
return builder.build()
}
override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem {
val local = requireNotNull(mediaItem.localConfiguration) {
"MediaItem must have a localConfiguration to be cast"
}
val uri = local.uri.toString()
val extras = mediaItem.requestMetadata.extras
val contentId = extras?.getString(OmniPlayer.CAST_ID_EXTRA) ?: uri
val meta = mediaItem.mediaMetadata
val castMetadata = CastMediaMetadata(CastMediaMetadata.MEDIA_TYPE_MOVIE).apply {
meta.title?.let { putString(CastMediaMetadata.KEY_TITLE, it.toString()) }
meta.artist?.let { putString(CastMediaMetadata.KEY_ARTIST, it.toString()) }
meta.albumTitle?.let { putString(CastMediaMetadata.KEY_ALBUM_TITLE, it.toString()) }
meta.artworkUri?.let { addImage(WebImage(it)) }
}
val customData = extras?.getString(OmniPlayer.CAST_DATA_EXTRA)?.let {
try {
JSONObject(it)
} catch (_: Throwable) {
null
}
}
val tracks = local.subtitleConfigurations.mapIndexed { index, subtitle ->
MediaTrack.Builder(index.toLong(), MediaTrack.TYPE_TEXT)
.setSubtype(MediaTrack.SUBTYPE_SUBTITLES)
.setContentId(subtitle.uri.toString())
.setContentType(subtitle.mimeType ?: "text/vtt")
.apply {
subtitle.label?.let { setName(it) }
subtitle.language?.let { setLanguage(it) }
}
.build()
}
val path = uri.toUri().path?.lowercase() ?: uri.lowercase()
val contentType = local.mimeType ?: when {
path.endsWith(".mpd") -> "application/dash+xml"
path.endsWith(".mp4") -> "video/mp4"
path.endsWith(".webm") -> "video/webm"
path.endsWith(".mkv") -> "video/x-matroska"
else -> "application/x-mpegurl"
}
val mediaInfo = MediaInfo.Builder(contentId)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentUrl(uri)
.setContentType(contentType)
.setMetadata(castMetadata)
.apply {
if (tracks.isNotEmpty()) setMediaTracks(tracks)
customData?.let { setCustomData(it) }
}
.build()
return MediaQueueItem.Builder(mediaInfo)
.setAutoplay(true)
.build()
}
}
@@ -32,21 +32,32 @@ import androidx.core.net.toUri
import androidx.media3.common.MediaItem.RequestMetadata
import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.RemoteCastPlayer
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.mediarouter.app.MediaRouteChooserDialog
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.CastStateListener
import com.margelo.nitro.omni.CastOptions
import dev.zoriya.omni.utils.ThreadHelper.mainThreadProperty
import dev.zoriya.omni.utils.ThreadHelper.runOnMainThread
import dev.zoriya.omni.utils.ThreadHelper.runOnMainThreadSync
import org.json.JSONObject
@SuppressLint("UnsafeOptInUsageError")
class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : HybridOmniPlayerSpec() {
class OmniPlayer(
private val backend: AndroidBackend = AndroidBackend.VLC,
castOptions: CastOptions? = null,
) : HybridOmniPlayerSpec() {
private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
// exoplayer only, used to specify request headers.
private var httpDataSourceFactory: DefaultHttpDataSource.Factory? = null
val player: Player = runOnMainThreadSync {
val localPlayer: Player = runOnMainThreadSync {
when (backend) {
AndroidBackend.EXOPLAYER -> {
val http = DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true)
@@ -61,7 +72,37 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
AndroidBackend.VLC -> VlcPlayer(ctx)
}
}
override val eventMap = EventMap(player)
override val eventMap = EventMap()
private var castContext: CastContext? = null
private val castStateListener =
CastStateListener { eventMap.emitCastStatus(computeCastStatus()) }
val player: Player = runOnMainThreadSync {
castOptions?.receiverApplicationId?.let { receiverApplicationId = it }
val cc = try {
CastContext.getSharedInstance(ctx)
} catch (_: Throwable) {
// No Google Play Services / Cast SDK -> casting unsupported.
null
}
castContext = cc
val active = if (cc == null) {
localPlayer
} else {
cc.addCastStateListener(castStateListener)
val remote = RemoteCastPlayer.Builder(ctx)
.setMediaItemConverter(OmniMediaItemConverter())
.setTrackSelector(OmniCastTrackSelector())
.build()
CastPlayer.Builder(ctx)
.setLocalPlayer(localPlayer)
.setRemotePlayer(remote)
.build()
}
eventMap.player = active
active
}
override var showNotification: Boolean? = false
set(value) {
@@ -69,20 +110,41 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
if (notificationPlayer != null && notificationPlayer?.isPlaying == true) {
throw Error("Two players can't display notifications at the same time.")
}
notificationPlayer = player
notificationPlayer = localPlayer
ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java))
} else if (field == true && notificationPlayer == player) {
} else if (field == true && notificationPlayer == localPlayer) {
ctx.stopService(Intent(ctx, OmniPlayerService::class.java))
notificationPlayer = null
}
field = value
}
// TODO: cast is not implemented on the native (Android) side yet.
override val castStatus: CastStatus get() = CastStatus.UNSUPPORTED
override val castStatus: CastStatus
get() = runOnMainThreadSync { computeCastStatus() }
private fun computeCastStatus(): CastStatus {
val cc = castContext ?: return CastStatus.UNSUPPORTED
return when (cc.castState) {
CastState.NO_DEVICES_AVAILABLE -> CastStatus.UNAVAILABLE
CastState.NOT_CONNECTED -> CastStatus.AVAILABLE
CastState.CONNECTING -> CastStatus.CONNECTING
CastState.CONNECTED -> CastStatus.CONNECTED
else -> CastStatus.UNAVAILABLE
}
}
override fun toggleCastStatus() {
// TODO: implement casting on Android.
runOnMainThread {
val cc = castContext ?: return@runOnMainThread
val session = cc.sessionManager.currentCastSession
if (session != null && session.isConnected) {
cc.sessionManager.endCurrentSession(true)
return@runOnMainThread
}
val activity = ctx.currentActivity ?: return@runOnMainThread
val selector = cc.mergedSelector ?: return@runOnMainThread
MediaRouteChooserDialog(activity).apply { routeSelector = selector }.show()
}
}
override fun dispose() {
@@ -90,18 +152,20 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
super.dispose()
eventMap.dispose()
runOnMainThread { player.release() }
runOnMainThread {
castContext?.removeCastStateListener(castStateListener)
// release both cast and local players.
player.release()
}
}
private fun buildMediaItem(
src: com.margelo.nitro.omni.VideoSrc,
metadata: com.margelo.nitro.omni.Metadata?,
subtitles: Array<com.margelo.nitro.omni.Subtitle>
subtitles: Array<com.margelo.nitro.omni.Subtitle>,
castId: String? = null,
castData: Map<String, String>? = null,
): MediaItem {
// vlc only path (and it only supports a few headers :c)
val extras = if (src.headers.isEmpty()) null else Bundle(src.headers.size).apply {
for ((name, value) in src.headers) putString(name, value)
}
return MediaItem.Builder()
.setUri(src.uri)
.setMimeType(src.mimeType)
@@ -126,7 +190,16 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
.setRequestMetadata(
RequestMetadata.Builder()
.setMediaUri(src.uri.toUri())
.apply { extras?.let { setExtras(it) } }
.setExtras(
Bundle().apply {
castId?.let { putString(CAST_ID_EXTRA, it) }
castData?.let { data ->
putString(CAST_DATA_EXTRA, JSONObject(data as Map<*, *>).toString())
}
// vlc only path (and it only supports a few headers :c).
for ((name, value) in src.headers) putString(name, value)
}
)
.build()
)
.build()
@@ -135,9 +208,9 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
fun setSurface(holder: SurfaceHolder?) {
runOnMainThread {
if (holder == null) {
player.clearVideoSurface()
localPlayer.clearVideoSurface()
} else {
player.setVideoSurfaceHolder(holder)
localPlayer.setVideoSurfaceHolder(holder)
}
}
}
@@ -211,7 +284,9 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build()
runOnMainThread { player.setAudioAttributes(audioAttributes, handleAudioFocus) }
// audio focus only makes sense locally. not in a cast session
runOnMainThread { localPlayer.setAudioAttributes(audioAttributes, handleAudioFocus) }
val firstSrc = value.src.firstOrNull()
if (firstSrc == null) {
runOnMainThreadSync {
@@ -220,25 +295,23 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
}
return
}
httpDataSourceFactory?.setDefaultRequestProperties(firstSrc.headers)
val currentItem = buildMediaItem(firstSrc, value.metadata, value.subtitles)
val currentItem = buildMediaItem(
firstSrc,
value.metadata,
value.subtitles,
value.castId,
value.castData,
)
val mediaItems = mutableListOf<MediaItem>()
if (value.metadata?.hasPrev == true) {
mediaItems.add(currentItem)
}
if (value.metadata?.hasPrev == true) mediaItems.add(currentItem)
mediaItems.add(currentItem)
if (value.metadata?.hasNext == true) {
mediaItems.add(currentItem)
}
if (value.metadata?.hasNext == true) mediaItems.add(currentItem)
runOnMainThreadSync {
val startIndex = if (value.metadata?.hasPrev == true) 1 else 0
val startPositionMs = (value.startTime?.coerceAtLeast(0.0) ?: 0.0) * 1000.0
val startPositionMs = ((value.startTime ?: 0.0).coerceAtLeast(0.0)) * 1000.0
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
player.prepare()
}
@@ -288,8 +361,23 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
}
override fun selectSubtitle(subtitle: Track?) {
// Custom (ASS/PGS) subtitles can't be rendered by the cast receiver's
// native pipeline, so while casting they are forwarded over omni's
// message channel (the receiver draws them as an overlay). This mirrors
// the web behavior.
val custom = subtitle?.let { track ->
source?.subtitles?.firstOrNull { it.id == track.id }?.let { sub ->
val mime = sub.mimeType?.lowercase() ?: ""
val ext = sub.link.substringBefore('?').substringBefore('#')
.substringAfterLast('.', "").lowercase()
mime.contains("ass") || mime.contains("ssa") || mime.contains("pgs") ||
ext == "ass" || ext == "ssa" || ext == "sup"
}
} ?: false
runOnMainThreadSync {
if (subtitle == null) {
val session = castContext?.sessionManager?.currentCastSession
val casting = session?.isConnected == true
if (subtitle == null || (custom && casting)) {
player.trackSelectionParameters = player.trackSelectionParameters
.buildUpon()
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
@@ -302,6 +390,15 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
.setPreferredTextLabels(*(subtitle.label?.let { arrayOf(it) } ?: emptyArray()))
.build()
}
if (casting) {
val id = if (custom) subtitle?.id else null
val payload = JSONObject().apply { put("subtitle", id ?: JSONObject.NULL) }
try {
session?.sendMessage(CAST_MESSAGE_NAMESPACE, payload.toString())
} catch (_: Throwable) {
// best effort; receiver may not support the namespace.
}
}
}
}
@@ -383,6 +480,18 @@ class OmniPlayer(private val backend: AndroidBackend = AndroidBackend.VLC) : Hyb
companion object {
var notificationPlayer: Player? = null
// Cast custom-message channel (shared with the web receiver) used to
// forward overlay (ASS/PGS) subtitle selection to the receiver.
const val CAST_MESSAGE_NAMESPACE = "urn:x-cast:dev.zoriya.omni"
// MediaItem RequestMetadata extras keys carrying cast-only data.
const val CAST_ID_EXTRA = "dev.zoriya.omni.castId"
const val CAST_DATA_EXTRA = "dev.zoriya.omni.castData"
// Receiver application id passed at runtime via OmniProvider's `cast`
// prop; read by OmniCastOptionsProvider when the Cast SDK initializes.
var receiverApplicationId: String? = null
}
}
@@ -1,16 +1,20 @@
package dev.zoriya.omni
import android.util.Log
import com.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.omni.AndroidBackend
import com.margelo.nitro.omni.CastOptions
import com.margelo.nitro.omni.HybridOmniPlayerFactorySpec
import com.margelo.nitro.omni.HybridOmniPlayerSpec
import com.margelo.nitro.omni.PlayerBackend
import com.margelo.nitro.omni.Source
class OmniPlayerFactory(val context: ThemedReactContext) : HybridOmniPlayerFactorySpec() {
override fun createPlayer(props: Source?, backend: PlayerBackend?): HybridOmniPlayerSpec {
return OmniPlayer(backend?.android ?: AndroidBackend.VLC).apply {
override fun createPlayer(
props: Source?,
backend: PlayerBackend?,
cast: CastOptions?,
): HybridOmniPlayerSpec {
return OmniPlayer(backend?.android ?: AndroidBackend.VLC, cast).apply {
source = props
}
}
@@ -138,10 +138,10 @@ class OmniView(val context: ThemedReactContext) :
return
}
boundPlayer?.player?.removeListener(this)
boundPlayer?.localPlayer?.removeListener(this)
boundPlayer?.setSurface(null)
boundPlayer = omniPlayer
omniPlayer.player.addListener(this)
omniPlayer.localPlayer.addListener(this)
if (surfaceReady) {
omniPlayer.setSurface(surfaceView.holder)
@@ -163,7 +163,7 @@ class OmniView(val context: ThemedReactContext) :
if (!::player.isInitialized) return
val omniPlayer = player as? OmniPlayer ?: return
boundPlayer?.player?.removeListener(this)
boundPlayer?.localPlayer?.removeListener(this)
omniPlayer.setSurface(null)
boundPlayer = null
}
@@ -118,6 +118,7 @@ class VlcPlayer(ctx: Context) :
.add(COMMAND_SET_VOLUME)
.add(COMMAND_SET_VIDEO_SURFACE)
.add(COMMAND_SET_MEDIA_ITEM)
.add(COMMAND_CHANGE_MEDIA_ITEMS)
.add(COMMAND_GET_CURRENT_MEDIA_ITEM)
.add(COMMAND_GET_METADATA)
.add(COMMAND_GET_TIMELINE)
@@ -438,8 +439,10 @@ class VlcPlayer(ctx: Context) :
override fun getPlaybackState(): Int =
when {
playerError != null -> STATE_IDLE
currentMediaItemIndex == INDEX_UNSET -> STATE_IDLE
player.media == null -> STATE_IDLE
player.playerState == IMedia.State.Opening -> STATE_BUFFERING
player.isPlaying -> STATE_READY
player.isSeekable && player.time >= player.length && player.length > 0 -> STATE_ENDED
else -> STATE_READY
@@ -520,6 +523,7 @@ class VlcPlayer(ctx: Context) :
Format.Builder()
.setId(track.id)
.setLabel(track.name)
.setLanguage(track.language)
.setSampleMimeType("video/x-unknown")
.build()
}
@@ -535,6 +539,7 @@ class VlcPlayer(ctx: Context) :
val format = Format.Builder()
.setId(track.id)
.setLabel(track.name)
.setLanguage(track.language)
.setSampleMimeType("audio/x-unknown")
.build()
val group = TrackGroup("vlc-audio-${track.id}", format)
@@ -547,6 +552,7 @@ class VlcPlayer(ctx: Context) :
val format = Format.Builder()
.setId(track.id)
.setLabel(track.name)
.setLanguage(track.language)
.setSampleMimeType("text/x-unknown")
.build()
val group = TrackGroup("vlc-sub-${track.id}", format)