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