Compare commits

...
6 Commits
9 changed files with 152 additions and 41 deletions
@@ -60,13 +60,18 @@ class OmniCastTrackSelector : CastTrackSelector() {
val overrideMatch = indices.firstOrNull { i -> val overrideMatch = indices.firstOrNull { i ->
overrides.keys.any { it.id == groups[i].id } overrides.keys.any { it.id == groups[i].id }
} }
val match = indices.firstOrNull { i -> val matchesLabel = { i: Int ->
val name: String? = tracks[i].name val name: String? = tracks[i].name
val language: String? = tracks[i].language labels.isNotEmpty() && name != null && name in labels
(labels.isNotEmpty() && name != null && name in labels) ||
(languages.isNotEmpty() && language != null &&
Util.normalizeLanguageCode(language) in languages)
} }
val matchesLanguage = { i: Int ->
val language: String? = tracks[i].language
languages.isNotEmpty() && language != null &&
Util.normalizeLanguageCode(language) in languages
}
val match = indices.firstOrNull { matchesLabel(it) && matchesLanguage(it) }
?: indices.firstOrNull { matchesLabel(it) }
?: indices.firstOrNull { matchesLanguage(it) }
val chosen = when { val chosen = when {
overrideMatch != null -> overrideMatch overrideMatch != null -> overrideMatch
match != null -> match match != null -> match
@@ -1,6 +1,8 @@
package dev.zoriya.omni package dev.zoriya.omni
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.os.Looper
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@@ -13,11 +15,16 @@ import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.Renderer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory 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.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
@@ -62,9 +69,26 @@ class OmniPlayer(
AndroidBackend.EXOPLAYER -> { AndroidBackend.EXOPLAYER -> {
val http = DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true) val http = DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true)
httpDataSourceFactory = http 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( .setMediaSourceFactory(
DefaultMediaSourceFactory(DefaultDataSource.Factory(ctx, http)) DefaultMediaSourceFactory(DefaultDataSource.Factory(ctx, http))
.experimentalParseSubtitlesDuringExtraction(false)
) )
.build() .build()
} }
@@ -187,6 +211,19 @@ class OmniPlayer(
super.dispose() 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( 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?,
@@ -212,7 +249,10 @@ class OmniPlayer(
.setId(subtitle.id) .setId(subtitle.id)
.setLanguage(subtitle.language) .setLanguage(subtitle.language)
.setLabel(subtitle.label) .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() .build()
}) })
.setRequestMetadata( .setRequestMetadata(
@@ -441,15 +481,15 @@ class OmniPlayer(
}?.mediaTrackGroup }?.mediaTrackGroup
if (group != null) { if (group != null) {
params.setOverrideForType(TrackSelectionOverride(group, 0)) params.setOverrideForType(TrackSelectionOverride(group, 0))
}
// also set preferred flags for cast selection
val labels = track.label?.let { arrayOf(it) } ?: emptyArray()
if (type == C.TRACK_TYPE_AUDIO) {
params.setPreferredAudioLanguage(track.language)
.setPreferredAudioLabels(*labels)
} else { } else {
val labels = track.label?.let { arrayOf(it) } ?: emptyArray() params.setPreferredTextLanguage(track.language)
if (type == C.TRACK_TYPE_AUDIO) { .setPreferredTextLabels(*labels)
params.setPreferredAudioLanguage(track.language)
.setPreferredAudioLabels(*labels)
} else {
params.setPreferredTextLanguage(track.language)
.setPreferredTextLabels(*labels)
}
} }
} }
player.trackSelectionParameters = params.build() player.trackSelectionParameters = params.build()
@@ -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)
} }
@@ -46,6 +46,7 @@ import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.interfaces.IMedia import org.videolan.libvlc.interfaces.IMedia
import org.videolan.libvlc.interfaces.IMedia.VideoTrack import org.videolan.libvlc.interfaces.IMedia.VideoTrack
import org.videolan.libvlc.interfaces.IVLCVout import org.videolan.libvlc.interfaces.IVLCVout
import java.security.MessageDigest
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
class VlcPlayer(ctx: Context) : class VlcPlayer(ctx: Context) :
@@ -106,6 +107,7 @@ class VlcPlayer(ctx: Context) :
@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)
@@ -138,6 +140,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)
} }
@@ -160,6 +163,7 @@ class VlcPlayer(ctx: Context) :
it.onIsPlayingChanged(true) it.onIsPlayingChanged(true)
it.onTracksChanged(getCurrentTracks()) it.onTracksChanged(getCurrentTracks())
} }
maybeNotifyVideoSizeChanged()
} }
MediaPlayer.Event.Paused -> { MediaPlayer.Event.Paused -> {
@@ -213,12 +217,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 -> {
@@ -377,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 // vlc doesn't allow arbitrary headers :c
private fun applyRequestHeaders(media: Media, extras: Bundle?) { private fun applyRequestHeaders(media: Media, extras: Bundle?) {
if (extras == null) return if (extras == null) return
@@ -589,10 +614,12 @@ class VlcPlayer(ctx: Context) :
} }
player.getTracks(IMedia.Track.Type.Text)?.forEach { track -> 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() val format = Format.Builder()
.setId(track.id) .setId(track.id)
.setLabel(track.name) .setLabel(slave?.label ?: track.name)
.setLanguage(track.language) .setLanguage(slave?.language ?: track.language)
.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)
@@ -909,10 +936,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
+13 -6
View File
@@ -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=="],
+6 -1
View File
@@ -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"
}, },
-1
View File
@@ -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;
+19 -5
View File
@@ -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);
+2 -10
View File
@@ -1,4 +1,4 @@
import type { HlsMediaConfig } from "@videojs/core/dom/media/hls-js"; import type { HlsMediaConfig } from "@videojs/media/dom/hls-js";
import { HlsJsVideo } from "@videojs/react/media/hlsjs-video"; import { 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