Skip to content

Decide camera, microphone and screen-capture requests (Canvas 35) - #71

Draft
shannah wants to merge 8 commits into
masterfrom
claude/media-permissions
Draft

shannah wants to merge 8 commits into
masterfrom
claude/media-permissions

Conversation

@shannah

@shannah shannah commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR lets a host app answer a page's camera, microphone and screen-capture requests. Video calls in XataWorks Builder creations (agentic-app-framework Canvas 392) need it on the desk.

API

  • wv.setPermissionHandler(WebViewPermissionHandler).
  • The handler receives a WebViewPermissionRequest with origin(), url(), and kinds() (CAMERA / MICROPHONE / DISPLAY). It is called on the EDT.
  • The request is answered with allow(), deny(), useDefault(), or answer(PermissionDecision). Any thread can answer, now or later, and only the first answer counts.
  • If the handler throws, or the component is disposed with requests still pending, those requests are denied.
  • With no handler set, each engine behaves exactly as it does today: WebKitGTK refuses, and WebView2 and WKWebView show their own prompts.
  • WebViewComponent.isPermissionHandlerSupported() reports whether the loaded native library has the hook.

Per engine

Engine Hook Status
WebKitGTK (Linux, lightweight) permission-request: user-media, plus display capture on 2.42+. Symbols are loaded optionally through webkit_loader.h. Turns on enable-media-stream / enable-webrtc where the build has them. Tested (Xvfb, mock capture devices)
WebView2 (Windows) add_PermissionRequested with GetDeferral for camera and microphone. The SDK in use has no screen-capture kind, so WebView2 keeps its own picker for that. Compiled by CI only, untested
WKWebView (macOS 12+) requestMediaCapturePermissionForOrigin… under __builtin_available. Screen capture has no public hook, so WebKit decides it. Compiled by CI only, untested

Host apps on macOS must declare NSCameraUsageDescription and NSMicrophoneUsageDescription, or the app is killed on first camera access. The README documents this.

SPDD

Story 11, its analysis, and Canvas 35.

Tests

  • mvn test passes, including the new PermissionDispatcherTest (12 tests).
  • xvfb-run -a ./run-linux-media-smoketest.sh extends the existing smoke test with WebViewMediaPermissionSmokeTest. In lightweight mode it checks:
    • deny results in NotAllowedError, with kinds [CAMERA] and the request's origin;
    • allow results in an audio and video stream;
    • an allow answered 1.5 s later from another thread works, and the EDT stays responsive;
    • with no handler, and with useDefault(), the result is NotAllowedError;
    • getDisplayMedia reaches the handler as [DISPLAY].
  • On Ubuntu 24.04, WebKitGTK still has no RTCPeerConnection. Pages must check typeof RTCPeerConnection.
  • New demo: WebViewMediaPermissionDemo, with run scripts for each OS.

Needs testing on real hardware

Windows and macOS have not been run yet. On Windows in particular, DEFAULT after a deferral should fall back to WebView2's own prompt, and that is untested.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WxLMiE3sgxfvWqy44xRLDY


Generated by Claude Code

A host app can now answer a page's media-capture requests:
setPermissionHandler(WebViewPermissionHandler) receives origin, url and the
kinds asked for (camera, microphone, display) on the EDT and answers allow,
deny or default from any thread, now or later. Native hooks: WebKitGTK's
permission-request (plus enable-media-stream/enable-webrtc where the build
has them), WebView2's PermissionRequested with a deferral, and WKWebView's
requestMediaCapturePermissionForOrigin on macOS 12+. With no handler each
engine behaves as before. Linux is tested in lightweight mode with mock
capture devices; Windows and macOS are compiled by CI but untested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxLMiE3sgxfvWqy44xRLDY
The JNI entry point sits outside namespace embed_win, so MSVC could not find
win_perm_respond (C3861). No behaviour change; the Canvas names the JNI
functions, not the internal helper, so it needs no sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxLMiE3sgxfvWqy44xRLDY

shannah commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

