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
+2
View File
@@ -12,6 +12,7 @@ videojs v10 on the web (ios not implemented yet, PR welcome)
tracks at runtime.
- **Rich subtitle support**: vtt, srt, ass (via [jassub](https://github.com/ThaUnknown/jassub)) and pgs (via [libpgs](https://github.com/Arcus92/libpgs-js))
- **Picture-in-Picture**: enter PiP automatically or on demand on Android.
- **Chromecast**: cast to a receiver on both web and Android .
- **and basic player stuff**: media sessions, playlists, hook based api...
@@ -41,6 +42,7 @@ The plugin will:
- Enable Picture-in-Picture on your `MainActivity` (declares
`supportsPictureInPicture`, adds the required `configChanges`, and hooks the
pip lifecycle callbacks).
- Register omni's Chromecast `OptionsProvider`
If you are not using Expo, replicate those manifest/activity changes manually.
+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)
+16
View File
@@ -69,6 +69,7 @@ function PlayerExample({
const muted = usePlayerState("muted");
const volume = usePlayerState("volume");
const isAutoQuality = usePlayerState("isAutoQuality");
const castStatus = usePlayerState("castStatus");
const [logs, setLogs] = useState<string[]>([]);
const [tracks, setTracks] = useState(() => ({
videos: [...player.videos],
@@ -265,6 +266,21 @@ function PlayerExample({
</Pressable>
</View>
<View style={styles.row}>
<Pressable
style={styles.button}
disabled={castStatus === "unsupported"}
onPress={() => player.toggleCastStatus()}
>
<Text style={styles.buttonText}>
{castStatus === "connected" || castStatus === "connecting"
? "Stop cast"
: "Cast"}
</Text>
</Pressable>
<Text style={styles.buttonText}>Cast: {castStatus}</Text>
</View>
<View style={styles.statsCard}>
<Text style={styles.statText}>Status: {status}</Text>
<Text style={styles.statText}>
@@ -25,5 +25,8 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="dev.zoriya.omni.OmniCastOptionsProvider" />
</application>
</manifest>
+2
View File
@@ -19,6 +19,7 @@
#include "JFunc_void_double.hpp"
#include "JFunc_void_bool.hpp"
#include "JFunc_void_PlayerStatus.hpp"
#include "JFunc_void_CastStatus.hpp"
#include "JFunc_void.hpp"
#include "JFunc_void_std__string_std__string.hpp"
#include "JFunc_void_std__string.hpp"
@@ -65,6 +66,7 @@ void registerAllNatives() {
margelo::nitro::omni::JFunc_void_double_cxx::registerNatives();
margelo::nitro::omni::JFunc_void_bool_cxx::registerNatives();
margelo::nitro::omni::JFunc_void_PlayerStatus_cxx::registerNatives();
margelo::nitro::omni::JFunc_void_CastStatus_cxx::registerNatives();
margelo::nitro::omni::JFunc_void_cxx::registerNatives();
margelo::nitro::omni::JFunc_void_std__string_std__string_cxx::registerNatives();
margelo::nitro::omni::JFunc_void_std__string_cxx::registerNatives();
+58
View File
@@ -0,0 +1,58 @@
///
/// JCastOptions.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#include <fbjni/fbjni.h>
#include "CastOptions.hpp"
#include <optional>
#include <string>
namespace margelo::nitro::omni {
using namespace facebook;
/**
* The C++ JNI bridge between the C++ struct "CastOptions" and the Kotlin data class "CastOptions".
*/
struct JCastOptions final: public jni::JavaClass<JCastOptions> {
public:
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/CastOptions;";
public:
/**
* Convert this Java/Kotlin-based struct to the C++ struct CastOptions by copying all values to C++.
*/
[[maybe_unused]]
[[nodiscard]]
CastOptions toCpp() const {
static const auto clazz = javaClassStatic();
static const auto fieldReceiverApplicationId = clazz->getField<jni::JString>("receiverApplicationId");
jni::local_ref<jni::JString> receiverApplicationId = this->getFieldValue(fieldReceiverApplicationId);
return CastOptions(
receiverApplicationId != nullptr ? std::make_optional(receiverApplicationId->toStdString()) : std::nullopt
);
}
public:
/**
* Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java.
*/
[[maybe_unused]]
static jni::local_ref<JCastOptions::javaobject> fromCpp(const CastOptions& value) {
using JSignature = JCastOptions(jni::alias_ref<jni::JString>);
static const auto clazz = javaClassStatic();
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
return create(
clazz,
value.receiverApplicationId.has_value() ? jni::make_jstring(value.receiverApplicationId.value()) : nullptr
);
}
};
} // namespace margelo::nitro::omni
+77
View File
@@ -0,0 +1,77 @@
///
/// JFunc_void_CastStatus.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#include <fbjni/fbjni.h>
#include <functional>
#include "CastStatus.hpp"
#include <functional>
#include <NitroModules/JNICallable.hpp>
#include "JCastStatus.hpp"
namespace margelo::nitro::omni {
using namespace facebook;
/**
* Represents the Java/Kotlin callback `(value: CastStatus) -> Unit`.
* This can be passed around between C++ and Java/Kotlin.
*/
struct JFunc_void_CastStatus: public jni::JavaClass<JFunc_void_CastStatus> {
public:
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Func_void_CastStatus;";
public:
/**
* Invokes the function this `JFunc_void_CastStatus` instance holds through JNI.
*/
void invoke(CastStatus value) const {
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JCastStatus> /* value */)>("invoke");
method(self(), JCastStatus::fromCpp(value));
}
};
/**
* An implementation of Func_void_CastStatus that is backed by a C++ implementation (using `std::function<...>`)
*/
class JFunc_void_CastStatus_cxx final: public jni::HybridClass<JFunc_void_CastStatus_cxx, JFunc_void_CastStatus> {
public:
static jni::local_ref<JFunc_void_CastStatus::javaobject> fromCpp(const std::function<void(CastStatus /* value */)>& func) {
return JFunc_void_CastStatus_cxx::newObjectCxxArgs(func);
}
public:
/**
* Invokes the C++ `std::function<...>` this `JFunc_void_CastStatus_cxx` instance holds.
*/
void invoke_cxx(jni::alias_ref<JCastStatus> value) {
_func(value->toCpp());
}
public:
[[nodiscard]]
inline const std::function<void(CastStatus /* value */)>& getFunction() const {
return _func;
}
public:
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Func_void_CastStatus_cxx;";
static void registerNatives() {
registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_CastStatus_cxx::invoke_cxx)});
}
private:
explicit JFunc_void_CastStatus_cxx(const std::function<void(CastStatus /* value */)>& func): _func(func) { }
private:
friend HybridBase;
std::function<void(CastStatus /* value */)> _func;
};
} // namespace margelo::nitro::omni
@@ -13,6 +13,8 @@ namespace margelo::nitro::omni { enum class NumberProperty; }
namespace margelo::nitro::omni { enum class BoolProperty; }
// Forward declaration of `PlayerStatus` to properly resolve imports.
namespace margelo::nitro::omni { enum class PlayerStatus; }
// Forward declaration of `CastStatus` to properly resolve imports.
namespace margelo::nitro::omni { enum class CastStatus; }
// Forward declaration of `Track` to properly resolve imports.
namespace margelo::nitro::omni { struct Track; }
// Forward declaration of `Rendition` to properly resolve imports.
@@ -29,6 +31,9 @@ namespace margelo::nitro::omni { struct Rendition; }
#include "PlayerStatus.hpp"
#include "JFunc_void_PlayerStatus.hpp"
#include "JPlayerStatus.hpp"
#include "CastStatus.hpp"
#include "JFunc_void_CastStatus.hpp"
#include "JCastStatus.hpp"
#include "JFunc_void.hpp"
#include <string>
#include "JFunc_void_std__string_std__string.hpp"
@@ -99,6 +104,14 @@ namespace margelo::nitro::omni {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_PlayerStatus::javaobject> /* cb */)>("removePlayerStatusListener_cxx");
method(_javaPart, JFunc_void_PlayerStatus_cxx::fromCpp(cb));
}
void JHybridOmniEventMapSpec::addCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_CastStatus::javaobject> /* cb */)>("addCastStatusListener_cxx");
method(_javaPart, JFunc_void_CastStatus_cxx::fromCpp(cb));
}
void JHybridOmniEventMapSpec::removeCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_CastStatus::javaobject> /* cb */)>("removeCastStatusListener_cxx");
method(_javaPart, JFunc_void_CastStatus_cxx::fromCpp(cb));
}
void JHybridOmniEventMapSpec::addOnEndListener(const std::function<void()>& cb) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void::javaobject> /* cb */)>("addOnEndListener_cxx");
method(_javaPart, JFunc_void_cxx::fromCpp(cb));
@@ -60,6 +60,8 @@ namespace margelo::nitro::omni {
void removeStateBoolListener(BoolProperty key, const std::function<void(bool /* value */)>& cb) override;
void addPlayerStatusListener(const std::function<void(PlayerStatus /* value */)>& cb) override;
void removePlayerStatusListener(const std::function<void(PlayerStatus /* value */)>& cb) override;
void addCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) override;
void removeCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) override;
void addOnEndListener(const std::function<void()>& cb) override;
void removeOnEndListener(const std::function<void()>& cb) override;
void addOnPrevListener(const std::function<void()>& cb) override;
@@ -23,6 +23,8 @@ namespace margelo::nitro::omni { enum class MixAudioMode; }
namespace margelo::nitro::omni { struct PlayerBackend; }
// Forward declaration of `AndroidBackend` to properly resolve imports.
namespace margelo::nitro::omni { enum class AndroidBackend; }
// Forward declaration of `CastOptions` to properly resolve imports.
namespace margelo::nitro::omni { struct CastOptions; }
#include <memory>
#include "HybridOmniPlayerSpec.hpp"
@@ -45,6 +47,8 @@ namespace margelo::nitro::omni { enum class AndroidBackend; }
#include "JPlayerBackend.hpp"
#include "AndroidBackend.hpp"
#include "JAndroidBackend.hpp"
#include "CastOptions.hpp"
#include "JCastOptions.hpp"
namespace margelo::nitro::omni {
@@ -79,9 +83,9 @@ namespace margelo::nitro::omni {
// Methods
std::shared_ptr<HybridOmniPlayerSpec> JHybridOmniPlayerFactorySpec::createPlayer(const std::optional<Source>& props, const std::optional<PlayerBackend>& backend) {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JHybridOmniPlayerSpec::JavaPart>(jni::alias_ref<JSource> /* props */, jni::alias_ref<JPlayerBackend> /* backend */)>("createPlayer");
auto __result = method(_javaPart, props.has_value() ? JSource::fromCpp(props.value()) : nullptr, backend.has_value() ? JPlayerBackend::fromCpp(backend.value()) : nullptr);
std::shared_ptr<HybridOmniPlayerSpec> JHybridOmniPlayerFactorySpec::createPlayer(const std::optional<Source>& props, const std::optional<PlayerBackend>& backend, const std::optional<CastOptions>& cast) {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JHybridOmniPlayerSpec::JavaPart>(jni::alias_ref<JSource> /* props */, jni::alias_ref<JPlayerBackend> /* backend */, jni::alias_ref<JCastOptions> /* cast */)>("createPlayer");
auto __result = method(_javaPart, props.has_value() ? JSource::fromCpp(props.value()) : nullptr, backend.has_value() ? JPlayerBackend::fromCpp(backend.value()) : nullptr, cast.has_value() ? JCastOptions::fromCpp(cast.value()) : nullptr);
return __result->getJHybridOmniPlayerSpec();
}
@@ -54,7 +54,7 @@ namespace margelo::nitro::omni {
public:
// Methods
std::shared_ptr<HybridOmniPlayerSpec> createPlayer(const std::optional<Source>& props, const std::optional<PlayerBackend>& backend) override;
std::shared_ptr<HybridOmniPlayerSpec> createPlayer(const std::optional<Source>& props, const std::optional<PlayerBackend>& backend, const std::optional<CastOptions>& cast) override;
private:
jni::global_ref<JHybridOmniPlayerFactorySpec::JavaPart> _javaPart;
@@ -0,0 +1,51 @@
///
/// CastOptions.kt
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
package com.margelo.nitro.omni
import androidx.annotation.Keep
import com.facebook.proguard.annotations.DoNotStrip
import java.util.Objects
/**
* Represents the JavaScript object/struct "CastOptions".
*/
@DoNotStrip
@Keep
data class CastOptions(
@DoNotStrip
@Keep
val receiverApplicationId: String?
) {
/* primary constructor */
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CastOptions) return false
return Objects.deepEquals(this.receiverApplicationId, other.receiverApplicationId)
}
override fun hashCode(): Int {
return arrayOf<Any?>(
receiverApplicationId
).contentDeepHashCode()
}
companion object {
/**
* Constructor called from C++
*/
@DoNotStrip
@Keep
@Suppress("unused")
@JvmStatic
private fun fromCpp(receiverApplicationId: String?): CastOptions {
return CastOptions(receiverApplicationId)
}
}
}
@@ -0,0 +1,80 @@
///
/// Func_void_CastStatus.kt
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
package com.margelo.nitro.omni
import androidx.annotation.Keep
import com.facebook.jni.HybridData
import com.facebook.proguard.annotations.DoNotStrip
import dalvik.annotation.optimization.FastNative
/**
* Represents the JavaScript callback `(value: enum) => void`.
* This can be either implemented in C++ (in which case it might be a callback coming from JS),
* or in Kotlin/Java (in which case it is a native callback).
*/
@DoNotStrip
@Keep
@Suppress("ClassName", "RedundantUnitReturnType")
fun interface Func_void_CastStatus: (CastStatus) -> Unit {
/**
* Call the given JS callback.
* @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted.
*/
@DoNotStrip
@Keep
override fun invoke(value: CastStatus): Unit
}
/**
* Represents the JavaScript callback `(value: enum) => void`.
* This is implemented in C++, via a `std::function<...>`.
* The callback might be coming from JS.
*/
@DoNotStrip
@Keep
@Suppress(
"KotlinJniMissingFunction", "unused",
"RedundantSuppression", "RedundantUnitReturnType", "FunctionName",
"ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName",
)
class Func_void_CastStatus_cxx: Func_void_CastStatus {
@DoNotStrip
@Keep
private val mHybridData: HybridData
@DoNotStrip
@Keep
private constructor(hybridData: HybridData) {
mHybridData = hybridData
}
@DoNotStrip
@Keep
override fun invoke(value: CastStatus): Unit
= invoke_cxx(value)
@FastNative
private external fun invoke_cxx(value: CastStatus): Unit
}
/**
* Represents the JavaScript callback `(value: enum) => void`.
* This is implemented in Java/Kotlin, via a `(CastStatus) -> Unit`.
* The callback is always coming from native.
*/
@DoNotStrip
@Keep
@Suppress("ClassName", "RedundantUnitReturnType", "unused")
class Func_void_CastStatus_java(private val function: (CastStatus) -> Unit): Func_void_CastStatus {
@DoNotStrip
@Keep
override fun invoke(value: CastStatus): Unit {
return this.function(value)
}
}
@@ -82,6 +82,24 @@ abstract class HybridOmniEventMapSpec: HybridObject() {
return __result
}
abstract fun addCastStatusListener(cb: (value: CastStatus) -> Unit): Unit
@DoNotStrip
@Keep
private fun addCastStatusListener_cxx(cb: Func_void_CastStatus): Unit {
val __result = addCastStatusListener(cb)
return __result
}
abstract fun removeCastStatusListener(cb: (value: CastStatus) -> Unit): Unit
@DoNotStrip
@Keep
private fun removeCastStatusListener_cxx(cb: Func_void_CastStatus): Unit {
val __result = removeCastStatusListener(cb)
return __result
}
abstract fun addOnEndListener(cb: () -> Unit): Unit
@DoNotStrip
@@ -30,7 +30,7 @@ abstract class HybridOmniPlayerFactorySpec: HybridObject() {
// Methods
@DoNotStrip
@Keep
abstract fun createPlayer(props: Source?, backend: PlayerBackend?): HybridOmniPlayerSpec
abstract fun createPlayer(props: Source?, backend: PlayerBackend?, cast: CastOptions?): HybridOmniPlayerSpec
// Default implementation of `HybridObject.toString()`
override fun toString(): String {
+84
View File
@@ -0,0 +1,84 @@
///
/// CastOptions.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#include <string>
#include <optional>
namespace margelo::nitro::omni {
/**
* A struct which can be represented as a JavaScript object (CastOptions).
*/
struct CastOptions final {
public:
std::optional<std::string> receiverApplicationId SWIFT_PRIVATE;
public:
CastOptions() = default;
explicit CastOptions(std::optional<std::string> receiverApplicationId): receiverApplicationId(receiverApplicationId) {}
public:
friend bool operator==(const CastOptions& lhs, const CastOptions& rhs) = default;
};
} // namespace margelo::nitro::omni
namespace margelo::nitro {
// C++ CastOptions <> JS CastOptions (object)
template <>
struct JSIConverter<margelo::nitro::omni::CastOptions> final {
static inline margelo::nitro::omni::CastOptions fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::omni::CastOptions(
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::omni::CastOptions& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.receiverApplicationId));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "receiverApplicationId")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -20,6 +20,8 @@ namespace margelo::nitro::omni {
prototype.registerHybridMethod("removeStateBoolListener", &HybridOmniEventMapSpec::removeStateBoolListener);
prototype.registerHybridMethod("addPlayerStatusListener", &HybridOmniEventMapSpec::addPlayerStatusListener);
prototype.registerHybridMethod("removePlayerStatusListener", &HybridOmniEventMapSpec::removePlayerStatusListener);
prototype.registerHybridMethod("addCastStatusListener", &HybridOmniEventMapSpec::addCastStatusListener);
prototype.registerHybridMethod("removeCastStatusListener", &HybridOmniEventMapSpec::removeCastStatusListener);
prototype.registerHybridMethod("addOnEndListener", &HybridOmniEventMapSpec::addOnEndListener);
prototype.registerHybridMethod("removeOnEndListener", &HybridOmniEventMapSpec::removeOnEndListener);
prototype.registerHybridMethod("addOnPrevListener", &HybridOmniEventMapSpec::addOnPrevListener);
@@ -19,6 +19,8 @@ namespace margelo::nitro::omni { enum class NumberProperty; }
namespace margelo::nitro::omni { enum class BoolProperty; }
// Forward declaration of `PlayerStatus` to properly resolve imports.
namespace margelo::nitro::omni { enum class PlayerStatus; }
// Forward declaration of `CastStatus` to properly resolve imports.
namespace margelo::nitro::omni { enum class CastStatus; }
// Forward declaration of `Track` to properly resolve imports.
namespace margelo::nitro::omni { struct Track; }
// Forward declaration of `Rendition` to properly resolve imports.
@@ -28,6 +30,7 @@ namespace margelo::nitro::omni { struct Rendition; }
#include <functional>
#include "BoolProperty.hpp"
#include "PlayerStatus.hpp"
#include "CastStatus.hpp"
#include <string>
#include "Track.hpp"
#include <optional>
@@ -70,6 +73,8 @@ namespace margelo::nitro::omni {
virtual void removeStateBoolListener(BoolProperty key, const std::function<void(bool /* value */)>& cb) = 0;
virtual void addPlayerStatusListener(const std::function<void(PlayerStatus /* value */)>& cb) = 0;
virtual void removePlayerStatusListener(const std::function<void(PlayerStatus /* value */)>& cb) = 0;
virtual void addCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) = 0;
virtual void removeCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) = 0;
virtual void addOnEndListener(const std::function<void()>& cb) = 0;
virtual void removeOnEndListener(const std::function<void()>& cb) = 0;
virtual void addOnPrevListener(const std::function<void()>& cb) = 0;
@@ -19,12 +19,15 @@ namespace margelo::nitro::omni { class HybridOmniPlayerSpec; }
namespace margelo::nitro::omni { struct Source; }
// Forward declaration of `PlayerBackend` to properly resolve imports.
namespace margelo::nitro::omni { struct PlayerBackend; }
// Forward declaration of `CastOptions` to properly resolve imports.
namespace margelo::nitro::omni { struct CastOptions; }
#include <memory>
#include "HybridOmniPlayerSpec.hpp"
#include "Source.hpp"
#include <optional>
#include "PlayerBackend.hpp"
#include "CastOptions.hpp"
namespace margelo::nitro::omni {
@@ -57,7 +60,7 @@ namespace margelo::nitro::omni {
public:
// Methods
virtual std::shared_ptr<HybridOmniPlayerSpec> createPlayer(const std::optional<Source>& props, const std::optional<PlayerBackend>& backend) = 0;
virtual std::shared_ptr<HybridOmniPlayerSpec> createPlayer(const std::optional<Source>& props, const std::optional<PlayerBackend>& backend, const std::optional<CastOptions>& cast) = 0;
protected:
// Hybrid Setup
+3 -4
View File
@@ -1,15 +1,14 @@
import type { ConfigPlugin } from "@expo/config-plugins";
import { createRunOncePlugin } from "@expo/config-plugins";
import pkg from "../../package.json";
import { withCast } from "./withCast";
import { withMediaNotifications } from "./withMediaNotifications";
import { withPip } from "./withPip";
const withOmni: ConfigPlugin = (config) => {
let nextConfig = withPip(config);
nextConfig = withMediaNotifications(nextConfig);
return nextConfig;
return withCast(withMediaNotifications(withPip(config)));
};
export default createRunOncePlugin(withOmni, pkg.name, pkg.version);
export { withMediaNotifications, withPip };
export { withCast, withMediaNotifications, withPip };
+47
View File
@@ -0,0 +1,47 @@
import {
AndroidConfig,
type ConfigPlugin,
withAndroidManifest,
} from "@expo/config-plugins";
const OPTIONS_PROVIDER_NAME =
"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME";
const OPTIONS_PROVIDER_VALUE = "dev.zoriya.omni.OmniCastOptionsProvider";
function patchManifestForCast(
androidManifest: AndroidConfig.Manifest.AndroidManifest,
): AndroidConfig.Manifest.AndroidManifest {
const mainApplication =
AndroidConfig.Manifest.getMainApplication(androidManifest);
if (!mainApplication) {
console.warn(
"AndroidManifest.xml is missing a <application> element - skipping Omni cast config.",
);
return androidManifest;
}
const metaData = mainApplication["meta-data"] ?? [];
const existing = metaData.find(
(item) => item.$["android:name"] === OPTIONS_PROVIDER_NAME,
);
if (existing) {
existing.$["android:value"] = OPTIONS_PROVIDER_VALUE;
} else {
metaData.push({
$: {
"android:name": OPTIONS_PROVIDER_NAME,
"android:value": OPTIONS_PROVIDER_VALUE,
},
});
}
mainApplication["meta-data"] = metaData;
return androidManifest;
}
export const withCast: ConfigPlugin = (config) => {
return withAndroidManifest(config, (manifestConfig) => {
manifestConfig.modResults = patchManifestForCast(manifestConfig.modResults);
return manifestConfig;
});
};
+2 -2
View File
@@ -53,8 +53,8 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
em.addPlayerStatusListener(setState);
return () => em.removePlayerStatusListener(setState);
case "castStatus":
// TODO: implement this
return;
em.addCastStatusListener(setState);
return () => em.removeCastStatusListener(setState);
}
}, [player, key]);
+2 -2
View File
@@ -15,7 +15,7 @@ export const OmniProvider = ({
source,
backend = { android: "vlc" },
showNotification = false,
cast: _,
cast,
}: {
source?: Source;
cast?: CastOptions;
@@ -24,7 +24,7 @@ export const OmniProvider = ({
showNotification?: boolean;
}) => {
const player = useLazyRef(() =>
ProviderFactory.createPlayer(source, backend),
ProviderFactory.createPlayer(source, backend, cast),
);
useEffect(() => {
+10 -2
View File
@@ -1,12 +1,13 @@
import type { HybridObject } from "react-native-nitro-modules";
import type { OmniEvents } from "../types/events";
import type {
CastStatus,
OmniPlayerState,
OmniPlayer as OmniPlayerT,
PlayerBackend,
PlayerStatus,
} from "../types/player";
import type { Source } from "../types/source";
import type { CastOptions, Source } from "../types/source";
export type NumberProperty = Exclude<
keyof OmniPlayerState,
@@ -25,6 +26,9 @@ export interface OmniEventMap extends HybridObject<{ android: "kotlin" }> {
addPlayerStatusListener(cb: (value: PlayerStatus) => void): void;
removePlayerStatusListener(cb: (value: PlayerStatus) => void): void;
addCastStatusListener(cb: (value: CastStatus) => void): void;
removeCastStatusListener(cb: (value: CastStatus) => void): void;
addOnEndListener(cb: OmniEvents["end"]): void;
removeOnEndListener(cb: OmniEvents["end"]): void;
@@ -60,5 +64,9 @@ export interface OmniPlayer
}
export interface OmniPlayerFactory extends HybridObject<{ android: "kotlin" }> {
createPlayer(props?: Source, backend?: PlayerBackend): OmniPlayer;
createPlayer(
props?: Source,
backend?: PlayerBackend,
cast?: CastOptions,
): OmniPlayer;
}