mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7578fe944e | ||
|
|
1789d00fdc | ||
|
|
49f08137f0 | ||
|
|
153b492e0a | ||
|
|
6ef72f22d2 | ||
|
|
26a2731679 | ||
|
|
77c3a519e7 | ||
|
|
9fea0949bd | ||
|
|
9945f2b339 | ||
|
|
e80c382a25 | ||
|
|
685d3beb2a | ||
|
|
a44cbfbdea |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Generated
+80
@@ -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)
|
||||
}
|
||||
}
|
||||
+80
@@ -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)
|
||||
}
|
||||
}
|
||||
+36
@@ -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
|
||||
|
||||
+20
-16
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Generated
+5
-1
@@ -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
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user