native (linux_64) and native (linux_arm64) are red, but the cause isn't in this PR. Both fail in the debian:11 container's apt-get install. archive.debian.org returns 404 Not Found for package files that its own index lists.

  • First run (0eb8d7c): libjavascriptcoregtk-4.0-18, libwebkit2gtk-4.0-dev and libsoup2.4-dev failed.
  • This run (d711e98): libxcb-shm0, libjavascriptcoregtk-4.0-dev and libwebkit2gtk-4.0-dev failed.

The missing files differ between runs. The same URLs return 200 when fetched from outside the runner, so this looks like a CDN edge serving stale 404s to the GitHub runners. The PR doesn't touch the workflow or the container setup. The last master run (Oct 3) passed this step.

No fix exists yet. A possible hardening for build.yml would be -o Acquire::Retries=5 on the apt-get calls, but I've kept it out of this PR. I'm re-running the failed jobs once.

The Windows compile error from the first run (C3861, win_perm_respond not found) was this PR's. d711e98 fixes it.


Generated by Claude Code

An embedded WKWebView leaves navigator.mediaDevices undefined unless
WebKit's private _mediaDevicesEnabled preference is on, so on a real Mac the
permission delegate was never reached (TypeError on md.getUserMedia). Set
_setMediaDevicesEnabled:, _setMediaStreamEnabled: and
_setScreenCaptureEnabled: on the configuration's preferences before the
web view (and any popup child) is created, each guarded by
respondsToSelector:. Documented: these are private WebKit preferences, and
host apps need the camera/microphone usage strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxLMiE3sgxfvWqy44xRLDY
…anvas 35 D13, D14)

macOS terminates a process that opens the camera or microphone without
NSCameraUsageDescription / NSMicrophoneUsageDescription in its main bundle.
The WKWebView delegate now checks for the needed keys first and denies the
request (never Prompt or Grant, handler or not) with one stderr line, so a
host without them gets NotAllowedError instead of a crash; hosts can check up
front with WebViewComponent.missingMacUsageDescriptions(). The macOS demo now
runs from a small .app whose clang-built launcher embeds an Info.plist and
starts the JVM through JLI_Launch; --no-bundle keeps the plain-java run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxLMiE3sgxfvWqy44xRLDY
archive.debian.org's CDN intermittently answers 404 to GitHub runners for
package files its index lists (libwebkit2gtk-4.0-dev, libxcb-shm0,
libepoxy0, ... a different set each run), failing the Linux native jobs.
Pass Acquire::Retries=5 to apt-get update and install in build.yml and
maven-release.yml. CI-only; Canvas 8 Operation 6 now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxLMiE3sgxfvWqy44xRLDY
…app (Canvas 35 D13, D14)

Run from a shell, the demo's camera use was charged by TCC to the app that
launched the shell (a terminal or IDE), which had no camera usage string, so
macOS still killed it. The demo now starts through LaunchServices
(open -n -W, output piped back to the terminal, env passed as launcher args)
so it is its own responsible process, and its Info.plist gains
NSCameraUseContinuityCameraDeviceType. When a process was not started by
launchd, the safeguard also checks the launching app's Info.plist and denies,
naming that app, instead of letting macOS abort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxLMiE3sgxfvWqy44xRLDY
Acquire::Retries does not retry a 404, and archive.debian.org's CDN keeps
serving stale 404s to the runners for files its index lists (a different
package each run). The container now writes an apt config with
Acquire::http::No-Cache (Cache-Control: no-cache, so the CDN refetches from
origin), Acquire::Retries and Check-Valid-Until, and retries the whole
install up to four times with a fresh update in between. CI-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxLMiE3sgxfvWqy44xRLDY
…nvas 8)

archive.debian.org is missing some files its own bullseye index lists
(libdw1 0.183-1 arm64, libgstreamer-gl1.0-0 1.18.4-2+deb11u2 amd64 answer
404 from everywhere, while the directory listing shows them), so no retry
can help. deb.debian.org still serves bullseye 11.11 with every file. The
container now uses an apt mirror+file list with deb.debian.org first and
the archive as fallback, and pins release n=bullseye instead of the archive
origin. CI-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxLMiE3sgxfvWqy44xRLDY

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants