fix(web): make source a listenable state, fixes the view

This commit is contained in:
2026-07-31 19:43:49 +02:00
parent d2ca76b4a9
commit 0c7c2b3848
21 changed files with 323 additions and 54 deletions
+3
View File
@@ -55,6 +55,9 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
case "castStatus":
em.addCastStatusListener(setState);
return () => em.removeCastStatusListener(setState);
case "source":
em.addSourceListener(setState);
return () => em.removeSourceListener(setState);
}
}, [player, key]);
+14 -3
View File
@@ -12,7 +12,7 @@ import {
selectVolume,
usePlayer as useStoreSelector,
} from "@videojs/react";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useSyncExternalStore } from "react";
import type { WebOmniPlayer } from "./player.web";
import { usePlayer } from "./provider.web";
import type { OmniEvents } from "./types/events";
@@ -204,6 +204,7 @@ export const stateMapper = {
}),
};
// biome-ignore-start lint/correctness/useHookAtTopLevel: key must be const
export function usePlayerState<Key extends keyof OmniPlayerState>(
key: Key,
): OmniPlayerState[Key];
@@ -215,8 +216,18 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
key: Key,
_refresh?: number,
): OmniPlayerState[Key] {
const config = stateMapper[key];
if (key === "source") {
const player = usePlayer() as WebOmniPlayer;
const source = useSyncExternalStore(
player.subscribeSource,
() => player.source,
() => player.source,
);
return source as OmniPlayerState[Key];
}
const config = stateMapper[key as keyof Omit<OmniPlayerState, "source">];
const ret = useStoreSelector(config?.selector as Selector<any, any>);
if (!config) throw new Error(`No mapper for ${key}`);
const ret = useStoreSelector(config.selector as Selector<any, any>);
return config.mapper(ret) as OmniPlayerState[Key];
}
// biome-ignore-end lint/correctness/useHookAtTopLevel: key must be const
+8
View File
@@ -70,6 +70,13 @@ export class WebOmniPlayer implements OmniPlayer {
private overlaySubtitle: Subtitle | null = null;
private overlayListeners = new Set<() => void>();
private sourceListeners = new Set<() => void>();
subscribeSource = (callback: () => void): (() => void) => {
this.sourceListeners.add(callback);
return () => this.sourceListeners.delete(callback);
};
get source(): Source | undefined {
return this._source;
}
@@ -84,6 +91,7 @@ export class WebOmniPlayer implements OmniPlayer {
this.setOverlaySubtitle(null);
}
this.updateMediaSession();
for (const listener of this.sourceListeners) listener();
}
get showNotification(): boolean {
+4 -1
View File
@@ -11,7 +11,7 @@ import type { CastOptions, Source } from "../types/source";
export type NumberProperty = Exclude<
keyof OmniPlayerState,
"status" | "isPlaying" | "muted"
"status" | "isPlaying" | "muted" | "source"
>;
export type BoolProperty = "isPlaying" | "muted" | "isAutoQuality";
@@ -29,6 +29,9 @@ export interface OmniEventMap extends HybridObject<{ android: "kotlin" }> {
addCastStatusListener(cb: (value: CastStatus) => void): void;
removeCastStatusListener(cb: (value: CastStatus) => void): void;
addSourceListener(cb: (value?: Source) => void): void;
removeSourceListener(cb: (value?: Source) => void): void;
addOnEndListener(cb: OmniEvents["end"]): void;
removeOnEndListener(cb: OmniEvents["end"]): void;
+1 -2
View File
@@ -3,8 +3,6 @@ import type { Source } from "./source";
export interface OmniPlayer extends OmniPlayerState {
showNotification?: boolean;
source?: Source;
play(): void;
pause(): void;
seekBy(offset: number): void;
@@ -28,6 +26,7 @@ export interface OmniPlayer extends OmniPlayerState {
}
export interface OmniPlayerState {
source?: Source;
readonly status: PlayerStatus;
readonly isPlaying: boolean;
currentTime: number;
+11 -11
View File
@@ -109,18 +109,18 @@ export const OmniView = ({
const player = usePlayer() as WebOmniPlayer;
const containerRef = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLVideoElement>(undefined!);
const source = usePlayerState("source");
const src = player.source?.src;
const isHls =
src?.mimeType?.toLowerCase().includes("mpegurl") ||
src?.uri.split(/[?#]/)[0]?.toLowerCase().endsWith(".m3u8") ||
source?.src?.mimeType?.toLowerCase().includes("mpegurl") ||
source?.src?.uri.split(/[?#]/)[0]?.toLowerCase().endsWith(".m3u8") ||
false;
const Tech = isHls ? HlsJsVideo : Video;
const headersRef = useRef(src?.headers);
headersRef.current = src?.headers;
const castId = player.source?.castId;
const castData = player.source?.castData;
const headersRef = useRef(source?.src?.headers);
headersRef.current = source?.src?.headers;
const castId = source?.castId;
const castData = source?.castData;
const config = useMemo<HlsMediaConfig>(
() => ({
@@ -151,17 +151,17 @@ export const OmniView = ({
ref={containerRef}
style={{ position: "relative", ...style }}
>
{src && (
{source?.src && (
<Tech
ref={ref}
src={src.uri}
src={source?.src.uri}
config={config}
autoPlay={autoplay}
playsInline
crossOrigin="anonymous"
style={{ width: "100%", height: "100%", objectFit: "contain" }}
>
{(player.source?.subtitles ?? []).map((subtitle) => (
{(source?.subtitles ?? []).map((subtitle) => (
<track
key={subtitle.id}
id={subtitle.id}
@@ -177,7 +177,7 @@ export const OmniView = ({
<SubtitleOverlay
video={ref}
assets={subtitleAssets}
fonts={player.source?.fonts}
fonts={source?.fonts}
/>
)}
</VideoPlayer.Container>