Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ The WebView configuration is set to:
- `androidScheme: https` — ensures WebRTC and cookies work correctly on Android
- `contentInset: always` — iOS safe area respected so game UI is not obscured by notch

Camera and microphone permission requests are handled natively by each platform. See platform READMEs in `android/` and `ios/` once those PRs land.
Camera and microphone permission requests are handled natively by each platform. iframe allow policies continue to pass through from the web app (`allowPolicy` / `allow` attributes), so existing WA scripting API and embedded-site behavior stays in the server-rendered app.

Known mobile limitation:
- Screen sharing stays browser-only. Android WebView and WKWebView do not provide a reliable `getDisplayMedia` path for the Universe shell, so the front-end hides the screen-share control when running inside the Capacitor app instead of presenting a broken prompt.

See platform READMEs in `android/` and `ios/` once those PRs land.

## Fastlane

Expand Down
5 changes: 5 additions & 0 deletions play/src/front/Stores/ScreenSharingStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Readable } from "svelte/store";
import { get, derived, readable, writable } from "svelte/store";
import type { DesktopCapturerSource } from "../Interfaces/DesktopAppInterfaces";
import { isNativeMobileApp } from "../WebRtc/DeviceUtils";
import { localUserStore } from "../Connection/LocalUserStore";
import LL from "../../i18n/i18n-svelte";
import { isSpeakerStore, type LocalStreamStoreValue } from "./MediaStore";
Expand Down Expand Up @@ -140,6 +141,10 @@ export const screenSharingConstraintsStore = derived(
);

export function isScreenSharingSupported(): boolean {
if (isNativeMobileApp()) {
return false;
}

if (window.WAD?.getDesktopCapturerSources) {
return true;
}
Expand Down
81 changes: 81 additions & 0 deletions play/src/front/WebRtc/DeviceUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { isScreenSharingSupported } from "../Stores/ScreenSharingStore";
import { getNativeAppPlatform, isNativeMobileApp } from "./DeviceUtils";

type TestCapacitor = {
getPlatform?: () => "android" | "ios" | "web";
isNativePlatform?: () => boolean;
};

const originalCapacitor = (window as typeof window & { Capacitor?: TestCapacitor }).Capacitor;
const originalMediaDevices = navigator.mediaDevices;

function setCapacitor(capacitor?: TestCapacitor) {
Object.defineProperty(window, "Capacitor", {
configurable: true,
value: capacitor,
});
}

function setMediaDevices(mediaDevices: MediaDevices | undefined) {
Object.defineProperty(window.navigator, "mediaDevices", {
configurable: true,
value: mediaDevices,
});
}

afterEach(() => {
setCapacitor(originalCapacitor);
setMediaDevices(originalMediaDevices);
vi.restoreAllMocks();
});

describe("DeviceUtils", () => {
it("detects Capacitor native mobile platforms", () => {
setCapacitor({
getPlatform: () => "android",
isNativePlatform: () => true,
});

expect(getNativeAppPlatform()).toBe("android");
expect(isNativeMobileApp()).toBe(true);
});

it("treats web or missing Capacitor bridge as non-native", () => {
setCapacitor({
getPlatform: () => "web",
isNativePlatform: () => false,
});

expect(getNativeAppPlatform()).toBeUndefined();
expect(isNativeMobileApp()).toBe(false);

setCapacitor(undefined);

expect(getNativeAppPlatform()).toBeUndefined();
expect(isNativeMobileApp()).toBe(false);
});
});

describe("isScreenSharingSupported", () => {
it("hides screen sharing inside native Capacitor apps", () => {
setCapacitor({
getPlatform: () => "ios",
isNativePlatform: () => true,
});
setMediaDevices({
getDisplayMedia: vi.fn(),
} as unknown as MediaDevices);

expect(isScreenSharingSupported()).toBe(false);
});

it("keeps screen sharing available in regular browsers with getDisplayMedia", () => {
setCapacitor(undefined);
setMediaDevices({
getDisplayMedia: vi.fn(),
} as unknown as MediaDevices);

expect(isScreenSharingSupported()).toBe(true);
});
});
35 changes: 35 additions & 0 deletions play/src/front/WebRtc/DeviceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,41 @@ export function isAndroid(): boolean {
return window.navigator.userAgent.includes("Android");
}

type CapacitorPlatform = "android" | "ios" | "web";

type CapacitorGlobal = {
Capacitor?: {
getPlatform?: () => CapacitorPlatform;
isNativePlatform?: () => boolean;
};
};

function getCapacitorBridge() {
return (window as typeof window & CapacitorGlobal).Capacitor;
}

export function getNativeAppPlatform(): Exclude<CapacitorPlatform, "web"> | undefined {
const capacitor = getCapacitorBridge();
const platform = capacitor?.getPlatform?.();

if (!platform || platform === "web") {
return undefined;
}

return platform;
}

export function isNativeMobileApp(): boolean {
const capacitor = getCapacitorBridge();
const nativePlatform = getNativeAppPlatform();

if (nativePlatform) {
return capacitor?.isNativePlatform?.() ?? true;
}

return false;
}

export function isFirefox(): boolean {
return window.navigator.userAgent.toLowerCase().indexOf("firefox") !== -1;
}
Expand Down