Compare commits

...
1 Commits
Author SHA1 Message Date
zoriya a335509b4e feat(chromecast): send custom message for overlay subtitles 2026-07-21 19:28:36 +02:00
15 changed files with 366 additions and 241 deletions
+1 -1
View File
@@ -1 +1 @@
module.exports = require('./plugin/build').default; module.exports = require("./plugin/build").default;
+2 -2
View File
@@ -88,7 +88,7 @@ function PlayerExample({
useEffect(() => { useEffect(() => {
refreshTracks(); refreshTracks();
}, [refreshTracks, trackLabel]); }, [refreshTracks]);
const pushLog = useCallback((message: string) => { const pushLog = useCallback((message: string) => {
setLogs((prev) => { setLogs((prev) => {
@@ -194,7 +194,7 @@ function PlayerExample({
const cyclePlaybackRate = () => { const cyclePlaybackRate = () => {
const rates = [0.75, 1, 1.25, 1.5, 2]; const rates = [0.75, 1, 1.25, 1.5, 2];
const index = rates.findIndex((rate) => rate === playbackRate); const index = rates.indexOf(playbackRate);
const nextIndex = (index + 1) % rates.length; const nextIndex = (index + 1) % rates.length;
player.playbackRate = rates[nextIndex]; player.playbackRate = rates[nextIndex];
}; };
+1 -1
View File
@@ -9,5 +9,5 @@ import { name as appName } from "./app.json";
AppRegistry.registerComponent(appName, () => App); AppRegistry.registerComponent(appName, () => App);
AppRegistry.runApplication(appName, { AppRegistry.runApplication(appName, {
rootTag: document.getElementById("root"), rootTag: document.getElementById("root"),
}); });
+20 -16
View File
@@ -11,22 +11,26 @@ const useWeb = process.env.USE_WEB === "true" || process.env.PLATFORM === "web";
* @type {import('metro-config').MetroConfig} * @type {import('metro-config').MetroConfig}
*/ */
const config = { const config = {
watchFolders: [root], watchFolders: [root],
...(useWeb && { ...(useWeb && {
resolver: { resolver: {
extraNodeModules: { extraNodeModules: {
"react-native": path.resolve(__dirname, "node_modules", "react-native-web"), "react-native": path.resolve(
}, __dirname,
}, "node_modules",
}), "react-native-web",
transformer: { ),
getTransformOptions: async () => ({ },
transform: { },
experimentalImportSupport: false, }),
inlineRequires: true, transformer: {
}, getTransformOptions: async () => ({
}), transform: {
}, experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
}; };
module.exports = mergeConfig(getDefaultConfig(__dirname), config); module.exports = mergeConfig(getDefaultConfig(__dirname), config);
+1 -1
View File
@@ -1,4 +1,4 @@
const path = require("path"); const path = require("node:path");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const appDirectory = path.resolve(__dirname); const appDirectory = path.resolve(__dirname);
+11 -11
View File
@@ -1,13 +1,13 @@
{ {
"uiViewClassName": "OmniView", "uiViewClassName": "OmniView",
"supportsRawText": false, "supportsRawText": false,
"bubblingEventTypes": {}, "bubblingEventTypes": {},
"directEventTypes": {}, "directEventTypes": {},
"validAttributes": { "validAttributes": {
"player": true, "player": true,
"autoplay": true, "autoplay": true,
"autoPip": true, "autoPip": true,
"subtitleAssets": true, "subtitleAssets": true,
"hybridRef": true "hybridRef": true
} }
} }
+9 -9
View File
@@ -1,15 +1,15 @@
import { createRunOncePlugin } from '@expo/config-plugins'; import type { ConfigPlugin } from "@expo/config-plugins";
import type { ConfigPlugin } from '@expo/config-plugins'; import { createRunOncePlugin } from "@expo/config-plugins";
import pkg from '../../package.json'; import pkg from "../../package.json";
import { withMediaNotifications } from './withMediaNotifications'; import { withMediaNotifications } from "./withMediaNotifications";
import { withPip } from './withPip'; import { withPip } from "./withPip";
const withOmni: ConfigPlugin = (config) => { const withOmni: ConfigPlugin = (config) => {
let nextConfig = withPip(config); let nextConfig = withPip(config);
nextConfig = withMediaNotifications(nextConfig); nextConfig = withMediaNotifications(nextConfig);
return nextConfig; return nextConfig;
}; };
export default createRunOncePlugin(withOmni, pkg.name, pkg.version); export default createRunOncePlugin(withOmni, pkg.name, pkg.version);
export { withPip, withMediaNotifications }; export { withMediaNotifications, withPip };
+70 -53
View File
@@ -1,68 +1,85 @@
import { AndroidConfig, withAndroidManifest, type ConfigPlugin } from '@expo/config-plugins'; import {
AndroidConfig,
type ConfigPlugin,
withAndroidManifest,
} from "@expo/config-plugins";
function ensureUsesPermission( function ensureUsesPermission(
androidManifest: AndroidConfig.Manifest.AndroidManifest, androidManifest: AndroidConfig.Manifest.AndroidManifest,
permission: string permission: string,
): void { ): void {
const usesPermissions = androidManifest.manifest['uses-permission'] ?? []; const usesPermissions = androidManifest.manifest["uses-permission"] ?? [];
const alreadyDefined = usesPermissions.some((item) => item.$['android:name'] === permission); const alreadyDefined = usesPermissions.some(
(item) => item.$["android:name"] === permission,
);
if (!alreadyDefined) { if (!alreadyDefined) {
usesPermissions.push({ usesPermissions.push({
$: { $: {
'android:name': permission, "android:name": permission,
}, },
}); });
} }
androidManifest.manifest['uses-permission'] = usesPermissions; androidManifest.manifest["uses-permission"] = usesPermissions;
} }
function patchManifestForMediaNotifications( function patchManifestForMediaNotifications(
androidManifest: AndroidConfig.Manifest.AndroidManifest androidManifest: AndroidConfig.Manifest.AndroidManifest,
): AndroidConfig.Manifest.AndroidManifest { ): AndroidConfig.Manifest.AndroidManifest {
const mainApplication = AndroidConfig.Manifest.getMainApplication(androidManifest); const mainApplication =
if (!mainApplication) { AndroidConfig.Manifest.getMainApplication(androidManifest);
console.warn( if (!mainApplication) {
'AndroidManifest.xml is missing a <application android:name=".MainApplication" /> element - skipping Omni media service config.' console.warn(
); 'AndroidManifest.xml is missing a <application android:name=".MainApplication" /> element - skipping Omni media service config.',
return androidManifest; );
} return androidManifest;
}
const services = mainApplication.service ?? []; const services = mainApplication.service ?? [];
const serviceName = 'dev.zoriya.omni.OmniPlayerService'; const serviceName = "dev.zoriya.omni.OmniPlayerService";
const hasService = services.some((service) => service.$['android:name'] === serviceName); const hasService = services.some(
if (!hasService) { (service) => service.$["android:name"] === serviceName,
services.push({ );
$: { if (!hasService) {
'android:name': serviceName, services.push({
'android:enabled': 'true', $: {
'android:exported': 'true', "android:name": serviceName,
'android:foregroundServiceType': 'mediaPlayback', "android:enabled": "true",
}, "android:exported": "true",
'intent-filter': [ "android:foregroundServiceType": "mediaPlayback",
{ },
action: [ "intent-filter": [
{ {
$: { action: [
'android:name': 'androidx.media3.session.MediaSessionService', {
}, $: {
}, "android:name": "androidx.media3.session.MediaSessionService",
], },
}, },
], ],
}); },
} ],
mainApplication.service = services; });
}
mainApplication.service = services;
ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE'); ensureUsesPermission(
ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK'); androidManifest,
return androidManifest; "android.permission.FOREGROUND_SERVICE",
);
ensureUsesPermission(
androidManifest,
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
);
return androidManifest;
} }
export const withMediaNotifications: ConfigPlugin = (config) => { export const withMediaNotifications: ConfigPlugin = (config) => {
return withAndroidManifest(config, (manifestConfig) => { return withAndroidManifest(config, (manifestConfig) => {
manifestConfig.modResults = patchManifestForMediaNotifications(manifestConfig.modResults); manifestConfig.modResults = patchManifestForMediaNotifications(
return manifestConfig; manifestConfig.modResults,
}); );
return manifestConfig;
});
}; };
+139 -99
View File
@@ -1,129 +1,169 @@
import { AndroidConfig, withAndroidManifest, withMainActivity, type ConfigPlugin } from '@expo/config-plugins'; import {
import { addImports, appendContentsInsideDeclarationBlock } from '@expo/config-plugins/build/android/codeMod'; AndroidConfig,
type ConfigPlugin,
withAndroidManifest,
withMainActivity,
} from "@expo/config-plugins";
import {
addImports,
appendContentsInsideDeclarationBlock,
} from "@expo/config-plugins/build/android/codeMod";
const MODE_MARKER = 'OmniView.onActivityPipModeChanged('; const MODE_MARKER = "OmniView.onActivityPipModeChanged(";
const UI_STATE_MARKER = 'OmniView.onActivityPipTransitionToPip('; const UI_STATE_MARKER = "OmniView.onActivityPipTransitionToPip(";
function patchMainActivityForPip(mainActivity: string, language: 'java' | 'kt'): string { function patchMainActivityForPip(
const isJava = language === 'java'; mainActivity: string,
language: "java" | "kt",
): string {
const isJava = language === "java";
if (mainActivity.includes(MODE_MARKER) && mainActivity.includes(UI_STATE_MARKER)) { if (
return mainActivity; mainActivity.includes(MODE_MARKER) &&
} mainActivity.includes(UI_STATE_MARKER)
) {
return mainActivity;
}
const withRequiredImports = addImports( const withRequiredImports = addImports(
mainActivity, mainActivity,
['android.app.PictureInPictureUiState', 'android.os.Build', 'dev.zoriya.omni.OmniView'], [
isJava "android.app.PictureInPictureUiState",
); "android.os.Build",
"dev.zoriya.omni.OmniView",
],
isJava,
);
let output = withRequiredImports; let output = withRequiredImports;
if (!output.includes(MODE_MARKER)) { if (!output.includes(MODE_MARKER)) {
const modeChangedBlock = isJava const modeChangedBlock = isJava
? [ ? [
'\n @Override', "\n @Override",
' public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {', " public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {",
' super.onPictureInPictureModeChanged(isInPictureInPictureMode);', " super.onPictureInPictureModeChanged(isInPictureInPictureMode);",
' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode);', " OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode);",
' }\n', " }\n",
] ]
: [ : [
'\n override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {', "\n override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {",
' super.onPictureInPictureModeChanged(isInPictureInPictureMode)', " super.onPictureInPictureModeChanged(isInPictureInPictureMode)",
' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode)', " OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode)",
' }\n', " }\n",
]; ];
output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', modeChangedBlock.join('\n')); output = appendContentsInsideDeclarationBlock(
} output,
"class MainActivity",
modeChangedBlock.join("\n"),
);
}
if (!output.includes(UI_STATE_MARKER)) { if (!output.includes(UI_STATE_MARKER)) {
const uiStateChangedBlock = isJava const uiStateChangedBlock = isJava
? [ ? [
'\n @Override', "\n @Override",
' public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {', " public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {",
' super.onPictureInPictureUiStateChanged(pipState);', " super.onPictureInPictureUiStateChanged(pipState);",
' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip()) {', " if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip()) {",
' OmniView.onActivityPipTransitionToPip(this);', " OmniView.onActivityPipTransitionToPip(this);",
' }', " }",
' }\n', " }\n",
] ]
: [ : [
'\n override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {', "\n override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {",
' super.onPictureInPictureUiStateChanged(pipState)', " super.onPictureInPictureUiStateChanged(pipState)",
' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip) {', " if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip) {",
' OmniView.onActivityPipTransitionToPip(this)', " OmniView.onActivityPipTransitionToPip(this)",
' }', " }",
' }\n', " }\n",
]; ];
output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', uiStateChangedBlock.join('\n')); output = appendContentsInsideDeclarationBlock(
} output,
"class MainActivity",
uiStateChangedBlock.join("\n"),
);
}
return output; return output;
} }
function ensureUsesFeature( function ensureUsesFeature(
androidManifest: AndroidConfig.Manifest.AndroidManifest, androidManifest: AndroidConfig.Manifest.AndroidManifest,
featureName: string, featureName: string,
required: 'true' | 'false' required: "true" | "false",
): void { ): void {
const usesFeature = androidManifest.manifest['uses-feature'] ?? []; const usesFeature = androidManifest.manifest["uses-feature"] ?? [];
const existing = usesFeature.find((feature) => feature.$['android:name'] === featureName); const existing = usesFeature.find(
(feature) => feature.$["android:name"] === featureName,
);
if (existing) { if (existing) {
existing.$['android:required'] = required; existing.$["android:required"] = required;
} else { } else {
usesFeature.push({ usesFeature.push({
$: { $: {
'android:name': featureName, "android:name": featureName,
'android:required': required, "android:required": required,
}, },
}); });
} }
androidManifest.manifest['uses-feature'] = usesFeature; androidManifest.manifest["uses-feature"] = usesFeature;
} }
function ensureConfigChanges(mainActivity: AndroidConfig.Manifest.ManifestActivity): void { function ensureConfigChanges(
const requiredChanges = ['screenSize', 'smallestScreenSize', 'screenLayout', 'orientation']; mainActivity: AndroidConfig.Manifest.ManifestActivity,
const existing = mainActivity.$['android:configChanges'] ?? ''; ): void {
const existingSet = new Set( const requiredChanges = [
existing "screenSize",
.split('|') "smallestScreenSize",
.map((item) => item.trim()) "screenLayout",
.filter(Boolean) "orientation",
); ];
const existing = mainActivity.$["android:configChanges"] ?? "";
const existingSet = new Set(
existing
.split("|")
.map((item) => item.trim())
.filter(Boolean),
);
requiredChanges.forEach((item) => existingSet.add(item)); requiredChanges.forEach((item) => existingSet.add(item));
mainActivity.$['android:configChanges'] = Array.from(existingSet).join('|'); mainActivity.$["android:configChanges"] = Array.from(existingSet).join("|");
} }
function patchManifestForPip( function patchManifestForPip(
androidManifest: AndroidConfig.Manifest.AndroidManifest androidManifest: AndroidConfig.Manifest.AndroidManifest,
): AndroidConfig.Manifest.AndroidManifest { ): AndroidConfig.Manifest.AndroidManifest {
ensureUsesFeature(androidManifest, 'android.software.picture_in_picture', 'false'); ensureUsesFeature(
androidManifest,
"android.software.picture_in_picture",
"false",
);
const mainActivity = AndroidConfig.Manifest.getMainActivityOrThrow(androidManifest); const mainActivity =
mainActivity.$['android:supportsPictureInPicture'] = 'true'; AndroidConfig.Manifest.getMainActivityOrThrow(androidManifest);
mainActivity.$['android:resizeableActivity'] = 'true'; mainActivity.$["android:supportsPictureInPicture"] = "true";
ensureConfigChanges(mainActivity); mainActivity.$["android:resizeableActivity"] = "true";
ensureConfigChanges(mainActivity);
return androidManifest; return androidManifest;
} }
export const withPip: ConfigPlugin = (config) => { export const withPip: ConfigPlugin = (config) => {
const withMainActivityPatched = withMainActivity(config, (activityConfig) => { const withMainActivityPatched = withMainActivity(config, (activityConfig) => {
activityConfig.modResults.contents = patchMainActivityForPip( activityConfig.modResults.contents = patchMainActivityForPip(
activityConfig.modResults.contents, activityConfig.modResults.contents,
activityConfig.modResults.language activityConfig.modResults.language,
); );
return activityConfig; return activityConfig;
}); });
return withAndroidManifest(withMainActivityPatched, (manifestConfig) => { return withAndroidManifest(withMainActivityPatched, (manifestConfig) => {
manifestConfig.modResults = patchManifestForPip(manifestConfig.modResults); manifestConfig.modResults = patchManifestForPip(manifestConfig.modResults);
return manifestConfig; return manifestConfig;
}); });
}; };
+12 -12
View File
@@ -1,14 +1,14 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2019", "target": "ES2019",
"module": "CommonJS", "module": "CommonJS",
"declaration": true, "declaration": true,
"outDir": "build", "outDir": "build",
"rootDir": "src", "rootDir": "src",
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": ["src/**/*"] "include": ["src/**/*"]
} }
+8 -2
View File
@@ -20,7 +20,10 @@ const updateViewManagerFiles = async (file) => {
const viewManagerStr = await readFile(viewManagerFile, { encoding: "utf8" }); const viewManagerStr = await readFile(viewManagerFile, { encoding: "utf8" });
await writeFile( await writeFile(
viewManagerFile, viewManagerFile,
viewManagerStr.replace(/com\.margelo\.nitro\.omni\.\*/g, "dev.zoriya.omni.*"), viewManagerStr.replace(
/com\.margelo\.nitro\.omni\.\*/g,
"dev.zoriya.omni.*",
),
); );
}; };
@@ -49,6 +52,9 @@ const androidWorkaround = async () => {
} }
const str = await readFile(androidOnLoadFile, { encoding: "utf8" }); const str = await readFile(androidOnLoadFile, { encoding: "utf8" });
await writeFile(androidOnLoadFile, str.replace(/com\/margelo\/nitro\//g, "dev/zoriya/")); await writeFile(
androidOnLoadFile,
str.replace(/com\/margelo\/nitro\//g, "dev/zoriya/"),
);
}; };
androidWorkaround(); androidWorkaround();
+22 -1
View File
@@ -126,6 +126,27 @@ export const useEvent = <Event extends keyof OmniEvents>(
} }
return undefined; return undefined;
}, [event, player]); }, [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>( function createMapper<Key extends keyof OmniPlayerState, State, Result>(
@@ -182,7 +203,7 @@ export const stateMapper = {
if (!r) return "unavailable"; if (!r) return "unavailable";
if (r.remotePlaybackState === "connecting") return "connecting"; if (r.remotePlaybackState === "connecting") return "connecting";
if (r.remotePlaybackState === "connected") return "connected"; if (r.remotePlaybackState === "connected") return "connected";
return r.remotePlaybackAvailability return r.remotePlaybackAvailability;
}), }),
}; };
+20
View File
@@ -242,6 +242,26 @@ export class WebOmniPlayer implements OmniPlayer {
const tracks = selectTextTrack(this._store.state); const tracks = selectTextTrack(this._store.state);
tracks?.selectSubtitlesTrack(overlay || !subtitle ? "off" : subtitle.id); tracks?.selectSubtitlesTrack(overlay || !subtitle ? "off" : subtitle.id);
this.setOverlaySubtitle(overlay ?? null); 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 { private setOverlaySubtitle(sub: Subtitle | null): void {
+2
View File
@@ -7,6 +7,8 @@ export interface Source {
fonts?: string[]; fonts?: string[];
metadata?: Metadata; metadata?: Metadata;
mixAudio?: MixAudioMode; 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 // Opaque, consumer-defined payload forwarded verbatim to the cast receiver
// as the load request's customData. omni does not interpret it. // as the load request's customData. omni does not interpret it.
castData?: Record<string, string>; castData?: Record<string, string>;
+48 -33
View File
@@ -9,6 +9,7 @@ import {
useRef, useRef,
useSyncExternalStore, useSyncExternalStore,
} from "react"; } from "react";
import { usePlayerState } from "./events";
import { import {
getSubtitleFormat, getSubtitleFormat,
isCustomSubtitle, isCustomSubtitle,
@@ -58,35 +59,41 @@ const SubtitleOverlay = ({
const renditions = player.rendition; const renditions = player.rendition;
const videoWidth = Math.max(0, ...renditions.map((r) => r.width)); const videoWidth = Math.max(0, ...renditions.map((r) => r.width));
const videoHeight = Math.max(0, ...renditions.map((r) => r.height)); const videoHeight = Math.max(0, ...renditions.map((r) => r.height));
import("jassub").then(({ default: JASSUB }) => { import("jassub")
const instance = new JASSUB({ .then(({ default: JASSUB }) => {
video: el, const instance = new JASSUB({
subUrl: subtitle.link, video: el,
...(videoWidth && { videoWidth }), subUrl: subtitle.link,
...(videoHeight && { videoHeight }), ...(videoWidth && { videoWidth }),
...(fonts?.length && { fonts }), ...(videoHeight && { videoHeight }),
...(jassub?.workerUrl && { workerUrl: jassub.workerUrl }), ...(fonts?.length && { fonts }),
...(jassub?.wasmUrl && { wasmUrl: jassub.wasmUrl }), ...(jassub?.workerUrl && { workerUrl: jassub.workerUrl }),
...(jassub?.modernWasmUrl && { modernWasmUrl: jassub.modernWasmUrl }), ...(jassub?.wasmUrl && { wasmUrl: jassub.wasmUrl }),
...(jassub?.fontUrl && { ...(jassub?.modernWasmUrl && {
availableFonts: { "liberation sans": jassub.fontUrl }, modernWasmUrl: jassub.modernWasmUrl,
defaultFont: "liberation sans", }),
}), ...(jassub?.fontUrl && {
}); availableFonts: { "liberation sans": jassub.fontUrl },
attach({ destroy: () => instance.destroy() }); defaultFont: "liberation sans",
}); }),
});
attach({ destroy: () => instance.destroy() });
})
.catch((e) => console.error("[omni] failed to render ass subtitle", e));
} else { } else {
const pgs = assetsRef.current?.pgs; const pgs = assetsRef.current?.pgs;
import("libpgs").then(({ PgsRenderer }) => { import("libpgs")
const instance = new PgsRenderer({ .then(({ PgsRenderer }) => {
video: el, const instance = new PgsRenderer({
subUrl: subtitle.link, video: el,
workerUrl: subUrl: subtitle.link,
pgs?.workerUrl ?? workerUrl:
new URL("libpgs/dist/libpgs.worker.js", import.meta.url).href, pgs?.workerUrl ??
}); new URL("libpgs/dist/libpgs.worker.js", import.meta.url).href,
attach({ destroy: () => instance.dispose() }); });
}); attach({ destroy: () => instance.dispose() });
})
.catch((e) => console.error("[omni] failed to render pgs subtitle", e));
} }
return () => { return () => {
@@ -116,6 +123,7 @@ export const OmniView = ({
const headersRef = useRef(src?.headers); const headersRef = useRef(src?.headers);
headersRef.current = src?.headers; headersRef.current = src?.headers;
const castId = player.source?.castId;
const castData = player.source?.castData; const castData = player.source?.castData;
const config = useMemo<HlsMediaConfig>( const config = useMemo<HlsMediaConfig>(
@@ -132,11 +140,16 @@ export const OmniView = ({
googleCast: { googleCast: {
receiver: player.castOptions?.receiverApplicationId, receiver: player.castOptions?.receiverApplicationId,
customData: castData ?? null, 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 ( return (
<VideoPlayer.Container <VideoPlayer.Container
ref={containerRef} ref={containerRef}
@@ -166,11 +179,13 @@ export const OmniView = ({
))} ))}
</Tech> </Tech>
)} )}
<SubtitleOverlay {castStatus !== "connected" && castStatus !== "connecting" && (
video={ref} <SubtitleOverlay
assets={subtitleAssets} video={ref}
fonts={player.source?.fonts} assets={subtitleAssets}
/> fonts={player.source?.fonts}
/>
)}
</VideoPlayer.Container> </VideoPlayer.Container>
); );
}; };