Compare commits

...
5 Commits
5 changed files with 159 additions and 64 deletions
@@ -29,11 +29,13 @@ import com.margelo.nitro.NitroModules
import com.margelo.nitro.omni.AndroidBackend import com.margelo.nitro.omni.AndroidBackend
import com.margelo.nitro.omni.CastStatus import com.margelo.nitro.omni.CastStatus
import com.margelo.nitro.omni.HybridOmniPlayerSpec import com.margelo.nitro.omni.HybridOmniPlayerSpec
import com.margelo.nitro.omni.Metadata
import com.margelo.nitro.omni.MixAudioMode import com.margelo.nitro.omni.MixAudioMode
import com.margelo.nitro.omni.PlayerStatus import com.margelo.nitro.omni.PlayerStatus
import com.margelo.nitro.omni.Rendition import com.margelo.nitro.omni.Rendition
import com.margelo.nitro.omni.Source import com.margelo.nitro.omni.Source
import com.margelo.nitro.omni.Track import com.margelo.nitro.omni.Track
import com.margelo.nitro.omni.VideoSrc
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.media3.common.MediaItem.RequestMetadata import androidx.media3.common.MediaItem.RequestMetadata
import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.MediaItem.SubtitleConfiguration
@@ -45,6 +47,7 @@ import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService import androidx.media3.session.MediaSessionService
import androidx.mediarouter.app.MediaRouteChooserDialog import androidx.mediarouter.app.MediaRouteChooserDialog
import androidx.mediarouter.app.SystemOutputSwitcherDialogController 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.CastContext
import com.google.android.gms.cast.framework.CastState import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.CastStateListener import com.google.android.gms.cast.framework.CastStateListener
@@ -298,6 +301,33 @@ class OmniPlayer(
.build() .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) { fun setVideoView(surfaceView: android.view.SurfaceView) {
runOnMainThread { runOnMainThread {
localPlayer.setVideoSurfaceView(surfaceView) localPlayer.setVideoSurfaceView(surfaceView)
@@ -408,15 +438,39 @@ class OmniPlayer(
} }
} }
init { 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) { set(value) {
field = value _source = value
eventMap.emitSource(value) eventMap.emitSource(value)
if (value == null) { if (value == null) {
ownItem = null
runOnMainThreadSync { runOnMainThreadSync {
player.clearMediaItems() player.clearMediaItems()
} }
@@ -442,6 +496,7 @@ class OmniPlayer(
value.castId, value.castId,
value.castData, value.castData,
) )
ownItem = currentItem
runOnMainThreadSync { runOnMainThreadSync {
val startPositionMs = ((value.startTime ?: 0.0).coerceAtLeast(0.0)) * 1000.0 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) { override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) scheduleVideoRebuild()
updatePictureInPictureParams() updatePictureInPictureParams()
} }
@@ -377,22 +378,16 @@ class OmniView(val context: ThemedReactContext) :
movedSurfaceToRootForPip = false movedSurfaceToRootForPip = false
} }
// A surface that is destroyed and recreated while playback runs (the PIP private val rebuildRunnable = Runnable { boundPlayer?.rebuildVideoOutput() }
// 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 private fun scheduleVideoRebuild() {
// once the (PIP) window has settled at its final size — firing it mid-resize surfaceView.removeCallbacks(rebuildRunnable)
// leaves the vout stopped — so it is debounced off surfaceChanged. surfaceView.postDelayed(rebuildRunnable, 200)
private var pendingVideoRebuild = false
private val rebuildRunnable = Runnable {
if (!pendingVideoRebuild) return@Runnable
pendingVideoRebuild = false
boundPlayer?.rebuildVideoOutput()
} }
override fun surfaceCreated(holder: SurfaceHolder) { override fun surfaceCreated(holder: SurfaceHolder) {
surfaceReady = true surfaceReady = true
boundPlayer?.setVideoView(surfaceView) boundPlayer?.setVideoView(surfaceView)
if (boundPlayer?.localPlayer?.isPlaying == true) pendingVideoRebuild = true
updatePictureInPictureParams() updatePictureInPictureParams()
} }
@@ -403,10 +398,7 @@ class OmniView(val context: ThemedReactContext) :
height: Int height: Int
) { ) {
boundPlayer?.updateVideoLayout(width, height) boundPlayer?.updateVideoLayout(width, height)
if (pendingVideoRebuild) { scheduleVideoRebuild()
surfaceView.removeCallbacks(rebuildRunnable)
surfaceView.postDelayed(rebuildRunnable, 200)
}
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
@@ -83,13 +83,15 @@ class VlcPlayer(ctx: Context) :
if (Looper.myLooper() == Looper.getMainLooper()) flush() else applicationHandler.post(flush) if (Looper.myLooper() == Looper.getMainLooper()) flush() else applicationHandler.post(flush)
} }
private val libVLC = LibVLC(ctx, arrayListOf( private val libVLC = LibVLC(
ctx, arrayListOf(
"--network-caching=1500", "--network-caching=1500",
"--file-caching=1500", "--file-caching=1500",
"--no-stats", "--no-stats",
"--no-osd", "--no-osd",
"--quiet" "--quiet"
)) )
)
private val player = MediaPlayer(libVLC) private val player = MediaPlayer(libVLC)
@@ -98,15 +100,28 @@ class VlcPlayer(ctx: Context) :
private var mediaItems: List<MediaItem> = emptyList() private var mediaItems: List<MediaItem> = emptyList()
private var currentMediaItemIndex: Int = INDEX_UNSET private var currentMediaItemIndex: Int = INDEX_UNSET
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 @Volatile
private var released = false 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
// 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 boundSurfaceView: SurfaceView? = null
private var videoOutputStale = false
private var lastVideoSize: VideoSize = VideoSize.UNKNOWN private var lastVideoSize: VideoSize = VideoSize.UNKNOWN
private val availableCommands: Player.Commands = Player.Commands.Builder() private val availableCommands: Player.Commands = Player.Commands.Builder()
@@ -177,6 +192,7 @@ class VlcPlayer(ctx: Context) :
MediaPlayer.Event.Stopped -> { MediaPlayer.Event.Stopped -> {
abandonAudioFocus() abandonAudioFocus()
pendingSeekPosition = TIME_UNSET
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
it.onPlaybackStateChanged(STATE_IDLE) it.onPlaybackStateChanged(STATE_IDLE)
} }
@@ -184,6 +200,7 @@ class VlcPlayer(ctx: Context) :
MediaPlayer.Event.EndReached -> { MediaPlayer.Event.EndReached -> {
abandonAudioFocus() abandonAudioFocus()
pendingSeekPosition = TIME_UNSET
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
it.onPlaybackStateChanged(STATE_ENDED) it.onPlaybackStateChanged(STATE_ENDED)
} }
@@ -215,7 +232,14 @@ class VlcPlayer(ctx: Context) :
// progress is polled instead of pushed dozens of times per seconds. // progress is polled instead of pushed dozens of times per seconds.
// 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 -> {
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. // vlc has no video-size event, Vout event still has unknown size.
// This is the first callback with known size. // This is the first callback with known size.
@@ -293,7 +317,11 @@ class VlcPlayer(ctx: Context) :
audioManager.requestAudioFocus(request) audioManager.requestAudioFocus(request)
} else { } else {
@Suppress("DEPRECATION") @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 hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
return hasAudioFocus return hasAudioFocus
@@ -346,8 +374,10 @@ class VlcPlayer(ctx: Context) :
currentMediaItemIndex = targetIndex currentMediaItemIndex = targetIndex
playlistMetadata = mediaItems.getOrNull(targetIndex)?.mediaMetadata ?: MediaMetadata.EMPTY playlistMetadata = mediaItems.getOrNull(targetIndex)?.mediaMetadata ?: MediaMetadata.EMPTY
playerError = null playerError = null
pendingSeekPosition = TIME_UNSET
player.stop() player.stop()
videoOutputStale = false
mediaItems.getOrNull(targetIndex)?.let { item -> mediaItems.getOrNull(targetIndex)?.let { item ->
val uri = item.localConfiguration?.uri?.toString() val uri = item.localConfiguration?.uri?.toString()
@@ -376,7 +406,8 @@ class VlcPlayer(ctx: Context) :
EVENT_PLAYLIST_METADATA_CHANGED EVENT_PLAYLIST_METADATA_CHANGED
) )
if (prev != currentMediaItemIndex) events += EVENT_MEDIA_ITEM_TRANSITION 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 userInitiatedTransition = false
notifyListeners(events.toTypedArray()) { notifyListeners(events.toTypedArray()) {
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
@@ -430,6 +461,7 @@ class VlcPlayer(ctx: Context) :
mediaItems = emptyList() mediaItems = emptyList()
currentMediaItemIndex = INDEX_UNSET currentMediaItemIndex = INDEX_UNSET
playlistMetadata = MediaMetadata.EMPTY playlistMetadata = MediaMetadata.EMPTY
pendingSeekPosition = TIME_UNSET
player.stop() player.stop()
val events = mutableListOf( val events = mutableListOf(
@@ -495,7 +527,8 @@ class VlcPlayer(ctx: Context) :
seekCommand: Int, seekCommand: Int,
isRepeatingCurrentItem: Boolean 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 == INDEX_UNSET || targetIndex !in mediaItems.indices) return
if (targetIndex != currentMediaItemIndex) { if (targetIndex != currentMediaItemIndex) {
@@ -507,6 +540,10 @@ class VlcPlayer(ctx: Context) :
val from = getCurrentPosition() val from = getCurrentPosition()
val target = positionMs.coerceAtLeast(0L).takeIf { it != TIME_UNSET } ?: 0L val target = positionMs.coerceAtLeast(0L).takeIf { it != TIME_UNSET } ?: 0L
if (player.isSeekable) {
seekOriginPosition = player.time.coerceAtLeast(0L)
pendingSeekPosition = target
}
player.time = target player.time = target
// since vlc reports progress events every few ms they don't have a seek finished event. // since vlc reports progress events every few ms they don't have a seek finished event.
notifyListeners(EVENT_POSITION_DISCONTINUITY) { notifyListeners(EVENT_POSITION_DISCONTINUITY) {
@@ -586,7 +623,10 @@ class VlcPlayer(ctx: Context) :
.setAverageBitrate(track.bitrate.takeIf { it > 0 } ?: Format.NO_VALUE) .setAverageBitrate(track.bitrate.takeIf { it > 0 } ?: Format.NO_VALUE)
.build() .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 -> val selected = BooleanArray(videoFormats.size) { idx ->
selectedVideo != null && selectedVideo.id == videoTracks[idx].id selectedVideo != null && selectedVideo.id == videoTracks[idx].id
} }
@@ -605,7 +645,10 @@ class VlcPlayer(ctx: Context) :
.setSampleMimeType("audio/x-unknown") .setSampleMimeType("audio/x-unknown")
.build() .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 -> val selected = BooleanArray(audioFormats.size) { idx ->
selectedAudio != null && selectedAudio.id == audioTracks[idx].id selectedAudio != null && selectedAudio.id == audioTracks[idx].id
} }
@@ -623,7 +666,8 @@ class VlcPlayer(ctx: Context) :
.setSampleMimeType("text/x-unknown") .setSampleMimeType("text/x-unknown")
.build() .build()
val group = TrackGroup("vlc-sub-${track.id}", format) 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) val support = intArrayOf(FORMAT_HANDLED)
result.add(Group(group, false, support, selected)) result.add(Group(group, false, support, selected))
} }
@@ -661,10 +705,12 @@ class VlcPlayer(ctx: Context) :
player.setVideoTrackEnabled(true) player.setVideoTrackEnabled(true)
player.selectTrack(trackId) player.selectTrack(trackId)
} }
TRACK_TYPE_AUDIO -> { TRACK_TYPE_AUDIO -> {
hasAudioOverride = true hasAudioOverride = true
player.selectTrack(trackId) player.selectTrack(trackId)
} }
TRACK_TYPE_TEXT -> { TRACK_TYPE_TEXT -> {
hasTextOverride = true hasTextOverride = true
player.selectTrack(trackId) player.selectTrack(trackId)
@@ -709,7 +755,12 @@ class VlcPlayer(ctx: Context) :
val preferredLabels = labels.filter { it.isNotEmpty() } val preferredLabels = labels.filter { it.isNotEmpty() }
if (preferredLanguages.isEmpty() && preferredLabels.isEmpty()) return null 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 val candidates = tracks.groups
.filter { it.type == trackType } .filter { it.type == trackType }
@@ -759,7 +810,8 @@ class VlcPlayer(ctx: Context) :
// timelines are immutable // timelines are immutable
val items = mediaItems val items = mediaItems
val currentIndex = currentMediaItemIndex 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 if (items.isEmpty()) return Timeline.EMPTY
return object : Timeline() { return object : Timeline() {
private fun durationUsForIndex(index: Int): Long = 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 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 { override fun getBufferedPosition(): Long {
val duration = getDuration() val duration = getDuration()
@@ -897,30 +953,22 @@ class VlcPlayer(ctx: Context) :
if (surfaceView == null) return clearVideoSurface() if (surfaceView == null) return clearVideoSurface()
if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return
boundSurfaceView = surfaceView boundSurfaceView = surfaceView
// Hand VLC the SurfaceView (not a raw Surface) so it reads the real view videoOutputStale = (
// size and keeps the video layout correct as the surface resizes (e.g. the player.playerState == IMedia.State.Playing ||
// PIP window shrinking/growing); see updateVideoLayout for size updates. player.playerState == IMedia.State.Paused
)
vlcVout.setVideoView(surfaceView) vlcVout.setVideoView(surfaceView)
vlcVout.attachViews() 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() { fun rebuildVideoOutput() {
if (!vlcVout.areViewsAttached() || !player.isPlaying) return if (!videoOutputStale || !player.isPlaying || !vlcVout.areViewsAttached()) return
videoOutputStale = false
player.setVideoTrackEnabled(false) player.setVideoTrackEnabled(false)
player.setVideoTrackEnabled(true) player.setVideoTrackEnabled(true)
player.updateVideoSurfaces() 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) { fun updateVideoLayout(width: Int, height: Int) {
if (!vlcVout.areViewsAttached()) return if (!vlcVout.areViewsAttached()) return
if (width > 0 && height > 0) vlcVout.setWindowSize(width, height) if (width > 0 && height > 0) vlcVout.setWindowSize(width, height)
@@ -952,7 +1000,8 @@ class VlcPlayer(ctx: Context) :
} }
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
val par = if (videoTrack.sarNum > 0 && videoTrack.sarDen > 0) { val par = if (videoTrack.sarNum > 0 && videoTrack.sarDen > 0) {
videoTrack.sarNum.toFloat() / videoTrack.sarDen.toFloat() videoTrack.sarNum.toFloat() / videoTrack.sarDen.toFloat()
+2 -14
View File
@@ -1,13 +1,7 @@
import { createPlayer } from "@videojs/react"; import { createPlayer } from "@videojs/react";
import { GoogleCast } from "@videojs/react/media/google-cast"; import { GoogleCast } from "@videojs/react/media/google-cast";
import { videoFeatures } from "@videojs/react/video"; import { videoFeatures } from "@videojs/react/video";
import { import { createContext, type ReactNode, useContext, useEffect } from "react";
createContext,
type ReactNode,
useContext,
useEffect,
useRef,
} from "react";
import { usePlayerState } from "./events"; 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";
@@ -58,16 +52,10 @@ const PlayerInitializer = ({
}) => { }) => {
const store = VideoPlayer.usePlayer(); const store = VideoPlayer.usePlayer();
const player = useLazyRef(() => new WebOmniPlayer(store)); const player = useLazyRef(() => new WebOmniPlayer(store));
const seekedForSrc = useRef<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
player.source = source; player.source = source;
const uri = source?.src?.uri; }, [source]);
if (uri !== seekedForSrc.current) {
seekedForSrc.current = uri;
if (source?.startTime) store.seek(source.startTime);
}
}, [source, store]);
useEffect(() => { useEffect(() => {
player.showNotification = showNotification; player.showNotification = showNotification;
+11
View File
@@ -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 // While casting, the receiver renders subtitles (the player forwards the
// selection); skip rendering them locally on the idle <video>. // selection); skip rendering them locally on the idle <video>.
const castStatus = usePlayerState("castStatus"); const castStatus = usePlayerState("castStatus");