This commit is contained in:
2026-07-27 22:57:03 +02:00
commit 70a0a07ecc
107 changed files with 2369 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
"use strict";
import { useEffect, useState } from "react";
import { usePlayer } from "./provider";
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export const useEvent = (event, callback) => {
const player = usePlayer();
useEffect(() => {
player.eventMap[`addOn${capitalize(event)}Listener`](callback);
return () => player.eventMap[`removeOn${capitalize(event)}Listener`](callback);
}, [player, event, callback]);
};
export function usePlayerState(key, refresh) {
const player = usePlayer();
const [ret, setState] = useState(player[key]);
useEffect(() => {
const em = player.eventMap;
switch (key) {
case "currentTime":
case "buffered":
case "duration":
case "playbackRate":
case "volume":
em.addStateListener(key, setState);
return () => em.removeStateListener(key, setState);
case "isPlaying":
case "muted":
case "isAutoQuality":
em.addStateBoolListener(key, setState);
return () => em.removeStateBoolListener(key, setState);
case "status":
em.addPlayerStatusListener(setState);
return () => em.removePlayerStatusListener(setState);
case "castStatus":
em.addCastStatusListener(setState);
return () => em.removeCastStatusListener(setState);
}
}, [player, key]);
if (key === "currentTime") refresh ??= 1;
useEffect(() => {
if (!refresh || refresh <= 0) return;
const int = setInterval(() => {
setState(player[key]);
}, refresh * 1000);
return () => clearInterval(int);
}, [refresh, key, player]);
return ret;
}
//# sourceMappingURL=events.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":["useEffect","useState","usePlayer","capitalize","str","charAt","toUpperCase","slice","useEvent","event","callback","player","eventMap","usePlayerState","key","refresh","ret","setState","em","addStateListener","removeStateListener","addStateBoolListener","removeStateBoolListener","addPlayerStatusListener","removePlayerStatusListener","addCastStatusListener","removeCastStatusListener","int","setInterval","clearInterval"],"sourceRoot":"../../src","sources":["events.tsx"],"mappings":";;AAAA,SAASA,SAAS,EAAEC,QAAQ,QAAQ,OAAO;AAC3C,SAASC,SAAS,QAAQ,YAAY;AAKtC,SAASC,UAAUA,CAAmBC,GAAM,EAAiB;EAC5D,OAAQA,GAAG,CAACC,MAAM,CAAC,CAAC,CAAC,CAACC,WAAW,CAAC,CAAC,GAAGF,GAAG,CAACG,KAAK,CAAC,CAAC,CAAC;AACnD;AAEA,OAAO,MAAMC,QAAQ,GAAGA,CACvBC,KAAY,EACZC,QAA2B,KACvB;EACJ,MAAMC,MAAM,GAAGT,SAAS,CAAC,CAAe;EACxCF,SAAS,CAAC,MAAM;IACfW,MAAM,CAACC,QAAQ,CAAC,QAAQT,UAAU,CAACM,KAAK,CAAC,UAAU,CAAC,CAACC,QAAe,CAAC;IACrE,OAAO,MACNC,MAAM,CAACC,QAAQ,CAAC,WAAWT,UAAU,CAACM,KAAK,CAAC,UAAU,CAAC,CAACC,QAAe,CAAC;EAC1E,CAAC,EAAE,CAACC,MAAM,EAAEF,KAAK,EAAEC,QAAQ,CAAC,CAAC;AAC9B,CAAC;AASD,OAAO,SAASG,cAAcA,CAC7BC,GAAQ,EACRC,OAAgB,EACO;EACvB,MAAMJ,MAAM,GAAGT,SAAS,CAAC,CAAe;EACxC,MAAM,CAACc,GAAG,EAAEC,QAAQ,CAAC,GAAGhB,QAAQ,CAAMU,MAAM,CAACG,GAAG,CAAC,CAAC;EAElDd,SAAS,CAAC,MAAM;IACf,MAAMkB,EAAE,GAAGP,MAAM,CAACC,QAAQ;IAC1B,QAAQE,GAAG;MACV,KAAK,aAAa;MAClB,KAAK,UAAU;MACf,KAAK,UAAU;MACf,KAAK,cAAc;MACnB,KAAK,QAAQ;QACZI,EAAE,CAACC,gBAAgB,CAACL,GAAG,EAAEG,QAAQ,CAAC;QAClC,OAAO,MAAMC,EAAE,CAACE,mBAAmB,CAACN,GAAG,EAAEG,QAAQ,CAAC;MACnD,KAAK,WAAW;MAChB,KAAK,OAAO;MACZ,KAAK,eAAe;QACnBC,EAAE,CAACG,oBAAoB,CAACP,GAAG,EAAEG,QAAQ,CAAC;QACtC,OAAO,MAAMC,EAAE,CAACI,uBAAuB,CAACR,GAAG,EAAEG,QAAQ,CAAC;MACvD,KAAK,QAAQ;QACZC,EAAE,CAACK,uBAAuB,CAACN,QAAQ,CAAC;QACpC,OAAO,MAAMC,EAAE,CAACM,0BAA0B,CAACP,QAAQ,CAAC;MACrD,KAAK,YAAY;QAChBC,EAAE,CAACO,qBAAqB,CAACR,QAAQ,CAAC;QAClC,OAAO,MAAMC,EAAE,CAACQ,wBAAwB,CAACT,QAAQ,CAAC;IACpD;EACD,CAAC,EAAE,CAACN,MAAM,EAAEG,GAAG,CAAC,CAAC;EAEjB,IAAIA,GAAG,KAAK,aAAa,EAAEC,OAAO,KAAK,CAAC;EACxCf,SAAS,CAAC,MAAM;IACf,IAAI,CAACe,OAAO,IAAIA,OAAO,IAAI,CAAC,EAAE;IAC9B,MAAMY,GAAG,GAAGC,WAAW,CAAC,MAAM;MAC7BX,QAAQ,CAACN,MAAM,CAACG,GAAG,CAAC,CAAC;IACtB,CAAC,EAAEC,OAAO,GAAG,IAAI,CAAC;IAClB,OAAO,MAAMc,aAAa,CAACF,GAAG,CAAC;EAChC,CAAC,EAAE,CAACZ,OAAO,EAAED,GAAG,EAAEH,MAAM,CAAC,CAAC;EAE1B,OAAOK,GAAG;AACX","ignoreList":[]}
+150
View File
@@ -0,0 +1,150 @@
"use strict";
import { selectAudioTrack, selectBuffer, selectError, selectPlayback, selectPlaybackRate, selectQuality, selectRemotePlayback, selectTextTrack, selectTime, selectVolume, usePlayer as useStoreSelector } from "@videojs/react";
import { useEffect, useRef } from "react";
import { usePlayer } from "./provider.web";
function createEventMapper(key, selector, handler) {
return {
[key]: {
selector,
handler
}
};
}
const eventMapper = {
...createEventMapper("end", selectPlayback, (cb, value, prev) => {
if (value?.ended && prev && !prev.ended) cb();
}),
...createEventMapper("error", selectError, (cb, value, prev) => {
if (value?.error && value.error !== prev?.error) {
cb(value.error.message ?? "Unknown error", value.error.message ?? "Unknown error");
}
}),
...createEventMapper("subtitleChange", selectTextTrack, (cb, value, prev) => {
if (value === prev) return;
const tracks = value?.textTrackList;
if (tracks?.length) {
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
if (track?.mode === "showing") {
cb({
id: `text-${i}`,
label: track.label,
language: track.language,
selected: true
});
return;
}
}
}
cb(undefined);
}),
...createEventMapper("audioTrackChange", selectAudioTrack, (cb, value) => {
if (!value) return;
const track = value.audioTrackList.find(t => t.enabled);
if (!track) return;
cb({
id: track.id,
label: track.label,
language: track.language,
selected: true
});
}),
...createEventMapper("renditionChange", selectQuality, (cb, value) => {
const active = value?.activeVideoRendition;
if (!active) return;
cb({
id: active.id,
width: active.width ?? 0,
height: active.height ?? 0,
bitrate: active.bitrate ?? 0,
selected: true
});
})
};
export const useEvent = (event, callback) => {
const config = eventMapper[event];
const callbackRef = useRef(callback);
callbackRef.current = callback;
const prevRef = useRef(undefined);
const value = useStoreSelector(config?.selector ?? (() => undefined));
const player = usePlayer();
useEffect(() => {
if (!config) return;
config.handler(callbackRef.current, value, prevRef.current);
prevRef.current = value;
}, [value, config]);
useEffect(() => {
// don't use callbackRef.current directly to keep future callback updates
const cb = () => callbackRef.current();
if (event === "prev") {
player.onPrev.add(cb);
return () => {
player.onPrev.delete(cb);
};
}
if (event === "next") {
player.onNext.add(cb);
return () => {
player.onNext.delete(cb);
};
}
return undefined;
}, [event, player]);
};
function createMapper(key, selector, mapper) {
return {
[key]: {
selector,
mapper
}
};
}
export const stateMapper = {
...createMapper("status", selectPlayback, s => {
if (s?.waiting) return "loading";
if (s?.ended) return "idle";
return "readyToPlay";
}),
...createMapper("isPlaying", selectPlayback, s => {
if (!s) return false;
return !s.paused;
}),
...createMapper("currentTime", selectTime, s => {
return s?.currentTime ?? 0;
}),
...createMapper("buffered", selectBuffer, s => {
if (!s?.buffered.length) return 0;
const last = s.buffered[s.buffered.length - 1];
return last?.[1] ?? 0;
}),
...createMapper("duration", selectTime, s => {
return s?.duration ?? 0;
}),
...createMapper("playbackRate", selectPlaybackRate, s => {
return s?.playbackRate ?? 1;
}),
...createMapper("volume", selectVolume, s => {
return s?.volume ?? 1;
}),
...createMapper("muted", selectVolume, s => {
return s?.muted ?? false;
}),
...createMapper("isAutoQuality", selectQuality, q => {
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, _refresh) {
const config = stateMapper[key];
if (!config) throw new Error(`No mapper for ${key}`);
const ret = useStoreSelector(config.selector);
return config.mapper(ret);
}
//# sourceMappingURL=events.web.js.map
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
"use strict";
export { useEvent, usePlayerState } from "./events";
export { OmniProvider, usePlayer } from "./provider";
export { OmniView } from "./view";
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":["useEvent","usePlayerState","OmniProvider","usePlayer","OmniView"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,SAASA,QAAQ,EAAEC,cAAc,QAAQ,UAAU;AACnD,SAASC,YAAY,EAAEC,SAAS,QAAQ,YAAY;AAqBpD,SAASC,QAAQ,QAAQ,QAAQ","ignoreList":[]}
+252
View File
@@ -0,0 +1,252 @@
"use strict";
import { selectAudioTrack, selectQuality, selectTextTrack } from "@videojs/react";
import { stateMapper } from "./events.web";
export const getSubtitleFormat = subtitle => {
const mime = subtitle.mimeType?.toLowerCase() ?? "";
const ext = subtitle.link.split(/[?#]/)[0]?.split(".").pop()?.toLowerCase();
if (mime.includes("ass") || mime.includes("ssa") || ext === "ass" || ext === "ssa") return "ass";
if (mime.includes("pgs") || ext === "sup") return "pgs";
if (mime.includes("vtt") || ext === "vtt") return "vtt";
return "native";
};
export const isCustomSubtitle = subtitle => {
const format = getSubtitleFormat(subtitle);
return format === "ass" || format === "pgs";
};
export class WebOmniPlayer {
onPrev = new Set();
onNext = new Set();
constructor(store) {
this._store = store;
}
get castStatus() {
return stateMapper.castStatus.mapper(this._store.state);
}
toggleCastStatus() {
this._store.state.toggleRemotePlayback();
}
castOptions = null;
_source = undefined;
_showNotification = false;
// Selected ASS/PGS subtitle (drawn by the overlay); `null` when the active
// subtitle is native or off. Exposed as an external store so the view can
// react to selection changes.
overlaySubtitle = null;
overlayListeners = new Set();
get source() {
return this._source;
}
set source(source) {
this._source = source;
// Drop the overlay subtitle if it is not part of the new source.
if (this.overlaySubtitle && !source?.subtitles.some(s => s.id === this.overlaySubtitle?.id)) {
this.setOverlaySubtitle(null);
}
this.updateMediaSession();
}
get showNotification() {
return this._showNotification;
}
set showNotification(value) {
this._showNotification = value;
this.updateMediaSession();
}
get status() {
const state = this._store.state;
return stateMapper.status.mapper(state);
}
get isPlaying() {
return !this._store.state.paused && !this._store.state.ended;
}
get currentTime() {
return this._store.state.currentTime;
}
set currentTime(time) {
this._store.seek(time).catch(() => {});
}
get buffered() {
const buffered = this._store.state.buffered;
if (buffered.length === 0) return 0;
return buffered[buffered.length - 1]?.[1] ?? 0;
}
get duration() {
return this._store.state.duration;
}
get playbackRate() {
return this._store.state.playbackRate;
}
set playbackRate(rate) {
this._store.setPlaybackRate(rate);
}
get volume() {
return this._store.state.volume;
}
set volume(vol) {
this._store.setVolume(vol);
}
get muted() {
return this._store.state.muted;
}
set muted(value) {
if (value !== this.muted) {
this._store.toggleMuted();
}
}
get isAutoQuality() {
const quality = selectQuality(this._store.state);
if (!quality) return true;
// ABR ("auto") is on whenever no rendition is explicitly pinned.
return !quality.videoRenditionList.some(r => r.selected);
}
play() {
this._store.play();
this.setPlaybackState("playing");
}
pause() {
this._store.pause();
this.setPlaybackState("paused");
}
seekBy(offset) {
this._store.seek(this.currentTime + offset).catch(() => {});
}
playPrev() {
for (const cb of this.onPrev) cb();
}
playNext() {
for (const cb of this.onNext) cb();
}
get hasPrev() {
return this._source?.metadata?.hasPrev ?? false;
}
get hasNext() {
return this._source?.metadata?.hasNext ?? false;
}
get videos() {
// hls.js does not support alternative video tracks (e.g. camera angles)
return [];
}
selectVideo(_video) {
// hls.js does not support alternative video tracks
}
get audios() {
const audio = selectAudioTrack(this._store.state);
if (!audio) return [];
return audio.audioTrackList.map((track, i) => ({
id: track.id ?? i.toString(),
label: track.label,
language: track.language,
selected: track.enabled
}));
}
selectAudio(audio) {
const tracks = selectAudioTrack(this._store.state);
if (!tracks) return;
tracks.selectAudioTrack(audio.id);
}
get overlaySubtitles() {
return (this._source?.subtitles ?? []).filter(isCustomSubtitle);
}
get subtitles() {
// Every subtitle (incl. ass/pgs) is a real text track now; the cast
// receiver draws the ones it can't render natively, keyed off the active
// track. So the text-track list already contains all of them.
const textTracks = selectTextTrack(this._store.state)?.textTrackList ?? [];
return textTracks.filter(x => x.kind === "subtitles" || x.kind === "captions").map(track => ({
id: track.id,
label: track.label,
language: track.language,
selected: track.mode === "showing"
}));
}
selectSubtitle(subtitle) {
// Selecting a text track drives local playback and, while casting,
// video.js forwards it to the receiver as an active cast track. ass/pgs
// additionally need our local overlay since the browser can't render them.
const tracks = selectTextTrack(this._store.state);
tracks?.selectSubtitlesTrack(subtitle ? subtitle.id : "off");
const overlay = subtitle ? this.overlaySubtitles.find(s => s.id === subtitle.id) : undefined;
this.setOverlaySubtitle(overlay ?? null);
}
setOverlaySubtitle(sub) {
if (this.overlaySubtitle === sub) return;
this.overlaySubtitle = sub;
for (const listener of this.overlayListeners) listener();
}
// External store used by the view to render the ASS/PGS overlay.
subscribeOverlaySubtitle = callback => {
this.overlayListeners.add(callback);
return () => this.overlayListeners.delete(callback);
};
getOverlaySubtitle = () => this.overlaySubtitle;
get rendition() {
function isSameRendition(a, b) {
return b != null && a.id === b.id && a.width === b.width && a.height === b.height && a.bitrate === b.bitrate;
}
const quality = selectQuality(this._store.state);
if (!quality) return [];
const active = quality.activeVideoRendition;
return quality.videoRenditionList.map((rendition, i) => ({
id: rendition.id ?? i.toString(),
width: rendition.width ?? 0,
height: rendition.height ?? 0,
bitrate: rendition.bitrate ?? 0,
selected: rendition.selected || isSameRendition(rendition, active)
}));
}
selectRendition(rendition) {
const tracks = selectQuality(this._store.state);
if (!tracks) return;
tracks.selectVideoRendition(rendition ? rendition.id : "auto");
}
setPlaybackState(state) {
if (typeof navigator === "undefined" || !("mediaSession" in navigator)) {
return;
}
if (this._showNotification) navigator.mediaSession.playbackState = state;
}
updateMediaSession() {
if (typeof navigator === "undefined" || !("mediaSession" in navigator)) {
return;
}
const session = navigator.mediaSession;
const actions = ["play", "pause", "seekbackward", "seekforward", "seekto", "previoustrack", "nexttrack"];
if (!this._showNotification) {
session.metadata = null;
for (const action of actions) {
try {
session.setActionHandler(action, null);
} catch {}
}
return;
}
const metadata = this._source?.metadata;
if (metadata && typeof MediaMetadata !== "undefined") {
session.metadata = new MediaMetadata({
title: metadata.title,
artist: metadata.artist ?? "",
album: metadata.album ?? "",
artwork: metadata.imageLink ? [{
src: metadata.imageLink
}] : []
});
}
const set = (action, handler) => {
try {
session.setActionHandler(action, handler);
} catch {}
};
set("play", () => this.play());
set("pause", () => this.pause());
set("seekbackward", d => this.seekBy(-(d.seekOffset ?? 10)));
set("seekforward", d => this.seekBy(d.seekOffset ?? 10));
set("seekto", d => {
if (d.seekTime != null) this.currentTime = d.seekTime;
});
set("previoustrack", this.hasPrev ? () => this.playPrev() : null);
set("nexttrack", this.hasNext ? () => this.playNext() : null);
}
}
//# sourceMappingURL=player.web.js.map
File diff suppressed because one or more lines are too long
+33
View File
@@ -0,0 +1,33 @@
"use strict";
import { createContext, useContext, useEffect } from "react";
import { NitroModules } from "react-native-nitro-modules";
import { useLazyRef } from "./utils/lazy-ref";
import { jsx as _jsx } from "react/jsx-runtime";
const ProviderFactory = NitroModules.createHybridObject("OmniPlayerFactory");
const PlayerCtx = /*#__PURE__*/createContext(null);
export const OmniProvider = ({
children,
source,
backend = {
android: "vlc"
},
showNotification = false,
cast
}) => {
const player = useLazyRef(() => ProviderFactory.createPlayer(source, backend, cast));
useEffect(() => {
player.source = source;
}, [source]);
useEffect(() => {
player.showNotification = showNotification;
}, [showNotification]);
return /*#__PURE__*/_jsx(PlayerCtx.Provider, {
value: player,
children: children
});
};
export const usePlayer = () => {
return useContext(PlayerCtx);
};
//# sourceMappingURL=provider.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":["createContext","useContext","useEffect","NitroModules","useLazyRef","jsx","_jsx","ProviderFactory","createHybridObject","PlayerCtx","OmniProvider","children","source","backend","android","showNotification","cast","player","createPlayer","Provider","value","usePlayer"],"sourceRoot":"../../src","sources":["provider.tsx"],"mappings":";;AAAA,SAASA,aAAa,EAAkBC,UAAU,EAAEC,SAAS,QAAQ,OAAO;AAC5E,SAASC,YAAY,QAAQ,4BAA4B;AAIzD,SAASC,UAAU,QAAQ,kBAAkB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAE9C,MAAMC,eAAe,GACpBJ,YAAY,CAACK,kBAAkB,CAAoB,mBAAmB,CAAC;AAExE,MAAMC,SAAS,gBAAGT,aAAa,CAAa,IAAK,CAAC;AAElD,OAAO,MAAMU,YAAY,GAAGA,CAAC;EAC5BC,QAAQ;EACRC,MAAM;EACNC,OAAO,GAAG;IAAEC,OAAO,EAAE;EAAM,CAAC;EAC5BC,gBAAgB,GAAG,KAAK;EACxBC;AAOD,CAAC,KAAK;EACL,MAAMC,MAAM,GAAGb,UAAU,CAAC,MACzBG,eAAe,CAACW,YAAY,CAACN,MAAM,EAAEC,OAAO,EAAEG,IAAI,CACnD,CAAC;EAEDd,SAAS,CAAC,MAAM;IACfe,MAAM,CAACL,MAAM,GAAGA,MAAM;EACvB,CAAC,EAAE,CAACA,MAAM,CAAC,CAAC;EAEZV,SAAS,CAAC,MAAM;IACfe,MAAM,CAACF,gBAAgB,GAAGA,gBAAgB;EAC3C,CAAC,EAAE,CAACA,gBAAgB,CAAC,CAAC;EAEtB,oBAAOT,IAAA,CAACG,SAAS,CAACU,QAAQ;IAACC,KAAK,EAAEH,MAAO;IAAAN,QAAA,EAAEA;EAAQ,CAAqB,CAAC;AAC1E,CAAC;AAED,OAAO,MAAMU,SAAS,GAAGA,CAAA,KAAM;EAC9B,OAAOpB,UAAU,CAACQ,SAAS,CAAC;AAC7B,CAAC","ignoreList":[]}
+59
View File
@@ -0,0 +1,59 @@
"use strict";
import { createPlayer } from "@videojs/react";
import { videoFeatures } from "@videojs/react/video";
import { createContext, useContext, useEffect, useRef } from "react";
import { WebOmniPlayer } from "./player.web";
import { useLazyRef } from "./utils/lazy-ref";
import { jsx as _jsx } from "react/jsx-runtime";
export const VideoPlayer = createPlayer({
features: videoFeatures
});
const PlayerCtx = /*#__PURE__*/createContext(null);
export const OmniProvider = ({
children,
source,
cast,
// Web always uses video.js; accepted for API parity with native.
backend: _backend,
showNotification = false
}) => {
return /*#__PURE__*/_jsx(VideoPlayer.Provider, {
children: /*#__PURE__*/_jsx(PlayerInitializer, {
source: source,
cast: cast,
showNotification: showNotification,
children: children
})
});
};
const PlayerInitializer = ({
children,
source,
cast,
showNotification
}) => {
const store = VideoPlayer.usePlayer();
const player = useLazyRef(() => new WebOmniPlayer(store));
const seekedForSrc = useRef(undefined);
useEffect(() => {
player.source = source;
const uri = source?.src?.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]);
return /*#__PURE__*/_jsx(PlayerCtx.Provider, {
value: player,
children: children
});
};
export const usePlayer = () => useContext(PlayerCtx);
//# sourceMappingURL=provider.web.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":["createPlayer","videoFeatures","createContext","useContext","useEffect","useRef","WebOmniPlayer","useLazyRef","jsx","_jsx","VideoPlayer","features","PlayerCtx","OmniProvider","children","source","cast","backend","_backend","showNotification","Provider","PlayerInitializer","store","usePlayer","player","seekedForSrc","undefined","uri","src","current","startTime","seek","castOptions","value"],"sourceRoot":"../../src","sources":["provider.web.tsx"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,gBAAgB;AAC7C,SAASC,aAAa,QAAQ,sBAAsB;AACpD,SACCC,aAAa,EAEbC,UAAU,EACVC,SAAS,EACTC,MAAM,QACA,OAAO;AACd,SAASC,aAAa,QAAQ,cAAc;AAG5C,SAASC,UAAU,QAAQ,kBAAkB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAE9C,OAAO,MAAMC,WAAW,GAAGV,YAAY,CAAC;EAAEW,QAAQ,EAAEV;AAAc,CAAC,CAAC;AAEpE,MAAMW,SAAS,gBAAGV,aAAa,CAAa,IAAK,CAAC;AAElD,OAAO,MAAMW,YAAY,GAAGA,CAAC;EAC5BC,QAAQ;EACRC,MAAM;EACNC,IAAI;EACJ;EACAC,OAAO,EAAEC,QAAQ;EACjBC,gBAAgB,GAAG;AAOpB,CAAC,KAAK;EACL,oBACCV,IAAA,CAACC,WAAW,CAACU,QAAQ;IAAAN,QAAA,eACpBL,IAAA,CAACY,iBAAiB;MACjBN,MAAM,EAAEA,MAAO;MACfC,IAAI,EAAEA,IAAK;MACXG,gBAAgB,EAAEA,gBAAiB;MAAAL,QAAA,EAElCA;IAAQ,CACS;EAAC,CACC,CAAC;AAEzB,CAAC;AAED,MAAMO,iBAAiB,GAAGA,CAAC;EAC1BP,QAAQ;EACRC,MAAM;EACNC,IAAI;EACJG;AAMD,CAAC,KAAK;EACL,MAAMG,KAAK,GAAGZ,WAAW,CAACa,SAAS,CAAC,CAAC;EACrC,MAAMC,MAAM,GAAGjB,UAAU,CAAC,MAAM,IAAID,aAAa,CAACgB,KAAK,CAAC,CAAC;EACzD,MAAMG,YAAY,GAAGpB,MAAM,CAAqBqB,SAAS,CAAC;EAE1DtB,SAAS,CAAC,MAAM;IACfoB,MAAM,CAACT,MAAM,GAAGA,MAAM;IACtB,MAAMY,GAAG,GAAGZ,MAAM,EAAEa,GAAG,EAAED,GAAG;IAC5B,IAAIA,GAAG,KAAKF,YAAY,CAACI,OAAO,EAAE;MACjCJ,YAAY,CAACI,OAAO,GAAGF,GAAG;MAC1B,IAAIZ,MAAM,EAAEe,SAAS,EAAER,KAAK,CAACS,IAAI,CAAChB,MAAM,CAACe,SAAS,CAAC;IACpD;EACD,CAAC,EAAE,CAACf,MAAM,EAAEO,KAAK,CAAC,CAAC;EAEnBlB,SAAS,CAAC,MAAM;IACfoB,MAAM,CAACQ,WAAW,GAAGhB,IAAI,IAAI,IAAI;EAClC,CAAC,EAAE,CAACA,IAAI,CAAC,CAAC;EAEVZ,SAAS,CAAC,MAAM;IACfoB,MAAM,CAACL,gBAAgB,GAAGA,gBAAgB;EAC3C,CAAC,EAAE,CAACA,gBAAgB,CAAC,CAAC;EAEtB,oBAAOV,IAAA,CAACG,SAAS,CAACQ,QAAQ;IAACa,KAAK,EAAET,MAAO;IAAAV,QAAA,EAAEA;EAAQ,CAAqB,CAAC;AAC1E,CAAC;AAED,OAAO,MAAMS,SAAS,GAAGA,CAAA,KAAMpB,UAAU,CAACS,SAAS,CAAC","ignoreList":[]}
+4
View File
@@ -0,0 +1,4 @@
"use strict";
export {};
//# sourceMappingURL=omni-player.nitro.js.map
@@ -0,0 +1 @@
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["specs/omni-player.nitro.ts"],"mappings":"","ignoreList":[]}
+4
View File
@@ -0,0 +1,4 @@
"use strict";
export {};
//# sourceMappingURL=omni-view.nitro.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["specs/omni-view.nitro.ts"],"mappings":"","ignoreList":[]}
+4
View File
@@ -0,0 +1,4 @@
"use strict";
export {};
//# sourceMappingURL=events.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["types/events.ts"],"mappings":"","ignoreList":[]}
+4
View File
@@ -0,0 +1,4 @@
"use strict";
export {};
//# sourceMappingURL=player.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["types/player.ts"],"mappings":"","ignoreList":[]}
+2
View File
@@ -0,0 +1,2 @@
"use strict";
//# sourceMappingURL=source.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["types/source.ts"],"mappings":"","ignoreList":[]}
+2
View File
@@ -0,0 +1,2 @@
"use strict";
//# sourceMappingURL=subtitles.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["types/subtitles.ts"],"mappings":"","ignoreList":[]}
+4
View File
@@ -0,0 +1,4 @@
"use strict";
export {};
//# sourceMappingURL=view.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["types/view.ts"],"mappings":"","ignoreList":[]}
+8
View File
@@ -0,0 +1,8 @@
"use strict";
import { useState } from "react";
export const useLazyRef = init => {
const [ret] = useState(init);
return ret;
};
//# sourceMappingURL=lazy-ref.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":["useState","useLazyRef","init","ret"],"sourceRoot":"../../../src","sources":["utils/lazy-ref.ts"],"mappings":";;AAAA,SAASA,QAAQ,QAAQ,OAAO;AAEhC,OAAO,MAAMC,UAAU,GAAOC,IAAa,IAAQ;EAClD,MAAM,CAACC,GAAG,CAAC,GAAGH,QAAQ,CAAIE,IAAI,CAAC;EAC/B,OAAOC,GAAG;AACX,CAAC","ignoreList":[]}
+20
View File
@@ -0,0 +1,20 @@
"use strict";
import { memo } from "react";
import { getHostComponent } from "react-native-nitro-modules";
import OmniConfig from "../nitrogen/generated/shared/json/OmniViewConfig.json";
import { usePlayer } from "./provider";
import { jsx as _jsx } from "react/jsx-runtime";
const NativeView = /*#__PURE__*/memo(getHostComponent("OmniView", () => OmniConfig));
export const OmniView = /*#__PURE__*/memo(({
// Web-only; not forwarded to the native view.
subtitleAssets: _subtitleAssets,
...props
}) => {
const player = usePlayer();
return /*#__PURE__*/_jsx(NativeView, {
player: player,
...props
});
});
//# sourceMappingURL=view.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":["memo","getHostComponent","OmniConfig","usePlayer","jsx","_jsx","NativeView","OmniView","subtitleAssets","_subtitleAssets","props","player"],"sourceRoot":"../../src","sources":["view.tsx"],"mappings":";;AAAA,SAASA,IAAI,QAAQ,OAAO;AAE5B,SACCC,gBAAgB,QAEV,4BAA4B;AACnC,OAAOC,UAAU,MAAM,uDAAuD;AAC9E,SAASC,SAAS,QAAQ,YAAY;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAKvC,MAAMC,UAAU,gBAAGN,IAAI,CACtBC,gBAAgB,CAA2B,UAAU,EAAE,MAAMC,UAAU,CACxE,CAAC;AAED,OAAO,MAAMK,QAAQ,gBAAGP,IAAI,CAC3B,CAAC;EACA;EACAQ,cAAc,EAAEC,eAAe;EAC/B,GAAGC;AACkC,CAAC,KAAK;EAC3C,MAAMC,MAAM,GAAGR,SAAS,CAAC,CAAe;EACxC,oBAAOE,IAAA,CAACC,UAAU;IAACK,MAAM,EAAEA,MAAO;IAAA,GAAKD;EAAK,CAAG,CAAC;AACjD,CACD,CAAC","ignoreList":[]}
+165
View File
@@ -0,0 +1,165 @@
"use strict";
import { HlsJsVideo } from "@videojs/react/media/hlsjs-video";
import { Video } from "@videojs/react/video";
import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
import { usePlayerState } from "./events";
import { getSubtitleFormat } from "./player.web";
import { usePlayer, VideoPlayer } from "./provider.web";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const SubtitleOverlay = ({
video,
assets,
fonts
}) => {
const player = usePlayer();
const subtitle = useSyncExternalStore(player.subscribeOverlaySubtitle, player.getOverlaySubtitle, () => null);
const assetsRef = useRef(assets);
assetsRef.current = assets;
const fontsRef = useRef(fonts);
fontsRef.current = fonts;
useEffect(() => {
const el = video.current;
if (!el || !subtitle) return;
let renderer = null;
let cancelled = false;
const attach = created => {
if (cancelled) created.destroy();else renderer = created;
};
if (getSubtitleFormat(subtitle) === "ass") {
const jassub = assetsRef.current?.jassub;
const fonts = fontsRef.current;
// The video's source resolution (its highest available rendition).
// jassub uses this as libass' storage size so subtitles stay sharp
// even while a low-bitrate rendition is playing — otherwise the
// raster tracks the currently-decoded (possibly tiny) frame size.
const renditions = player.rendition;
const videoWidth = Math.max(0, ...renditions.map(r => r.width));
const videoHeight = Math.max(0, ...renditions.map(r => r.height));
import("jassub").then(({
default: JASSUB
}) => {
const instance = new JASSUB({
video: el,
subUrl: subtitle.link,
...(videoWidth && {
videoWidth
}),
...(videoHeight && {
videoHeight
}),
...(fonts?.length && {
fonts
}),
...(jassub?.workerUrl && {
workerUrl: jassub.workerUrl
}),
...(jassub?.wasmUrl && {
wasmUrl: jassub.wasmUrl
}),
...(jassub?.modernWasmUrl && {
modernWasmUrl: jassub.modernWasmUrl
}),
...(jassub?.fontUrl && {
availableFonts: {
"liberation sans": jassub.fontUrl
},
defaultFont: "liberation sans"
})
});
attach({
destroy: () => instance.destroy()
});
}).catch(e => console.error("[omni] failed to render ass subtitle", e));
} else {
const pgs = assetsRef.current?.pgs;
import("libpgs").then(({
PgsRenderer
}) => {
const instance = new PgsRenderer({
video: el,
subUrl: subtitle.link,
workerUrl: pgs?.workerUrl ?? new URL("libpgs/dist/libpgs.worker.js", import.meta.url).href
});
attach({
destroy: () => instance.dispose()
});
}).catch(e => console.error("[omni] failed to render pgs subtitle", e));
}
return () => {
cancelled = true;
renderer?.destroy();
};
}, [video, subtitle, player]);
return null;
};
export const OmniView = ({
style,
autoplay,
subtitleAssets
}) => {
const player = usePlayer();
const containerRef = useRef(null);
const ref = useRef(undefined);
const src = player.source?.src;
const isHls = src?.mimeType?.toLowerCase().includes("mpegurl") || 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 config = useMemo(() => ({
hlsJs: {
xhrSetup: xhr => {
const headers = headersRef.current;
if (!headers) return;
for (const [key, value] of Object.entries(headers)) {
if (value) xhr.setRequestHeader(key, value);
}
}
},
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
// selection); skip rendering them locally on the idle <video>.
const castStatus = usePlayerState("castStatus");
return /*#__PURE__*/_jsxs(VideoPlayer.Container, {
ref: containerRef,
style: {
position: "relative",
...style
},
children: [src && /*#__PURE__*/_jsx(Tech, {
ref: ref,
src: src.uri,
config: config,
autoPlay: autoplay,
playsInline: true,
crossOrigin: "anonymous",
style: {
width: "100%",
height: "100%",
objectFit: "contain"
},
children: (player.source?.subtitles ?? []).map(subtitle => /*#__PURE__*/_jsx("track", {
id: subtitle.id,
kind: "subtitles",
src: subtitle.link,
srcLang: subtitle.language,
label: subtitle.label ?? subtitle.language ?? subtitle.id
}, subtitle.id))
}), castStatus !== "connected" && castStatus !== "connecting" && /*#__PURE__*/_jsx(SubtitleOverlay, {
video: ref,
assets: subtitleAssets,
fonts: player.source?.fonts
})]
});
};
//# sourceMappingURL=view.web.js.map
File diff suppressed because one or more lines are too long