mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-15 10:23:28 +00:00
feat(chromecast): send custom message for overlay subtitles
This commit is contained in:
+22
-1
@@ -126,6 +126,27 @@ export const useEvent = <Event extends keyof OmniEvents>(
|
||||
}
|
||||
return undefined;
|
||||
}, [event, player]);
|
||||
|
||||
// ASS/PGS (overlay) subtitle selection is not a videojs text-track change, so
|
||||
// re-fire subtitleChange when the overlay subtitle changes too — consumers
|
||||
// only need useEvent("subtitleChange").
|
||||
useEffect(() => {
|
||||
if (event !== "subtitleChange") return;
|
||||
return player.subscribeOverlaySubtitle(() => {
|
||||
const sel = player.subtitles.find((s) => s.selected);
|
||||
const cb = callbackRef.current as (s: unknown) => void;
|
||||
cb(
|
||||
sel
|
||||
? {
|
||||
id: sel.id,
|
||||
label: sel.label,
|
||||
language: sel.language,
|
||||
selected: true,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
});
|
||||
}, [event, player]);
|
||||
};
|
||||
|
||||
function createMapper<Key extends keyof OmniPlayerState, State, Result>(
|
||||
@@ -182,7 +203,7 @@ export const stateMapper = {
|
||||
if (!r) return "unavailable";
|
||||
if (r.remotePlaybackState === "connecting") return "connecting";
|
||||
if (r.remotePlaybackState === "connected") return "connected";
|
||||
return r.remotePlaybackAvailability
|
||||
return r.remotePlaybackAvailability;
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -242,6 +242,26 @@ export class WebOmniPlayer implements OmniPlayer {
|
||||
const tracks = selectTextTrack(this._store.state);
|
||||
tracks?.selectSubtitlesTrack(overlay || !subtitle ? "off" : subtitle.id);
|
||||
this.setOverlaySubtitle(overlay ?? null);
|
||||
|
||||
if (this.castStatus === "connected") {
|
||||
try {
|
||||
// if the cast receiver handles overlay subtitles like us, send this message.
|
||||
const ctx = (
|
||||
window as unknown as {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: cast sender SDK global
|
||||
cast?: { framework?: { CastContext?: { getInstance(): any } } };
|
||||
}
|
||||
).cast?.framework?.CastContext?.getInstance?.();
|
||||
ctx
|
||||
?.getCurrentSession?.()
|
||||
?.sendMessage("urn:x-cast:dev.zoriya.omni", {
|
||||
subtitle: overlay?.id ?? null,
|
||||
})
|
||||
?.catch?.(() => {});
|
||||
} catch {
|
||||
// no active cast session / sender SDK not loaded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setOverlaySubtitle(sub: Subtitle | null): void {
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface Source {
|
||||
fonts?: string[];
|
||||
metadata?: Metadata;
|
||||
mixAudio?: MixAudioMode;
|
||||
// contentId of a cast request. defaults to src but can be overridden here.
|
||||
castId?: string;
|
||||
// Opaque, consumer-defined payload forwarded verbatim to the cast receiver
|
||||
// as the load request's customData. omni does not interpret it.
|
||||
castData?: Record<string, string>;
|
||||
|
||||
+48
-33
@@ -9,6 +9,7 @@ import {
|
||||
useRef,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { usePlayerState } from "./events";
|
||||
import {
|
||||
getSubtitleFormat,
|
||||
isCustomSubtitle,
|
||||
@@ -58,35 +59,41 @@ const SubtitleOverlay = ({
|
||||
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() });
|
||||
});
|
||||
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() });
|
||||
});
|
||||
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 () => {
|
||||
@@ -116,6 +123,7 @@ export const OmniView = ({
|
||||
|
||||
const headersRef = useRef(src?.headers);
|
||||
headersRef.current = src?.headers;
|
||||
const castId = player.source?.castId;
|
||||
const castData = player.source?.castData;
|
||||
|
||||
const config = useMemo<HlsMediaConfig>(
|
||||
@@ -132,11 +140,16 @@ export const OmniView = ({
|
||||
googleCast: {
|
||||
receiver: player.castOptions?.receiverApplicationId,
|
||||
customData: castData ?? null,
|
||||
...(castId && { src: castId }),
|
||||
},
|
||||
}),
|
||||
[player.castOptions, castData],
|
||||
[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 (
|
||||
<VideoPlayer.Container
|
||||
ref={containerRef}
|
||||
@@ -166,11 +179,13 @@ export const OmniView = ({
|
||||
))}
|
||||
</Tech>
|
||||
)}
|
||||
<SubtitleOverlay
|
||||
video={ref}
|
||||
assets={subtitleAssets}
|
||||
fonts={player.source?.fonts}
|
||||
/>
|
||||
{castStatus !== "connected" && castStatus !== "connecting" && (
|
||||
<SubtitleOverlay
|
||||
video={ref}
|
||||
assets={subtitleAssets}
|
||||
fonts={player.source?.fonts}
|
||||
/>
|
||||
)}
|
||||
</VideoPlayer.Container>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user