diff --git a/app.plugin.js b/app.plugin.js
index 040f76d..0706fd9 100644
--- a/app.plugin.js
+++ b/app.plugin.js
@@ -1 +1 @@
-module.exports = require('./plugin/build').default;
+module.exports = require("./plugin/build").default;
diff --git a/example/App.tsx b/example/App.tsx
index 4fcf70c..eca4ea8 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -88,7 +88,7 @@ function PlayerExample({
useEffect(() => {
refreshTracks();
- }, [refreshTracks, trackLabel]);
+ }, [refreshTracks]);
const pushLog = useCallback((message: string) => {
setLogs((prev) => {
@@ -194,7 +194,7 @@ function PlayerExample({
const cyclePlaybackRate = () => {
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;
player.playbackRate = rates[nextIndex];
};
diff --git a/example/index.web.js b/example/index.web.js
index c50f079..e7c3ae1 100644
--- a/example/index.web.js
+++ b/example/index.web.js
@@ -9,5 +9,5 @@ import { name as appName } from "./app.json";
AppRegistry.registerComponent(appName, () => App);
AppRegistry.runApplication(appName, {
- rootTag: document.getElementById("root"),
+ rootTag: document.getElementById("root"),
});
diff --git a/example/metro.config.js b/example/metro.config.js
index 679db6c..18d9250 100644
--- a/example/metro.config.js
+++ b/example/metro.config.js
@@ -11,22 +11,26 @@ const useWeb = process.env.USE_WEB === "true" || process.env.PLATFORM === "web";
* @type {import('metro-config').MetroConfig}
*/
const config = {
- watchFolders: [root],
- ...(useWeb && {
- resolver: {
- extraNodeModules: {
- "react-native": path.resolve(__dirname, "node_modules", "react-native-web"),
- },
- },
- }),
- transformer: {
- getTransformOptions: async () => ({
- transform: {
- experimentalImportSupport: false,
- inlineRequires: true,
- },
- }),
- },
+ watchFolders: [root],
+ ...(useWeb && {
+ resolver: {
+ extraNodeModules: {
+ "react-native": path.resolve(
+ __dirname,
+ "node_modules",
+ "react-native-web",
+ ),
+ },
+ },
+ }),
+ transformer: {
+ getTransformOptions: async () => ({
+ transform: {
+ experimentalImportSupport: false,
+ inlineRequires: true,
+ },
+ }),
+ },
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
diff --git a/example/webpack.config.js b/example/webpack.config.js
index 1202b79..de5f60d 100644
--- a/example/webpack.config.js
+++ b/example/webpack.config.js
@@ -1,4 +1,4 @@
-const path = require("path");
+const path = require("node:path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const appDirectory = path.resolve(__dirname);
diff --git a/nitrogen/generated/shared/json/OmniViewConfig.json b/nitrogen/generated/shared/json/OmniViewConfig.json
index fac84a7..c2ad2f1 100644
--- a/nitrogen/generated/shared/json/OmniViewConfig.json
+++ b/nitrogen/generated/shared/json/OmniViewConfig.json
@@ -1,13 +1,13 @@
{
- "uiViewClassName": "OmniView",
- "supportsRawText": false,
- "bubblingEventTypes": {},
- "directEventTypes": {},
- "validAttributes": {
- "player": true,
- "autoplay": true,
- "autoPip": true,
- "subtitleAssets": true,
- "hybridRef": true
- }
+ "uiViewClassName": "OmniView",
+ "supportsRawText": false,
+ "bubblingEventTypes": {},
+ "directEventTypes": {},
+ "validAttributes": {
+ "player": true,
+ "autoplay": true,
+ "autoPip": true,
+ "subtitleAssets": true,
+ "hybridRef": true
+ }
}
diff --git a/plugin/src/index.ts b/plugin/src/index.ts
index 2ee7086..9b42643 100644
--- a/plugin/src/index.ts
+++ b/plugin/src/index.ts
@@ -1,15 +1,15 @@
-import { createRunOncePlugin } from '@expo/config-plugins';
-import type { ConfigPlugin } from '@expo/config-plugins';
-import pkg from '../../package.json';
-import { withMediaNotifications } from './withMediaNotifications';
-import { withPip } from './withPip';
+import type { ConfigPlugin } from "@expo/config-plugins";
+import { createRunOncePlugin } from "@expo/config-plugins";
+import pkg from "../../package.json";
+import { withMediaNotifications } from "./withMediaNotifications";
+import { withPip } from "./withPip";
const withOmni: ConfigPlugin = (config) => {
- let nextConfig = withPip(config);
- nextConfig = withMediaNotifications(nextConfig);
- return nextConfig;
+ let nextConfig = withPip(config);
+ nextConfig = withMediaNotifications(nextConfig);
+ return nextConfig;
};
export default createRunOncePlugin(withOmni, pkg.name, pkg.version);
-export { withPip, withMediaNotifications };
+export { withMediaNotifications, withPip };
diff --git a/plugin/src/withMediaNotifications.ts b/plugin/src/withMediaNotifications.ts
index f937ea3..19b4169 100644
--- a/plugin/src/withMediaNotifications.ts
+++ b/plugin/src/withMediaNotifications.ts
@@ -1,68 +1,85 @@
-import { AndroidConfig, withAndroidManifest, type ConfigPlugin } from '@expo/config-plugins';
+import {
+ AndroidConfig,
+ type ConfigPlugin,
+ withAndroidManifest,
+} from "@expo/config-plugins";
function ensureUsesPermission(
- androidManifest: AndroidConfig.Manifest.AndroidManifest,
- permission: string
+ androidManifest: AndroidConfig.Manifest.AndroidManifest,
+ permission: string,
): void {
- const usesPermissions = androidManifest.manifest['uses-permission'] ?? [];
- const alreadyDefined = usesPermissions.some((item) => item.$['android:name'] === permission);
+ const usesPermissions = androidManifest.manifest["uses-permission"] ?? [];
+ const alreadyDefined = usesPermissions.some(
+ (item) => item.$["android:name"] === permission,
+ );
- if (!alreadyDefined) {
- usesPermissions.push({
- $: {
- 'android:name': permission,
- },
- });
- }
+ if (!alreadyDefined) {
+ usesPermissions.push({
+ $: {
+ "android:name": permission,
+ },
+ });
+ }
- androidManifest.manifest['uses-permission'] = usesPermissions;
+ androidManifest.manifest["uses-permission"] = usesPermissions;
}
function patchManifestForMediaNotifications(
- androidManifest: AndroidConfig.Manifest.AndroidManifest
+ androidManifest: AndroidConfig.Manifest.AndroidManifest,
): AndroidConfig.Manifest.AndroidManifest {
- const mainApplication = AndroidConfig.Manifest.getMainApplication(androidManifest);
- if (!mainApplication) {
- console.warn(
- 'AndroidManifest.xml is missing a element - skipping Omni media service config.'
- );
- return androidManifest;
- }
+ const mainApplication =
+ AndroidConfig.Manifest.getMainApplication(androidManifest);
+ if (!mainApplication) {
+ console.warn(
+ 'AndroidManifest.xml is missing a element - skipping Omni media service config.',
+ );
+ return androidManifest;
+ }
- const services = mainApplication.service ?? [];
- const serviceName = 'dev.zoriya.omni.OmniPlayerService';
- const hasService = services.some((service) => service.$['android:name'] === serviceName);
- if (!hasService) {
- services.push({
- $: {
- 'android:name': serviceName,
- 'android:enabled': 'true',
- 'android:exported': 'true',
- 'android:foregroundServiceType': 'mediaPlayback',
- },
- 'intent-filter': [
- {
- action: [
- {
- $: {
- 'android:name': 'androidx.media3.session.MediaSessionService',
- },
- },
- ],
- },
- ],
- });
- }
- mainApplication.service = services;
+ const services = mainApplication.service ?? [];
+ const serviceName = "dev.zoriya.omni.OmniPlayerService";
+ const hasService = services.some(
+ (service) => service.$["android:name"] === serviceName,
+ );
+ if (!hasService) {
+ services.push({
+ $: {
+ "android:name": serviceName,
+ "android:enabled": "true",
+ "android:exported": "true",
+ "android:foregroundServiceType": "mediaPlayback",
+ },
+ "intent-filter": [
+ {
+ action: [
+ {
+ $: {
+ "android:name": "androidx.media3.session.MediaSessionService",
+ },
+ },
+ ],
+ },
+ ],
+ });
+ }
+ mainApplication.service = services;
- ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE');
- ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK');
- return androidManifest;
+ ensureUsesPermission(
+ androidManifest,
+ "android.permission.FOREGROUND_SERVICE",
+ );
+ ensureUsesPermission(
+ androidManifest,
+ "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
+ );
+ return androidManifest;
}
export const withMediaNotifications: ConfigPlugin = (config) => {
- return withAndroidManifest(config, (manifestConfig) => {
- manifestConfig.modResults = patchManifestForMediaNotifications(manifestConfig.modResults);
- return manifestConfig;
- });
+ return withAndroidManifest(config, (manifestConfig) => {
+ manifestConfig.modResults = patchManifestForMediaNotifications(
+ manifestConfig.modResults,
+ );
+ return manifestConfig;
+ });
};
diff --git a/plugin/src/withPip.ts b/plugin/src/withPip.ts
index af5e0a1..d148284 100644
--- a/plugin/src/withPip.ts
+++ b/plugin/src/withPip.ts
@@ -1,129 +1,169 @@
-import { AndroidConfig, withAndroidManifest, withMainActivity, type ConfigPlugin } from '@expo/config-plugins';
-import { addImports, appendContentsInsideDeclarationBlock } from '@expo/config-plugins/build/android/codeMod';
+import {
+ 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 UI_STATE_MARKER = 'OmniView.onActivityPipTransitionToPip(';
+const MODE_MARKER = "OmniView.onActivityPipModeChanged(";
+const UI_STATE_MARKER = "OmniView.onActivityPipTransitionToPip(";
-function patchMainActivityForPip(mainActivity: string, language: 'java' | 'kt'): string {
- const isJava = language === 'java';
+function patchMainActivityForPip(
+ mainActivity: string,
+ language: "java" | "kt",
+): string {
+ const isJava = language === "java";
- if (mainActivity.includes(MODE_MARKER) && mainActivity.includes(UI_STATE_MARKER)) {
- return mainActivity;
- }
+ if (
+ mainActivity.includes(MODE_MARKER) &&
+ mainActivity.includes(UI_STATE_MARKER)
+ ) {
+ return mainActivity;
+ }
- const withRequiredImports = addImports(
- mainActivity,
- ['android.app.PictureInPictureUiState', 'android.os.Build', 'dev.zoriya.omni.OmniView'],
- isJava
- );
+ const withRequiredImports = addImports(
+ mainActivity,
+ [
+ "android.app.PictureInPictureUiState",
+ "android.os.Build",
+ "dev.zoriya.omni.OmniView",
+ ],
+ isJava,
+ );
- let output = withRequiredImports;
+ let output = withRequiredImports;
- if (!output.includes(MODE_MARKER)) {
- const modeChangedBlock = isJava
- ? [
- '\n @Override',
- ' public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {',
- ' super.onPictureInPictureModeChanged(isInPictureInPictureMode);',
- ' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode);',
- ' }\n',
- ]
- : [
- '\n override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {',
- ' super.onPictureInPictureModeChanged(isInPictureInPictureMode)',
- ' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode)',
- ' }\n',
- ];
+ if (!output.includes(MODE_MARKER)) {
+ const modeChangedBlock = isJava
+ ? [
+ "\n @Override",
+ " public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {",
+ " super.onPictureInPictureModeChanged(isInPictureInPictureMode);",
+ " OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode);",
+ " }\n",
+ ]
+ : [
+ "\n override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {",
+ " super.onPictureInPictureModeChanged(isInPictureInPictureMode)",
+ " OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode)",
+ " }\n",
+ ];
- output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', modeChangedBlock.join('\n'));
- }
+ output = appendContentsInsideDeclarationBlock(
+ output,
+ "class MainActivity",
+ modeChangedBlock.join("\n"),
+ );
+ }
- if (!output.includes(UI_STATE_MARKER)) {
- const uiStateChangedBlock = isJava
- ? [
- '\n @Override',
- ' public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {',
- ' super.onPictureInPictureUiStateChanged(pipState);',
- ' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip()) {',
- ' OmniView.onActivityPipTransitionToPip(this);',
- ' }',
- ' }\n',
- ]
- : [
- '\n override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {',
- ' super.onPictureInPictureUiStateChanged(pipState)',
- ' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip) {',
- ' OmniView.onActivityPipTransitionToPip(this)',
- ' }',
- ' }\n',
- ];
+ if (!output.includes(UI_STATE_MARKER)) {
+ const uiStateChangedBlock = isJava
+ ? [
+ "\n @Override",
+ " public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {",
+ " super.onPictureInPictureUiStateChanged(pipState);",
+ " if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip()) {",
+ " OmniView.onActivityPipTransitionToPip(this);",
+ " }",
+ " }\n",
+ ]
+ : [
+ "\n override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {",
+ " super.onPictureInPictureUiStateChanged(pipState)",
+ " if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip) {",
+ " OmniView.onActivityPipTransitionToPip(this)",
+ " }",
+ " }\n",
+ ];
- output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', uiStateChangedBlock.join('\n'));
- }
+ output = appendContentsInsideDeclarationBlock(
+ output,
+ "class MainActivity",
+ uiStateChangedBlock.join("\n"),
+ );
+ }
- return output;
+ return output;
}
function ensureUsesFeature(
- androidManifest: AndroidConfig.Manifest.AndroidManifest,
- featureName: string,
- required: 'true' | 'false'
+ androidManifest: AndroidConfig.Manifest.AndroidManifest,
+ featureName: string,
+ required: "true" | "false",
): void {
- const usesFeature = androidManifest.manifest['uses-feature'] ?? [];
- const existing = usesFeature.find((feature) => feature.$['android:name'] === featureName);
+ const usesFeature = androidManifest.manifest["uses-feature"] ?? [];
+ const existing = usesFeature.find(
+ (feature) => feature.$["android:name"] === featureName,
+ );
- if (existing) {
- existing.$['android:required'] = required;
- } else {
- usesFeature.push({
- $: {
- 'android:name': featureName,
- 'android:required': required,
- },
- });
- }
+ if (existing) {
+ existing.$["android:required"] = required;
+ } else {
+ usesFeature.push({
+ $: {
+ "android:name": featureName,
+ "android:required": required,
+ },
+ });
+ }
- androidManifest.manifest['uses-feature'] = usesFeature;
+ androidManifest.manifest["uses-feature"] = usesFeature;
}
-function ensureConfigChanges(mainActivity: AndroidConfig.Manifest.ManifestActivity): void {
- const requiredChanges = ['screenSize', 'smallestScreenSize', 'screenLayout', 'orientation'];
- const existing = mainActivity.$['android:configChanges'] ?? '';
- const existingSet = new Set(
- existing
- .split('|')
- .map((item) => item.trim())
- .filter(Boolean)
- );
+function ensureConfigChanges(
+ mainActivity: AndroidConfig.Manifest.ManifestActivity,
+): void {
+ const requiredChanges = [
+ "screenSize",
+ "smallestScreenSize",
+ "screenLayout",
+ "orientation",
+ ];
+ const existing = mainActivity.$["android:configChanges"] ?? "";
+ const existingSet = new Set(
+ existing
+ .split("|")
+ .map((item) => item.trim())
+ .filter(Boolean),
+ );
- requiredChanges.forEach((item) => existingSet.add(item));
- mainActivity.$['android:configChanges'] = Array.from(existingSet).join('|');
+ requiredChanges.forEach((item) => existingSet.add(item));
+ mainActivity.$["android:configChanges"] = Array.from(existingSet).join("|");
}
function patchManifestForPip(
- androidManifest: AndroidConfig.Manifest.AndroidManifest
+ 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);
- mainActivity.$['android:supportsPictureInPicture'] = 'true';
- mainActivity.$['android:resizeableActivity'] = 'true';
- ensureConfigChanges(mainActivity);
+ const mainActivity =
+ AndroidConfig.Manifest.getMainActivityOrThrow(androidManifest);
+ mainActivity.$["android:supportsPictureInPicture"] = "true";
+ mainActivity.$["android:resizeableActivity"] = "true";
+ ensureConfigChanges(mainActivity);
- return androidManifest;
+ return androidManifest;
}
export const withPip: ConfigPlugin = (config) => {
- const withMainActivityPatched = withMainActivity(config, (activityConfig) => {
- activityConfig.modResults.contents = patchMainActivityForPip(
- activityConfig.modResults.contents,
- activityConfig.modResults.language
- );
- return activityConfig;
- });
+ const withMainActivityPatched = withMainActivity(config, (activityConfig) => {
+ activityConfig.modResults.contents = patchMainActivityForPip(
+ activityConfig.modResults.contents,
+ activityConfig.modResults.language,
+ );
+ return activityConfig;
+ });
- return withAndroidManifest(withMainActivityPatched, (manifestConfig) => {
- manifestConfig.modResults = patchManifestForPip(manifestConfig.modResults);
- return manifestConfig;
- });
+ return withAndroidManifest(withMainActivityPatched, (manifestConfig) => {
+ manifestConfig.modResults = patchManifestForPip(manifestConfig.modResults);
+ return manifestConfig;
+ });
};
diff --git a/plugin/tsconfig.json b/plugin/tsconfig.json
index 4cfffe2..c8ce4a5 100644
--- a/plugin/tsconfig.json
+++ b/plugin/tsconfig.json
@@ -1,14 +1,14 @@
{
- "compilerOptions": {
- "target": "ES2019",
- "module": "CommonJS",
- "declaration": true,
- "outDir": "build",
- "rootDir": "src",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "resolveJsonModule": true
- },
- "include": ["src/**/*"]
+ "compilerOptions": {
+ "target": "ES2019",
+ "module": "CommonJS",
+ "declaration": true,
+ "outDir": "build",
+ "rootDir": "src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true
+ },
+ "include": ["src/**/*"]
}
diff --git a/post-script.js b/post-script.js
index 87c1870..6c37cd7 100644
--- a/post-script.js
+++ b/post-script.js
@@ -20,7 +20,10 @@ const updateViewManagerFiles = async (file) => {
const viewManagerStr = await readFile(viewManagerFile, { encoding: "utf8" });
await writeFile(
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" });
- await writeFile(androidOnLoadFile, str.replace(/com\/margelo\/nitro\//g, "dev/zoriya/"));
+ await writeFile(
+ androidOnLoadFile,
+ str.replace(/com\/margelo\/nitro\//g, "dev/zoriya/"),
+ );
};
androidWorkaround();
diff --git a/src/events.web.tsx b/src/events.web.tsx
index e91bde7..18f4780 100644
--- a/src/events.web.tsx
+++ b/src/events.web.tsx
@@ -126,6 +126,27 @@ export const useEvent = (
}
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(
@@ -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;
}),
};
diff --git a/src/player.web.tsx b/src/player.web.tsx
index cf76e56..f2fc0f3 100644
--- a/src/player.web.tsx
+++ b/src/player.web.tsx
@@ -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 {
diff --git a/src/types/source.ts b/src/types/source.ts
index b291e19..da476d5 100644
--- a/src/types/source.ts
+++ b/src/types/source.ts
@@ -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;
diff --git a/src/view.web.tsx b/src/view.web.tsx
index b9ca345..e47a365 100644
--- a/src/view.web.tsx
+++ b/src/view.web.tsx
@@ -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(
@@ -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