mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49f08137f0 | ||
|
|
153b492e0a | ||
|
|
6ef72f22d2 | ||
|
|
26a2731679 | ||
|
|
77c3a519e7 | ||
|
|
9fea0949bd |
@@ -100,7 +100,6 @@ class OmniPlayer(
|
|||||||
.setRemotePlayer(remote)
|
.setRemotePlayer(remote)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
eventMap.player = active
|
|
||||||
active
|
active
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,18 +168,25 @@ class OmniPlayer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
@Volatile
|
||||||
showNotification = false
|
private var released = false
|
||||||
super.dispose()
|
|
||||||
|
|
||||||
eventMap.dispose()
|
override fun release() {
|
||||||
|
if (released) return
|
||||||
|
released = true
|
||||||
|
showNotification = false
|
||||||
runOnMainThread {
|
runOnMainThread {
|
||||||
|
eventMap.dispose()
|
||||||
castContext?.removeCastStateListener(castStateListener)
|
castContext?.removeCastStateListener(castStateListener)
|
||||||
// release both cast and local players.
|
|
||||||
player.release()
|
player.release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
release()
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildMediaItem(
|
private fun buildMediaItem(
|
||||||
src: com.margelo.nitro.omni.VideoSrc,
|
src: com.margelo.nitro.omni.VideoSrc,
|
||||||
metadata: com.margelo.nitro.omni.Metadata?,
|
metadata: com.margelo.nitro.omni.Metadata?,
|
||||||
@@ -227,13 +233,15 @@ class OmniPlayer(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setVideoView(surfaceView: android.view.SurfaceView?) {
|
fun setVideoView(surfaceView: android.view.SurfaceView) {
|
||||||
runOnMainThread {
|
runOnMainThread {
|
||||||
if (surfaceView == null) {
|
localPlayer.setVideoSurfaceView(surfaceView)
|
||||||
localPlayer.clearVideoSurface()
|
}
|
||||||
} else {
|
}
|
||||||
localPlayer.setVideoSurfaceView(surfaceView)
|
|
||||||
}
|
fun clearVideoView(surfaceView: android.view.SurfaceView) {
|
||||||
|
runOnMainThread {
|
||||||
|
localPlayer.clearVideoSurfaceView(surfaceView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +343,10 @@ class OmniPlayer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
runOnMainThreadSync { eventMap.player = player }
|
||||||
|
}
|
||||||
|
|
||||||
override var source: Source? = null
|
override var source: Source? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dev.zoriya.omni
|
package dev.zoriya.omni
|
||||||
|
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Rational
|
import android.util.Rational
|
||||||
@@ -117,6 +118,7 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
override var subtitleAssets: SubtitleAssets? = null
|
override var subtitleAssets: SubtitleAssets? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
view.setBackgroundColor(Color.BLACK)
|
||||||
context.addLifecycleEventListener(this)
|
context.addLifecycleEventListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,15 +156,11 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
val curPip = activeView.get()
|
val curPip = activeView.get()
|
||||||
when {
|
when {
|
||||||
autoPip == true && curPip == this -> {}
|
autoPip == true && curPip == this -> {}
|
||||||
autoPip == true && curPip == null -> {
|
autoPip == true -> {
|
||||||
activeView = WeakReference(this)
|
activeView = WeakReference(this)
|
||||||
updatePictureInPictureParams()
|
updatePictureInPictureParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
autoPip == true -> {
|
|
||||||
throw Error("Only one OmniView can have `autoPip` set at a time.")
|
|
||||||
}
|
|
||||||
|
|
||||||
autoPip == false && curPip == this -> {
|
autoPip == false && curPip == this -> {
|
||||||
activeView = WeakReference(null)
|
activeView = WeakReference(null)
|
||||||
clearPictureInPictureParams()
|
clearPictureInPictureParams()
|
||||||
@@ -180,7 +178,7 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
boundPlayer?.localPlayer?.removeListener(this)
|
boundPlayer?.localPlayer?.removeListener(this)
|
||||||
boundPlayer?.setVideoView(null)
|
boundPlayer?.clearVideoView(surfaceView)
|
||||||
boundPlayer = omniPlayer
|
boundPlayer = omniPlayer
|
||||||
omniPlayer.localPlayer.addListener(this)
|
omniPlayer.localPlayer.addListener(this)
|
||||||
|
|
||||||
@@ -206,7 +204,7 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
|
|
||||||
val omniPlayer = player as? OmniPlayer ?: return
|
val omniPlayer = player as? OmniPlayer ?: return
|
||||||
boundPlayer?.localPlayer?.removeListener(this)
|
boundPlayer?.localPlayer?.removeListener(this)
|
||||||
omniPlayer.setVideoView(null)
|
omniPlayer.clearVideoView(surfaceView)
|
||||||
boundPlayer = null
|
boundPlayer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +412,6 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||||
surfaceReady = false
|
surfaceReady = false
|
||||||
surfaceView.removeCallbacks(rebuildRunnable)
|
surfaceView.removeCallbacks(rebuildRunnable)
|
||||||
boundPlayer?.setVideoView(null)
|
boundPlayer?.clearVideoView(surfaceView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,11 +99,14 @@ class VlcPlayer(ctx: Context) :
|
|||||||
private var currentTrackSelectionParameters = TrackSelectionParameters.Builder().build()
|
private var currentTrackSelectionParameters = TrackSelectionParameters.Builder().build()
|
||||||
@Volatile
|
@Volatile
|
||||||
private var playerError: PlaybackException? = null
|
private var playerError: PlaybackException? = null
|
||||||
|
@Volatile
|
||||||
|
private var released = false
|
||||||
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
|
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
|
||||||
private var userInitiatedTransition: Boolean = false
|
private var userInitiatedTransition: Boolean = false
|
||||||
@Volatile
|
@Volatile
|
||||||
private var cachedBufferedPosition: Long = 0L
|
private var cachedBufferedPosition: Long = 0L
|
||||||
private var boundSurfaceView: SurfaceView? = null
|
private var boundSurfaceView: SurfaceView? = null
|
||||||
|
private var lastVideoSize: VideoSize = VideoSize.UNKNOWN
|
||||||
|
|
||||||
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
||||||
.add(COMMAND_PLAY_PAUSE)
|
.add(COMMAND_PLAY_PAUSE)
|
||||||
@@ -136,6 +139,7 @@ class VlcPlayer(ctx: Context) :
|
|||||||
override fun onEvent(event: MediaPlayer.Event) {
|
override fun onEvent(event: MediaPlayer.Event) {
|
||||||
when (event.type) {
|
when (event.type) {
|
||||||
MediaPlayer.Event.Opening -> {
|
MediaPlayer.Event.Opening -> {
|
||||||
|
lastVideoSize = VideoSize.UNKNOWN
|
||||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||||
it.onPlaybackStateChanged(STATE_BUFFERING)
|
it.onPlaybackStateChanged(STATE_BUFFERING)
|
||||||
}
|
}
|
||||||
@@ -158,6 +162,7 @@ class VlcPlayer(ctx: Context) :
|
|||||||
it.onIsPlayingChanged(true)
|
it.onIsPlayingChanged(true)
|
||||||
it.onTracksChanged(getCurrentTracks())
|
it.onTracksChanged(getCurrentTracks())
|
||||||
}
|
}
|
||||||
|
maybeNotifyVideoSizeChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaPlayer.Event.Paused -> {
|
MediaPlayer.Event.Paused -> {
|
||||||
@@ -211,12 +216,19 @@ class VlcPlayer(ctx: Context) :
|
|||||||
// this improves perf & battery life (native -> js bridge is expensive)
|
// this improves perf & battery life (native -> js bridge is expensive)
|
||||||
MediaPlayer.Event.TimeChanged -> Unit
|
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.ESAdded,
|
||||||
MediaPlayer.Event.ESDeleted,
|
MediaPlayer.Event.ESDeleted,
|
||||||
MediaPlayer.Event.ESSelected -> {
|
MediaPlayer.Event.ESSelected -> {
|
||||||
notifyListeners(EVENT_TRACKS_CHANGED) {
|
notifyListeners(EVENT_TRACKS_CHANGED) {
|
||||||
it.onTracksChanged(getCurrentTracks())
|
it.onTracksChanged(getCurrentTracks())
|
||||||
}
|
}
|
||||||
|
maybeNotifyVideoSizeChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaPlayer.Event.LengthChanged -> {
|
MediaPlayer.Event.LengthChanged -> {
|
||||||
@@ -353,6 +365,7 @@ class VlcPlayer(ctx: Context) :
|
|||||||
media.addOption(":start-time=${targetMs / 1000.0}")
|
media.addOption(":start-time=${targetMs / 1000.0}")
|
||||||
|
|
||||||
player.setMedia(media)
|
player.setMedia(media)
|
||||||
|
media.release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +443,7 @@ class VlcPlayer(ctx: Context) :
|
|||||||
when {
|
when {
|
||||||
playerError != null -> STATE_IDLE
|
playerError != null -> STATE_IDLE
|
||||||
currentMediaItemIndex == INDEX_UNSET -> 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.playerState == IMedia.State.Opening -> STATE_BUFFERING
|
||||||
player.isPlaying -> STATE_READY
|
player.isPlaying -> STATE_READY
|
||||||
player.isSeekable && player.time >= player.length && player.length > 0 -> STATE_ENDED
|
player.isSeekable && player.time >= player.length && player.length > 0 -> STATE_ENDED
|
||||||
@@ -519,13 +532,17 @@ class VlcPlayer(ctx: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun release() {
|
override fun release() {
|
||||||
|
if (released) return
|
||||||
|
released = true
|
||||||
player.setEventListener(null)
|
player.setEventListener(null)
|
||||||
listeners.release()
|
listeners.release()
|
||||||
abandonAudioFocus()
|
abandonAudioFocus()
|
||||||
player.stop()
|
player.stop()
|
||||||
clearVideoSurface()
|
clearVideoSurface()
|
||||||
player.release()
|
applicationHandler.post {
|
||||||
libVLC.release()
|
player.release()
|
||||||
|
libVLC.release()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentTracks(): Tracks {
|
override fun getCurrentTracks(): Tracks {
|
||||||
@@ -894,6 +911,7 @@ class VlcPlayer(ctx: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||||
|
if (surfaceView != null && boundSurfaceView !== surfaceView) return
|
||||||
clearVideoSurface()
|
clearVideoSurface()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,10 +919,30 @@ class VlcPlayer(ctx: Context) :
|
|||||||
|
|
||||||
override fun clearVideoTextureView(textureView: TextureView?) = Unit
|
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 {
|
override fun getVideoSize(): VideoSize {
|
||||||
val videoTrack = player.getSelectedTrack(IMedia.Track.Type.Video) as? VideoTrack ?: return VideoSize.UNKNOWN
|
val videoTrack = player.getSelectedTrack(IMedia.Track.Type.Video) as? VideoTrack ?: return VideoSize.UNKNOWN
|
||||||
if (videoTrack.width <= 0 || videoTrack.height <= 0) 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
|
override fun getSurfaceSize(): Size = Size.UNKNOWN
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
"name": "react-native-omni",
|
"name": "react-native-omni",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config-plugins": "^57.0.3",
|
"@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",
|
"jassub": "^2.5.7",
|
||||||
"libpgs": "^0.8.1",
|
"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=="],
|
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
|
|||||||
+8
-39
@@ -1,5 +1,5 @@
|
|||||||
import type React from "react";
|
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 { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
type AndroidBackend,
|
type AndroidBackend,
|
||||||
@@ -134,10 +134,6 @@ function PlayerExample({
|
|||||||
);
|
);
|
||||||
const switchBackend = (target: AndroidBackend) => {
|
const switchBackend = (target: AndroidBackend) => {
|
||||||
if (target === backend) return;
|
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);
|
onSwitchBackend(target);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -461,27 +457,13 @@ function App(): React.JSX.Element {
|
|||||||
// notification stays hidden) before any source is set.
|
// notification stays hidden) before any source is set.
|
||||||
const [hasSource, setHasSource] = useState(false);
|
const [hasSource, setHasSource] = useState(false);
|
||||||
const [backend, setBackend] = useState<AndroidBackend>("vlc");
|
const [backend, setBackend] = useState<AndroidBackend>("vlc");
|
||||||
const [pendingBackend, setPendingBackend] = useState<AndroidBackend | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Switching backend recreates the native player, so we fully unmount the
|
// Switching the backend just updates the prop: OmniProvider recreates (and
|
||||||
// provider first (rendering nothing) and remount it on the next tick with
|
// disposes) the native player internally, so the change applies live.
|
||||||
// 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.
|
|
||||||
const handleSwitchBackend = useCallback((next: AndroidBackend) => {
|
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(() => {
|
const handlePrev = useCallback(() => {
|
||||||
setCurrentIndex((index) => (index === 0 ? PLAYLIST.length - 1 : index - 1));
|
setCurrentIndex((index) => (index === 0 ? PLAYLIST.length - 1 : index - 1));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -525,18 +507,8 @@ function App(): React.JSX.Element {
|
|||||||
[currentIndex],
|
[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 (
|
return (
|
||||||
<OmniProvider
|
<OmniProvider
|
||||||
key={backend}
|
|
||||||
source={hasSource ? source : undefined}
|
source={hasSource ? source : undefined}
|
||||||
backend={{ android: backend }}
|
backend={{ android: backend }}
|
||||||
cast={{ receiverApplicationId: "D8FB0FC1" }}
|
cast={{ receiverApplicationId: "D8FB0FC1" }}
|
||||||
@@ -564,12 +536,6 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
switching: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: "#0b1020",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
heading: {
|
heading: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
@@ -581,7 +547,10 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
video: {
|
video: {
|
||||||
width: "100%",
|
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,
|
borderRadius: 14,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -241,6 +241,10 @@ namespace margelo::nitro::omni {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
void JHybridOmniPlayerSpec::release() {
|
||||||
|
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("release");
|
||||||
|
method(_javaPart);
|
||||||
|
}
|
||||||
void JHybridOmniPlayerSpec::play() {
|
void JHybridOmniPlayerSpec::play() {
|
||||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("play");
|
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("play");
|
||||||
method(_javaPart);
|
method(_javaPart);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ namespace margelo::nitro::omni {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
// Methods
|
// Methods
|
||||||
|
void release() override;
|
||||||
void play() override;
|
void play() override;
|
||||||
void pause() override;
|
void pause() override;
|
||||||
void seekBy(double offset) override;
|
void seekBy(double offset) override;
|
||||||
|
|||||||
+4
@@ -114,6 +114,10 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
|
|||||||
abstract val renditions: Array<Rendition>
|
abstract val renditions: Array<Rendition>
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
@DoNotStrip
|
||||||
|
@Keep
|
||||||
|
abstract fun release(): Unit
|
||||||
|
|
||||||
@DoNotStrip
|
@DoNotStrip
|
||||||
@Keep
|
@Keep
|
||||||
abstract fun play(): Unit
|
abstract fun play(): Unit
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ namespace margelo::nitro::omni {
|
|||||||
prototype.registerHybridGetter("audios", &HybridOmniPlayerSpec::getAudios);
|
prototype.registerHybridGetter("audios", &HybridOmniPlayerSpec::getAudios);
|
||||||
prototype.registerHybridGetter("subtitles", &HybridOmniPlayerSpec::getSubtitles);
|
prototype.registerHybridGetter("subtitles", &HybridOmniPlayerSpec::getSubtitles);
|
||||||
prototype.registerHybridGetter("renditions", &HybridOmniPlayerSpec::getRenditions);
|
prototype.registerHybridGetter("renditions", &HybridOmniPlayerSpec::getRenditions);
|
||||||
|
prototype.registerHybridMethod("release", &HybridOmniPlayerSpec::release);
|
||||||
prototype.registerHybridMethod("play", &HybridOmniPlayerSpec::play);
|
prototype.registerHybridMethod("play", &HybridOmniPlayerSpec::play);
|
||||||
prototype.registerHybridMethod("pause", &HybridOmniPlayerSpec::pause);
|
prototype.registerHybridMethod("pause", &HybridOmniPlayerSpec::pause);
|
||||||
prototype.registerHybridMethod("seekBy", &HybridOmniPlayerSpec::seekBy);
|
prototype.registerHybridMethod("seekBy", &HybridOmniPlayerSpec::seekBy);
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ namespace margelo::nitro::omni {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
// Methods
|
// Methods
|
||||||
|
virtual void release() = 0;
|
||||||
virtual void play() = 0;
|
virtual void play() = 0;
|
||||||
virtual void pause() = 0;
|
virtual void pause() = 0;
|
||||||
virtual void seekBy(double offset) = 0;
|
virtual void seekBy(double offset) = 0;
|
||||||
|
|||||||
+6
-1
@@ -63,7 +63,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config-plugins": "^57.0.3",
|
"@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",
|
"jassub": "^2.5.7",
|
||||||
"libpgs": "^0.8.1"
|
"libpgs": "^0.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export class WebOmniPlayer implements OmniPlayer {
|
|||||||
this._store.state.toggleRemotePlayback();
|
this._store.state.toggleRemotePlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
castOptions: CastOptions | null = null;
|
|
||||||
_source: Source | undefined = undefined;
|
_source: Source | undefined = undefined;
|
||||||
private _showNotification = false;
|
private _showNotification = false;
|
||||||
|
|
||||||
|
|||||||
+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 { NitroModules } from "react-native-nitro-modules";
|
||||||
import type { OmniPlayerFactory } from "./specs/omni-player.nitro";
|
import type {
|
||||||
import type { OmniPlayer, PlayerBackend } from "./types/player";
|
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 type { CastOptions, Source } from "./types/source";
|
||||||
import { useLazyRef } from "./utils/lazy-ref";
|
|
||||||
|
|
||||||
const ProviderFactory =
|
const ProviderFactory =
|
||||||
NitroModules.createHybridObject<OmniPlayerFactory>("OmniPlayerFactory");
|
NitroModules.createHybridObject<OmniPlayerFactory>("OmniPlayerFactory");
|
||||||
@@ -23,18 +32,30 @@ export const OmniProvider = ({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
showNotification?: boolean;
|
showNotification?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const player = useLazyRef(() =>
|
const [player, setPlayer] = useState<NativeOmniPlayer | null>(null);
|
||||||
ProviderFactory.createPlayer(source, backend, cast),
|
|
||||||
|
const createPlayer = useEffectEvent((aBackend: AndroidBackend) =>
|
||||||
|
ProviderFactory.createPlayer(source, { android: aBackend }, cast),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
player.source = source;
|
setPlayer(createPlayer(backend.android ?? "vlc"));
|
||||||
}, [source]);
|
}, [backend.android]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
player.showNotification = showNotification;
|
if (!player) return;
|
||||||
}, [showNotification]);
|
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>;
|
return <PlayerCtx.Provider value={player}>{children}</PlayerCtx.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+19
-5
@@ -1,4 +1,5 @@
|
|||||||
import { createPlayer } from "@videojs/react";
|
import { createPlayer } from "@videojs/react";
|
||||||
|
import { GoogleCast } from "@videojs/react/media/google-cast";
|
||||||
import { videoFeatures } from "@videojs/react/video";
|
import { videoFeatures } from "@videojs/react/video";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -7,6 +8,7 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { usePlayerState } from "./events";
|
||||||
import { WebOmniPlayer } from "./player.web";
|
import { WebOmniPlayer } from "./player.web";
|
||||||
import type { OmniPlayer, PlayerBackend } from "./types/player";
|
import type { OmniPlayer, PlayerBackend } from "./types/player";
|
||||||
import type { CastOptions, Source } from "./types/source";
|
import type { CastOptions, Source } from "./types/source";
|
||||||
@@ -67,15 +69,27 @@ const PlayerInitializer = ({
|
|||||||
}
|
}
|
||||||
}, [source, store]);
|
}, [source, store]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
player.castOptions = cast ?? null;
|
|
||||||
}, [cast]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
player.showNotification = showNotification;
|
player.showNotification = 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);
|
export const usePlayer = () => useContext(PlayerCtx);
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export interface OmniPlayer
|
|||||||
extends HybridObject<{ android: "kotlin" }>,
|
extends HybridObject<{ android: "kotlin" }>,
|
||||||
OmniPlayerT {
|
OmniPlayerT {
|
||||||
readonly eventMap: OmniEventMap;
|
readonly eventMap: OmniEventMap;
|
||||||
|
|
||||||
|
release(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OmniPlayerFactory extends HybridObject<{ android: "kotlin" }> {
|
export interface OmniPlayerFactory extends HybridObject<{ android: "kotlin" }> {
|
||||||
|
|||||||
+2
-10
@@ -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 { HlsJsVideo } from "@videojs/react/media/hlsjs-video";
|
||||||
import { Video } from "@videojs/react/video";
|
import { Video } from "@videojs/react/video";
|
||||||
import {
|
import {
|
||||||
@@ -106,7 +106,6 @@ export const OmniView = ({
|
|||||||
autoplay,
|
autoplay,
|
||||||
subtitleAssets,
|
subtitleAssets,
|
||||||
}: OmniViewProps & { style: CSSProperties }) => {
|
}: OmniViewProps & { style: CSSProperties }) => {
|
||||||
const player = usePlayer() as WebOmniPlayer;
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const ref = useRef<HTMLVideoElement>(undefined!);
|
const ref = useRef<HTMLVideoElement>(undefined!);
|
||||||
const source = usePlayerState("source");
|
const source = usePlayerState("source");
|
||||||
@@ -119,8 +118,6 @@ export const OmniView = ({
|
|||||||
|
|
||||||
const headersRef = useRef(source?.src?.headers);
|
const headersRef = useRef(source?.src?.headers);
|
||||||
headersRef.current = source?.src?.headers;
|
headersRef.current = source?.src?.headers;
|
||||||
const castId = source?.castId;
|
|
||||||
const castData = source?.castData;
|
|
||||||
|
|
||||||
const config = useMemo<HlsMediaConfig>(
|
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
|
// While casting, the receiver renders subtitles (the player forwards the
|
||||||
|
|||||||
Reference in New Issue
Block a user