@ohos-ports/agora-rtc-sdk-ng 4.24.8-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ # AgoraRTC SDK (Version 4.x)
2
+
3
+ [English](./README.md) | 简体中文
4
+
5
+ ## 开始
6
+
7
+ ### 安装
8
+
9
+ 使用 `npm`, `pnpm` 或 `yarn`
10
+
11
+ ```bash
12
+ # 使用 npm
13
+ npm i agora-rtc-sdk-ng
14
+ # 或者使用 pnpm
15
+ pnpm add agora-rtc-sdk-ng
16
+ # 或者使用 yarn
17
+ yarn add agora-rtc-sdk-ng
18
+ ```
19
+
20
+ 我们也提供了 CDN。SDK 会在全局导出 `AgoraRTC`。
21
+
22
+ ```html
23
+ <!-- 默认 UMD,不含 AI QoS -->
24
+ <script src="https://download.agora.io/sdk/release/AgoraRTC_N-4.24.8.js"></script>
25
+ ```
26
+
27
+ CDN 使用 AI QoS 时只引入 full 包,AI 已注册,不必再挂扩展脚本:
28
+
29
+ ```html
30
+ <script src="https://download.agora.io/sdk/release/AgoraRTC_N-4.24.8-full.js"></script>
31
+ <script>
32
+ const client = AgoraRTC.createClient({ mode: "live", codec: "vp8", aiClientMode: true });
33
+ await client.join(appId, channel, token, uid);
34
+ </script>
35
+ ```
36
+
37
+ npm 的 UMD 同样拆分。默认入口不含 AI QoS;完整包走 `agora-rtc-sdk-ng/full`:
38
+
39
+ ```javascript
40
+ import AgoraRTC from "agora-rtc-sdk-ng/full";
41
+
42
+ const client = AgoraRTC.createClient({ mode: "live", codec: "vp8", aiClientMode: true });
43
+ await client.join(appId, channel, token, uid);
44
+ ```
45
+
46
+ ESM 逻辑不变:主 ESM + 可选 service:
47
+
48
+ ```javascript
49
+ import AgoraRTC from "agora-rtc-sdk-ng/esm";
50
+ import { AiAudioModeService } from "agora-rtc-sdk-ng/services/ai-audio-mode";
51
+
52
+ AgoraRTC.use(AiAudioModeService);
53
+ const client = AgoraRTC.createClient({ mode: "live", codec: "vp8", aiClientMode: true });
54
+ await client.join(appId, channel, token, uid);
55
+ ```
56
+
57
+ ### 快速开始
58
+
59
+ 我们提供了 `React`, `Vue`, `Svelte` 的 Demo, 访问地址 [Quick Start Demo](https://github.com/AgoraIO/agora-rtc-web/blob/main/projects).
60
+
61
+ 详细可参考文档 [api-ref.agora.io](https://doc.shengwang.cn/api-ref/rtc/javascript/overview).
62
+
63
+ ## 变更记录
64
+
65
+ 变更记录访问地址为 [Release Notes](https://doc.shengwang.cn/doc/rtc/javascript/overview/release-notes).
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # AgoraRTC SDK (Version 4.x)
2
+
3
+ English | [简体中文](https://github.com/AgoraIO/agora-rtc-web/blob/main/README-zh_CN.md)
4
+
5
+ ## Getting Started
6
+
7
+ ### Install
8
+
9
+ Use `npm`, `pnpm` or `yarn`
10
+
11
+ ```bash
12
+ # with npm
13
+ npm i agora-rtc-sdk-ng
14
+ # or with pnpm
15
+ pnpm add agora-rtc-sdk-ng
16
+ # or with yarn
17
+ yarn add agora-rtc-sdk-ng
18
+ ```
19
+
20
+ CDN is also provided. The SDK exports a global `AgoraRTC` object.
21
+
22
+ ```html
23
+ <!-- default UMD, no AI QoS -->
24
+ <script src="https://download.agora.io/sdk/release/AgoraRTC_N-4.24.8.js"></script>
25
+ ```
26
+
27
+ To use AI QoS from CDN, load the full UMD instead. AI is already registered; only this one script is needed:
28
+
29
+ ```html
30
+ <script src="https://download.agora.io/sdk/release/AgoraRTC_N-4.24.8-full.js"></script>
31
+ <script>
32
+ const client = AgoraRTC.createClient({ mode: "live", codec: "vp8", aiClientMode: true });
33
+ await client.join(appId, channel, token, uid);
34
+ </script>
35
+ ```
36
+
37
+ npm UMD is the same split. Default has no AI QoS; `agora-rtc-sdk-ng/full` is the complete build:
38
+
39
+ ```javascript
40
+ import AgoraRTC from "agora-rtc-sdk-ng/full";
41
+
42
+ const client = AgoraRTC.createClient({ mode: "live", codec: "vp8", aiClientMode: true });
43
+ await client.join(appId, channel, token, uid);
44
+ ```
45
+
46
+ ESM is unchanged: import the main ESM, then the optional service:
47
+
48
+ ```javascript
49
+ import AgoraRTC from "agora-rtc-sdk-ng/esm";
50
+ import { AiAudioModeService } from "agora-rtc-sdk-ng/services/ai-audio-mode";
51
+
52
+ AgoraRTC.use(AiAudioModeService);
53
+ const client = AgoraRTC.createClient({ mode: "live", codec: "vp8", aiClientMode: true });
54
+ await client.join(appId, channel, token, uid);
55
+ ```
56
+
57
+ ### Quick start
58
+
59
+ `React`, `Vue`, `Svelte` quick start demo are provided, please check at [Quick Start Demo](https://github.com/AgoraIO/agora-rtc-web/blob/main/projects).
60
+ We also have [More Demos](https://github.com/AgoraIO/API-Examples-Web/tree/main/Demo).
61
+
62
+ Please follow the documentation at [api-ref.agora.io](https://api-ref.agora.io/en/voice-sdk/web/4.x/index.html).
63
+
64
+ ## Changelogs
65
+
66
+ Please check the changelogs at [Release Notes](https://docs.agora.io/en/video-calling/overview/release-notes?platform=web).
@@ -0,0 +1,424 @@
1
+ /**
2
+ * browser-shim.cjs — HarmonyOS / Node.js browser-runtime shim for agora-rtc-sdk-ng.
3
+ *
4
+ * The Agora WebRTC SDK is authored for browsers: its UMD bundle dereferences
5
+ * `window` at module top level and its APIs expect browser globals
6
+ * (window, navigator, document, location, ...).
7
+ *
8
+ * Strategy (in order):
9
+ * 1. Browser/bundler environment (window already defined) → no-op.
10
+ * 2. jsdom available (declared as optionalDependency) → provision a real
11
+ * browser DOM environment. jsdom is a genuine DOM implementation — it is
12
+ * NOT a stub of the SDK's functionality. Preferred path.
13
+ * 3. jsdom unavailable (e.g. npm did not install optionalDependencies of a
14
+ * file: dependency) → minimal platform-adaptation layer that provides
15
+ * only the global object structure the UMD bundle needs to load. It does
16
+ * NOT fake any RTC capability.
17
+ *
18
+ * Scope / limitations (documented honestly):
19
+ * - Provided: window, document, navigator, location, DOM element classes.
20
+ * - NOT provided (deliberately, no verified implementation exists):
21
+ * * RTCPeerConnection / RTCDataChannel — no W3C-compatible WebRTC
22
+ * implementation is available for Node.js on HarmonyOS
23
+ * (@ohos-ports/node-datachannel exposes only libdatachannel's
24
+ * low-level PeerConnection API, not the W3C surface).
25
+ * * navigator.mediaDevices — camera/microphone capture is a browser
26
+ * capability.
27
+ * * W3C WebSocket signaling to Agora SD-RTN.
28
+ * RTC-dependent APIs report NOT_SUPPORTED at runtime.
29
+ */
30
+
31
+ function noop() {}
32
+
33
+ function createEventTargetShim(target) {
34
+ const listeners = new Map();
35
+ target.addEventListener = function (type, fn) {
36
+ if (!listeners.has(type)) listeners.set(type, new Set());
37
+ listeners.get(type).add(fn);
38
+ };
39
+ target.removeEventListener = function (type, fn) {
40
+ if (listeners.has(type)) listeners.get(type).delete(fn);
41
+ };
42
+ target.dispatchEvent = function (event) {
43
+ const set = listeners.get(event && event.type);
44
+ if (set) for (const fn of Array.from(set)) fn(event);
45
+ return true;
46
+ };
47
+ return target;
48
+ }
49
+
50
+ function createElementShim() {
51
+ return createEventTargetShim({
52
+ style: {},
53
+ children: [],
54
+ classList: { add: noop, remove: noop, toggle: noop },
55
+ setAttribute: noop,
56
+ getAttribute: () => null,
57
+ appendChild(child) { this.children.push(child); return child; },
58
+ removeChild: noop,
59
+ getContext: () => null,
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Minimal platform-adaptation layer: enough global object structure for the
65
+ * UMD bundle to load under Node.js. Provides NO RTC capability.
66
+ */
67
+ function applyMinimalShim() {
68
+ createEventTargetShim(globalThis);
69
+ if (typeof globalThis.window === "undefined") globalThis.window = globalThis;
70
+ if (typeof globalThis.self === "undefined") globalThis.self = globalThis;
71
+
72
+ if (typeof globalThis.location === "undefined") {
73
+ globalThis.location = {
74
+ href: "https://localhost:8080/",
75
+ origin: "https://localhost:8080",
76
+ protocol: "https:",
77
+ host: "localhost:8080",
78
+ hostname: "localhost",
79
+ port: "8080",
80
+ pathname: "/",
81
+ search: "",
82
+ hash: "",
83
+ toString() { return this.href; },
84
+ };
85
+ }
86
+
87
+ if (typeof globalThis.document === "undefined") {
88
+ globalThis.document = createEventTargetShim({
89
+ createElement: createElementShim,
90
+ createElementNS: createElementShim,
91
+ createTextNode: () => ({}),
92
+ documentElement: createEventTargetShim({ style: {} }),
93
+ head: createEventTargetShim({ appendChild: noop }),
94
+ body: createEventTargetShim({ appendChild: noop }),
95
+ getElementById: () => null,
96
+ querySelector: () => null,
97
+ querySelectorAll: () => [],
98
+ visibilityState: "visible",
99
+ hidden: false,
100
+ });
101
+ }
102
+
103
+ // DOM element/media type classes used for instanceof-style checks.
104
+ const classNames = [
105
+ "HTMLElement", "HTMLVideoElement", "HTMLAudioElement", "HTMLCanvasElement",
106
+ "HTMLDivElement", "HTMLImageElement", "HTMLIFrameElement", "HTMLScriptElement",
107
+ "MediaStream", "MediaStreamTrack", "MediaRecorder",
108
+ "Blob", "File", "FileReader", "FormData", "XMLHttpRequest",
109
+ "CustomEvent", "Event", "EventTarget",
110
+ "MessageChannel", "Worker",
111
+ "MutationObserver", "ResizeObserver", "IntersectionObserver", "PerformanceObserver",
112
+ "AudioContext", "AudioBuffer", "GainNode",
113
+ "MediaStreamAudioSourceNode", "MediaStreamAudioDestinationNode",
114
+ "ScreenOrientation", "ClipboardItem", "DOMException",
115
+ ];
116
+ for (const name of classNames) {
117
+ if (typeof globalThis[name] === "undefined") {
118
+ globalThis[name] = class ShimClass {
119
+ constructor() { this.style = {}; }
120
+ };
121
+ }
122
+ }
123
+ // AudioContext needs a minimal real-ish surface used during construction.
124
+ if (typeof globalThis.AudioContext === "function") {
125
+ const AC = globalThis.AudioContext;
126
+ if (!AC.prototype.createGain) {
127
+ AC.prototype.createGain = function () {
128
+ return {
129
+ gain: { value: 1, setValueAtTime: noop, linearRampToValueAtTime: noop },
130
+ connect: () => ({}), disconnect: noop,
131
+ };
132
+ };
133
+ AC.prototype.createMediaStreamSource = function () { return { connect: () => ({}) }; };
134
+ AC.prototype.createMediaStreamDestination = function () {
135
+ return { connect: noop, stream: { getTracks: () => [], getAudioTracks: () => [] } };
136
+ };
137
+ AC.prototype.decodeAudioData = function () { return Promise.reject(new Error("decodeAudioData: not supported in Node.js shim")); };
138
+ AC.prototype.resume = () => Promise.resolve();
139
+ AC.prototype.close = () => Promise.resolve();
140
+ }
141
+ }
142
+ return true;
143
+ }
144
+
145
+ function applyJSDomShim() {
146
+ const { JSDOM } = require("jsdom");
147
+
148
+ const dom = new JSDOM("<!doctype html><html><head></head><body></body></html>", {
149
+ url: "https://localhost:8080/",
150
+ pretendToBeVisual: true,
151
+ });
152
+
153
+ const g = globalThis;
154
+ const w = dom.window;
155
+
156
+ if (!g.window) g.window = w;
157
+ if (!g.document) g.document = w.document;
158
+ if (!g.navigator) g.navigator = w.navigator;
159
+ if (!g.location) g.location = w.location;
160
+ if (!g.self) g.self = w;
161
+ if (!g.HTMLElement) g.HTMLElement = w.HTMLElement;
162
+ if (!g.HTMLVideoElement) g.HTMLVideoElement = w.HTMLVideoElement;
163
+ if (!g.HTMLAudioElement) g.HTMLAudioElement = w.HTMLAudioElement;
164
+ if (!g.HTMLCanvasElement) g.HTMLCanvasElement = w.HTMLCanvasElement;
165
+ if (!g.MediaStream) g.MediaStream = w.MediaStream || class MediaStream { getTracks() { return []; } };
166
+ if (!g.MediaStreamTrack) g.MediaStreamTrack = w.MediaStreamTrack || class MediaStreamTrack {};
167
+ if (!g.Blob) g.Blob = w.Blob;
168
+ if (!g.FileReader) g.FileReader = w.FileReader;
169
+ if (!g.CustomEvent) g.CustomEvent = w.CustomEvent;
170
+ if (!g.Event) g.Event = w.Event;
171
+ if (!g.EventTarget) g.EventTarget = w.EventTarget;
172
+ if (!g.XMLHttpRequest) g.XMLHttpRequest = w.XMLHttpRequest;
173
+ if (!g.FormData) g.FormData = w.FormData;
174
+ if (!g.URL) g.URL = w.URL;
175
+ if (!g.URLSearchParams) g.URLSearchParams = w.URLSearchParams;
176
+ if (!g.requestAnimationFrame) g.requestAnimationFrame = w.requestAnimationFrame.bind(w);
177
+ if (!g.cancelAnimationFrame) g.cancelAnimationFrame = w.cancelAnimationFrame.bind(w);
178
+
179
+ // Round 6 fix: FormData realm mismatch breaking jsdom XHR multipart bodies.
180
+ //
181
+ // Node.js >= 18 ships a global FormData (from undici). The UMD bundle runs
182
+ // in the Node global scope, so `new FormData()` inside the SDK constructs an
183
+ // UNDICI FormData — the `if (!g.FormData) g.FormData = w.FormData` line above
184
+ // never fires because the global already exists. jsdom's XMLHttpRequest only
185
+ // recognises ITS OWN realm's FormData (via jsdom's internal FormData impl
186
+ // check); an undici FormData falls through the default branch of jsdom's
187
+ // send() and is coerced with ToString() to the literal string
188
+ // "[object FormData]" (17 bytes). Every multipart/form-data request the SDK
189
+ // makes (AP access point POST /api/v2/transpond/webrtc, webproxy join) then
190
+ // carries that garbage body: Agora's nginx returns 502 (no CORS headers on
191
+ // the error response), jsdom's CORS validator rejects it, and the SDK sees
192
+ // NETWORK_ERROR before the access-point layer is ever exercised.
193
+ //
194
+ // Fix: wrap jsdom XMLHttpRequest.prototype.send so a foreign (non-jsdom)
195
+ // FormData-like body is converted into a real jsdom FormData with the same
196
+ // entries. The resulting multipart/form-data body is genuinely serialized
197
+ // and sent — this is a realm-bridge polyfill, not a mock: the server
198
+ // receives the real field values the SDK appended.
199
+ try {
200
+ const jsdomXHR = w.XMLHttpRequest;
201
+ const jsdomFormData = w.FormData;
202
+ const origXhrSend = jsdomXHR.prototype.send;
203
+ jsdomXHR.prototype.send = function (body) {
204
+ let out = body;
205
+ if (
206
+ body &&
207
+ typeof body === "object" &&
208
+ typeof body.entries === "function" &&
209
+ typeof body.append === "function" &&
210
+ !(body instanceof jsdomFormData)
211
+ ) {
212
+ const fd = new jsdomFormData();
213
+ for (const [name, value] of body.entries()) {
214
+ // Entries are strings (the SDK only appends string fields).
215
+ // Foreign Blob/File values are forwarded as-is; jsdom accepts any
216
+ // Blob for append.
217
+ fd.append(name, value);
218
+ }
219
+ out = fd;
220
+ }
221
+ return origXhrSend.call(this, out);
222
+ };
223
+ } catch (e) {
224
+ // If the jsdom prototype is not writable in this jsdom version, leave it
225
+ // untouched — behavior degrades to the pre-Round-6 state, nothing masked.
226
+ }
227
+
228
+ // WebCrypto capability exposure (Round 4).
229
+ //
230
+ // Node.js >= 19 ships a genuine WebCrypto implementation as a global
231
+ // (globalThis.crypto.subtle). jsdom's window.crypto does NOT implement
232
+ // SubtleCrypto, so the SDK's capability probe
233
+ // supportWebCrypto = typeof window !== "undefined"
234
+ // && window.crypto !== undefined
235
+ // && window.crypto.subtle !== undefined
236
+ // reported false even though a real WebCrypto backend exists in the host
237
+ // runtime. Exposing the host's real implementation on the jsdom window
238
+ // makes the detection reflect actual capability (this is NOT a polyfill or
239
+ // stub — it is the runtime's own WebCrypto).
240
+ try {
241
+ const hostCrypto = globalThis.crypto;
242
+ if (
243
+ hostCrypto &&
244
+ typeof hostCrypto.subtle !== "undefined" &&
245
+ (typeof w.crypto === "undefined" || typeof w.crypto.subtle === "undefined")
246
+ ) {
247
+ Object.defineProperty(w, "crypto", { value: hostCrypto, configurable: true });
248
+ }
249
+ } catch (e) {
250
+ // If the jsdom window rejects the property redefinition, keep jsdom's
251
+ // crypto object — detection stays honest (false), functionality unaffected.
252
+ }
253
+
254
+ if (!g.getComputedStyle) g.getComputedStyle = w.getComputedStyle.bind(w);
255
+ if (!g.matchMedia) {
256
+ g.matchMedia = w.matchMedia
257
+ ? w.matchMedia.bind(w)
258
+ : () => ({ matches: false, addListener: noop, removeListener: noop, addEventListener: noop, removeEventListener: noop });
259
+ }
260
+ return true;
261
+ }
262
+
263
+ function createInMemoryStorage() {
264
+ const store = new Map();
265
+ return {
266
+ get length() { return store.size; },
267
+ key(i) { return Array.from(store.keys())[i] ?? null; },
268
+ getItem(k) { return store.has(String(k)) ? store.get(String(k)) : null; },
269
+ setItem(k, v) { store.set(String(k), String(v)); },
270
+ removeItem(k) { store.delete(String(k)); },
271
+ clear() { store.clear(); },
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Storage shims. In-memory Storage is a real (if non-persistent)
277
+ * implementation of the Web Storage API semantics.
278
+ */
279
+ function applyStorageShim() {
280
+ if (typeof globalThis.localStorage === "undefined") {
281
+ Object.defineProperty(globalThis, "localStorage", {
282
+ value: createInMemoryStorage(), configurable: true, writable: false,
283
+ });
284
+ }
285
+ if (typeof globalThis.sessionStorage === "undefined") {
286
+ Object.defineProperty(globalThis, "sessionStorage", {
287
+ value: createInMemoryStorage(), configurable: true, writable: false,
288
+ });
289
+ }
290
+ }
291
+
292
+ /**
293
+ * navigator.mediaDevices shim — honest for a Node.js runtime where no media
294
+ * capture devices are accessible:
295
+ * - enumerateDevices() → resolves with an EMPTY list (factually correct:
296
+ * no devices are exposed to a Node.js process).
297
+ * - getUserMedia() → rejects (no capture devices available).
298
+ * This prevents the SDK's async device detection from producing unhandled
299
+ * rejections, without faking any device or capture capability.
300
+ */
301
+ function applyMediaDevicesShim() {
302
+ const nav = globalThis.navigator;
303
+ if (!nav || nav.mediaDevices) return;
304
+ const notSupportedError = () => {
305
+ const err = new Error("getUserMedia is not supported in Node.js: no media capture devices available");
306
+ err.name = "NotSupportedError";
307
+ return err;
308
+ };
309
+ const mediaDevices = {
310
+ ondevicechange: null,
311
+ enumerateDevices: () => Promise.resolve([]),
312
+ getSupportedConstraints: () => ({}),
313
+ getUserMedia: () => Promise.reject(notSupportedError()),
314
+ getDisplayMedia: () => Promise.reject(notSupportedError()),
315
+ addEventListener: noop,
316
+ removeEventListener: noop,
317
+ dispatchEvent: () => true,
318
+ };
319
+ Object.defineProperty(nav, "mediaDevices", {
320
+ value: mediaDevices, configurable: true, writable: false,
321
+ });
322
+ }
323
+
324
+ /**
325
+ * WebRTC runtime injection (Round 2).
326
+ *
327
+ * HarmonyOS Node.js does not ship a W3C WebRTC implementation. The
328
+ * @ohos-ports/node-datachannel package provides a native OHOS arm64
329
+ * libdatachannel stack (RTCPeerConnection / RTCDataChannel / ICE / DTLS /
330
+ * SCTP) exposed through a genuine W3C-shaped polyfill (verified with a real
331
+ * in-process loopback: SDP exchange, ICE, DTLS handshake, DataChannel open).
332
+ *
333
+ * When this package is installed, its real W3C-surface classes are promoted
334
+ * to globals so the SDK's RTC transport (client.join / publish signaling)
335
+ * can run:
336
+ * RTCPeerConnection, RTCSessionDescription, RTCIceCandidate,
337
+ * RTCDataChannel, RTCPeerConnectionIceEvent, RTCDataChannelEvent, RTCError.
338
+ *
339
+ * W3C WebSocket: Node.js >= 22 ships a real, spec-compliant WebSocket client
340
+ * as a global — used as-is (no shim, no override).
341
+ *
342
+ * Media capture (navigator.mediaDevices.getUserMedia) stays honest: see
343
+ * applyMediaDevicesShim — libdatachannel has no camera/microphone capture,
344
+ * so local track creation reports NotSupportedError. join() without locally
345
+ * pre-created tracks works over the native stack.
346
+ */
347
+ function applyWebRtcRuntimeShim() {
348
+ // Real WebRTC already present (browser or a runtime that provides it).
349
+ if (typeof globalThis.RTCPeerConnection !== "undefined") return true;
350
+
351
+ let polyfill = null;
352
+ try {
353
+ polyfill = require("@ohos-ports/node-datachannel/polyfill");
354
+ } catch (e) {
355
+ // Native WebRTC runtime not installed — RTC globals stay absent (honest).
356
+ return false;
357
+ }
358
+ if (!polyfill || typeof polyfill.RTCPeerConnection !== "function") return false;
359
+
360
+ const rtcGlobals = [
361
+ "RTCPeerConnection",
362
+ "RTCSessionDescription",
363
+ "RTCIceCandidate",
364
+ "RTCDataChannel",
365
+ "RTCPeerConnectionIceEvent",
366
+ "RTCDataChannelEvent",
367
+ "RTCError",
368
+ ];
369
+ for (const name of rtcGlobals) {
370
+ if (typeof globalThis[name] === "undefined" && typeof polyfill[name] !== "undefined") {
371
+ globalThis[name] = polyfill[name];
372
+ }
373
+ }
374
+
375
+ // Round 4: also promote the same real classes onto the browser `window`
376
+ // object. The SDK probes capabilities through window.* (e.g.
377
+ // window.RTCPeerConnection, window.RTCRtpSender) in several feature
378
+ // detectors; without this, those probes report false even though the real
379
+ // native WebRTC runtime is present and reachable via bare globals.
380
+ const jsdomWindow = globalThis.window;
381
+ if (jsdomWindow && jsdomWindow !== globalThis) {
382
+ for (const name of rtcGlobals) {
383
+ try {
384
+ if (
385
+ typeof jsdomWindow[name] === "undefined" &&
386
+ typeof polyfill[name] !== "undefined"
387
+ ) {
388
+ Object.defineProperty(jsdomWindow, name, { value: polyfill[name], configurable: true, writable: true });
389
+ }
390
+ } catch (e) {
391
+ // Property not definable on this window implementation — leave absent
392
+ // so detection stays honest.
393
+ }
394
+ }
395
+ }
396
+ return true;
397
+ }
398
+
399
+ function applyBrowserShim() {
400
+ // 1. Already a browser-like environment → nothing to do.
401
+ if (typeof globalThis.window !== "undefined" && globalThis.window.document) {
402
+ return false;
403
+ }
404
+
405
+ // 2. Full DOM environment via jsdom (preferred).
406
+ let applied;
407
+ try {
408
+ applied = applyJSDomShim();
409
+ } catch (e) {
410
+ // jsdom not installed — fall through to minimal shim.
411
+ applied = applyMinimalShim();
412
+ }
413
+
414
+ // 3. Shared honest shims for storage and media device enumeration.
415
+ applyStorageShim();
416
+ applyMediaDevicesShim();
417
+
418
+ // 4. Native WebRTC runtime injection (Round 2).
419
+ applyWebRtcRuntimeShim();
420
+
421
+ return applied;
422
+ }
423
+
424
+ module.exports = { applyBrowserShim };