Compare commits

...
9 Commits
13 changed files with 111 additions and 48 deletions
+2 -6
View File
@@ -43,16 +43,12 @@ jobs:
restore-keys: |
${{ runner.os }}-bun-
- name: Install npm dependencies (bun)
- name: Install/Typecheck/Build
# We use a `prepare` script that run builds to support git installs.
run: bun install
- name: Build lib
run: bun run build
- name: Release
env:
# No NPM_TOKEN: auth happens via OIDC trusted publishing (id-token: write above).
# Provenance is generated automatically, so NPM_CONFIG_PROVENANCE is not needed.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: ${{ github.actor }}
GIT_AUTHOR_EMAIL: '${{ github.actor }}@users.noreply.github.com'
+1 -9
View File
@@ -1,4 +1,4 @@
# omni
# react-native-omni
A library to have real players on android and web. It uses VLC on android and
videojs v10 on the web (ios not implemented yet, PR welcome)
@@ -53,14 +53,6 @@ subtitles rendering:
<summary>ass rendering</summary>
#### Fonts
JASSUB only renders glyphs for fonts it has, and does **not** ship a usable
default font. Provide the fonts a subtitle references through
`source.fonts` (an array of font-file URLs). Optionally set
`subtitleAssets.jassub.fontUrl` as a fallback for styles whose font isn't
listed. If no matching font is available, that text simply won't appear.
#### Cross-origin isolation
jassub's wasm is multi-threaded and relies on `SharedArrayBuffer`, which
+3 -3
View File
@@ -23,9 +23,9 @@
"typescript": "^7.0.2",
},
"peerDependencies": {
"react": "19.2.7",
"react-native": "0.86.0",
"react-native-nitro-modules": "0.36.1",
"react": ">=19.0.0",
"react-native": ">=0.86.0",
"react-native-nitro-modules": ">=0.36.0",
},
},
"example": {
+8 -4
View File
@@ -14,6 +14,7 @@
"typecheck": "tsc --noEmit && tsc -p plugin/tsconfig.json --noEmit",
"build:plugin": "tsc -p plugin/tsconfig.json",
"build": "bun run typecheck && bob build && bun run build:plugin",
"prepare": "bun run build",
"codegen": "nitrogen --logLevel=\"debug\" && node post-script.js",
"release": "semantic-release"
},
@@ -48,7 +49,10 @@
"workspaces": [
"example"
],
"repository": "https://github.com/zoriya/react-native-omni.git",
"repository": {
"type": "git",
"url": "git+https://github.com/zoriya/react-native-omni.git"
},
"author": "Zoe Roux",
"license": "MIT",
"bugs": "https://github.com/zoriya/react-native-omni/issues",
@@ -76,9 +80,9 @@
"typescript": "^7.0.2"
},
"peerDependencies": {
"react": "19.2.7",
"react-native": "0.86.0",
"react-native-nitro-modules": "0.36.1"
"react": ">=19.0.0",
"react-native": ">=0.86.0",
"react-native-nitro-modules": ">=0.36.0"
},
"react-native-builder-bob": {
"source": "src",
+3
View File
@@ -52,6 +52,9 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
case "status":
em.addPlayerStatusListener(setState);
return () => em.removePlayerStatusListener(setState);
case "castStatus":
// TODO: implement this
return;
}
}, [player, key]);
+9 -4
View File
@@ -6,6 +6,7 @@ import {
selectPlayback,
selectPlaybackRate,
selectQuality,
selectRemotePlayback,
selectTextTrack,
selectTime,
selectVolume,
@@ -108,18 +109,16 @@ export const useEvent = <Event extends keyof OmniEvents>(
prevRef.current = value;
}, [value, config]);
// prev/next are triggered by the app (or the media session) rather than the
// media element
useEffect(() => {
// don't use callbackRef.current directly to keep future callback updates
const cb = () => (callbackRef.current as () => void)();
if (event === "prev") {
const cb = callbackRef.current as () => void;
player.onPrev.add(cb);
return () => {
player.onPrev.delete(cb);
};
}
if (event === "next") {
const cb = callbackRef.current as () => void;
player.onNext.add(cb);
return () => {
player.onNext.delete(cb);
@@ -179,6 +178,12 @@ export const stateMapper = {
if (!q) return true;
return !q.videoRenditionList.some((r) => r.selected);
}),
...createMapper("castStatus", selectRemotePlayback, (r) => {
if (!r) return "unavailable";
if (r.remotePlaybackState === "connecting") return "connecting";
if (r.remotePlaybackState === "connected") return "connected";
return r.remotePlaybackAvailability
}),
};
export function usePlayerState<Key extends keyof OmniPlayerState>(
+2
View File
@@ -2,6 +2,7 @@ export { useEvent, usePlayerState } from "./events";
export { OmniProvider, usePlayer } from "./provider";
export type { OmniEvents } from "./types/events";
export type {
CastStatus,
OmniPlayer,
OmniPlayerState,
PlayerStatus,
@@ -9,6 +10,7 @@ export type {
Track,
} from "./types/player";
export type {
CastOptions,
Metadata,
MixAudioMode,
Source,
+11 -1
View File
@@ -7,12 +7,13 @@ import {
} from "@videojs/react";
import { stateMapper } from "./events.web";
import type {
CastStatus,
OmniPlayer,
PlayerStatus,
Rendition,
Track,
} from "./types/player";
import type { Source, Subtitle } from "./types/source";
import type { CastOptions, Source, Subtitle } from "./types/source";
export type SubtitleFormat = "vtt" | "ass" | "pgs" | "native";
@@ -51,6 +52,15 @@ export class WebOmniPlayer implements OmniPlayer {
this._store = store;
}
get castStatus(): CastStatus {
return stateMapper.castStatus.mapper(this._store.state);
}
toggleCastStatus(): void {
this._store.state.toggleRemotePlayback();
}
castOptions: CastOptions | null = null;
_source: Source | null = null;
private _showNotification = false;
+3 -1
View File
@@ -2,7 +2,7 @@ import { createContext, type ReactNode, useContext, useEffect } from "react";
import { NitroModules } from "react-native-nitro-modules";
import type { OmniPlayerFactory } from "./specs/omni-player.nitro";
import type { OmniPlayer } from "./types/player";
import type { Source } from "./types/source";
import type { CastOptions, Source } from "./types/source";
import { useLazyRef } from "./utils/lazy-ref";
const ProviderFactory =
@@ -14,8 +14,10 @@ export const OmniProvider = ({
children,
source,
showNotification = false,
cast: _,
}: {
source: Source;
cast?: CastOptions;
children: ReactNode;
showNotification?: boolean;
}) => {
+27 -4
View File
@@ -1,9 +1,15 @@
import { createPlayer } from "@videojs/react";
import { videoFeatures } from "@videojs/react/video";
import { createContext, type ReactNode, useContext, useEffect } from "react";
import {
createContext,
type ReactNode,
useContext,
useEffect,
useRef,
} from "react";
import { WebOmniPlayer } from "./player.web";
import type { OmniPlayer } from "./types/player";
import type { Source } from "./types/source";
import type { CastOptions, Source } from "./types/source";
import { useLazyRef } from "./utils/lazy-ref";
export const VideoPlayer = createPlayer({ features: videoFeatures });
@@ -13,15 +19,21 @@ const PlayerCtx = createContext<OmniPlayer>(null!);
export const OmniProvider = ({
children,
source,
cast,
showNotification = false,
}: {
source: Source;
cast?: CastOptions;
children: ReactNode;
showNotification?: boolean;
}) => {
return (
<VideoPlayer.Provider>
<PlayerInitializer source={source} showNotification={showNotification}>
<PlayerInitializer
source={source}
cast={cast}
showNotification={showNotification}
>
{children}
</PlayerInitializer>
</VideoPlayer.Provider>
@@ -31,20 +43,31 @@ export const OmniProvider = ({
const PlayerInitializer = ({
children,
source,
cast,
showNotification,
}: {
children: ReactNode;
source: Source;
cast?: CastOptions;
showNotification: boolean;
}) => {
const store = VideoPlayer.usePlayer();
const player = useLazyRef(() => new WebOmniPlayer(store));
const seekedForSrc = useRef<string | undefined>(undefined);
useEffect(() => {
player.source = source;
if (source.startTime) store.seek(source.startTime).catch(() => {});
const uri = source.src[0]?.uri;
if (uri !== seekedForSrc.current) {
seekedForSrc.current = uri;
if (source.startTime) store.seek(source.startTime);
}
}, [source, store]);
useEffect(() => {
player.castOptions = cast ?? null;
}, [cast]);
useEffect(() => {
player.showNotification = showNotification;
}, [showNotification]);
+10
View File
@@ -5,6 +5,8 @@ export interface OmniPlayer extends OmniPlayerState {
pause(): void;
seekBy(offset: number): void;
toggleCastStatus(): void;
// trigger the prev/next event manually, the user has to implement the event
playPrev(): void;
playNext(): void;
@@ -32,10 +34,18 @@ export interface OmniPlayerState {
volume: number;
muted: boolean;
readonly isAutoQuality: boolean;
readonly castStatus: CastStatus;
}
export type PlayerStatus = "idle" | "loading" | "readyToPlay" | "error";
export type CastStatus =
| "connecting"
| "connected"
| "available"
| "unavailable"
| "unsupported";
export interface Track {
readonly id: string;
readonly label?: string;
+7
View File
@@ -7,6 +7,13 @@ export interface Source {
fonts?: string[];
metadata?: Metadata;
mixAudio?: MixAudioMode;
// Opaque, consumer-defined payload forwarded verbatim to the cast receiver
// as the load request's customData. omni does not interpret it.
castData?: unknown;
}
export interface CastOptions {
receiverApplicationId?: string;
}
export interface VideoSrc {
+25 -16
View File
@@ -1,6 +1,5 @@
import type { HlsMediaConfig } from "@videojs/core/dom/media/hls-js";
import { HlsJsVideo } from "@videojs/react/media/hlsjs-video";
import { Video } from "@videojs/react/video";
import {
type CSSProperties,
type RefObject,
@@ -108,21 +107,30 @@ export const OmniView = ({
const ref = useRef<HTMLVideoElement>(undefined!);
const src = player.source?.src[0];
const Tech = src?.uri.endsWith("m3u8") ? HlsJsVideo : Video;
const config = useMemo<HlsMediaConfig | undefined>(() => {
const headers = src?.headers;
if (!headers || Object.keys(headers).length === 0) return undefined;
return {
hlsJs: {
xhrSetup: (xhr: XMLHttpRequest) => {
for (const [key, value] of Object.entries(headers)) {
if (value) xhr.setRequestHeader(key, value);
}
const headersRef = useRef(src?.headers);
headersRef.current = src?.headers;
const castData = player.source?.castData;
const config = useMemo<HlsMediaConfig>(
() =>
({
hlsJs: {
xhrSetup: (xhr: XMLHttpRequest) => {
const headers = headersRef.current;
if (!headers) return;
for (const [key, value] of Object.entries(headers)) {
if (value) xhr.setRequestHeader(key, value);
}
},
},
},
};
}, [src?.headers]);
googleCast: {
receiver: player.castOptions?.receiverApplicationId,
customData: castData ?? null,
},
}) as HlsMediaConfig,
[player.castOptions, castData],
);
return (
<VideoPlayer.Container
@@ -130,7 +138,7 @@ export const OmniView = ({
style={{ position: "relative", ...style }}
>
{src && (
<Tech
<HlsJsVideo
ref={ref}
src={src.uri}
config={config}
@@ -144,13 +152,14 @@ export const OmniView = ({
.map((subtitle) => (
<track
key={subtitle.id}
id={subtitle.id}
kind="subtitles"
src={subtitle.link}
srcLang={subtitle.language}
label={subtitle.label ?? subtitle.language ?? subtitle.id}
/>
))}
</Tech>
</HlsJsVideo>
)}
<SubtitleOverlay
video={ref}