Compare commits

...
12 Commits
37 changed files with 1208 additions and 435 deletions
+13 -5
View File
@@ -132,9 +132,9 @@ component that uses `<OmniView>`, `usePlayer`, `usePlayerState`, or `useEvent`.
| Prop | Type | Default | Description |
| ------------------ | --------- | ------- | ---------------------------------------------------- |
| `source` | `Source` | — | The media to play. |
| `source` | `Source` | x | The media to play. |
| `showNotification` | `boolean` | `false` | Create a media session (OS controls/notification). |
| `children` | `ReactNode` | — | Your player UI. |
| `children` | `ReactNode` | x | Your player UI. |
Updating `source` swaps the media in place (and seeks to `source.startTime` if
set) without recreating the player.
@@ -152,7 +152,7 @@ plus:
### `usePlayer()`
Returns the `OmniPlayer` instance — an imperative handle for controlling
Returns the `OmniPlayer` instance: an imperative handle for controlling
playback and reading/writing state.
**Playback controls**
@@ -199,11 +199,11 @@ player.selectAudio(track);
player.subtitles; // Track[]
player.selectSubtitle(track); // pass undefined to turn subtitles off
player.rendition; // Rendition[]
player.renditions; // Rendition[]
player.selectRendition(rendition); // pass undefined for automatic quality
```
> Prefer `usePlayerState`/`useEvent` for values that change over time — reading
> Prefer `usePlayerState` for values that change over time. Reading
> them directly off `player` gives you a one-time snapshot and won't re-render.
### `usePlayerState(key, refresh?)`
@@ -216,6 +216,10 @@ const status = usePlayerState("status");
const isPlaying = usePlayerState("isPlaying");
const duration = usePlayerState("duration");
const currentTime = usePlayerState("currentTime", 1); // poll every 1s
const audios = usePlayerState("audios"); // Track[]
const subtitles = usePlayerState("subtitles"); // Track[]
const renditions = usePlayerState("renditions"); // Rendition[]
```
The optional `refresh` argument (seconds) sets a polling interval.
@@ -250,6 +254,10 @@ useEvent("renditionChange", (rendition) => {/* … */});
| `subtitleChange` | `(track?) => void` | The active subtitle changed (`undefined` = off). |
| `renditionChange` | `(rendition) => void` | The active quality/rendition changed. |
The track/rendition change events fire when the _active_ selection changes;
`usePlayerState("audios" | "subtitles" | "renditions" | "videos")` exposes the
full lists as reactive state. Use whichever fits.
## Example app
A full example (playlist navigation, track/rendition selectors, an event log,
@@ -26,9 +26,22 @@ import com.margelo.nitro.omni.PlayerStatus
import com.margelo.nitro.omni.Rendition
import com.margelo.nitro.omni.Source
import com.margelo.nitro.omni.Track
import com.margelo.nitro.omni.TrackProperty
/**
* Source of the current track/rendition lists. Implemented by [OmniPlayer] so
* state emissions reuse the same computation (incl. cast metadata recovery) as
* its readonly getters.
*/
interface TrackProvider {
val videos: Array<Track>
val audios: Array<Track>
val subtitles: Array<Track>
val renditions: Array<Rendition>
}
@SuppressLint("UnsafeOptInUsageError")
class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Player.Listener {
private val onPrevListeners = mutableSetOf<() -> Unit>()
private val onNextListeners = mutableSetOf<() -> Unit>()
private val onEndListeners = mutableSetOf<() -> Unit>()
@@ -38,6 +51,8 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
private val onAudioTrackChangeListeners = mutableSetOf<(track: Track) -> Unit>()
private val onSubtitleChangeListeners = mutableSetOf<(track: Track?) -> Unit>()
private val onRenditionChangeListeners = mutableSetOf<(rendition: Rendition) -> Unit>()
private val tracksListeners = mutableMapOf<TrackProperty, MutableSet<(Array<Track>) -> Unit>>()
private val renditionsListeners = mutableSetOf<(Array<Rendition>) -> Unit>()
private val stateListeners = mutableMapOf<NumberProperty, MutableSet<(Double) -> Unit>>()
private val stateBoolListeners = mutableMapOf<BoolProperty, MutableSet<(Boolean) -> Unit>>()
private val playerStatusListeners = mutableSetOf<(PlayerStatus) -> Unit>()
@@ -46,6 +61,7 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
private var lastMediaItemIndex = 0
private var lastRendition: Rendition? = null
private var lastRenditions: Array<Rendition>? = null
private var lastIsAutoQuality: Boolean? = null
// swapped on cast start/end
@@ -59,6 +75,7 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
value.addListener(this)
lastMediaItemIndex = value.currentMediaItemIndex
lastRendition = null
lastRenditions = null
lastIsAutoQuality = null
// immediatly send all the state
@@ -100,21 +117,13 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
}
private fun selectedTrack(trackType: Int): Track? {
val groups = player.currentTracks.groups.filter { it.type == trackType }
for (group in groups) {
val mediaGroup = group.mediaTrackGroup
for (i in 0 until group.length) {
if (!group.isTrackSelected(i)) continue
val format = group.getTrackFormat(i)
return Track(
id = format.id ?: mediaGroup.id,
label = format.label,
language = format.language,
selected = true
)
}
val list = when (trackType) {
TRACK_TYPE_VIDEO -> tracks.videos
TRACK_TYPE_AUDIO -> tracks.audios
TRACK_TYPE_TEXT -> tracks.subtitles
else -> return null
}
return null
return list.firstOrNull { it.selected }
}
private fun getCurrentRendition(): Rendition? {
@@ -158,6 +167,21 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
stateBoolListeners[BoolProperty.ISAUTOQUALITY]?.forEach { it(isAuto) }
}
private fun emitTracks() {
tracksListeners[TrackProperty.VIDEOS]?.let { cbs ->
val videos = tracks.videos
cbs.forEach { it(videos) }
}
tracksListeners[TrackProperty.AUDIOS]?.let { cbs ->
val audios = tracks.audios
cbs.forEach { it(audios) }
}
tracksListeners[TrackProperty.SUBTITLES]?.let { cbs ->
val subtitles = tracks.subtitles
cbs.forEach { it(subtitles) }
}
}
private fun emitRenditionChange() {
val rendition = getCurrentRendition() ?: return
if (rendition == lastRendition) return
@@ -165,6 +189,12 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
onRenditionChangeListeners.forEach { it(rendition) }
}
private fun emitRenditions() {
if (renditionsListeners.isEmpty()) return
val renditions = tracks.renditions
renditionsListeners.forEach { it(renditions) }
}
override fun onPlaybackStateChanged(playbackState: Int) {
val state = when (player.playbackState) {
STATE_IDLE -> PlayerStatus.IDLE
@@ -227,20 +257,18 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
selectedTrack(TRACK_TYPE_AUDIO)?.let { track ->
onAudioTrackChangeListeners.forEach { it(track) }
}
onSubtitleChangeListeners.forEach {
it(
selectedTrack(
TRACK_TYPE_TEXT
)
)
}
val subtitle = selectedTrack(TRACK_TYPE_TEXT)
onSubtitleChangeListeners.forEach { it(subtitle) }
emitTracks()
emitIsAutoQualityChange()
emitRenditionChange()
emitRenditions()
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
emitIsAutoQualityChange()
emitRenditionChange()
emitRenditions()
}
override fun onPlayerError(error: PlaybackException) {
@@ -397,6 +425,22 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
onRenditionChangeListeners.remove(cb)
}
override fun addTracksListener(key: TrackProperty, cb: (value: Array<Track>) -> Unit) {
tracksListeners.getOrPut(key) { mutableSetOf() }.add(cb)
}
override fun removeTracksListener(key: TrackProperty, cb: (value: Array<Track>) -> Unit) {
tracksListeners[key]?.remove(cb)
}
override fun addRenditionsListener(cb: (value: Array<Rendition>) -> Unit) {
renditionsListeners.add(cb)
}
override fun removeRenditionsListener(cb: (value: Array<Rendition>) -> Unit) {
renditionsListeners.remove(cb)
}
override fun dispose() {
player.removeListener(this)
super.dispose()
@@ -1,6 +1,8 @@
package dev.zoriya.omni
import android.annotation.SuppressLint
import android.content.Context
import android.os.Looper
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -13,11 +15,16 @@ import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.Player
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.Renderer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.text.TextOutput
import androidx.media3.exoplayer.text.TextRenderer
import com.margelo.nitro.NitroModules
import com.margelo.nitro.omni.AndroidBackend
import com.margelo.nitro.omni.CastStatus
@@ -51,7 +58,7 @@ import org.json.JSONObject
class OmniPlayer(
private val backend: AndroidBackend = AndroidBackend.VLC,
castOptions: CastOptions? = null,
) : HybridOmniPlayerSpec() {
) : HybridOmniPlayerSpec(), TrackProvider {
private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
// exoplayer only, used to specify request headers.
@@ -62,9 +69,26 @@ class OmniPlayer(
AndroidBackend.EXOPLAYER -> {
val http = DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true)
httpDataSourceFactory = http
ExoPlayer.Builder(ctx)
// karaoke subs make exoplayer crash without this...
val renderersFactory = object : DefaultRenderersFactory(ctx) {
override fun buildTextRenderers(
context: Context,
output: TextOutput,
outputLooper: Looper,
extensionRendererMode: Int,
out: ArrayList<Renderer>,
) {
out.add(
TextRenderer(output, outputLooper).apply {
experimentalSetLegacyDecodingEnabled(true)
}
)
}
}
ExoPlayer.Builder(ctx, renderersFactory)
.setMediaSourceFactory(
DefaultMediaSourceFactory(DefaultDataSource.Factory(ctx, http))
.experimentalParseSubtitlesDuringExtraction(false)
)
.build()
}
@@ -72,7 +96,7 @@ class OmniPlayer(
AndroidBackend.VLC -> VlcPlayer(ctx)
}
}
override val eventMap = EventMap()
override val eventMap = EventMap(this)
private var castContext: CastContext? = null
private val castStateListener =
@@ -100,7 +124,6 @@ class OmniPlayer(
.setRemotePlayer(remote)
.build()
}
eventMap.player = active
active
}
@@ -169,18 +192,38 @@ class OmniPlayer(
}
}
override fun dispose() {
showNotification = false
super.dispose()
@Volatile
private var released = false
eventMap.dispose()
override fun release() {
if (released) return
released = true
showNotification = false
runOnMainThread {
eventMap.dispose()
castContext?.removeCastStateListener(castStateListener)
// release both cast and local players.
player.release()
}
}
override fun dispose() {
release()
super.dispose()
}
private fun inferSubtitleMimeType(link: String): String? {
val ext = link.toUri().lastPathSegment?.substringAfterLast('.', "")?.lowercase()
return when (ext) {
"vtt", "webvtt" -> MimeTypes.TEXT_VTT
"srt" -> MimeTypes.APPLICATION_SUBRIP
"ass", "ssa" -> MimeTypes.TEXT_SSA
"ttml", "dfxp", "xml" -> MimeTypes.APPLICATION_TTML
"sup" -> MimeTypes.APPLICATION_PGS
"sub" -> MimeTypes.APPLICATION_VOBSUB
else -> null
}
}
private fun buildMediaItem(
src: com.margelo.nitro.omni.VideoSrc,
metadata: com.margelo.nitro.omni.Metadata?,
@@ -206,7 +249,10 @@ class OmniPlayer(
.setId(subtitle.id)
.setLanguage(subtitle.language)
.setLabel(subtitle.label)
.setMimeType(subtitle.mimeType)
// exoplayer requires a mime type to pick a subtitle parser.
// fall back to inferring it from the link's extension so
// consumers don't have to provide one.
.setMimeType(subtitle.mimeType ?: inferSubtitleMimeType(subtitle.link))
.build()
})
.setRequestMetadata(
@@ -227,13 +273,15 @@ class OmniPlayer(
.build()
}
fun setVideoView(surfaceView: android.view.SurfaceView?) {
fun setVideoView(surfaceView: android.view.SurfaceView) {
runOnMainThread {
if (surfaceView == null) {
localPlayer.clearVideoSurface()
} else {
localPlayer.setVideoSurfaceView(surfaceView)
}
localPlayer.setVideoSurfaceView(surfaceView)
}
}
fun clearVideoView(surfaceView: android.view.SurfaceView) {
runOnMainThread {
localPlayer.clearVideoSurfaceView(surfaceView)
}
}
@@ -295,7 +343,39 @@ class OmniPlayer(
override val videos by mainThreadProperty { tracksByType(C.TRACK_TYPE_VIDEO) }
override val audios by mainThreadProperty { tracksByType(C.TRACK_TYPE_AUDIO) }
override val subtitles by mainThreadProperty { tracksByType(C.TRACK_TYPE_TEXT) }
override val rendition by mainThreadProperty { getRenditions() }
override val renditions by mainThreadProperty {
val group =
player.currentTracks.groups.firstOrNull { it.isSelected && it.type == C.TRACK_TYPE_VIDEO }
?: return@mainThreadProperty emptyArray<Rendition>()
val currentIndex = when {
isAutoQuality -> {
if (player.videoSize.width > 0 && player.videoSize.height > 0) {
(0 until group.length).firstOrNull { i ->
val format = group.getTrackFormat(i)
format.width == player.videoSize.width && format.height == player.videoSize.height
}
} else null
}
else -> (0 until group.length).firstOrNull { group.isTrackSelected(it) }
}
val result = ArrayList<Rendition>()
for (i in 0 until group.length) {
val format = group.getTrackFormat(i)
result.add(
Rendition(
id = i.toString(),
width = format.width.toDouble().coerceAtLeast(0.0),
height = format.height.toDouble().coerceAtLeast(0.0),
bitrate = format.bitrate.toDouble().coerceAtLeast(0.0),
selected = i == currentIndex
)
)
}
return@mainThreadProperty result.toTypedArray()
}
override var isAutoQuality by mainThreadProperty {
player.trackSelectionParameters.overrides.none {
@@ -303,6 +383,10 @@ class OmniPlayer(
}
}
init {
runOnMainThreadSync { eventMap.player = player }
}
override var source: Source? = null
set(value) {
field = value
@@ -419,13 +503,19 @@ class OmniPlayer(
// when casting tracks are stripped of some metadata, recover it
val castTracks = castMediaTracksById()
// external (sideloaded) subs aren't in the container, so their index is -1
val sideloadedIds = source?.subtitles?.map { it.id }?.toHashSet().orEmpty()
var index = 0
return groups.map { group ->
val format = group.getTrackFormat(0)
val castTrackId = group.mediaTrackGroup.id.substringAfterLast("track=", "").toLongOrNull()
val id = group.mediaTrackGroup.id
val castTrackId = id.substringAfterLast("track=", "").toLongOrNull()
val cast = castTracks[castTrackId]
val sideloaded = trackType == C.TRACK_TYPE_TEXT && id in sideloadedIds
Track(
id = group.mediaTrackGroup.id,
id = id,
index = if (sideloaded) -1.0 else (index++).toDouble(),
label = cast?.name ?: format.label,
language = cast?.language ?: format.language,
selected = group.isSelected
@@ -440,40 +530,6 @@ class OmniPlayer(
return tracks.associateBy { it.id }
}
private fun getRenditions(): Array<Rendition> {
val group =
player.currentTracks.groups.firstOrNull { it.isSelected && it.type == C.TRACK_TYPE_VIDEO }
?: return emptyArray()
val currentIndex = when {
isAutoQuality -> {
if (player.videoSize.width > 0 && player.videoSize.height > 0) {
(0 until group.length).firstOrNull { i ->
val format = group.getTrackFormat(i)
format.width == player.videoSize.width && format.height == player.videoSize.height
}
} else null
}
else -> (0 until group.length).firstOrNull { group.isTrackSelected(it) }
}
val result = ArrayList<Rendition>()
for (i in 0 until group.length) {
val format = group.getTrackFormat(i)
result.add(
Rendition(
id = i.toString(),
width = format.width.toDouble().coerceAtLeast(0.0),
height = format.height.toDouble().coerceAtLeast(0.0),
bitrate = format.bitrate.toDouble().coerceAtLeast(0.0),
selected = i == currentIndex
)
)
}
return result.toTypedArray()
}
override fun selectRendition(rendition: Rendition?) {
runOnMainThreadSync {
if (rendition == null) {
@@ -1,6 +1,7 @@
package dev.zoriya.omni
import android.app.PictureInPictureParams
import android.graphics.Color
import android.graphics.Rect
import android.os.Build
import android.util.Rational
@@ -117,6 +118,7 @@ class OmniView(val context: ThemedReactContext) :
override var subtitleAssets: SubtitleAssets? = null
init {
view.setBackgroundColor(Color.BLACK)
context.addLifecycleEventListener(this)
}
@@ -154,15 +156,11 @@ class OmniView(val context: ThemedReactContext) :
val curPip = activeView.get()
when {
autoPip == true && curPip == this -> {}
autoPip == true && curPip == null -> {
autoPip == true -> {
activeView = WeakReference(this)
updatePictureInPictureParams()
}
autoPip == true -> {
throw Error("Only one OmniView can have `autoPip` set at a time.")
}
autoPip == false && curPip == this -> {
activeView = WeakReference(null)
clearPictureInPictureParams()
@@ -180,7 +178,7 @@ class OmniView(val context: ThemedReactContext) :
}
boundPlayer?.localPlayer?.removeListener(this)
boundPlayer?.setVideoView(null)
boundPlayer?.clearVideoView(surfaceView)
boundPlayer = omniPlayer
omniPlayer.localPlayer.addListener(this)
@@ -206,7 +204,7 @@ class OmniView(val context: ThemedReactContext) :
val omniPlayer = player as? OmniPlayer ?: return
boundPlayer?.localPlayer?.removeListener(this)
omniPlayer.setVideoView(null)
omniPlayer.clearVideoView(surfaceView)
boundPlayer = null
}
@@ -414,6 +412,6 @@ class OmniView(val context: ThemedReactContext) :
override fun surfaceDestroyed(holder: SurfaceHolder) {
surfaceReady = false
surfaceView.removeCallbacks(rebuildRunnable)
boundPlayer?.setVideoView(null)
boundPlayer?.clearVideoView(surfaceView)
}
}
@@ -46,6 +46,7 @@ import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.interfaces.IMedia
import org.videolan.libvlc.interfaces.IMedia.VideoTrack
import org.videolan.libvlc.interfaces.IVLCVout
import java.security.MessageDigest
@SuppressLint("UnsafeOptInUsageError")
class VlcPlayer(ctx: Context) :
@@ -99,11 +100,14 @@ class VlcPlayer(ctx: Context) :
private var currentTrackSelectionParameters = TrackSelectionParameters.Builder().build()
@Volatile
private var playerError: PlaybackException? = null
@Volatile
private var released = false
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
private var userInitiatedTransition: Boolean = false
@Volatile
private var cachedBufferedPosition: Long = 0L
private var boundSurfaceView: SurfaceView? = null
private var lastVideoSize: VideoSize = VideoSize.UNKNOWN
private val availableCommands: Player.Commands = Player.Commands.Builder()
.add(COMMAND_PLAY_PAUSE)
@@ -136,6 +140,7 @@ class VlcPlayer(ctx: Context) :
override fun onEvent(event: MediaPlayer.Event) {
when (event.type) {
MediaPlayer.Event.Opening -> {
lastVideoSize = VideoSize.UNKNOWN
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
it.onPlaybackStateChanged(STATE_BUFFERING)
}
@@ -158,6 +163,7 @@ class VlcPlayer(ctx: Context) :
it.onIsPlayingChanged(true)
it.onTracksChanged(getCurrentTracks())
}
maybeNotifyVideoSizeChanged()
}
MediaPlayer.Event.Paused -> {
@@ -211,12 +217,19 @@ class VlcPlayer(ctx: Context) :
// this improves perf & battery life (native -> js bridge is expensive)
MediaPlayer.Event.TimeChanged -> Unit
// vlc has no video-size event, Vout event still has unknown size.
// This is the first callback with known size.
MediaPlayer.Event.PositionChanged -> {
if (lastVideoSize == VideoSize.UNKNOWN) maybeNotifyVideoSizeChanged()
}
MediaPlayer.Event.ESAdded,
MediaPlayer.Event.ESDeleted,
MediaPlayer.Event.ESSelected -> {
notifyListeners(EVENT_TRACKS_CHANGED) {
it.onTracksChanged(getCurrentTracks())
}
maybeNotifyVideoSizeChanged()
}
MediaPlayer.Event.LengthChanged -> {
@@ -353,6 +366,7 @@ class VlcPlayer(ctx: Context) :
media.addOption(":start-time=${targetMs / 1000.0}")
player.setMedia(media)
media.release()
}
}
@@ -374,6 +388,20 @@ class VlcPlayer(ctx: Context) :
}
}
// vlc keys each slave's tracks by the md5 of its uri ("<md5(uri)>/spu/<n>")
private fun subtitleSlaveForTrackId(trackId: String): MediaItem.SubtitleConfiguration? {
val hash = trackId.substringBefore("/spu/", "")
if (hash.isEmpty()) return null
return mediaItems.getOrNull(currentMediaItemIndex)
?.localConfiguration?.subtitleConfigurations
?.firstOrNull { md5(it.uri.toString()) == hash }
}
private fun md5(value: String): String =
MessageDigest.getInstance("MD5")
.digest(value.toByteArray())
.joinToString("") { "%02x".format(it.toInt() and 0xFF) }
// vlc doesn't allow arbitrary headers :c
private fun applyRequestHeaders(media: Media, extras: Bundle?) {
if (extras == null) return
@@ -430,7 +458,7 @@ class VlcPlayer(ctx: Context) :
when {
playerError != null -> STATE_IDLE
currentMediaItemIndex == INDEX_UNSET -> STATE_IDLE
player.media == null -> STATE_IDLE
player.media?.also { it.release() } == 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
@@ -519,13 +547,17 @@ class VlcPlayer(ctx: Context) :
}
override fun release() {
if (released) return
released = true
player.setEventListener(null)
listeners.release()
abandonAudioFocus()
player.stop()
clearVideoSurface()
player.release()
libVLC.release()
applicationHandler.post {
player.release()
libVLC.release()
}
}
override fun getCurrentTracks(): Tracks {
@@ -582,10 +614,12 @@ class VlcPlayer(ctx: Context) :
}
player.getTracks(IMedia.Track.Type.Text)?.forEach { track ->
// external subs don't have label/language, match them back to surface them
val slave = subtitleSlaveForTrackId(track.id)
val format = Format.Builder()
.setId(track.id)
.setLabel(track.name)
.setLanguage(track.language)
.setLabel(slave?.label ?: track.name)
.setLanguage(slave?.language ?: track.language)
.setSampleMimeType("text/x-unknown")
.build()
val group = TrackGroup("vlc-sub-${track.id}", format)
@@ -894,6 +928,7 @@ class VlcPlayer(ctx: Context) :
}
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
if (surfaceView != null && boundSurfaceView !== surfaceView) return
clearVideoSurface()
}
@@ -901,10 +936,30 @@ class VlcPlayer(ctx: Context) :
override fun clearVideoTextureView(textureView: TextureView?) = Unit
/**
* VLC never pushes a video-size event, so emit our own once the selected video
* track (and its sample aspect ratio) is known. Listeners such as OmniView use
* this to size the content frame -> the PIP aspect ratio matches the video
* instead of the raw SurfaceView bounds (ExoPlayer does this on its own).
*/
private fun maybeNotifyVideoSizeChanged() {
val size = getVideoSize()
if (size == VideoSize.UNKNOWN || size == lastVideoSize) return
lastVideoSize = size
notifyListeners(EVENT_VIDEO_SIZE_CHANGED) {
it.onVideoSizeChanged(size)
}
}
override fun getVideoSize(): VideoSize {
val videoTrack = player.getSelectedTrack(IMedia.Track.Type.Video) as? VideoTrack ?: return VideoSize.UNKNOWN
if (videoTrack.width <= 0 || videoTrack.height <= 0) return VideoSize.UNKNOWN
return VideoSize(videoTrack.width, videoTrack.height)
val par = if (videoTrack.sarNum > 0 && videoTrack.sarDen > 0) {
videoTrack.sarNum.toFloat() / videoTrack.sarDen.toFloat()
} else {
1f
}
return VideoSize(videoTrack.width, videoTrack.height, par)
}
override fun getSurfaceSize(): Size = Size.UNKNOWN
+13 -6
View File
@@ -6,7 +6,12 @@
"name": "react-native-omni",
"dependencies": {
"@expo/config-plugins": "^57.0.3",
"@videojs/react": "10.0.0-beta.25",
"@videojs/core": "github:zoriya/v10#build26-core",
"@videojs/media": "github:zoriya/v10#build26-media",
"@videojs/react": "github:zoriya/v10#build26-react",
"@videojs/spf": "github:zoriya/v10#build26-spf",
"@videojs/store": "github:zoriya/v10#build26-store",
"@videojs/utils": "github:zoriya/v10#build26-utils",
"jassub": "^2.5.7",
"libpgs": "^0.8.1",
},
@@ -723,15 +728,17 @@
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
"@videojs/core": ["@videojs/core@10.0.0-beta.25", "", { "dependencies": { "@videojs/spf": "10.0.0-beta.25", "@videojs/store": "10.0.0-beta.25", "@videojs/utils": "10.0.0-beta.25", "@vimeo/player": "^2.30.3", "dashjs": "^5.2.0", "hls.js": "^1.6.7", "mux-embed": "^5.17.10" } }, "sha512-Yxd6c+u7LrYl6aKzZRyR5PjDqUt1lUbgFwIhg6pMzI8bJszIGjLS7TDzXdALpEFJpM1Z08dbqCEzCGaMj7Eu6A=="],
"@videojs/core": ["@videojs/core@github:zoriya/v10#02a4769", { "peerDependencies": { "@videojs/media": "*", "@videojs/store": "*", "@videojs/utils": "*" } }, "zoriya-v10-02a4769", "sha512-eq79BQJ+e2Lx6k9tCW68vBolcJD3OynwgE7Y8pmI9gb+RWQAYJFLEY8B576ueKHhcgOOT/taerI/Wg3eqougSg=="],
"@videojs/react": ["@videojs/react@10.0.0-beta.25", "", { "dependencies": { "@videojs/core": "10.0.0-beta.25", "@videojs/spf": "10.0.0-beta.25", "@videojs/store": "10.0.0-beta.25", "@videojs/utils": "10.0.0-beta.25" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-MUG8NFntK3u4OTArwqG4utZ11HU1Ku5kzzTfTA2BMG5shNj7KWqPz0BMR2Nnv6hwEvcxpm9vwZxk+AwkmoB5jA=="],
"@videojs/media": ["@videojs/media@github:zoriya/v10#46507f9", { "dependencies": { "@vimeo/player": "^2.30.3", "dashjs": "^5.2.0", "hls.js": "^1.6.7", "mux-embed": "^5.17.10" }, "peerDependencies": { "@videojs/spf": "*", "@videojs/utils": "*" } }, "zoriya-v10-46507f9", "sha512-1f7jIP5KPGKo4NeyG+Q22eS7KhvYZam8yvG985Y9edCp7wFcrVcraEQwKJrESaa/GDNgeKIeEg05MZ+l+ZHftQ=="],
"@videojs/spf": ["@videojs/spf@10.0.0-beta.25", "", { "dependencies": { "@videojs/utils": "10.0.0-beta.25", "signal-polyfill": "^0.2.2" } }, "sha512-SETOzCPbSxuzCoX+bF2u7/Z80AA/Xwpqpy5ki//yslIyPxWkMT0GFydXdoZ/bJpNsBqGmDbDxEBDo1uMjnM4mQ=="],
"@videojs/react": ["@videojs/react@github:zoriya/v10#784a383", { "peerDependencies": { "@videojs/core": "*", "@videojs/media": "*", "@videojs/spf": "*", "@videojs/store": "*", "@videojs/utils": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "zoriya-v10-784a383", "sha512-wLuXUjLfhLNZAVSxCsTmpidN8DZPy6Us788vXDSRJKh/a6WDTCWV2op26EK3eCgeC1P8Hwc5s5cOEhqx2fGMRQ=="],
"@videojs/store": ["@videojs/store@10.0.0-beta.25", "", { "dependencies": { "@videojs/utils": "10.0.0-beta.25" }, "peerDependencies": { "@videojs/element": "10.0.0-beta.25", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@videojs/element", "react"] }, "sha512-XvPC0+MQ6AZ/oKGpSzmjOM56aYESMpPsTg5zddBDXhcqeAOUxpEsXDqhU1rsm751tU4ZavGxHENx70lBOa5dUA=="],
"@videojs/spf": ["@videojs/spf@github:zoriya/v10#8920135", { "dependencies": { "signal-polyfill": "^0.2.2" }, "peerDependencies": { "@videojs/utils": "*" } }, "zoriya-v10-8920135", "sha512-jGXo5tyPpIk96XWksTKbWpi7POHzdt4KtvqDZ9+SDa+MOqLV3TcgM8soo6KO2jMnkv8d8APqaz+E27qfDf16kg=="],
"@videojs/utils": ["@videojs/utils@10.0.0-beta.25", "", {}, "sha512-tq/848qmzgTncopeYE5iiYs1Pfzj4OEdSe7zE2H6xoOB0PUMlnPOThA3aybHJ8Lh9bmw+wTb6QJ0NZanIzt2Zg=="],
"@videojs/store": ["@videojs/store@github:zoriya/v10#3f9bddd", { "peerDependencies": { "@videojs/element": "*", "@videojs/utils": "*", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@videojs/element", "react"] }, "zoriya-v10-3f9bddd", "sha512-QjSGJ6EO6uoij41F7KgMKrNi5XjvXfkODOrn17wcFa6+Ig1+ftnckkNZsJ44bARdJ1XUkuj3wNyGidYMrmKRmQ=="],
"@videojs/utils": ["@videojs/utils@github:zoriya/v10#9fed1b6", {}, "zoriya-v10-9fed1b6", "sha512-ZgbMw3v41aEru0056VVwy0UtiZk8zq6Tm6ddRAJtm5VXz2nEf36CoejFFvmGICW5cQ2WWhvMQmtqCSr+HmYGIw=="],
"@vimeo/player": ["@vimeo/player@2.30.4", "", { "dependencies": { "native-promise-only": "0.8.1", "weakmap-polyfill": "2.0.4" } }, "sha512-M8m1UAhJSb+KCWuXDLWHViwj+3YY/0ogwFquRfMHs9e9LYjXT9iB7n+sOCKwUusbiXuU2HKmXx+FEGHtYZfUSg=="],
+15 -103
View File
@@ -1,5 +1,5 @@
import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import {
type AndroidBackend,
@@ -81,25 +81,12 @@ function PlayerExample({
const castStatus = usePlayerState("castStatus");
const [logs, setLogs] = useState<{ id: number; message: string }[]>([]);
const logId = useRef(0);
const [tracks, setTracks] = useState(() => ({
videos: [...player.videos],
audios: [...player.audios],
subtitles: [...player.subtitles],
renditions: [...player.rendition],
}));
const refreshTracks = useCallback(() => {
setTracks({
videos: [...player.videos],
audios: [...player.audios],
subtitles: [...player.subtitles],
renditions: [...player.rendition],
});
}, [player]);
useEffect(() => {
refreshTracks();
}, [refreshTracks]);
const tracks = {
videos: usePlayerState("videos"),
audios: usePlayerState("audios"),
subtitles: usePlayerState("subtitles"),
renditions: usePlayerState("renditions"),
};
const pushLog = useCallback((message: string) => {
setLogs((prev) => {
@@ -145,53 +132,8 @@ function PlayerExample({
handleNext();
}, [handleNext, pushLog]),
);
useEvent(
"videoTrackChange",
useCallback(
(track) => {
pushLog(`Video track: ${track.label ?? track.id}`);
refreshTracks();
},
[pushLog, refreshTracks],
),
);
useEvent(
"audioTrackChange",
useCallback(
(track) => {
pushLog(`Audio track: ${track.label ?? track.id}`);
refreshTracks();
},
[pushLog, refreshTracks],
),
);
useEvent(
"subtitleChange",
useCallback(
(track) => {
pushLog(`Subtitle: ${track?.label ?? track?.id ?? "off"}`);
refreshTracks();
},
[pushLog, refreshTracks],
),
);
useEvent(
"renditionChange",
useCallback(
(rendition) => {
pushLog(`Rendition: ${rendition.width}x${rendition.height}`);
refreshTracks();
},
[pushLog, refreshTracks],
),
);
const switchBackend = (target: AndroidBackend) => {
if (target === backend) return;
// Pause before tearing down: the native media-session guard refuses a
// second player while the current one is still *playing*, so this keeps
// the transition clean (we don't preserve state across a switch anyway).
player.pause();
onSwitchBackend(target);
};
@@ -221,17 +163,14 @@ function PlayerExample({
const selectVideo = (video: (typeof tracks.videos)[number]) => {
player.selectVideo(video);
refreshTracks();
};
const selectAudio = (audio: (typeof tracks.audios)[number]) => {
player.selectAudio(audio);
refreshTracks();
};
const selectSubtitle = (subtitle?: (typeof tracks.subtitles)[number]) => {
player.selectSubtitle(subtitle);
refreshTracks();
};
return (
@@ -518,27 +457,13 @@ function App(): React.JSX.Element {
// notification stays hidden) before any source is set.
const [hasSource, setHasSource] = useState(false);
const [backend, setBackend] = useState<AndroidBackend>("vlc");
const [pendingBackend, setPendingBackend] = useState<AndroidBackend | null>(
null,
);
// Switching backend recreates the native player, so we fully unmount the
// provider first (rendering nothing) and remount it on the next tick with
// the new backend. This lets the old OmniView/player tear down before the
// new one is created, avoiding the "only one view"/"two players" guards.
// Switching the backend just updates the prop: OmniProvider recreates (and
// disposes) the native player internally, so the change applies live.
const handleSwitchBackend = useCallback((next: AndroidBackend) => {
setPendingBackend(next);
setBackend(next);
}, []);
useEffect(() => {
if (pendingBackend === null) return;
const id = setTimeout(() => {
setBackend(pendingBackend);
setPendingBackend(null);
}, 300);
return () => clearTimeout(id);
}, [pendingBackend]);
const handlePrev = useCallback(() => {
setCurrentIndex((index) => (index === 0 ? PLAYLIST.length - 1 : index - 1));
}, []);
@@ -582,21 +507,11 @@ function App(): React.JSX.Element {
[currentIndex],
);
// While switching, render nothing so the current player is torn down first.
if (pendingBackend !== null) {
return (
<View style={styles.switching}>
<Text style={styles.subheading}>Switching to {pendingBackend}…</Text>
</View>
);
}
return (
<OmniProvider
key={backend}
source={hasSource ? source : undefined}
backend={{ android: backend }}
cast={{receiverApplicationId: "D8FB0FC1"}}
cast={{ receiverApplicationId: "D8FB0FC1" }}
showNotification
>
<PlayerExample
@@ -621,12 +536,6 @@ const styles = StyleSheet.create({
paddingVertical: 12,
gap: 12,
},
switching: {
flex: 1,
backgroundColor: "#0b1020",
alignItems: "center",
justifyContent: "center",
},
heading: {
fontSize: 24,
fontWeight: "700",
@@ -638,7 +547,10 @@ const styles = StyleSheet.create({
},
video: {
width: "100%",
aspectRatio: 16 / 9,
// Intentionally wider than the 16:9 sample videos so the view's aspect
// ratio differs from the video's. This surfaces the PIP aspect-ratio bug
// where PIP would otherwise inherit the view's shape, not the video's.
aspectRatio: 21 / 9,
borderRadius: 14,
overflow: "hidden",
},
+4
View File
@@ -21,6 +21,8 @@
#include "JFunc_void_PlayerStatus.hpp"
#include "JFunc_void_CastStatus.hpp"
#include "JFunc_void_std__optional_Source_.hpp"
#include "JFunc_void_std__vector_Track_.hpp"
#include "JFunc_void_std__vector_Rendition_.hpp"
#include "JFunc_void.hpp"
#include "JFunc_void_std__string_std__string.hpp"
#include "JFunc_void_std__string.hpp"
@@ -69,6 +71,8 @@ void registerAllNatives() {
margelo::nitro::omni::JFunc_void_PlayerStatus_cxx::registerNatives();
margelo::nitro::omni::JFunc_void_CastStatus_cxx::registerNatives();
margelo::nitro::omni::JFunc_void_std__optional_Source__cxx::registerNatives();
margelo::nitro::omni::JFunc_void_std__vector_Track__cxx::registerNatives();
margelo::nitro::omni::JFunc_void_std__vector_Rendition__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();
@@ -0,0 +1,97 @@
///
/// JFunc_void_std__vector_Rendition_.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 "Rendition.hpp"
#include <vector>
#include <functional>
#include <NitroModules/JNICallable.hpp>
#include "JRendition.hpp"
#include <string>
namespace margelo::nitro::omni {
using namespace facebook;
/**
* Represents the Java/Kotlin callback `(value: Array<Rendition>) -> Unit`.
* This can be passed around between C++ and Java/Kotlin.
*/
struct JFunc_void_std__vector_Rendition_: public jni::JavaClass<JFunc_void_std__vector_Rendition_> {
public:
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Func_void_std__vector_Rendition_;";
public:
/**
* Invokes the function this `JFunc_void_std__vector_Rendition_` instance holds through JNI.
*/
void invoke(const std::vector<Rendition>& value) const {
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JArrayClass<JRendition>> /* value */)>("invoke");
method(self(), [&](auto&& __input) {
size_t __size = __input.size();
jni::local_ref<jni::JArrayClass<JRendition>> __array = jni::JArrayClass<JRendition>::newArray(__size);
for (size_t __i = 0; __i < __size; __i++) {
const auto& __element = __input[__i];
auto __elementJni = JRendition::fromCpp(__element);
__array->setElement(__i, *__elementJni);
}
return __array;
}(value));
}
};
/**
* An implementation of Func_void_std__vector_Rendition_ that is backed by a C++ implementation (using `std::function<...>`)
*/
class JFunc_void_std__vector_Rendition__cxx final: public jni::HybridClass<JFunc_void_std__vector_Rendition__cxx, JFunc_void_std__vector_Rendition_> {
public:
static jni::local_ref<JFunc_void_std__vector_Rendition_::javaobject> fromCpp(const std::function<void(const std::vector<Rendition>& /* value */)>& func) {
return JFunc_void_std__vector_Rendition__cxx::newObjectCxxArgs(func);
}
public:
/**
* Invokes the C++ `std::function<...>` this `JFunc_void_std__vector_Rendition__cxx` instance holds.
*/
void invoke_cxx(jni::alias_ref<jni::JArrayClass<JRendition>> value) {
_func([&](auto&& __input) {
size_t __size = __input->size();
std::vector<Rendition> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(value));
}
public:
[[nodiscard]]
inline const std::function<void(const std::vector<Rendition>& /* value */)>& getFunction() const {
return _func;
}
public:
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Func_void_std__vector_Rendition__cxx;";
static void registerNatives() {
registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_std__vector_Rendition__cxx::invoke_cxx)});
}
private:
explicit JFunc_void_std__vector_Rendition__cxx(const std::function<void(const std::vector<Rendition>& /* value */)>& func): _func(func) { }
private:
friend HybridBase;
std::function<void(const std::vector<Rendition>& /* value */)> _func;
};
} // namespace margelo::nitro::omni
@@ -0,0 +1,98 @@
///
/// JFunc_void_std__vector_Track_.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 "Track.hpp"
#include <vector>
#include <functional>
#include <NitroModules/JNICallable.hpp>
#include "JTrack.hpp"
#include <string>
#include <optional>
namespace margelo::nitro::omni {
using namespace facebook;
/**
* Represents the Java/Kotlin callback `(value: Array<Track>) -> Unit`.
* This can be passed around between C++ and Java/Kotlin.
*/
struct JFunc_void_std__vector_Track_: public jni::JavaClass<JFunc_void_std__vector_Track_> {
public:
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Func_void_std__vector_Track_;";
public:
/**
* Invokes the function this `JFunc_void_std__vector_Track_` instance holds through JNI.
*/
void invoke(const std::vector<Track>& value) const {
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JArrayClass<JTrack>> /* value */)>("invoke");
method(self(), [&](auto&& __input) {
size_t __size = __input.size();
jni::local_ref<jni::JArrayClass<JTrack>> __array = jni::JArrayClass<JTrack>::newArray(__size);
for (size_t __i = 0; __i < __size; __i++) {
const auto& __element = __input[__i];
auto __elementJni = JTrack::fromCpp(__element);
__array->setElement(__i, *__elementJni);
}
return __array;
}(value));
}
};
/**
* An implementation of Func_void_std__vector_Track_ that is backed by a C++ implementation (using `std::function<...>`)
*/
class JFunc_void_std__vector_Track__cxx final: public jni::HybridClass<JFunc_void_std__vector_Track__cxx, JFunc_void_std__vector_Track_> {
public:
static jni::local_ref<JFunc_void_std__vector_Track_::javaobject> fromCpp(const std::function<void(const std::vector<Track>& /* value */)>& func) {
return JFunc_void_std__vector_Track__cxx::newObjectCxxArgs(func);
}
public:
/**
* Invokes the C++ `std::function<...>` this `JFunc_void_std__vector_Track__cxx` instance holds.
*/
void invoke_cxx(jni::alias_ref<jni::JArrayClass<JTrack>> value) {
_func([&](auto&& __input) {
size_t __size = __input->size();
std::vector<Track> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(value));
}
public:
[[nodiscard]]
inline const std::function<void(const std::vector<Track>& /* value */)>& getFunction() const {
return _func;
}
public:
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Func_void_std__vector_Track__cxx;";
static void registerNatives() {
registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_std__vector_Track__cxx::invoke_cxx)});
}
private:
explicit JFunc_void_std__vector_Track__cxx(const std::function<void(const std::vector<Track>& /* value */)>& func): _func(func) { }
private:
friend HybridBase;
std::function<void(const std::vector<Track>& /* value */)> _func;
};
} // namespace margelo::nitro::omni
+26 -4
View File
@@ -25,6 +25,8 @@ namespace margelo::nitro::omni { struct Subtitle; }
namespace margelo::nitro::omni { struct Metadata; }
// Forward declaration of `MixAudioMode` to properly resolve imports.
namespace margelo::nitro::omni { enum class MixAudioMode; }
// Forward declaration of `TrackProperty` to properly resolve imports.
namespace margelo::nitro::omni { enum class TrackProperty; }
// Forward declaration of `Track` to properly resolve imports.
namespace margelo::nitro::omni { struct Track; }
// Forward declaration of `Rendition` to properly resolve imports.
@@ -59,16 +61,20 @@ namespace margelo::nitro::omni { struct Rendition; }
#include "JMetadata.hpp"
#include "MixAudioMode.hpp"
#include "JMixAudioMode.hpp"
#include "TrackProperty.hpp"
#include "JTrackProperty.hpp"
#include "Track.hpp"
#include "JFunc_void_std__vector_Track_.hpp"
#include "JTrack.hpp"
#include "Rendition.hpp"
#include "JFunc_void_std__vector_Rendition_.hpp"
#include "JRendition.hpp"
#include "JFunc_void.hpp"
#include "JFunc_void_std__string_std__string.hpp"
#include "JFunc_void_std__string.hpp"
#include "Track.hpp"
#include "JFunc_void_Track.hpp"
#include "JTrack.hpp"
#include "JFunc_void_std__optional_Track_.hpp"
#include "Rendition.hpp"
#include "JFunc_void_Rendition.hpp"
#include "JRendition.hpp"
namespace margelo::nitro::omni {
@@ -143,6 +149,22 @@ namespace margelo::nitro::omni {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_std__optional_Source_::javaobject> /* cb */)>("removeSourceListener_cxx");
method(_javaPart, JFunc_void_std__optional_Source__cxx::fromCpp(cb));
}
void JHybridOmniEventMapSpec::addTracksListener(TrackProperty key, const std::function<void(const std::vector<Track>& /* value */)>& cb) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JTrackProperty> /* key */, jni::alias_ref<JFunc_void_std__vector_Track_::javaobject> /* cb */)>("addTracksListener_cxx");
method(_javaPart, JTrackProperty::fromCpp(key), JFunc_void_std__vector_Track__cxx::fromCpp(cb));
}
void JHybridOmniEventMapSpec::removeTracksListener(TrackProperty key, const std::function<void(const std::vector<Track>& /* value */)>& cb) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JTrackProperty> /* key */, jni::alias_ref<JFunc_void_std__vector_Track_::javaobject> /* cb */)>("removeTracksListener_cxx");
method(_javaPart, JTrackProperty::fromCpp(key), JFunc_void_std__vector_Track__cxx::fromCpp(cb));
}
void JHybridOmniEventMapSpec::addRenditionsListener(const std::function<void(const std::vector<Rendition>& /* value */)>& cb) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_std__vector_Rendition_::javaobject> /* cb */)>("addRenditionsListener_cxx");
method(_javaPart, JFunc_void_std__vector_Rendition__cxx::fromCpp(cb));
}
void JHybridOmniEventMapSpec::removeRenditionsListener(const std::function<void(const std::vector<Rendition>& /* value */)>& cb) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_std__vector_Rendition_::javaobject> /* cb */)>("removeRenditionsListener_cxx");
method(_javaPart, JFunc_void_std__vector_Rendition__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));
@@ -64,6 +64,10 @@ namespace margelo::nitro::omni {
void removeCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) override;
void addSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) override;
void removeSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) override;
void addTracksListener(TrackProperty key, const std::function<void(const std::vector<Track>& /* value */)>& cb) override;
void removeTracksListener(TrackProperty key, const std::function<void(const std::vector<Track>& /* value */)>& cb) override;
void addRenditionsListener(const std::function<void(const std::vector<Rendition>& /* value */)>& cb) override;
void removeRenditionsListener(const std::function<void(const std::vector<Rendition>& /* 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;
+70 -66
View File
@@ -9,10 +9,6 @@
// Forward declaration of `HybridOmniEventMapSpec` to properly resolve imports.
namespace margelo::nitro::omni { class HybridOmniEventMapSpec; }
// Forward declaration of `Track` to properly resolve imports.
namespace margelo::nitro::omni { struct Track; }
// Forward declaration of `Rendition` to properly resolve imports.
namespace margelo::nitro::omni { struct Rendition; }
// Forward declaration of `Source` to properly resolve imports.
namespace margelo::nitro::omni { struct Source; }
// Forward declaration of `VideoSrc` to properly resolve imports.
@@ -27,23 +23,23 @@ namespace margelo::nitro::omni { enum class MixAudioMode; }
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.
namespace margelo::nitro::omni { struct Rendition; }
#include <memory>
#include "HybridOmniEventMapSpec.hpp"
#include "JHybridOmniEventMapSpec.hpp"
#include <optional>
#include "Track.hpp"
#include <vector>
#include "JTrack.hpp"
#include <string>
#include "Rendition.hpp"
#include "JRendition.hpp"
#include "Source.hpp"
#include "JSource.hpp"
#include "VideoSrc.hpp"
#include "JVideoSrc.hpp"
#include <string>
#include <unordered_map>
#include "Subtitle.hpp"
#include <vector>
#include "JSubtitle.hpp"
#include "Metadata.hpp"
#include "JMetadata.hpp"
@@ -53,6 +49,10 @@ namespace margelo::nitro::omni { enum class CastStatus; }
#include "JPlayerStatus.hpp"
#include "CastStatus.hpp"
#include "JCastStatus.hpp"
#include "Track.hpp"
#include "JTrack.hpp"
#include "Rendition.hpp"
#include "JRendition.hpp"
namespace margelo::nitro::omni {
@@ -108,62 +108,6 @@ namespace margelo::nitro::omni {
auto __result = method(_javaPart);
return static_cast<bool>(__result);
}
std::vector<Track> JHybridOmniPlayerSpec::getVideos() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<JTrack>>()>("getVideos");
auto __result = method(_javaPart);
return [&](auto&& __input) {
size_t __size = __input->size();
std::vector<Track> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(__result);
}
std::vector<Track> JHybridOmniPlayerSpec::getAudios() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<JTrack>>()>("getAudios");
auto __result = method(_javaPart);
return [&](auto&& __input) {
size_t __size = __input->size();
std::vector<Track> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(__result);
}
std::vector<Track> JHybridOmniPlayerSpec::getSubtitles() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<JTrack>>()>("getSubtitles");
auto __result = method(_javaPart);
return [&](auto&& __input) {
size_t __size = __input->size();
std::vector<Track> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(__result);
}
std::vector<Rendition> JHybridOmniPlayerSpec::getRendition() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<JRendition>>()>("getRendition");
auto __result = method(_javaPart);
return [&](auto&& __input) {
size_t __size = __input->size();
std::vector<Rendition> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(__result);
}
std::optional<Source> JHybridOmniPlayerSpec::getSource() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JSource>()>("getSource");
auto __result = method(_javaPart);
@@ -239,8 +183,68 @@ namespace margelo::nitro::omni {
auto __result = method(_javaPart);
return __result->toCpp();
}
std::vector<Track> JHybridOmniPlayerSpec::getVideos() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<JTrack>>()>("getVideos");
auto __result = method(_javaPart);
return [&](auto&& __input) {
size_t __size = __input->size();
std::vector<Track> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(__result);
}
std::vector<Track> JHybridOmniPlayerSpec::getAudios() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<JTrack>>()>("getAudios");
auto __result = method(_javaPart);
return [&](auto&& __input) {
size_t __size = __input->size();
std::vector<Track> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(__result);
}
std::vector<Track> JHybridOmniPlayerSpec::getSubtitles() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<JTrack>>()>("getSubtitles");
auto __result = method(_javaPart);
return [&](auto&& __input) {
size_t __size = __input->size();
std::vector<Track> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(__result);
}
std::vector<Rendition> JHybridOmniPlayerSpec::getRenditions() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<JRendition>>()>("getRenditions");
auto __result = method(_javaPart);
return [&](auto&& __input) {
size_t __size = __input->size();
std::vector<Rendition> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(__result);
}
// Methods
void JHybridOmniPlayerSpec::release() {
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("release");
method(_javaPart);
}
void JHybridOmniPlayerSpec::play() {
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("play");
method(_javaPart);
+5 -4
View File
@@ -55,10 +55,6 @@ namespace margelo::nitro::omni {
void setShowNotification(std::optional<bool> showNotification) override;
bool getHasPrev() override;
bool getHasNext() override;
std::vector<Track> getVideos() override;
std::vector<Track> getAudios() override;
std::vector<Track> getSubtitles() override;
std::vector<Rendition> getRendition() override;
std::optional<Source> getSource() override;
void setSource(const std::optional<Source>& source) override;
PlayerStatus getStatus() override;
@@ -75,9 +71,14 @@ namespace margelo::nitro::omni {
void setMuted(bool muted) override;
bool getIsAutoQuality() override;
CastStatus getCastStatus() override;
std::vector<Track> getVideos() override;
std::vector<Track> getAudios() override;
std::vector<Track> getSubtitles() override;
std::vector<Rendition> getRenditions() override;
public:
// Methods
void release() override;
void play() override;
void pause() override;
void seekBy(double offset) override;
+5 -1
View File
@@ -34,6 +34,8 @@ namespace margelo::nitro::omni {
static const auto clazz = javaClassStatic();
static const auto fieldId = clazz->getField<jni::JString>("id");
jni::local_ref<jni::JString> id = this->getFieldValue(fieldId);
static const auto fieldIndex = clazz->getField<double>("index");
double index = this->getFieldValue(fieldIndex);
static const auto fieldLabel = clazz->getField<jni::JString>("label");
jni::local_ref<jni::JString> label = this->getFieldValue(fieldLabel);
static const auto fieldLanguage = clazz->getField<jni::JString>("language");
@@ -42,6 +44,7 @@ namespace margelo::nitro::omni {
jboolean selected = this->getFieldValue(fieldSelected);
return Track(
id->toStdString(),
index,
label != nullptr ? std::make_optional(label->toStdString()) : std::nullopt,
language != nullptr ? std::make_optional(language->toStdString()) : std::nullopt,
static_cast<bool>(selected)
@@ -54,12 +57,13 @@ namespace margelo::nitro::omni {
*/
[[maybe_unused]]
static jni::local_ref<JTrack::javaobject> fromCpp(const Track& value) {
using JSignature = JTrack(jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jboolean);
using JSignature = JTrack(jni::alias_ref<jni::JString>, double, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jboolean);
static const auto clazz = javaClassStatic();
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
return create(
clazz,
jni::make_jstring(value.id),
value.index,
value.label.has_value() ? jni::make_jstring(value.label.value()) : nullptr,
value.language.has_value() ? jni::make_jstring(value.language.value()) : nullptr,
value.selected
+61
View File
@@ -0,0 +1,61 @@
///
/// JTrackProperty.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 "TrackProperty.hpp"
namespace margelo::nitro::omni {
using namespace facebook;
/**
* The C++ JNI bridge between the C++ enum "TrackProperty" and the Kotlin enum "TrackProperty".
*/
struct JTrackProperty final: public jni::JavaClass<JTrackProperty> {
public:
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/TrackProperty;";
public:
/**
* Convert this Java/Kotlin-based enum to the C++ enum TrackProperty.
*/
[[maybe_unused]]
[[nodiscard]]
TrackProperty toCpp() const {
static const auto clazz = javaClassStatic();
static const auto fieldOrdinal = clazz->getField<int>("value");
int ordinal = this->getFieldValue(fieldOrdinal);
return static_cast<TrackProperty>(ordinal);
}
public:
/**
* Create a Java/Kotlin-based enum with the given C++ enum's value.
*/
[[maybe_unused]]
static jni::alias_ref<JTrackProperty> fromCpp(TrackProperty value) {
static const auto clazz = javaClassStatic();
switch (value) {
case TrackProperty::VIDEOS:
static const auto fieldVIDEOS = clazz->getStaticField<JTrackProperty>("VIDEOS");
return clazz->getStaticFieldValue(fieldVIDEOS);
case TrackProperty::AUDIOS:
static const auto fieldAUDIOS = clazz->getStaticField<JTrackProperty>("AUDIOS");
return clazz->getStaticFieldValue(fieldAUDIOS);
case TrackProperty::SUBTITLES:
static const auto fieldSUBTITLES = clazz->getStaticField<JTrackProperty>("SUBTITLES");
return clazz->getStaticFieldValue(fieldSUBTITLES);
default:
std::string stringValue = std::to_string(static_cast<int>(value));
throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
}
}
};
} // namespace margelo::nitro::omni
@@ -0,0 +1,80 @@
///
/// Func_void_std__vector_Rendition_.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: array) => 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_std__vector_Rendition_: (Array<Rendition>) -> 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: Array<Rendition>): Unit
}
/**
* Represents the JavaScript callback `(value: array) => 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_std__vector_Rendition__cxx: Func_void_std__vector_Rendition_ {
@DoNotStrip
@Keep
private val mHybridData: HybridData
@DoNotStrip
@Keep
private constructor(hybridData: HybridData) {
mHybridData = hybridData
}
@DoNotStrip
@Keep
override fun invoke(value: Array<Rendition>): Unit
= invoke_cxx(value)
@FastNative
private external fun invoke_cxx(value: Array<Rendition>): Unit
}
/**
* Represents the JavaScript callback `(value: array) => void`.
* This is implemented in Java/Kotlin, via a `(Array<Rendition>) -> Unit`.
* The callback is always coming from native.
*/
@DoNotStrip
@Keep
@Suppress("ClassName", "RedundantUnitReturnType", "unused")
class Func_void_std__vector_Rendition__java(private val function: (Array<Rendition>) -> Unit): Func_void_std__vector_Rendition_ {
@DoNotStrip
@Keep
override fun invoke(value: Array<Rendition>): Unit {
return this.function(value)
}
}
@@ -0,0 +1,80 @@
///
/// Func_void_std__vector_Track_.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: array) => 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_std__vector_Track_: (Array<Track>) -> 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: Array<Track>): Unit
}
/**
* Represents the JavaScript callback `(value: array) => 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_std__vector_Track__cxx: Func_void_std__vector_Track_ {
@DoNotStrip
@Keep
private val mHybridData: HybridData
@DoNotStrip
@Keep
private constructor(hybridData: HybridData) {
mHybridData = hybridData
}
@DoNotStrip
@Keep
override fun invoke(value: Array<Track>): Unit
= invoke_cxx(value)
@FastNative
private external fun invoke_cxx(value: Array<Track>): Unit
}
/**
* Represents the JavaScript callback `(value: array) => void`.
* This is implemented in Java/Kotlin, via a `(Array<Track>) -> Unit`.
* The callback is always coming from native.
*/
@DoNotStrip
@Keep
@Suppress("ClassName", "RedundantUnitReturnType", "unused")
class Func_void_std__vector_Track__java(private val function: (Array<Track>) -> Unit): Func_void_std__vector_Track_ {
@DoNotStrip
@Keep
override fun invoke(value: Array<Track>): Unit {
return this.function(value)
}
}
@@ -118,6 +118,42 @@ abstract class HybridOmniEventMapSpec: HybridObject() {
return __result
}
abstract fun addTracksListener(key: TrackProperty, cb: (value: Array<Track>) -> Unit): Unit
@DoNotStrip
@Keep
private fun addTracksListener_cxx(key: TrackProperty, cb: Func_void_std__vector_Track_): Unit {
val __result = addTracksListener(key, cb)
return __result
}
abstract fun removeTracksListener(key: TrackProperty, cb: (value: Array<Track>) -> Unit): Unit
@DoNotStrip
@Keep
private fun removeTracksListener_cxx(key: TrackProperty, cb: Func_void_std__vector_Track_): Unit {
val __result = removeTracksListener(key, cb)
return __result
}
abstract fun addRenditionsListener(cb: (value: Array<Rendition>) -> Unit): Unit
@DoNotStrip
@Keep
private fun addRenditionsListener_cxx(cb: Func_void_std__vector_Rendition_): Unit {
val __result = addRenditionsListener(cb)
return __result
}
abstract fun removeRenditionsListener(cb: (value: Array<Rendition>) -> Unit): Unit
@DoNotStrip
@Keep
private fun removeRenditionsListener_cxx(cb: Func_void_std__vector_Rendition_): Unit {
val __result = removeRenditionsListener(cb)
return __result
}
abstract fun addOnEndListener(cb: () -> Unit): Unit
@DoNotStrip
@@ -43,22 +43,6 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
@get:Keep
abstract val hasNext: Boolean
@get:DoNotStrip
@get:Keep
abstract val videos: Array<Track>
@get:DoNotStrip
@get:Keep
abstract val audios: Array<Track>
@get:DoNotStrip
@get:Keep
abstract val subtitles: Array<Track>
@get:DoNotStrip
@get:Keep
abstract val rendition: Array<Rendition>
@get:DoNotStrip
@get:Keep
@set:DoNotStrip
@@ -112,8 +96,28 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
@get:DoNotStrip
@get:Keep
abstract val castStatus: CastStatus
@get:DoNotStrip
@get:Keep
abstract val videos: Array<Track>
@get:DoNotStrip
@get:Keep
abstract val audios: Array<Track>
@get:DoNotStrip
@get:Keep
abstract val subtitles: Array<Track>
@get:DoNotStrip
@get:Keep
abstract val renditions: Array<Rendition>
// Methods
@DoNotStrip
@Keep
abstract fun release(): Unit
@DoNotStrip
@Keep
abstract fun play(): Unit
@@ -23,6 +23,9 @@ data class Track(
val id: String,
@DoNotStrip
@Keep
val index: Double,
@DoNotStrip
@Keep
val label: String?,
@DoNotStrip
@Keep
@@ -37,6 +40,7 @@ data class Track(
if (this === other) return true
if (other !is Track) return false
return Objects.deepEquals(this.id, other.id)
&& Objects.deepEquals(this.index, other.index)
&& Objects.deepEquals(this.label, other.label)
&& Objects.deepEquals(this.language, other.language)
&& Objects.deepEquals(this.selected, other.selected)
@@ -45,6 +49,7 @@ data class Track(
override fun hashCode(): Int {
return arrayOf<Any?>(
id,
index,
label,
language,
selected
@@ -59,8 +64,8 @@ data class Track(
@Keep
@Suppress("unused")
@JvmStatic
private fun fromCpp(id: String, label: String?, language: String?, selected: Boolean): Track {
return Track(id, label, language, selected)
private fun fromCpp(id: String, index: Double, label: String?, language: String?, selected: Boolean): Track {
return Track(id, index, label, language, selected)
}
}
}
@@ -0,0 +1,24 @@
///
/// TrackProperty.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
/**
* Represents the JavaScript enum/union "TrackProperty".
*/
@DoNotStrip
@Keep
enum class TrackProperty(@DoNotStrip @Keep val value: Int) {
VIDEOS(0),
AUDIOS(1),
SUBTITLES(2);
companion object
}
@@ -24,6 +24,10 @@ namespace margelo::nitro::omni {
prototype.registerHybridMethod("removeCastStatusListener", &HybridOmniEventMapSpec::removeCastStatusListener);
prototype.registerHybridMethod("addSourceListener", &HybridOmniEventMapSpec::addSourceListener);
prototype.registerHybridMethod("removeSourceListener", &HybridOmniEventMapSpec::removeSourceListener);
prototype.registerHybridMethod("addTracksListener", &HybridOmniEventMapSpec::addTracksListener);
prototype.registerHybridMethod("removeTracksListener", &HybridOmniEventMapSpec::removeTracksListener);
prototype.registerHybridMethod("addRenditionsListener", &HybridOmniEventMapSpec::addRenditionsListener);
prototype.registerHybridMethod("removeRenditionsListener", &HybridOmniEventMapSpec::removeRenditionsListener);
prototype.registerHybridMethod("addOnEndListener", &HybridOmniEventMapSpec::addOnEndListener);
prototype.registerHybridMethod("removeOnEndListener", &HybridOmniEventMapSpec::removeOnEndListener);
prototype.registerHybridMethod("addOnPrevListener", &HybridOmniEventMapSpec::addOnPrevListener);
+9 -1
View File
@@ -23,6 +23,8 @@ namespace margelo::nitro::omni { enum class PlayerStatus; }
namespace margelo::nitro::omni { enum class CastStatus; }
// Forward declaration of `Source` to properly resolve imports.
namespace margelo::nitro::omni { struct Source; }
// Forward declaration of `TrackProperty` to properly resolve imports.
namespace margelo::nitro::omni { enum class TrackProperty; }
// Forward declaration of `Track` to properly resolve imports.
namespace margelo::nitro::omni { struct Track; }
// Forward declaration of `Rendition` to properly resolve imports.
@@ -35,9 +37,11 @@ namespace margelo::nitro::omni { struct Rendition; }
#include "CastStatus.hpp"
#include "Source.hpp"
#include <optional>
#include <string>
#include "TrackProperty.hpp"
#include "Track.hpp"
#include <vector>
#include "Rendition.hpp"
#include <string>
namespace margelo::nitro::omni {
@@ -80,6 +84,10 @@ namespace margelo::nitro::omni {
virtual void removeCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) = 0;
virtual void addSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) = 0;
virtual void removeSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) = 0;
virtual void addTracksListener(TrackProperty key, const std::function<void(const std::vector<Track>& /* value */)>& cb) = 0;
virtual void removeTracksListener(TrackProperty key, const std::function<void(const std::vector<Track>& /* value */)>& cb) = 0;
virtual void addRenditionsListener(const std::function<void(const std::vector<Rendition>& /* value */)>& cb) = 0;
virtual void removeRenditionsListener(const std::function<void(const std::vector<Rendition>& /* 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;
+5 -4
View File
@@ -19,10 +19,6 @@ namespace margelo::nitro::omni {
prototype.registerHybridSetter("showNotification", &HybridOmniPlayerSpec::setShowNotification);
prototype.registerHybridGetter("hasPrev", &HybridOmniPlayerSpec::getHasPrev);
prototype.registerHybridGetter("hasNext", &HybridOmniPlayerSpec::getHasNext);
prototype.registerHybridGetter("videos", &HybridOmniPlayerSpec::getVideos);
prototype.registerHybridGetter("audios", &HybridOmniPlayerSpec::getAudios);
prototype.registerHybridGetter("subtitles", &HybridOmniPlayerSpec::getSubtitles);
prototype.registerHybridGetter("rendition", &HybridOmniPlayerSpec::getRendition);
prototype.registerHybridGetter("source", &HybridOmniPlayerSpec::getSource);
prototype.registerHybridSetter("source", &HybridOmniPlayerSpec::setSource);
prototype.registerHybridGetter("status", &HybridOmniPlayerSpec::getStatus);
@@ -39,6 +35,11 @@ namespace margelo::nitro::omni {
prototype.registerHybridSetter("muted", &HybridOmniPlayerSpec::setMuted);
prototype.registerHybridGetter("isAutoQuality", &HybridOmniPlayerSpec::getIsAutoQuality);
prototype.registerHybridGetter("castStatus", &HybridOmniPlayerSpec::getCastStatus);
prototype.registerHybridGetter("videos", &HybridOmniPlayerSpec::getVideos);
prototype.registerHybridGetter("audios", &HybridOmniPlayerSpec::getAudios);
prototype.registerHybridGetter("subtitles", &HybridOmniPlayerSpec::getSubtitles);
prototype.registerHybridGetter("renditions", &HybridOmniPlayerSpec::getRenditions);
prototype.registerHybridMethod("release", &HybridOmniPlayerSpec::release);
prototype.registerHybridMethod("play", &HybridOmniPlayerSpec::play);
prototype.registerHybridMethod("pause", &HybridOmniPlayerSpec::pause);
prototype.registerHybridMethod("seekBy", &HybridOmniPlayerSpec::seekBy);
+12 -11
View File
@@ -15,26 +15,26 @@
// Forward declaration of `HybridOmniEventMapSpec` to properly resolve imports.
namespace margelo::nitro::omni { class HybridOmniEventMapSpec; }
// Forward declaration of `Track` to properly resolve imports.
namespace margelo::nitro::omni { struct Track; }
// Forward declaration of `Rendition` to properly resolve imports.
namespace margelo::nitro::omni { struct Rendition; }
// Forward declaration of `Source` to properly resolve imports.
namespace margelo::nitro::omni { struct Source; }
// 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.
namespace margelo::nitro::omni { struct Rendition; }
#include <memory>
#include "HybridOmniEventMapSpec.hpp"
#include <optional>
#include "Track.hpp"
#include <vector>
#include "Rendition.hpp"
#include "Source.hpp"
#include "PlayerStatus.hpp"
#include "CastStatus.hpp"
#include "Track.hpp"
#include <vector>
#include "Rendition.hpp"
namespace margelo::nitro::omni {
@@ -68,10 +68,6 @@ namespace margelo::nitro::omni {
virtual void setShowNotification(std::optional<bool> showNotification) = 0;
virtual bool getHasPrev() = 0;
virtual bool getHasNext() = 0;
virtual std::vector<Track> getVideos() = 0;
virtual std::vector<Track> getAudios() = 0;
virtual std::vector<Track> getSubtitles() = 0;
virtual std::vector<Rendition> getRendition() = 0;
virtual std::optional<Source> getSource() = 0;
virtual void setSource(const std::optional<Source>& source) = 0;
virtual PlayerStatus getStatus() = 0;
@@ -88,9 +84,14 @@ namespace margelo::nitro::omni {
virtual void setMuted(bool muted) = 0;
virtual bool getIsAutoQuality() = 0;
virtual CastStatus getCastStatus() = 0;
virtual std::vector<Track> getVideos() = 0;
virtual std::vector<Track> getAudios() = 0;
virtual std::vector<Track> getSubtitles() = 0;
virtual std::vector<Rendition> getRenditions() = 0;
public:
// Methods
virtual void release() = 0;
virtual void play() = 0;
virtual void pause() = 0;
virtual void seekBy(double offset) = 0;
+5 -1
View File
@@ -41,13 +41,14 @@ namespace margelo::nitro::omni {
struct Track final {
public:
std::string id SWIFT_PRIVATE;
double index SWIFT_PRIVATE;
std::optional<std::string> label SWIFT_PRIVATE;
std::optional<std::string> language SWIFT_PRIVATE;
bool selected SWIFT_PRIVATE;
public:
Track() = default;
explicit Track(std::string id, std::optional<std::string> label, std::optional<std::string> language, bool selected): id(id), label(label), language(language), selected(selected) {}
explicit Track(std::string id, double index, std::optional<std::string> label, std::optional<std::string> language, bool selected): id(id), index(index), label(label), language(language), selected(selected) {}
public:
friend bool operator==(const Track& lhs, const Track& rhs) = default;
@@ -64,6 +65,7 @@ namespace margelo::nitro {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::omni::Track(
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "index"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "label"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language"))),
JSIConverter<bool>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "selected")))
@@ -72,6 +74,7 @@ namespace margelo::nitro {
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::omni::Track& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "id"), JSIConverter<std::string>::toJSI(runtime, arg.id));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "index"), JSIConverter<double>::toJSI(runtime, arg.index));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "label"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.label));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "language"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.language));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "selected"), JSIConverter<bool>::toJSI(runtime, arg.selected));
@@ -86,6 +89,7 @@ namespace margelo::nitro {
return false;
}
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "index")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "label")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language")))) return false;
if (!JSIConverter<bool>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "selected")))) return false;
+80
View File
@@ -0,0 +1,80 @@
///
/// TrackProperty.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/NitroHash.hpp>)
#include <NitroModules/NitroHash.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#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
namespace margelo::nitro::omni {
/**
* An enum which can be represented as a JavaScript union (TrackProperty).
*/
enum class TrackProperty {
VIDEOS SWIFT_NAME(videos) = 0,
AUDIOS SWIFT_NAME(audios) = 1,
SUBTITLES SWIFT_NAME(subtitles) = 2,
} CLOSED_ENUM;
} // namespace margelo::nitro::omni
namespace margelo::nitro {
// C++ TrackProperty <> JS TrackProperty (union)
template <>
struct JSIConverter<margelo::nitro::omni::TrackProperty> final {
static inline margelo::nitro::omni::TrackProperty fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, arg);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("videos"): return margelo::nitro::omni::TrackProperty::VIDEOS;
case hashString("audios"): return margelo::nitro::omni::TrackProperty::AUDIOS;
case hashString("subtitles"): return margelo::nitro::omni::TrackProperty::SUBTITLES;
default: [[unlikely]]
throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum TrackProperty - invalid value!");
}
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::omni::TrackProperty arg) {
switch (arg) {
case margelo::nitro::omni::TrackProperty::VIDEOS: return JSIConverter<std::string>::toJSI(runtime, "videos");
case margelo::nitro::omni::TrackProperty::AUDIOS: return JSIConverter<std::string>::toJSI(runtime, "audios");
case margelo::nitro::omni::TrackProperty::SUBTITLES: return JSIConverter<std::string>::toJSI(runtime, "subtitles");
default: [[unlikely]]
throw std::invalid_argument("Cannot convert TrackProperty to JS - invalid value: "
+ std::to_string(static_cast<int>(arg)) + "!");
}
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isString()) {
return false;
}
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, value);
switch (hashString(unionValue.c_str(), unionValue.size())) {
case hashString("videos"):
case hashString("audios"):
case hashString("subtitles"):
return true;
default:
return false;
}
}
};
} // namespace margelo::nitro
+6 -1
View File
@@ -63,7 +63,12 @@
},
"dependencies": {
"@expo/config-plugins": "^57.0.3",
"@videojs/react": "10.0.0-beta.25",
"@videojs/core": "github:zoriya/v10#build26-core",
"@videojs/media": "github:zoriya/v10#build26-media",
"@videojs/react": "github:zoriya/v10#build26-react",
"@videojs/spf": "github:zoriya/v10#build26-spf",
"@videojs/store": "github:zoriya/v10#build26-store",
"@videojs/utils": "github:zoriya/v10#build26-utils",
"jassub": "^2.5.7",
"libpgs": "^0.8.1"
},
+8
View File
@@ -60,6 +60,14 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
case "source":
em.addSourceListener(setState);
return () => em.removeSourceListener(setState);
case "videos":
case "audios":
case "subtitles":
em.addTracksListener(key, setState);
return () => em.removeTracksListener(key, setState);
case "renditions":
em.addRenditionsListener(setState);
return () => em.removeRenditionsListener(setState);
}
}, [player, key]);
+74 -31
View File
@@ -49,32 +49,30 @@ const eventMapper: EventMapperConfig = {
);
}
}),
...createEventMapper("subtitleChange", selectTextTrack, (cb, value, prev) => {
if (value === prev) return;
const tracks = value?.textTrackList;
if (tracks?.length) {
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
if (track?.mode === "showing") {
cb({
id: `text-${i}`,
label: track.label,
language: track.language,
selected: true,
});
return;
}
}
...createEventMapper("subtitleChange", selectTextTrack, (cb, value) => {
if (!value) return;
const track = value.textTrackList.find((x) => x.mode === "showing");
if (!track) {
cb(undefined);
return;
}
cb(undefined);
const idx = value.textTrackList.indexOf(track);
cb({
id: track.id ?? `text-${idx}`,
index: idx,
label: track.label,
language: track.language,
selected: true,
});
}),
...createEventMapper("audioTrackChange", selectAudioTrack, (cb, value) => {
if (!value) return;
const track = value.audioTrackList.find((t) => t.enabled);
if (!track) return;
const idx = value.audioTrackList.indexOf(track);
cb({
id: track.id!,
id: track.id ?? `audio-${idx}`,
index: idx,
label: track.label,
language: track.language,
selected: true,
@@ -202,6 +200,33 @@ export const stateMapper = {
if (r.remotePlaybackState === "connected") return "connected";
return r.remotePlaybackAvailability;
}),
...createMapper("audios", selectAudioTrack, (audio) => {
if (!audio) return [];
return audio.audioTrackList.map((track, i) => ({
id: track.id ?? i.toString(),
index: i,
label: track.label,
language: track.language,
selected: track.enabled,
}));
}),
...createMapper("renditions", selectQuality, (quality) => {
if (!quality) return [];
const active = quality.activeVideoRendition;
return quality.videoRenditionList.map((rendition, i) => ({
id: rendition.id ?? i.toString(),
width: rendition.width ?? 0,
height: rendition.height ?? 0,
bitrate: rendition.bitrate ?? 0,
selected:
rendition.selected ||
(active != null &&
rendition.id === active.id &&
rendition.width === active.width &&
rendition.height === active.height &&
rendition.bitrate === active.bitrate),
}));
}),
};
// biome-ignore-start lint/correctness/useHookAtTopLevel: key must be const
@@ -216,18 +241,36 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
key: Key,
_refresh?: number,
): OmniPlayerState[Key] {
if (key === "source") {
const player = usePlayer() as WebOmniPlayer;
const source = useSyncExternalStore(
player.subscribeSource,
() => player.source,
() => player.source,
);
return source as OmniPlayerState[Key];
switch (key as keyof OmniPlayerState) {
case "source": {
const player = usePlayer() as WebOmniPlayer;
const source = useSyncExternalStore(
player.subscribeSource,
() => player.source,
() => player.source,
);
return source as OmniPlayerState[Key];
}
case "videos": {
const player = usePlayer();
return player.videos as OmniPlayerState[Key];
}
case "subtitles": {
const player = usePlayer() as WebOmniPlayer;
useStoreSelector(selectTextTrack);
useSyncExternalStore(
player.subscribeOverlaySubtitle,
player.getOverlaySubtitle,
() => null,
);
return player.subtitles as OmniPlayerState[Key];
}
default: {
const config = stateMapper[key as keyof typeof stateMapper];
const ret = useStoreSelector(config?.selector as Selector<any, any>);
if (!config) throw new Error(`No mapper for ${key}`);
return config.mapper(ret) as OmniPlayerState[Key];
}
}
const config = stateMapper[key as keyof Omit<OmniPlayerState, "source">];
const ret = useStoreSelector(config?.selector as Selector<any, any>);
if (!config) throw new Error(`No mapper for ${key}`);
return config.mapper(ret) as OmniPlayerState[Key];
}
// biome-ignore-end lint/correctness/useHookAtTopLevel: key must be const
+5 -36
View File
@@ -1,4 +1,3 @@
import type { MediaVideoRendition } from "@videojs/core";
import type { VideoPlayerStore } from "@videojs/core/dom";
import {
selectAudioTrack,
@@ -60,7 +59,6 @@ export class WebOmniPlayer implements OmniPlayer {
this._store.state.toggleRemotePlayback();
}
castOptions: CastOptions | null = null;
_source: Source | undefined = undefined;
private _showNotification = false;
@@ -76,7 +74,6 @@ export class WebOmniPlayer implements OmniPlayer {
return () => this.sourceListeners.delete(callback);
};
get source(): Source | undefined {
return this._source;
}
@@ -203,14 +200,7 @@ export class WebOmniPlayer implements OmniPlayer {
}
get audios(): Track[] {
const audio = selectAudioTrack(this._store.state);
if (!audio) return [];
return audio.audioTrackList.map((track, i) => ({
id: track.id ?? i.toString(),
label: track.label,
language: track.language,
selected: track.enabled,
}));
return stateMapper.audios.mapper(selectAudioTrack(this._store.state));
}
selectAudio(audio: Track): void {
@@ -227,8 +217,9 @@ export class WebOmniPlayer implements OmniPlayer {
const textTracks = selectTextTrack(this._store.state)?.textTrackList ?? [];
return textTracks
.filter((x) => x.kind === "subtitles" || x.kind === "captions")
.map((track) => ({
.map((track, i) => ({
id: track.id!,
index: i,
label: track.label,
language: track.language,
selected:
@@ -266,30 +257,8 @@ export class WebOmniPlayer implements OmniPlayer {
getOverlaySubtitle = (): Subtitle | null => this.overlaySubtitle;
get rendition(): Rendition[] {
function isSameRendition(
a: MediaVideoRendition,
b: MediaVideoRendition | null,
): boolean {
return (
b != null &&
a.id === b.id &&
a.width === b.width &&
a.height === b.height &&
a.bitrate === b.bitrate
);
}
const quality = selectQuality(this._store.state);
if (!quality) return [];
const active = quality.activeVideoRendition;
return quality.videoRenditionList.map((rendition, i) => ({
id: rendition.id ?? i.toString(),
width: rendition.width ?? 0,
height: rendition.height ?? 0,
bitrate: rendition.bitrate ?? 0,
selected: rendition.selected || isSameRendition(rendition, active),
}));
get renditions(): Rendition[] {
return stateMapper.renditions.mapper(selectQuality(this._store.state));
}
selectRendition(rendition?: Rendition): void {
+31 -10
View File
@@ -1,9 +1,18 @@
import { createContext, type ReactNode, useContext, useEffect } from "react";
import {
createContext,
type ReactNode,
useContext,
useEffect,
useEffectEvent,
useState,
} from "react";
import { NitroModules } from "react-native-nitro-modules";
import type { OmniPlayerFactory } from "./specs/omni-player.nitro";
import type { OmniPlayer, PlayerBackend } from "./types/player";
import type {
OmniPlayer as NativeOmniPlayer,
OmniPlayerFactory,
} from "./specs/omni-player.nitro";
import type { AndroidBackend, OmniPlayer, PlayerBackend } from "./types/player";
import type { CastOptions, Source } from "./types/source";
import { useLazyRef } from "./utils/lazy-ref";
const ProviderFactory =
NitroModules.createHybridObject<OmniPlayerFactory>("OmniPlayerFactory");
@@ -23,18 +32,30 @@ export const OmniProvider = ({
children: ReactNode;
showNotification?: boolean;
}) => {
const player = useLazyRef(() =>
ProviderFactory.createPlayer(source, backend, cast),
const [player, setPlayer] = useState<NativeOmniPlayer | null>(null);
const createPlayer = useEffectEvent((aBackend: AndroidBackend) =>
ProviderFactory.createPlayer(source, { android: aBackend }, cast),
);
useEffect(() => {
player.source = source;
}, [source]);
setPlayer(createPlayer(backend.android ?? "vlc"));
}, [backend.android]);
useEffect(() => {
player.showNotification = showNotification;
}, [showNotification]);
if (!player) return;
return () => player.release();
}, [player]);
useEffect(() => {
if (player) player.source = source;
}, [player, source]);
useEffect(() => {
if (player) player.showNotification = showNotification;
}, [player, showNotification]);
if (!player) return null;
return <PlayerCtx.Provider value={player}>{children}</PlayerCtx.Provider>;
};
+19 -5
View File
@@ -1,4 +1,5 @@
import { createPlayer } from "@videojs/react";
import { GoogleCast } from "@videojs/react/media/google-cast";
import { videoFeatures } from "@videojs/react/video";
import {
createContext,
@@ -7,6 +8,7 @@ import {
useEffect,
useRef,
} from "react";
import { usePlayerState } from "./events";
import { WebOmniPlayer } from "./player.web";
import type { OmniPlayer, PlayerBackend } from "./types/player";
import type { CastOptions, Source } from "./types/source";
@@ -67,15 +69,27 @@ const PlayerInitializer = ({
}
}, [source, store]);
useEffect(() => {
player.castOptions = cast ?? null;
}, [cast]);
useEffect(() => {
player.showNotification = showNotification;
}, [showNotification]);
return <PlayerCtx.Provider value={player}>{children}</PlayerCtx.Provider>;
return (
<PlayerCtx.Provider value={player}>
<CastBridge cast={cast} />
{children}
</PlayerCtx.Provider>
);
};
const CastBridge = ({ cast }: { cast?: CastOptions | null }) => {
const source = usePlayerState("source");
return (
<GoogleCast
receiver={cast?.receiverApplicationId}
src={source?.castId}
customData={source?.castData}
/>
);
};
export const usePlayer = () => useContext(PlayerCtx);
+19 -1
View File
@@ -6,14 +6,24 @@ import type {
OmniPlayer as OmniPlayerT,
PlayerBackend,
PlayerStatus,
Rendition,
Track,
} from "../types/player";
import type { CastOptions, Source } from "../types/source";
export type NumberProperty = Exclude<
keyof OmniPlayerState,
"status" | "isPlaying" | "muted" | "source"
| "status"
| "isPlaying"
| "muted"
| "source"
| "videos"
| "audios"
| "subtitles"
| "renditions"
>;
export type BoolProperty = "isPlaying" | "muted" | "isAutoQuality";
export type TrackProperty = "videos" | "audios" | "subtitles";
export interface OmniEventMap extends HybridObject<{ android: "kotlin" }> {
addStateListener(key: NumberProperty, cb: (value: number) => void): void;
@@ -32,6 +42,12 @@ export interface OmniEventMap extends HybridObject<{ android: "kotlin" }> {
addSourceListener(cb: (value?: Source) => void): void;
removeSourceListener(cb: (value?: Source) => void): void;
addTracksListener(key: TrackProperty, cb: (value: Track[]) => void): void;
removeTracksListener(key: TrackProperty, cb: (value: Track[]) => void): void;
addRenditionsListener(cb: (value: Rendition[]) => void): void;
removeRenditionsListener(cb: (value: Rendition[]) => void): void;
addOnEndListener(cb: OmniEvents["end"]): void;
removeOnEndListener(cb: OmniEvents["end"]): void;
@@ -64,6 +80,8 @@ export interface OmniPlayer
extends HybridObject<{ android: "kotlin" }>,
OmniPlayerT {
readonly eventMap: OmniEventMap;
release(): void;
}
export interface OmniPlayerFactory extends HybridObject<{ android: "kotlin" }> {
+7 -4
View File
@@ -15,13 +15,9 @@ export interface OmniPlayer extends OmniPlayerState {
readonly hasPrev: boolean;
readonly hasNext: boolean;
readonly videos: Track[];
selectVideo(video: Track): void;
readonly audios: Track[];
selectAudio(audio: Track): void;
readonly subtitles: Track[];
selectSubtitle(subtitle?: Track): void;
readonly rendition: Rendition[];
selectRendition(rendition?: Rendition): void;
}
@@ -38,6 +34,11 @@ export interface OmniPlayerState {
muted: boolean;
readonly isAutoQuality: boolean;
readonly castStatus: CastStatus;
readonly videos: Track[];
readonly audios: Track[];
readonly subtitles: Track[];
readonly renditions: Rendition[];
}
export type PlayerStatus = "idle" | "loading" | "readyToPlay" | "error";
@@ -51,6 +52,8 @@ export type CastStatus =
export interface Track {
readonly id: string;
// -1 for external tracks
readonly index: number;
readonly label?: string;
readonly language?: string;
readonly selected: boolean;
+23 -33
View File
@@ -1,4 +1,4 @@
import type { HlsMediaConfig } from "@videojs/core/dom/media/hls-js";
import type { HlsMediaConfig } from "@videojs/media/dom/hls-js";
import { HlsJsVideo } from "@videojs/react/media/hlsjs-video";
import { Video } from "@videojs/react/video";
import {
@@ -52,7 +52,7 @@ const SubtitleOverlay = ({
// jassub uses this as libass' storage size so subtitles stay sharp
// even while a low-bitrate rendition is playing — otherwise the
// raster tracks the currently-decoded (possibly tiny) frame size.
const renditions = player.rendition;
const renditions = player.renditions;
const videoWidth = Math.max(0, ...renditions.map((r) => r.width));
const videoHeight = Math.max(0, ...renditions.map((r) => r.height));
import("jassub")
@@ -106,7 +106,6 @@ export const OmniView = ({
autoplay,
subtitleAssets,
}: OmniViewProps & { style: CSSProperties }) => {
const player = usePlayer() as WebOmniPlayer;
const containerRef = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLVideoElement>(undefined!);
const source = usePlayerState("source");
@@ -119,8 +118,6 @@ export const OmniView = ({
const headersRef = useRef(source?.src?.headers);
headersRef.current = source?.src?.headers;
const castId = source?.castId;
const castData = source?.castData;
const config = useMemo<HlsMediaConfig>(
() => ({
@@ -133,13 +130,8 @@ export const OmniView = ({
}
},
},
googleCast: {
receiver: player.castOptions?.receiverApplicationId,
customData: castData ?? null,
...(castId && { src: castId }),
},
}),
[player.castOptions, castData, castId],
[],
);
// While casting, the receiver renders subtitles (the player forwards the
@@ -151,28 +143,26 @@ export const OmniView = ({
ref={containerRef}
style={{ position: "relative", ...style }}
>
{source?.src && (
<Tech
ref={ref}
src={source?.src.uri}
config={config}
autoPlay={autoplay}
playsInline
crossOrigin="anonymous"
style={{ width: "100%", height: "100%", objectFit: "contain" }}
>
{(source?.subtitles ?? []).map((subtitle) => (
<track
key={subtitle.id}
id={subtitle.id}
kind="subtitles"
src={subtitle.link}
srcLang={subtitle.language}
label={subtitle.label ?? subtitle.language ?? subtitle.id}
/>
))}
</Tech>
)}
<Tech
ref={ref}
src={source?.src.uri}
config={config}
autoPlay={autoplay}
playsInline
crossOrigin="anonymous"
style={{ width: "100%", height: "100%", objectFit: "contain" }}
>
{(source?.subtitles ?? []).map((subtitle) => (
<track
key={subtitle.id}
id={subtitle.id}
kind="subtitles"
src={subtitle.link}
srcLang={subtitle.language}
label={subtitle.label ?? subtitle.language ?? subtitle.id}
/>
))}
</Tech>
{castStatus !== "connected" && castStatus !== "connecting" && (
<SubtitleOverlay
video={ref}