diff --git a/ios/truapi-host/Sources/TrUAPIHost/Resources/truapi-container.js b/ios/truapi-host/Sources/TrUAPIHost/Resources/truapi-container.js index 0a78c43a5..c9a54165e 100644 --- a/ios/truapi-host/Sources/TrUAPIHost/Resources/truapi-container.js +++ b/ios/truapi-host/Sources/TrUAPIHost/Resources/truapi-container.js @@ -94,5 +94,4 @@ } return _createElement(tagName, options); }); - freezeAndDelete(window, "RTCPeerConnection"); })(); diff --git a/js/container/src/index.ts b/js/container/src/index.ts index a914568a0..6147cf7ad 100644 --- a/js/container/src/index.ts +++ b/js/container/src/index.ts @@ -121,7 +121,4 @@ freezeValue(document, 'createElement', (tagName: string, options?: ElementCreati return _createElement(tagName, options); }); -// --- WebRTC: no permission path in TrUAPI mode --- -freezeAndDelete(window, 'RTCPeerConnection'); - export {}; diff --git a/playground/src/lib/auto-test.ts b/playground/src/lib/auto-test.ts index 1a1e90282..ffdfce8b6 100644 --- a/playground/src/lib/auto-test.ts +++ b/playground/src/lib/auto-test.ts @@ -1,6 +1,7 @@ import { runExample, type LogEntry, type RunResult } from "./example-runner"; import { getClientSync } from "@parity/truapi/sandbox"; import type { MethodInfo, ServiceInfo } from "./services"; +import { WEBRTC_METHOD_NAME, WEBRTC_SERVICE_NAME } from "./webrtc-check"; import type { DiagnosisStatus } from "@/shared/diagnosis"; export const DIAGNOSIS_ID = "__diagnosis__"; @@ -47,6 +48,8 @@ const METHOD_TIMEOUT_MS = new Map([ ["Statement Store/create_proof_authorized", LIVE_ALLOCATION_TIMEOUT_MS], ["Statement Store/submit", LIVE_ALLOCATION_TIMEOUT_MS], ["Statement Store/subscribe", LIVE_ALLOCATION_TIMEOUT_MS], + // Two permission prompts (camera, microphone) may need the user. + [`${WEBRTC_SERVICE_NAME}/${WEBRTC_METHOD_NAME}`, SSO_TIMEOUT_MS], ]); type RunOneOpts = { diff --git a/playground/src/lib/services.ts b/playground/src/lib/services.ts index cfa3da55b..522ac7d8d 100644 --- a/playground/src/lib/services.ts +++ b/playground/src/lib/services.ts @@ -5,11 +5,14 @@ import type { ProductExecutionKind, ServiceInfo, } from "@parity/truapi/playground/services-types"; +import { WEBRTC_SERVICE } from "./webrtc-check"; export type { MethodInfo, ProductExecutionKind, ServiceInfo }; export { servicesForExecution }; -export const services: ServiceInfo[] = servicesForExecution( - generatedServices, - "Spa", -); +// Generated SPA-compatible services plus the synthetic WebRTC browser-capability +// method, which is exercised the same way (an example) but is not a wire method. +export const services: ServiceInfo[] = [ + ...servicesForExecution(generatedServices, "Spa"), + WEBRTC_SERVICE, +]; diff --git a/playground/src/lib/webrtc-check.ts b/playground/src/lib/webrtc-check.ts new file mode 100644 index 000000000..b3cce222c --- /dev/null +++ b/playground/src/lib/webrtc-check.ts @@ -0,0 +1,81 @@ +import type { ServiceInfo } from "@parity/truapi/playground/services-types"; + +// A first-class playground method backed by an example that combines TrUAPI +// permission calls with the browser WebRTC APIs. It runs through the same +// example runner as every other method (`truapi` and `assert`/`console` are +// injected; globals like `navigator`/`RTCPeerConnection` are reachable in the +// runner's function scope), so it shows up in the method browser, ⌘K, and the +// diagnosis alike. +// +// The example follows the product model — request every device permission via +// TrUAPI first, then use the capability: +// 1. requestDevicePermission("Camera") and ("Microphone") — device permissions; +// 2. only once granted, getUserMedia + RTCPeerConnection.createOffer. + +export const WEBRTC_SERVICE_NAME = "WebRTC"; +export const WEBRTC_METHOD_NAME = "peer_connection"; + +const WEBRTC_EXAMPLE_SOURCE = `// Fail fast: WebRTC media capture requires a secure context, so bail before +// prompting for any permission if the origin isn't secure. +console.log("secure context:", window.isSecureContext); +assert( + window.isSecureContext, + "Not a secure context — WebRTC media capture requires HTTPS or a localhost/loopback origin.", +); + +// Permission phase — request the camera and microphone through TrUAPI up front. +const camera = await truapi.permissions.requestDevicePermission("Camera"); +assert(camera.isOk(), "camera permission request failed:", camera); +assert(camera.value.granted, "camera permission denied"); +console.log("camera granted"); + +const microphone = await truapi.permissions.requestDevicePermission("Microphone"); +assert(microphone.isOk(), "microphone permission request failed:", microphone); +assert(microphone.value.granted, "microphone permission denied"); +console.log("microphone granted"); + +// Capability phase — permissions granted, now access the capability. +assert( + typeof RTCPeerConnection !== "undefined", + "RTCPeerConnection is unavailable — the host has not wired the WebRTC bridge (fail-closed).", +); +assert( + typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia, + "navigator.mediaDevices.getUserMedia is unavailable in this host.", +); + +const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true, +}); +console.log( + "media captured:", + stream.getTracks().map((t) => t.kind).join(", "), +); + +const pc = new RTCPeerConnection(); +try { + for (const track of stream.getTracks()) pc.addTrack(track, stream); + const offer = await pc.createOffer(); + assert(!!offer.sdp, "createOffer returned an empty SDP"); + console.log("offer created:", offer.type, offer.sdp.length + " bytes of SDP"); +} finally { + pc.close(); + stream.getTracks().forEach((t) => t.stop()); +}`; + +/** Synthetic WebRTC method — browsable, runnable, and part of the diagnosis. */ +export const WEBRTC_SERVICE: ServiceInfo = { + name: WEBRTC_SERVICE_NAME, + methods: [ + { + name: WEBRTC_METHOD_NAME, + type: "unary", + description: + "Requests camera + microphone (device permissions) through TrUAPI, " + + "then — once granted — opens an RTCPeerConnection and creates an " + + "offer with the captured media.", + exampleSource: WEBRTC_EXAMPLE_SOURCE, + }, + ], +};