mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be10a769dd | ||
|
|
e95fb80cba | ||
|
|
82675e565a | ||
|
|
236a828c0b | ||
|
|
ba7a2b6a6d | ||
|
|
1dbffadb62 | ||
|
|
4f6d544591 |
@@ -15,6 +15,8 @@ import androidx.media3.common.Player.STATE_READY
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.VideoSize
|
||||
import com.google.android.gms.cast.MediaStatus
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient
|
||||
import com.margelo.nitro.omni.BoolProperty
|
||||
import com.margelo.nitro.omni.CastStatus
|
||||
import com.margelo.nitro.omni.HybridOmniEventMapSpec
|
||||
@@ -103,6 +105,8 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
||||
onTracksChanged(value.currentTracks)
|
||||
}
|
||||
|
||||
var remote: RemoteMediaClient? = null
|
||||
|
||||
fun emitCastStatus(status: CastStatus) {
|
||||
castStatusListeners.forEach { it(status) }
|
||||
}
|
||||
@@ -200,7 +204,13 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
val state = when (player.playbackState) {
|
||||
STATE_IDLE -> PlayerStatus.IDLE
|
||||
STATE_IDLE -> {
|
||||
// cast doesn't have STATE_ENDED, so handle this manually
|
||||
if (remote?.mediaStatus?.idleReason == MediaStatus.IDLE_REASON_FINISHED)
|
||||
onEndListeners.forEach { it() }
|
||||
PlayerStatus.IDLE
|
||||
}
|
||||
|
||||
STATE_BUFFERING -> PlayerStatus.LOADING
|
||||
STATE_READY -> PlayerStatus.READYTOPLAY
|
||||
STATE_ENDED -> {
|
||||
|
||||
@@ -29,11 +29,13 @@ import com.margelo.nitro.NitroModules
|
||||
import com.margelo.nitro.omni.AndroidBackend
|
||||
import com.margelo.nitro.omni.CastStatus
|
||||
import com.margelo.nitro.omni.HybridOmniPlayerSpec
|
||||
import com.margelo.nitro.omni.Metadata
|
||||
import com.margelo.nitro.omni.MixAudioMode
|
||||
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.VideoSrc
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.MediaItem.RequestMetadata
|
||||
import androidx.media3.common.MediaItem.SubtitleConfiguration
|
||||
@@ -45,6 +47,7 @@ import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import androidx.mediarouter.app.MediaRouteChooserDialog
|
||||
import androidx.mediarouter.app.SystemOutputSwitcherDialogController
|
||||
import com.google.android.gms.cast.MediaStatus
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.cast.framework.CastState
|
||||
import com.google.android.gms.cast.framework.CastStateListener
|
||||
@@ -108,7 +111,9 @@ class OmniPlayer(
|
||||
// their own), so they ask us to do it when something else - google home, the
|
||||
// assistant, ... - requests a prev/next. same path as our own buttons.
|
||||
private fun listenToReceiver() {
|
||||
val session = castContext?.sessionManager?.currentCastSession ?: return
|
||||
val session = castContext?.sessionManager?.currentCastSession
|
||||
eventMap.remote = session?.remoteMediaClient
|
||||
if (session == null) return
|
||||
try {
|
||||
session.setMessageReceivedCallbacks(OMNI_NAMESPACE) { _, _, message ->
|
||||
when (JSONObject(message).optString("action")) {
|
||||
@@ -298,6 +303,33 @@ class OmniPlayer(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun sourceOf(item: MediaItem): Source {
|
||||
val meta = item.mediaMetadata
|
||||
val status = castContext?.sessionManager?.currentCastSession
|
||||
?.remoteMediaClient?.mediaStatus
|
||||
return Source(
|
||||
src = VideoSrc(
|
||||
uri = item.localConfiguration?.uri?.toString() ?: item.mediaId,
|
||||
mimeType = item.localConfiguration?.mimeType,
|
||||
headers = emptyMap(),
|
||||
),
|
||||
startTime = null,
|
||||
subtitles = emptyArray(),
|
||||
fonts = null,
|
||||
metadata = Metadata(
|
||||
title = meta.title?.toString() ?: "",
|
||||
album = meta.albumTitle?.toString(),
|
||||
artist = meta.artist?.toString(),
|
||||
imageLink = meta.artworkUri?.toString(),
|
||||
hasPrev = status?.isMediaCommandSupported(MediaStatus.COMMAND_QUEUE_PREVIOUS),
|
||||
hasNext = status?.isMediaCommandSupported(MediaStatus.COMMAND_QUEUE_NEXT),
|
||||
),
|
||||
mixAudio = null,
|
||||
castId = item.mediaId,
|
||||
castData = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun setVideoView(surfaceView: android.view.SurfaceView) {
|
||||
runOnMainThread {
|
||||
localPlayer.setVideoSurfaceView(surfaceView)
|
||||
@@ -408,15 +440,39 @@ class OmniPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
runOnMainThreadSync { eventMap.player = player }
|
||||
runOnMainThreadSync {
|
||||
eventMap.player = player
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(item: MediaItem?, reason: Int) = followMedia(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override var source: Source? = null
|
||||
private fun followMedia(item: MediaItem?) {
|
||||
// our media only leaves the player while it is being handed over to the
|
||||
// receiver (starting a cast), it is not gone.
|
||||
if (item == null && ownItem != null) return
|
||||
if (item != null && item == ownItem) return
|
||||
val source = item?.let { sourceOf(it) }
|
||||
if (source == _source) return
|
||||
_source = source
|
||||
eventMap.emitSource(source)
|
||||
syncNotificationService()
|
||||
}
|
||||
|
||||
private var _source: Source? = null
|
||||
private var ownItem: MediaItem? = null
|
||||
|
||||
|
||||
override var source: Source?
|
||||
get() = _source
|
||||
set(value) {
|
||||
field = value
|
||||
_source = value
|
||||
eventMap.emitSource(value)
|
||||
if (value == null) {
|
||||
ownItem = null
|
||||
runOnMainThreadSync {
|
||||
player.clearMediaItems()
|
||||
}
|
||||
@@ -442,6 +498,7 @@ class OmniPlayer(
|
||||
value.castId,
|
||||
value.castData,
|
||||
)
|
||||
ownItem = currentItem
|
||||
|
||||
runOnMainThreadSync {
|
||||
val startPositionMs = ((value.startTime ?: 0.0).coerceAtLeast(0.0)) * 1000.0
|
||||
|
||||
@@ -244,6 +244,7 @@ class OmniView(val context: ThemedReactContext) :
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (isPlaying) scheduleVideoRebuild()
|
||||
updatePictureInPictureParams()
|
||||
}
|
||||
|
||||
@@ -377,22 +378,16 @@ class OmniView(val context: ThemedReactContext) :
|
||||
movedSurfaceToRootForPip = false
|
||||
}
|
||||
|
||||
// A surface that is destroyed and recreated while playback runs (the PIP
|
||||
// reparent) makes VLC drop its video pipeline; it won't rebuild it for the
|
||||
// new surface on its own, so we must ask it to. The rebuild only succeeds
|
||||
// once the (PIP) window has settled at its final size — firing it mid-resize
|
||||
// leaves the vout stopped — so it is debounced off surfaceChanged.
|
||||
private var pendingVideoRebuild = false
|
||||
private val rebuildRunnable = Runnable {
|
||||
if (!pendingVideoRebuild) return@Runnable
|
||||
pendingVideoRebuild = false
|
||||
boundPlayer?.rebuildVideoOutput()
|
||||
private val rebuildRunnable = Runnable { boundPlayer?.rebuildVideoOutput() }
|
||||
|
||||
private fun scheduleVideoRebuild() {
|
||||
surfaceView.removeCallbacks(rebuildRunnable)
|
||||
surfaceView.postDelayed(rebuildRunnable, 200)
|
||||
}
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
surfaceReady = true
|
||||
boundPlayer?.setVideoView(surfaceView)
|
||||
if (boundPlayer?.localPlayer?.isPlaying == true) pendingVideoRebuild = true
|
||||
updatePictureInPictureParams()
|
||||
}
|
||||
|
||||
@@ -403,10 +398,7 @@ class OmniView(val context: ThemedReactContext) :
|
||||
height: Int
|
||||
) {
|
||||
boundPlayer?.updateVideoLayout(width, height)
|
||||
if (pendingVideoRebuild) {
|
||||
surfaceView.removeCallbacks(rebuildRunnable)
|
||||
surfaceView.postDelayed(rebuildRunnable, 200)
|
||||
}
|
||||
scheduleVideoRebuild()
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
|
||||
@@ -83,13 +83,15 @@ class VlcPlayer(ctx: Context) :
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) flush() else applicationHandler.post(flush)
|
||||
}
|
||||
|
||||
private val libVLC = LibVLC(ctx, arrayListOf(
|
||||
"--network-caching=1500",
|
||||
"--file-caching=1500",
|
||||
"--no-stats",
|
||||
"--no-osd",
|
||||
"--quiet"
|
||||
))
|
||||
private val libVLC = LibVLC(
|
||||
ctx, arrayListOf(
|
||||
"--network-caching=1500",
|
||||
"--file-caching=1500",
|
||||
"--no-stats",
|
||||
"--no-osd",
|
||||
"--quiet"
|
||||
)
|
||||
)
|
||||
|
||||
private val player = MediaPlayer(libVLC)
|
||||
|
||||
@@ -98,15 +100,28 @@ class VlcPlayer(ctx: Context) :
|
||||
private var mediaItems: List<MediaItem> = emptyList()
|
||||
private var currentMediaItemIndex: Int = INDEX_UNSET
|
||||
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
|
||||
|
||||
// vlc handles seeks on its input thread and keeps reporting the pre-seek time
|
||||
// until it decoded the new position, so remember where we asked to go and
|
||||
// report that instead
|
||||
@Volatile
|
||||
private var pendingSeekPosition: Long = TIME_UNSET
|
||||
|
||||
@Volatile
|
||||
private var seekOriginPosition: Long = 0L
|
||||
private var boundSurfaceView: SurfaceView? = null
|
||||
private var videoOutputStale = false
|
||||
private var lastVideoSize: VideoSize = VideoSize.UNKNOWN
|
||||
|
||||
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
||||
@@ -177,6 +192,7 @@ class VlcPlayer(ctx: Context) :
|
||||
|
||||
MediaPlayer.Event.Stopped -> {
|
||||
abandonAudioFocus()
|
||||
pendingSeekPosition = TIME_UNSET
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(STATE_IDLE)
|
||||
}
|
||||
@@ -184,6 +200,7 @@ class VlcPlayer(ctx: Context) :
|
||||
|
||||
MediaPlayer.Event.EndReached -> {
|
||||
abandonAudioFocus()
|
||||
pendingSeekPosition = TIME_UNSET
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
it.onPlaybackStateChanged(STATE_ENDED)
|
||||
}
|
||||
@@ -215,7 +232,14 @@ class VlcPlayer(ctx: Context) :
|
||||
|
||||
// progress is polled instead of pushed dozens of times per seconds.
|
||||
// this improves perf & battery life (native -> js bridge is expensive)
|
||||
MediaPlayer.Event.TimeChanged -> Unit
|
||||
MediaPlayer.Event.TimeChanged -> {
|
||||
val pending = pendingSeekPosition
|
||||
if (pending != TIME_UNSET &&
|
||||
(Math.abs(event.timeChanged - seekOriginPosition) > 1_000 || Math.abs(event.timeChanged - pending) < 1_000)
|
||||
) {
|
||||
pendingSeekPosition = TIME_UNSET
|
||||
}
|
||||
}
|
||||
|
||||
// vlc has no video-size event, Vout event still has unknown size.
|
||||
// This is the first callback with known size.
|
||||
@@ -293,7 +317,11 @@ class VlcPlayer(ctx: Context) :
|
||||
audioManager.requestAudioFocus(request)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
|
||||
audioManager.requestAudioFocus(
|
||||
this,
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.AUDIOFOCUS_GAIN
|
||||
)
|
||||
}
|
||||
hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
||||
return hasAudioFocus
|
||||
@@ -346,8 +374,10 @@ class VlcPlayer(ctx: Context) :
|
||||
currentMediaItemIndex = targetIndex
|
||||
playlistMetadata = mediaItems.getOrNull(targetIndex)?.mediaMetadata ?: MediaMetadata.EMPTY
|
||||
playerError = null
|
||||
pendingSeekPosition = TIME_UNSET
|
||||
|
||||
player.stop()
|
||||
videoOutputStale = false
|
||||
|
||||
mediaItems.getOrNull(targetIndex)?.let { item ->
|
||||
val uri = item.localConfiguration?.uri?.toString()
|
||||
@@ -376,7 +406,8 @@ class VlcPlayer(ctx: Context) :
|
||||
EVENT_PLAYLIST_METADATA_CHANGED
|
||||
)
|
||||
if (prev != currentMediaItemIndex) events += EVENT_MEDIA_ITEM_TRANSITION
|
||||
val reason = if (userInitiatedTransition) MEDIA_ITEM_TRANSITION_REASON_SEEK else MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
|
||||
val reason =
|
||||
if (userInitiatedTransition) MEDIA_ITEM_TRANSITION_REASON_SEEK else MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
|
||||
userInitiatedTransition = false
|
||||
notifyListeners(events.toTypedArray()) {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
||||
@@ -430,6 +461,7 @@ class VlcPlayer(ctx: Context) :
|
||||
mediaItems = emptyList()
|
||||
currentMediaItemIndex = INDEX_UNSET
|
||||
playlistMetadata = MediaMetadata.EMPTY
|
||||
pendingSeekPosition = TIME_UNSET
|
||||
player.stop()
|
||||
|
||||
val events = mutableListOf(
|
||||
@@ -495,7 +527,8 @@ class VlcPlayer(ctx: Context) :
|
||||
seekCommand: Int,
|
||||
isRepeatingCurrentItem: Boolean
|
||||
) {
|
||||
val targetIndex = if (mediaItemIndex == INDEX_UNSET) currentMediaItemIndex else mediaItemIndex
|
||||
val targetIndex =
|
||||
if (mediaItemIndex == INDEX_UNSET) currentMediaItemIndex else mediaItemIndex
|
||||
if (targetIndex == INDEX_UNSET || targetIndex !in mediaItems.indices) return
|
||||
|
||||
if (targetIndex != currentMediaItemIndex) {
|
||||
@@ -507,6 +540,10 @@ class VlcPlayer(ctx: Context) :
|
||||
|
||||
val from = getCurrentPosition()
|
||||
val target = positionMs.coerceAtLeast(0L).takeIf { it != TIME_UNSET } ?: 0L
|
||||
if (player.isSeekable) {
|
||||
seekOriginPosition = player.time.coerceAtLeast(0L)
|
||||
pendingSeekPosition = target
|
||||
}
|
||||
player.time = target
|
||||
// since vlc reports progress events every few ms they don't have a seek finished event.
|
||||
notifyListeners(EVENT_POSITION_DISCONTINUITY) {
|
||||
@@ -579,14 +616,17 @@ class VlcPlayer(ctx: Context) :
|
||||
.setWidth(videoTrack?.width?.takeIf { it > 0 } ?: Format.NO_VALUE)
|
||||
.setHeight(videoTrack?.height?.takeIf { it > 0 } ?: Format.NO_VALUE)
|
||||
.setFrameRate(
|
||||
videoTrack?.takeIf { it.frameRateDen > 0 }
|
||||
videoTrack?.takeIf { it.frameRateDen > 0 && it.frameRateNum > 0 }
|
||||
?.let { it.frameRateNum.toFloat() / it.frameRateDen }
|
||||
?: Format.NO_VALUE.toFloat()
|
||||
)
|
||||
.setAverageBitrate(track.bitrate.takeIf { it > 0 } ?: Format.NO_VALUE)
|
||||
.build()
|
||||
}
|
||||
val group = TrackGroup("vlc-video-${videoTracks.minOf { it.id }}", *videoFormats.toTypedArray())
|
||||
val group = TrackGroup(
|
||||
"vlc-video-${videoTracks.minOf { it.id }}",
|
||||
*videoFormats.toTypedArray()
|
||||
)
|
||||
val selected = BooleanArray(videoFormats.size) { idx ->
|
||||
selectedVideo != null && selectedVideo.id == videoTracks[idx].id
|
||||
}
|
||||
@@ -605,7 +645,10 @@ class VlcPlayer(ctx: Context) :
|
||||
.setSampleMimeType("audio/x-unknown")
|
||||
.build()
|
||||
}
|
||||
val group = TrackGroup("vlc-audio-${audioTracks.minOf { it.id }}", *audioFormats.toTypedArray())
|
||||
val group = TrackGroup(
|
||||
"vlc-audio-${audioTracks.minOf { it.id }}",
|
||||
*audioFormats.toTypedArray()
|
||||
)
|
||||
val selected = BooleanArray(audioFormats.size) { idx ->
|
||||
selectedAudio != null && selectedAudio.id == audioTracks[idx].id
|
||||
}
|
||||
@@ -623,7 +666,8 @@ class VlcPlayer(ctx: Context) :
|
||||
.setSampleMimeType("text/x-unknown")
|
||||
.build()
|
||||
val group = TrackGroup("vlc-sub-${track.id}", format)
|
||||
val selected = booleanArrayOf(selectedSubtitle != null && selectedSubtitle.id == track.id)
|
||||
val selected =
|
||||
booleanArrayOf(selectedSubtitle != null && selectedSubtitle.id == track.id)
|
||||
val support = intArrayOf(FORMAT_HANDLED)
|
||||
result.add(Group(group, false, support, selected))
|
||||
}
|
||||
@@ -661,10 +705,12 @@ class VlcPlayer(ctx: Context) :
|
||||
player.setVideoTrackEnabled(true)
|
||||
player.selectTrack(trackId)
|
||||
}
|
||||
|
||||
TRACK_TYPE_AUDIO -> {
|
||||
hasAudioOverride = true
|
||||
player.selectTrack(trackId)
|
||||
}
|
||||
|
||||
TRACK_TYPE_TEXT -> {
|
||||
hasTextOverride = true
|
||||
player.selectTrack(trackId)
|
||||
@@ -709,7 +755,12 @@ class VlcPlayer(ctx: Context) :
|
||||
val preferredLabels = labels.filter { it.isNotEmpty() }
|
||||
if (preferredLanguages.isEmpty() && preferredLabels.isEmpty()) return null
|
||||
|
||||
data class Candidate(val id: String, val langIndex: Int, val labelIndex: Int, val hasDefaultFlag: Boolean)
|
||||
data class Candidate(
|
||||
val id: String,
|
||||
val langIndex: Int,
|
||||
val labelIndex: Int,
|
||||
val hasDefaultFlag: Boolean
|
||||
)
|
||||
|
||||
val candidates = tracks.groups
|
||||
.filter { it.type == trackType }
|
||||
@@ -759,7 +810,8 @@ class VlcPlayer(ctx: Context) :
|
||||
// timelines are immutable
|
||||
val items = mediaItems
|
||||
val currentIndex = currentMediaItemIndex
|
||||
val currentDurationUs = duration.takeIf { it != TIME_UNSET }?.let { it * 1000L } ?: TIME_UNSET
|
||||
val currentDurationUs =
|
||||
duration.takeIf { it != TIME_UNSET }?.let { it * 1000L } ?: TIME_UNSET
|
||||
if (items.isEmpty()) return Timeline.EMPTY
|
||||
return object : Timeline() {
|
||||
private fun durationUsForIndex(index: Int): Long =
|
||||
@@ -814,7 +866,11 @@ class VlcPlayer(ctx: Context) :
|
||||
|
||||
override fun getDuration(): Long = player.length.takeIf { it > 0 } ?: TIME_UNSET
|
||||
|
||||
override fun getCurrentPosition() = player.time.coerceAtLeast(0L)
|
||||
override fun getCurrentPosition(): Long {
|
||||
val pending = pendingSeekPosition
|
||||
if (pending != TIME_UNSET) return pending
|
||||
return player.time.coerceAtLeast(0L)
|
||||
}
|
||||
|
||||
override fun getBufferedPosition(): Long {
|
||||
val duration = getDuration()
|
||||
@@ -897,30 +953,22 @@ class VlcPlayer(ctx: Context) :
|
||||
if (surfaceView == null) return clearVideoSurface()
|
||||
if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return
|
||||
boundSurfaceView = surfaceView
|
||||
// Hand VLC the SurfaceView (not a raw Surface) so it reads the real view
|
||||
// size and keeps the video layout correct as the surface resizes (e.g. the
|
||||
// PIP window shrinking/growing); see updateVideoLayout for size updates.
|
||||
videoOutputStale = (
|
||||
player.playerState == IMedia.State.Playing ||
|
||||
player.playerState == IMedia.State.Paused
|
||||
)
|
||||
vlcVout.setVideoView(surfaceView)
|
||||
vlcVout.attachViews()
|
||||
}
|
||||
|
||||
/**
|
||||
* VLC tears down its video decoder + output whenever the output Surface is
|
||||
* destroyed (e.g. the SurfaceView being reparented for PIP) and does not
|
||||
* rebuild them when a new Surface arrives — the picture stays black. Toggling
|
||||
* the video track forces VLC to spin up a fresh decoder/vout against the
|
||||
* currently-attached Surface. Safe no-op when nothing is attached/playing.
|
||||
*/
|
||||
fun rebuildVideoOutput() {
|
||||
if (!vlcVout.areViewsAttached() || !player.isPlaying) return
|
||||
if (!videoOutputStale || !player.isPlaying || !vlcVout.areViewsAttached()) return
|
||||
videoOutputStale = false
|
||||
player.setVideoTrackEnabled(false)
|
||||
player.setVideoTrackEnabled(true)
|
||||
player.updateVideoSurfaces()
|
||||
}
|
||||
|
||||
/** Recompute the video layout for the current surface size. Must run whenever
|
||||
* the surface resizes (VLC latches a stale geometry otherwise -> the picture
|
||||
* renders at the wrong size, anchored in a corner). */
|
||||
fun updateVideoLayout(width: Int, height: Int) {
|
||||
if (!vlcVout.areViewsAttached()) return
|
||||
if (width > 0 && height > 0) vlcVout.setWindowSize(width, height)
|
||||
@@ -952,7 +1000,8 @@ class VlcPlayer(ctx: Context) :
|
||||
}
|
||||
|
||||
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
|
||||
val par = if (videoTrack.sarNum > 0 && videoTrack.sarDen > 0) {
|
||||
videoTrack.sarNum.toFloat() / videoTrack.sarDen.toFloat()
|
||||
|
||||
+2
-14
@@ -1,13 +1,7 @@
|
||||
import { createPlayer } from "@videojs/react";
|
||||
import { GoogleCast } from "@videojs/react/media/google-cast";
|
||||
import { videoFeatures } from "@videojs/react/video";
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { createContext, type ReactNode, useContext, useEffect } from "react";
|
||||
import { usePlayerState } from "./events";
|
||||
import { WebOmniPlayer } from "./player.web";
|
||||
import type { OmniPlayer, PlayerBackend } from "./types/player";
|
||||
@@ -58,16 +52,10 @@ const PlayerInitializer = ({
|
||||
}) => {
|
||||
const store = VideoPlayer.usePlayer();
|
||||
const player = useLazyRef(() => new WebOmniPlayer(store));
|
||||
const seekedForSrc = useRef<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
player.source = source;
|
||||
const uri = source?.src?.uri;
|
||||
if (uri !== seekedForSrc.current) {
|
||||
seekedForSrc.current = uri;
|
||||
if (source?.startTime) store.seek(source.startTime);
|
||||
}
|
||||
}, [source, store]);
|
||||
}, [source]);
|
||||
|
||||
useEffect(() => {
|
||||
player.showNotification = showNotification;
|
||||
|
||||
@@ -134,6 +134,17 @@ export const OmniView = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const player = usePlayer();
|
||||
const uri = source?.src.uri;
|
||||
const startTime = source?.startTime;
|
||||
const prevUri = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (!uri) return;
|
||||
if (startTime) player.currentTime = startTime;
|
||||
if (autoplay && prevUri.current && prevUri.current !== uri) player.play();
|
||||
prevUri.current = uri;
|
||||
}, [player, uri, startTime, autoplay]);
|
||||
|
||||
// While casting, the receiver renders subtitles (the player forwards the
|
||||
// selection); skip rendering them locally on the idle <video>.
|
||||
const castStatus = usePlayerState("castStatus");
|
||||
|
||||
Reference in New Issue
Block a user