mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee4b1a3e32 | ||
|
|
8656fb8068 | ||
|
|
31bd0adcef | ||
|
|
0d8b33aee3 | ||
|
|
645b226359 | ||
|
|
61ee6238d5 | ||
|
|
a7717dfcb9 | ||
|
|
337f338224 | ||
|
|
72328aacf6 |
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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,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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user