Skip to content
Merged
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
36 changes: 25 additions & 11 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,13 @@ jobs:
set -euo pipefail
mkdir -p "natives/${{ matrix.native_dir }}"
# Bullseye is EOL: its -security pool is already emptied, so the
# container uses only the permanent "bullseye main" suite on
# archive.debian.org (signatures still verified; only the expired
# Valid-Until is waived). The pin downgrades the image's newer
# security builds of base packages (libc6, ...) to the archive's,
# or their -dev packages cannot be installed. Keep comments out
# container uses only the "bullseye main" suite, from deb.debian.org
# first and archive.debian.org as the fallback (apt mirror+file:
# the archive is missing some files its own index lists).
# Signatures are still verified; only an expired Valid-Until is
# waived. The pin downgrades the image's newer security builds of
# base packages (libc6, ...) to bullseye main's, or their -dev
# packages cannot be installed. Keep comments out
# of the single-quoted script below: an apostrophe would end it.
docker run --rm \
-v "$GITHUB_WORKSPACE:/src" -w /src \
Expand All @@ -117,14 +119,26 @@ jobs:
-e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
debian:11 bash -euo pipefail -c '
export DEBIAN_FRONTEND=noninteractive
echo "deb http://archive.debian.org/debian bullseye main" > /etc/apt/sources.list
printf "http://deb.debian.org/debian\nhttp://archive.debian.org/debian\n" \
> /etc/apt/bullseye-mirrors.txt
echo "deb mirror+file:/etc/apt/bullseye-mirrors.txt bullseye main" > /etc/apt/sources.list
rm -f /etc/apt/sources.list.d/*
printf "Package: *\nPin: origin \"archive.debian.org\"\nPin-Priority: 1001\n" \
printf "Package: *\nPin: release n=bullseye\nPin-Priority: 1001\n" \
> /etc/apt/preferences.d/archive-bullseye
apt-get -o Acquire::Check-Valid-Until=false update -qq
apt-get install -y -qq --allow-downgrades --no-install-recommends \
g++ pkg-config \
libgtk-3-dev libwebkit2gtk-4.0-dev libx11-dev libxt-dev >/dev/null
printf "Acquire::Retries \"5\";\nAcquire::http::No-Cache \"true\";\nAcquire::Check-Valid-Until \"false\";\n" \
> /etc/apt/apt.conf.d/80ci
apt-get update -qq
for attempt in 1 2 3 4; do
if apt-get install -y -qq --allow-downgrades --no-install-recommends \
g++ pkg-config \
libgtk-3-dev libwebkit2gtk-4.0-dev libx11-dev libxt-dev >/dev/null; then
break
fi
if [ "$attempt" = 4 ]; then exit 1; fi
echo "apt-get install failed (attempt $attempt), retrying" >&2
sleep $((attempt * 15))
apt-get update -qq
done
if pkg-config --exists webkit2gtk-4.1; then
WEBKIT_PKG=webkit2gtk-4.1
else
Expand Down
36 changes: 25 additions & 11 deletions .github/workflows/maven-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,13 @@ jobs:
set -euo pipefail
mkdir -p "natives/${{ matrix.native_dir }}"
# Bullseye is EOL: its -security pool is already emptied, so the
# container uses only the permanent "bullseye main" suite on
# archive.debian.org (signatures still verified; only the expired
# Valid-Until is waived). The pin downgrades the image's newer
# security builds of base packages (libc6, ...) to the archive's,
# or their -dev packages cannot be installed. Keep comments out
# container uses only the "bullseye main" suite, from deb.debian.org
# first and archive.debian.org as the fallback (apt mirror+file:
# the archive is missing some files its own index lists).
# Signatures are still verified; only an expired Valid-Until is
# waived. The pin downgrades the image's newer security builds of
# base packages (libc6, ...) to bullseye main's, or their -dev
# packages cannot be installed. Keep comments out
# of the single-quoted script below: an apostrophe would end it.
docker run --rm \
-v "$GITHUB_WORKSPACE:/src" -w /src \
Expand All @@ -126,14 +128,26 @@ jobs:
-e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
debian:11 bash -euo pipefail -c '
export DEBIAN_FRONTEND=noninteractive
echo "deb http://archive.debian.org/debian bullseye main" > /etc/apt/sources.list
printf "http://deb.debian.org/debian\nhttp://archive.debian.org/debian\n" \
> /etc/apt/bullseye-mirrors.txt
echo "deb mirror+file:/etc/apt/bullseye-mirrors.txt bullseye main" > /etc/apt/sources.list
rm -f /etc/apt/sources.list.d/*
printf "Package: *\nPin: origin \"archive.debian.org\"\nPin-Priority: 1001\n" \
printf "Package: *\nPin: release n=bullseye\nPin-Priority: 1001\n" \
> /etc/apt/preferences.d/archive-bullseye
apt-get -o Acquire::Check-Valid-Until=false update -qq
apt-get install -y -qq --allow-downgrades --no-install-recommends \
g++ pkg-config \
libgtk-3-dev libwebkit2gtk-4.0-dev libx11-dev libxt-dev >/dev/null
printf "Acquire::Retries \"5\";\nAcquire::http::No-Cache \"true\";\nAcquire::Check-Valid-Until \"false\";\n" \
> /etc/apt/apt.conf.d/80ci
apt-get update -qq
for attempt in 1 2 3 4; do
if apt-get install -y -qq --allow-downgrades --no-install-recommends \
g++ pkg-config \
libgtk-3-dev libwebkit2gtk-4.0-dev libx11-dev libxt-dev >/dev/null; then
break
fi
if [ "$attempt" = 4 ]; then exit 1; fi
echo "apt-get install failed (attempt $attempt), retrying" >&2
sleep $((attempt * 15))
apt-get update -qq
done
if pkg-config --exists webkit2gtk-4.1; then
WEBKIT_PKG=webkit2gtk-4.1
else
Expand Down
126 changes: 126 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,129 @@ wv.setNavigationHandler(e ->
popups of a page you restrict. See
[`demos/WebViewNavigationDemo/`](demos/WebViewNavigationDemo/README.md).

## Camera, microphone and screen capture

A page's `getUserMedia` / `getDisplayMedia` request is put to a permission
handler, which answers **allow**, **deny** or **default** (let the engine
decide) — at once, or later from any thread, for example after asking the
user:

```java
wv.setPermissionHandler(req -> { // runs on the EDT
if (!req.origin().equals("demo://app")) { req.deny(); return; }
int r = JOptionPane.showConfirmDialog(frame,
req.origin() + " wants " + req.kinds(), "Permission", JOptionPane.YES_NO_OPTION);
if (r == JOptionPane.YES_OPTION) req.allow(); else req.deny();
});
```

* **The request** gives `origin()` (`scheme://host[:port]`), `url()` and
`kinds()` — a set of `MediaCaptureKind.CAMERA`, `MICROPHONE` and `DISPLAY`.
Answer with `allow()`, `deny()`, `useDefault()` or `answer(PermissionDecision)`;
only the first answer counts (each returns whether it was the first).
* **Threading.** The handler runs on the Swing EDT, delivered with
`invokeLater`; no engine thread waits for it, so a modal dialog is fine.
The page's promise stays pending until the answer arrives. A handler that
throws denies; a request still unanswered when the component is disposed
is denied.
* **No handler, no change.** Without a handler (the default, or
`setPermissionHandler(null)`), and for `useDefault()`, each engine does what
it did before:

| Engine | Default |
|---|---|
| Linux (WebKitGTK) | refuses (`NotAllowedError`) |
| Windows (WebView2) | WebView2's own permission prompt |
| macOS (WKWebView) | WebKit's own permission prompt |

* **Platform coverage.** `WebViewComponent.isPermissionHandlerSupported()`
says whether the loaded native routes requests.
* **Linux** (lightweight): camera and microphone, and screen capture with
WebKitGTK 2.42+, through `permission-request`. Every view has
`enable-media-stream` and `enable-webrtc` switched on where the WebKitGTK
version has them. The origin is the top page's (WebKitGTK reports no
frame origin).
* **Windows**: camera and microphone through `PermissionRequested`. Screen
capture has no permission kind in the WebView2 SDK this library builds
against, so WebView2's own picker decides it.
* **macOS 12+**: camera and microphone through the UI delegate's
`requestMediaCapturePermissionForOrigin…`; screen capture keeps WebKit's
own behaviour. Before macOS 12, nothing changes.
* **macOS: `navigator.mediaDevices` is always exposed.** An embedded WKWebView
hides `navigator.mediaDevices` unless WebKit's private preferences are on
(Safari turns them on; ordinary apps don't). The library turns on
`_mediaDevicesEnabled`, `_mediaStreamEnabled` and `_screenCaptureEnabled`
for every web view and popup, each only where the running WebKit has it.
With no handler, WebKit's own permission prompt decides, as WebView2's does
on Windows. To keep a page off the camera and microphone, deny in your handler.
* **macOS host-app checklist.** Because any page can now ask for the camera
or microphone, a macOS host application needs:
* [ ] **`NSCameraUsageDescription` and `NSMicrophoneUsageDescription`** in
the Info.plist of the bundle whose executable runs the JVM — your `.app`,
not the JDK's `java`. Each value is the sentence macOS shows the user.
* **jpackage:** put a complete `Info.plist` with both keys in a folder and
pass it with `--resource-dir <folder>`; jpackage uses it in place of its
default.
* **jDeploy:** make sure the app bundle jDeploy generates carries both
keys in its Info.plist; check the jDeploy documentation for your version
for how to add usage descriptions.
* **Hardened runtime / notarized apps** also need the
`com.apple.security.device.camera` and
`com.apple.security.device.audio-input` entitlements.
* Optionally `NSCameraUseContinuityCameraDeviceType` (`true`), which
silences AVFoundation's "AVCaptureDeviceTypeExternal is deprecated for
Continuity Cameras" warning.
* [ ] **Launch the app through LaunchServices.** macOS's privacy system
(TCC) attributes a request to the *responsible* process. An app started
from Finder, the Dock, `open` or an installed jDeploy / jpackage bundle is
its own, so its Info.plist counts. An app whose binary is run from a
terminal or an IDE is attributed to *that* app (Terminal, iTerm2, your
IDE), and macOS kills it when that app's Info.plist lacks the key — even
though your bundle has it. During development, start the bundle with
`open -n -W --stdout "$(tty)" --stderr "$(tty)" MyApp.app --args …`
(environment variables do not pass through `open` before macOS 13), or
run from a terminal or IDE that declares the keys.
* [ ] **Check at start-up** with
`WebViewComponent.missingMacUsageDescriptions()`, which lists the keys the
running process lacks (empty off macOS): in its main bundle or, when it
was not launched by launchd, in the Info.plist of the app it is
attributed to. Warn, or hide video
features, when it isn't empty.
* [ ] **Know what happens without them: a clean deny, not a crash.** macOS
terminates a process that opens the camera or microphone without the key
(`SIGABRT`, "Namespace TCC"), so the library never lets such a request
through: it is answered *deny* — whether or not a permission handler is
installed, and without asking the handler — the page gets
`NotAllowedError`, and one line goes to stderr, e.g.
`swingwebview: camera access denied — this app's Info.plist has no
NSCameraUsageDescription (macOS would terminate the app)`. When only the
launching app lacks it, the line names that app and suggests launching
through LaunchServices. The library finds that app as the topmost
ancestor process below launchd (public APIs only; the exact attribution is
private), so it may deny where macOS would have prompted — a JVM spawned as
a child of a configured app, or a parent that disclaims responsibility —
but it does not let a request through that macOS would kill the app for.
* [ ] **Screen sharing needs no key.** `getDisplayMedia` uses macOS's Screen
Recording permission, which has no usage-description key and does not
terminate the app; macOS asks the user, or the user allows it in System
Settings › Privacy & Security › Screen & System Audio Recording.
* [ ] macOS's own one-time consent prompt still appears on top of your
handler's answer.
* **macOS private API.** These preferences are private WebKit API, the same
kind as `developerExtrasEnabled`, which debug mode already uses. They're
fine for applications distributed outside the Mac App Store, but App Store
review may reject an app that uses private API.
* **WebRTC.** Whether `RTCPeerConnection` exists depends on how the engine was
built — Ubuntu 24.04's WebKitGTK has none, even with `enable-webrtc` on. A
page detects it with `typeof RTCPeerConnection === 'function'` and falls
back (for example, to opening the page in the user's browser).
* **Media capture needs a secure context**: `https:`, `localhost`, or a custom
scheme registered with `WebViewSchemes`.
* **Testing on Linux without a camera.** `WEBVIEW_MOCK_CAPTURE_DEVICES=1`
turns on WebKitGTK's mock camera, microphone and screen (a test-only switch).
`xvfb-run -a ./run-linux-media-smoketest.sh` runs the pass/fail check. See
[`demos/WebViewMediaPermissionDemo/`](demos/WebViewMediaPermissionDemo/README.md).

## Demo

See [`demos/WebViewHeavyweightDemo/`](demos/WebViewHeavyweightDemo/README.md)
Expand Down Expand Up @@ -1048,6 +1171,9 @@ Additional demos:
* `demos/WebViewNavigationDemo/` — a page that tries every way to navigate
away to a local listener, each refused by a navigation handler before
anything is sent (`run-*-navigation-demo`).
* `demos/WebViewMediaPermissionDemo/` — camera, microphone and screen
sharing decided by a permission handler that asks you in a Swing dialog
(`run-*-media-permission-demo`).

## Building from source

Expand Down
Loading
Loading