@gjsify/webrtc 0.1.15

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.
Files changed (117) hide show
  1. package/lib/esm/get-user-media.js +93 -0
  2. package/lib/esm/gst-enum-maps.js +88 -0
  3. package/lib/esm/gst-init.js +34 -0
  4. package/lib/esm/gst-stats-parser.js +79 -0
  5. package/lib/esm/gst-utils.js +16 -0
  6. package/lib/esm/index.js +53 -0
  7. package/lib/esm/media-device-info.js +23 -0
  8. package/lib/esm/media-devices.js +147 -0
  9. package/lib/esm/media-stream-track.js +142 -0
  10. package/lib/esm/media-stream.js +78 -0
  11. package/lib/esm/register/data-channel.js +8 -0
  12. package/lib/esm/register/error.js +8 -0
  13. package/lib/esm/register/media-devices.js +7 -0
  14. package/lib/esm/register/media.js +12 -0
  15. package/lib/esm/register/peer-connection.js +16 -0
  16. package/lib/esm/register.js +5 -0
  17. package/lib/esm/rtc-certificate.js +70 -0
  18. package/lib/esm/rtc-data-channel.js +266 -0
  19. package/lib/esm/rtc-dtls-transport.js +41 -0
  20. package/lib/esm/rtc-dtmf-sender.js +109 -0
  21. package/lib/esm/rtc-error.js +24 -0
  22. package/lib/esm/rtc-events.js +35 -0
  23. package/lib/esm/rtc-ice-candidate.js +75 -0
  24. package/lib/esm/rtc-ice-transport.js +96 -0
  25. package/lib/esm/rtc-peer-connection.js +855 -0
  26. package/lib/esm/rtc-rtp-receiver.js +91 -0
  27. package/lib/esm/rtc-rtp-sender.js +298 -0
  28. package/lib/esm/rtc-rtp-transceiver.js +97 -0
  29. package/lib/esm/rtc-sctp-transport.js +40 -0
  30. package/lib/esm/rtc-session-description.js +57 -0
  31. package/lib/esm/rtc-stats-report.js +35 -0
  32. package/lib/esm/rtc-track-event.js +29 -0
  33. package/lib/esm/rtp-capabilities.js +41 -0
  34. package/lib/esm/tee-multiplexer.js +62 -0
  35. package/lib/esm/wpt-helpers.js +122 -0
  36. package/lib/types/get-user-media.d.ts +14 -0
  37. package/lib/types/gst-enum-maps.d.ts +10 -0
  38. package/lib/types/gst-init.d.ts +5 -0
  39. package/lib/types/gst-stats-parser.d.ts +16 -0
  40. package/lib/types/gst-utils.d.ts +11 -0
  41. package/lib/types/index.d.ts +41 -0
  42. package/lib/types/media-device-info.d.ts +14 -0
  43. package/lib/types/media-devices.d.ts +12 -0
  44. package/lib/types/media-stream-track.d.ts +59 -0
  45. package/lib/types/media-stream.d.ts +28 -0
  46. package/lib/types/register/data-channel.d.ts +1 -0
  47. package/lib/types/register/error.d.ts +1 -0
  48. package/lib/types/register/media-devices.d.ts +1 -0
  49. package/lib/types/register/media.d.ts +1 -0
  50. package/lib/types/register/peer-connection.d.ts +1 -0
  51. package/lib/types/register.d.ts +5 -0
  52. package/lib/types/register.spec.d.ts +3 -0
  53. package/lib/types/rtc-certificate.d.ts +23 -0
  54. package/lib/types/rtc-data-channel.d.ts +64 -0
  55. package/lib/types/rtc-dtls-transport.d.ts +20 -0
  56. package/lib/types/rtc-dtmf-sender.d.ts +31 -0
  57. package/lib/types/rtc-error.d.ts +19 -0
  58. package/lib/types/rtc-events.d.ts +27 -0
  59. package/lib/types/rtc-ice-candidate.d.ts +28 -0
  60. package/lib/types/rtc-ice-transport.d.ts +56 -0
  61. package/lib/types/rtc-peer-connection.d.ts +165 -0
  62. package/lib/types/rtc-rtp-receiver.d.ts +45 -0
  63. package/lib/types/rtc-rtp-sender.d.ts +98 -0
  64. package/lib/types/rtc-rtp-transceiver.d.ts +20 -0
  65. package/lib/types/rtc-sctp-transport.d.ts +20 -0
  66. package/lib/types/rtc-session-description.d.ts +18 -0
  67. package/lib/types/rtc-stats-report.d.ts +22 -0
  68. package/lib/types/rtc-track-event.d.ts +18 -0
  69. package/lib/types/rtp-capabilities.d.ts +3 -0
  70. package/lib/types/tee-multiplexer.d.ts +25 -0
  71. package/lib/types/webrtc.spec.d.ts +2 -0
  72. package/lib/types/wpt-helpers.d.ts +30 -0
  73. package/lib/types/wpt-media.spec.d.ts +2 -0
  74. package/lib/types/wpt.spec.d.ts +2 -0
  75. package/package.json +74 -0
  76. package/src/get-user-media.ts +131 -0
  77. package/src/gst-enum-maps.ts +125 -0
  78. package/src/gst-init.ts +52 -0
  79. package/src/gst-stats-parser.ts +137 -0
  80. package/src/gst-utils.ts +41 -0
  81. package/src/index.ts +104 -0
  82. package/src/media-device-info.ts +33 -0
  83. package/src/media-devices.ts +191 -0
  84. package/src/media-stream-track.ts +159 -0
  85. package/src/media-stream.ts +96 -0
  86. package/src/register/data-channel.ts +11 -0
  87. package/src/register/error.ts +11 -0
  88. package/src/register/media-devices.ts +10 -0
  89. package/src/register/media.ts +15 -0
  90. package/src/register/peer-connection.ts +20 -0
  91. package/src/register.spec.ts +55 -0
  92. package/src/register.ts +10 -0
  93. package/src/rtc-certificate.ts +110 -0
  94. package/src/rtc-data-channel.ts +284 -0
  95. package/src/rtc-dtls-transport.ts +48 -0
  96. package/src/rtc-dtmf-sender.ts +146 -0
  97. package/src/rtc-error.ts +49 -0
  98. package/src/rtc-events.ts +64 -0
  99. package/src/rtc-ice-candidate.ts +115 -0
  100. package/src/rtc-ice-transport.ts +104 -0
  101. package/src/rtc-peer-connection.ts +1017 -0
  102. package/src/rtc-rtp-receiver.ts +122 -0
  103. package/src/rtc-rtp-sender.ts +444 -0
  104. package/src/rtc-rtp-transceiver.ts +127 -0
  105. package/src/rtc-sctp-transport.ts +48 -0
  106. package/src/rtc-session-description.ts +64 -0
  107. package/src/rtc-stats-report.ts +39 -0
  108. package/src/rtc-track-event.ts +45 -0
  109. package/src/rtp-capabilities.ts +48 -0
  110. package/src/tee-multiplexer.ts +75 -0
  111. package/src/test.mts +11 -0
  112. package/src/webrtc.spec.ts +1186 -0
  113. package/src/wpt-helpers.ts +156 -0
  114. package/src/wpt-media.spec.ts +1154 -0
  115. package/src/wpt.spec.ts +1136 -0
  116. package/tsconfig.json +36 -0
  117. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,110 @@
1
+ // W3C RTCCertificate for GJS.
2
+ //
3
+ // Lightweight certificate representation for RTCPeerConnection.
4
+ // generateCertificate() validates algorithm parameters and returns a
5
+ // certificate with a future expiry. The actual DTLS certificate used by
6
+ // webrtcbin is generated internally by GStreamer — this class provides
7
+ // the W3C API surface for certificate management.
8
+ //
9
+ // Reference: W3C WebRTC spec § 4.10
10
+ // Reference: refs/wpt/webrtc/RTCPeerConnection-generateCertificate.html
11
+ // Reference: refs/wpt/webrtc/RTCCertificate.html
12
+
13
+ import GLib from 'gi://GLib?version=2.0';
14
+
15
+ export interface RTCDtlsFingerprint {
16
+ algorithm?: string;
17
+ value?: string;
18
+ }
19
+
20
+ // Default certificate lifetime: 30 days
21
+ const DEFAULT_EXPIRY_MS = 30 * 24 * 60 * 60 * 1000;
22
+
23
+ export class RTCCertificate {
24
+ readonly expires: number; // DOMTimeStamp (ms since epoch)
25
+ private _fingerprints: RTCDtlsFingerprint[];
26
+ private _algorithm: string;
27
+
28
+ /** @internal — use RTCPeerConnection.generateCertificate() */
29
+ constructor(algorithm: string, expires: number, fingerprints: RTCDtlsFingerprint[]) {
30
+ this._algorithm = algorithm;
31
+ this.expires = expires;
32
+ this._fingerprints = fingerprints;
33
+ }
34
+
35
+ getFingerprints(): RTCDtlsFingerprint[] {
36
+ return [...this._fingerprints];
37
+ }
38
+
39
+ /** @internal — the algorithm name for debugging/inspection */
40
+ get _algorithmName(): string {
41
+ return this._algorithm;
42
+ }
43
+ }
44
+
45
+ export type AlgorithmIdentifier = string | Record<string, unknown>;
46
+
47
+ /**
48
+ * Generate a self-signed certificate for use with RTCPeerConnection.
49
+ * Supports ECDSA P-256 and RSASSA-PKCS1-v1_5 with SHA-256.
50
+ *
51
+ * The actual DTLS certificate used by webrtcbin is generated internally
52
+ * by GStreamer — this provides the spec-compliant JS API surface.
53
+ */
54
+ export async function generateCertificate(keygenAlgorithm: AlgorithmIdentifier): Promise<RTCCertificate> {
55
+ let name: string;
56
+
57
+ if (typeof keygenAlgorithm === 'string') {
58
+ name = keygenAlgorithm.toLowerCase();
59
+ } else if (keygenAlgorithm && typeof keygenAlgorithm === 'object' && typeof keygenAlgorithm.name === 'string') {
60
+ name = (keygenAlgorithm.name as string).toLowerCase();
61
+ } else {
62
+ throw new DOMException(
63
+ 'generateCertificate: algorithm must have a name property',
64
+ 'NotSupportedError',
65
+ );
66
+ }
67
+
68
+ // Validate supported algorithms
69
+ if (name === 'ecdsa') {
70
+ const curve = (keygenAlgorithm as any).namedCurve;
71
+ if (curve && curve !== 'P-256') {
72
+ throw new DOMException(
73
+ `generateCertificate: unsupported ECDSA curve '${curve}'`,
74
+ 'NotSupportedError',
75
+ );
76
+ }
77
+ } else if (name === 'rsassa-pkcs1-v1_5') {
78
+ const hash = (keygenAlgorithm as any).hash;
79
+ const hashName = typeof hash === 'string' ? hash : hash?.name;
80
+ if (hashName && hashName.toUpperCase() === 'SHA-1') {
81
+ throw new DOMException(
82
+ 'generateCertificate: SHA-1 is not supported for RSA certificates',
83
+ 'NotSupportedError',
84
+ );
85
+ }
86
+ } else {
87
+ throw new DOMException(
88
+ `generateCertificate: unsupported algorithm '${name}'`,
89
+ 'NotSupportedError',
90
+ );
91
+ }
92
+
93
+ // Generate a pseudo-random fingerprint using GLib
94
+ const uuid = GLib.uuid_string_random();
95
+ const checksum = GLib.Checksum.new(GLib.ChecksumType.SHA256);
96
+ checksum!.update(new TextEncoder().encode(uuid + Date.now()));
97
+ const fingerprintHex = checksum!.get_string()!;
98
+
99
+ // Format as colon-separated hex pairs (sha-256 fingerprint format)
100
+ const formatted = fingerprintHex.slice(0, 64)
101
+ .match(/.{2}/g)!
102
+ .join(':')
103
+ .toUpperCase();
104
+
105
+ const expires = Date.now() + DEFAULT_EXPIRY_MS;
106
+
107
+ return new RTCCertificate(name, expires, [
108
+ { algorithm: 'sha-256', value: formatted },
109
+ ]);
110
+ }
@@ -0,0 +1,284 @@
1
+ // RTCDataChannel — W3C WebRTC data channel backed by GstWebRTC.WebRTCDataChannel.
2
+ //
3
+ // Reference: refs/node-gst-webrtc/src/webrtc/RTCDataChannel.ts (ISC) +
4
+ // refs/node-datachannel/src/polyfill/RTCDataChannel.ts (MIT)
5
+ //
6
+ // Uses `@gjsify/webrtc-native`'s DataChannelBridge to marshal the six
7
+ // GstWebRTCDataChannel signals (on-open / on-close / on-error /
8
+ // on-message-string / on-message-data / on-buffered-amount-low) from the
9
+ // GStreamer streaming thread onto the GLib main context. The raw `connect`
10
+ // path is unusable because GJS blocks JS callbacks invoked from non-main
11
+ // threads — see STATUS.md "WebRTC Status".
12
+
13
+ import GLib from 'gi://GLib?version=2.0';
14
+ import type GstWebRTC from 'gi://GstWebRTC?version=1.0';
15
+
16
+ import { DataChannelBridge, type DataChannelBridge as DataChannelBridgeType } from '@gjsify/webrtc-native';
17
+
18
+ import { RTCError } from './rtc-error.js';
19
+ import { RTCErrorEvent } from './rtc-events.js';
20
+
21
+ export type RTCDataChannelState = 'connecting' | 'open' | 'closing' | 'closed';
22
+ export type BinaryType = 'blob' | 'arraybuffer';
23
+
24
+ // NOTE: the GstWebRTCDataChannelState C enum is 1-based (CONNECTING=1 …
25
+ // CLOSED=4), but the TypeScript @girs/gstwebrtc-1.0 declaration omits the
26
+ // explicit initialiser so the `.d.ts` mis-infers 0-based values. Map
27
+ // against the real runtime values.
28
+ const STATE_MAP: Record<number, RTCDataChannelState> = {
29
+ 1: 'connecting',
30
+ 2: 'open',
31
+ 3: 'closing',
32
+ 4: 'closed',
33
+ };
34
+
35
+ type EventHandler<E extends Event = Event> = ((this: RTCDataChannel, ev: E) => any) | null;
36
+
37
+ /** Convert a JS typed array / ArrayBuffer to a GLib.Bytes. */
38
+ function toGBytes(buffer: ArrayBuffer | ArrayBufferView): GLib.Bytes {
39
+ let view: Uint8Array;
40
+ if (ArrayBuffer.isView(buffer)) {
41
+ view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
42
+ } else {
43
+ view = new Uint8Array(buffer);
44
+ }
45
+ return new GLib.Bytes(view);
46
+ }
47
+
48
+ /** Convert a GLib.Bytes payload to an ArrayBuffer. */
49
+ function bytesToArrayBuffer(bytes: GLib.Bytes): ArrayBuffer {
50
+ const arr = (bytes as any).toArray?.();
51
+ if (arr instanceof Uint8Array) {
52
+ return (arr.buffer as ArrayBuffer).slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
53
+ }
54
+ const data = (bytes as any).get_data?.();
55
+ if (data instanceof Uint8Array) {
56
+ return (data.buffer as ArrayBuffer).slice(data.byteOffset, data.byteOffset + data.byteLength);
57
+ }
58
+ return new ArrayBuffer(0);
59
+ }
60
+
61
+ export class RTCDataChannel extends EventTarget {
62
+ private readonly _native: GstWebRTC.WebRTCDataChannel;
63
+ private readonly _bridge: DataChannelBridgeType;
64
+ private _binaryType: BinaryType = 'arraybuffer';
65
+ private _bufferedAmount = 0;
66
+ private _closed = false;
67
+
68
+ // `on<event>` attribute handlers — W3C requires both addEventListener and on*.
69
+ private _onopen: EventHandler = null;
70
+ private _onclose: EventHandler = null;
71
+ private _onerror: EventHandler<RTCErrorEvent> = null;
72
+ private _onmessage: EventHandler<MessageEvent> = null;
73
+ private _onbufferedamountlow: EventHandler = null;
74
+ private _onclosing: EventHandler = null;
75
+
76
+ /**
77
+ * @internal
78
+ * Accepts either a raw GstWebRTCDataChannel (for locally-created channels)
79
+ * or a pre-made DataChannelBridge (for remotely-originated channels that
80
+ * the WebrtcbinBridge already wrapped on the streaming thread to avoid
81
+ * missing early messages).
82
+ */
83
+ constructor(source: GstWebRTC.WebRTCDataChannel | DataChannelBridgeType) {
84
+ super();
85
+ if ((source as DataChannelBridgeType).channel !== undefined && (source as any).dispose_bridge) {
86
+ this._bridge = source as DataChannelBridgeType;
87
+ this._native = this._bridge.channel as unknown as GstWebRTC.WebRTCDataChannel;
88
+ } else {
89
+ this._native = source as GstWebRTC.WebRTCDataChannel;
90
+ this._bridge = new (DataChannelBridge as any)({ channel: this._native });
91
+ }
92
+
93
+ this._bridge.connect('opened', () => this._handleOpen());
94
+ this._bridge.connect('closed', () => this._handleClose());
95
+ this._bridge.connect('error-occurred', (_b, message) => this._handleError(message));
96
+ this._bridge.connect('message-string', (_b, data) => this._handleString(data));
97
+ this._bridge.connect('message-data', (_b, data) => this._handleData(data));
98
+ this._bridge.connect('buffered-amount-low', () => this._handleBufferedAmountLow());
99
+ this._bridge.connect('ready-state-changed', () => this._handleReadyStateChange());
100
+ }
101
+
102
+ // ---- Properties --------------------------------------------------------
103
+
104
+ get label(): string { return this._native.label; }
105
+ get ordered(): boolean { return this._native.ordered; }
106
+ get protocol(): string { return this._native.protocol; }
107
+ get negotiated(): boolean { return this._native.negotiated; }
108
+ get id(): number | null { return this._native.id >= 0 ? this._native.id : null; }
109
+
110
+ get maxPacketLifeTime(): number | null {
111
+ const v = this._native.max_packet_lifetime;
112
+ return v >= 0 ? v : null;
113
+ }
114
+
115
+ get maxRetransmits(): number | null {
116
+ const v = this._native.max_retransmits;
117
+ return v >= 0 ? v : null;
118
+ }
119
+
120
+ get readyState(): RTCDataChannelState {
121
+ if (this._closed) return 'closed';
122
+ return STATE_MAP[this._native.ready_state as number] ?? 'connecting';
123
+ }
124
+
125
+ get bufferedAmount(): number {
126
+ try {
127
+ return Number(this._native.buffered_amount) || this._bufferedAmount;
128
+ } catch {
129
+ return this._bufferedAmount;
130
+ }
131
+ }
132
+
133
+ get bufferedAmountLowThreshold(): number {
134
+ return Number(this._native.buffered_amount_low_threshold) || 0;
135
+ }
136
+ set bufferedAmountLowThreshold(v: number) {
137
+ this._native.buffered_amount_low_threshold = v;
138
+ }
139
+
140
+ get binaryType(): BinaryType { return this._binaryType; }
141
+ set binaryType(v: BinaryType) {
142
+ // W3C §6.2 (and WPT RTCDataChannel-binaryType tests): invalid
143
+ // values must be silently ignored — keep the previous value.
144
+ // See: refs/wpt/webrtc/RTCDataChannel-binaryType.window.js
145
+ if (v !== 'arraybuffer' && v !== 'blob') return;
146
+ if (v === 'blob' && typeof (globalThis as any).Blob === 'undefined') {
147
+ const DOMExc = (globalThis as any).DOMException;
148
+ const msg = `binaryType 'blob' requires globalThis.Blob. Import '@gjsify/buffer/register' to provide it.`;
149
+ throw DOMExc ? new DOMExc(msg, 'NotSupportedError') : new Error(msg);
150
+ }
151
+ this._binaryType = v;
152
+ }
153
+
154
+ // ---- on<event> attribute accessors -------------------------------------
155
+
156
+ get onopen() { return this._onopen; }
157
+ set onopen(h: EventHandler) { this._onopen = h; }
158
+ get onclose() { return this._onclose; }
159
+ set onclose(h: EventHandler) { this._onclose = h; }
160
+ get onclosing() { return this._onclosing; }
161
+ set onclosing(h: EventHandler) { this._onclosing = h; }
162
+ get onerror() { return this._onerror; }
163
+ set onerror(h: EventHandler<RTCErrorEvent>) { this._onerror = h; }
164
+ get onmessage() { return this._onmessage; }
165
+ set onmessage(h: EventHandler<MessageEvent>) { this._onmessage = h; }
166
+ get onbufferedamountlow() { return this._onbufferedamountlow; }
167
+ set onbufferedamountlow(h: EventHandler) { this._onbufferedamountlow = h; }
168
+
169
+ // ---- Methods -----------------------------------------------------------
170
+
171
+ send(data: string | ArrayBuffer | ArrayBufferView | Blob): void {
172
+ const state = this.readyState;
173
+ if (state !== 'open') {
174
+ const DOMExc = (globalThis as any).DOMException;
175
+ const msg = `RTCDataChannel.send: readyState is '${state}', expected 'open'`;
176
+ throw DOMExc ? new DOMExc(msg, 'InvalidStateError') : new Error(msg);
177
+ }
178
+
179
+ if (typeof data === 'string') {
180
+ this._native.send_string(data);
181
+ this._bufferedAmount += new TextEncoder().encode(data).byteLength;
182
+ return;
183
+ }
184
+
185
+ if (typeof (globalThis as any).Blob !== 'undefined' && data instanceof (globalThis as any).Blob) {
186
+ const blob = data as Blob;
187
+ blob.arrayBuffer().then((buf) => {
188
+ try {
189
+ this._native.send_data(toGBytes(buf));
190
+ this._bufferedAmount += buf.byteLength;
191
+ } catch {
192
+ /* channel may have closed mid-flight */
193
+ }
194
+ });
195
+ return;
196
+ }
197
+
198
+ if (ArrayBuffer.isView(data)) {
199
+ const bytes = toGBytes(data);
200
+ this._native.send_data(bytes);
201
+ this._bufferedAmount += data.byteLength;
202
+ return;
203
+ }
204
+ if (data instanceof ArrayBuffer) {
205
+ const bytes = toGBytes(data);
206
+ this._native.send_data(bytes);
207
+ this._bufferedAmount += data.byteLength;
208
+ return;
209
+ }
210
+
211
+ throw new TypeError('RTCDataChannel.send: unsupported data type');
212
+ }
213
+
214
+ close(): void {
215
+ if (this._closed) return;
216
+ try { this._native.close(); } catch { /* ignore */ }
217
+ this._disconnectSignals();
218
+ this._closed = true;
219
+ }
220
+
221
+ /** @internal */
222
+ _disconnectSignals(): void {
223
+ try { this._bridge.dispose_bridge(); } catch { /* ignore */ }
224
+ }
225
+
226
+ // ---- Signal → event translators ---------------------------------------
227
+ // Already running on the main context (DataChannelBridge did the hop).
228
+
229
+ private _handleOpen(): void {
230
+ const ev = new Event('open');
231
+ this._onopen?.call(this, ev);
232
+ this.dispatchEvent(ev);
233
+ }
234
+
235
+ private _handleClose(): void {
236
+ this._closed = true;
237
+ const ev = new Event('close');
238
+ this._onclose?.call(this, ev);
239
+ this.dispatchEvent(ev);
240
+ }
241
+
242
+ private _handleError(message: string): void {
243
+ const rtcErr = new RTCError(
244
+ { errorDetail: 'data-channel-failure' },
245
+ message || 'RTCDataChannel error',
246
+ );
247
+ const ev = new RTCErrorEvent('error', { error: rtcErr });
248
+ this._onerror?.call(this, ev);
249
+ this.dispatchEvent(ev);
250
+ }
251
+
252
+ private _handleString(data: string): void {
253
+ const ev = new MessageEvent('message', { data });
254
+ this._onmessage?.call(this, ev);
255
+ this.dispatchEvent(ev);
256
+ }
257
+
258
+ private _handleData(bytes: GLib.Bytes): void {
259
+ if (!bytes) return;
260
+ const buf = bytesToArrayBuffer(bytes);
261
+ let data: ArrayBuffer | Blob = buf;
262
+ if (this._binaryType === 'blob' && typeof (globalThis as any).Blob !== 'undefined') {
263
+ data = new (globalThis as any).Blob([buf]);
264
+ }
265
+ const ev = new MessageEvent('message', { data });
266
+ this._onmessage?.call(this, ev);
267
+ this.dispatchEvent(ev);
268
+ }
269
+
270
+ private _handleBufferedAmountLow(): void {
271
+ this._bufferedAmount = Number(this._native.buffered_amount) || 0;
272
+ const ev = new Event('bufferedamountlow');
273
+ this._onbufferedamountlow?.call(this, ev);
274
+ this.dispatchEvent(ev);
275
+ }
276
+
277
+ private _handleReadyStateChange(): void {
278
+ if (this.readyState === 'closing') {
279
+ const ev = new Event('closing');
280
+ this._onclosing?.call(this, ev);
281
+ this.dispatchEvent(ev);
282
+ }
283
+ }
284
+ }
@@ -0,0 +1,48 @@
1
+ // W3C RTCDtlsTransport for GJS.
2
+ //
3
+ // Thin wrapper reflecting the DTLS state of a webrtcbin transport.
4
+ // State is updated by RTCPeerConnection via the WebrtcbinBridge.
5
+ //
6
+ // Reference: W3C WebRTC spec § 5.5
7
+ // Reference: refs/wpt/webrtc/RTCDtlsTransport-state.html
8
+
9
+ import '@gjsify/dom-events/register/event-target';
10
+
11
+ import { RTCIceTransport } from './rtc-ice-transport.js';
12
+
13
+ export type RTCDtlsTransportState = 'new' | 'connecting' | 'connected' | 'closed' | 'failed';
14
+
15
+ type EventHandler = ((ev: Event) => void) | null;
16
+
17
+ export class RTCDtlsTransport extends EventTarget {
18
+ readonly iceTransport: RTCIceTransport;
19
+ private _state: RTCDtlsTransportState = 'new';
20
+
21
+ private _onstatechange: EventHandler = null;
22
+ private _onerror: EventHandler = null;
23
+
24
+ constructor(iceTransport: RTCIceTransport) {
25
+ super();
26
+ this.iceTransport = iceTransport;
27
+ }
28
+
29
+ get state(): RTCDtlsTransportState { return this._state; }
30
+
31
+ get onstatechange(): EventHandler { return this._onstatechange; }
32
+ set onstatechange(v: EventHandler) { this._onstatechange = v; }
33
+ get onerror(): EventHandler { return this._onerror; }
34
+ set onerror(v: EventHandler) { this._onerror = v; }
35
+
36
+ getRemoteCertificates(): ArrayBuffer[] { return []; }
37
+
38
+ // ---- Internal setters (called by RTCPeerConnection) ---------------------
39
+
40
+ /** @internal */
41
+ _setState(state: RTCDtlsTransportState): void {
42
+ if (this._state === state) return;
43
+ this._state = state;
44
+ const ev = new Event('statechange');
45
+ this._onstatechange?.call(this, ev);
46
+ this.dispatchEvent(ev);
47
+ }
48
+ }
@@ -0,0 +1,146 @@
1
+ // W3C RTCDTMFSender for GJS.
2
+ //
3
+ // Implements tone insertion, validation, and tonechange event dispatch.
4
+ // Actual DTMF RTP packet generation requires GStreamer dtmfsrc integration
5
+ // (Phase 4.6+); the timer-based event loop is spec-compliant regardless.
6
+ //
7
+ // Reference: W3C WebRTC spec § 7
8
+ // Reference: refs/wpt/webrtc/RTCDTMFSender-insertDTMF.https.html
9
+ // Reference: refs/wpt/webrtc/RTCDTMFSender-helper.js
10
+
11
+ import '@gjsify/dom-events/register/event-target';
12
+
13
+ const VALID_DTMF_CHARS = new Set('0123456789ABCDabcd#*,');
14
+ const MIN_DURATION = 40;
15
+ const MAX_DURATION = 6000;
16
+ const DEFAULT_DURATION = 100;
17
+ const MIN_INTER_TONE_GAP = 30;
18
+ const DEFAULT_INTER_TONE_GAP = 70;
19
+ const COMMA_DELAY = 2000;
20
+
21
+ type EventHandler = ((ev: Event) => void) | null;
22
+
23
+ export interface RTCDTMFToneChangeEventInit extends EventInit {
24
+ tone?: string;
25
+ }
26
+
27
+ export class RTCDTMFToneChangeEvent extends Event {
28
+ readonly tone: string;
29
+
30
+ constructor(type: string, init: RTCDTMFToneChangeEventInit = {}) {
31
+ super(type, init);
32
+ this.tone = init.tone ?? '';
33
+ }
34
+ }
35
+
36
+ export class RTCDTMFSender extends EventTarget {
37
+ private _toneBuffer = '';
38
+ private _duration = DEFAULT_DURATION;
39
+ private _interToneGap = DEFAULT_INTER_TONE_GAP;
40
+ private _timerId: ReturnType<typeof setTimeout> | null = null;
41
+ private _canInsert = true;
42
+ private _ontonechange: EventHandler = null;
43
+
44
+ /** @internal — back-references set by RTCRtpSender */
45
+ _isStopped: () => boolean = () => false;
46
+ _getCurrentDirection: () => string | null = () => null;
47
+
48
+ get toneBuffer(): string { return this._toneBuffer; }
49
+ get canInsertDTMF(): boolean { return this._canInsert; }
50
+
51
+ get ontonechange(): EventHandler { return this._ontonechange; }
52
+ set ontonechange(v: EventHandler) { this._ontonechange = v; }
53
+
54
+ insertDTMF(tones: string, duration?: number, interToneGap?: number): void {
55
+ // Step 3: If transceiver.stopped is true, throw InvalidStateError
56
+ if (this._isStopped()) {
57
+ throw new DOMException(
58
+ "Failed to execute 'insertDTMF': The associated transceiver is stopped",
59
+ 'InvalidStateError',
60
+ );
61
+ }
62
+
63
+ // Step 4: If currentDirection is recvonly or inactive, throw InvalidStateError
64
+ const dir = this._getCurrentDirection();
65
+ if (dir === 'recvonly' || dir === 'inactive') {
66
+ throw new DOMException(
67
+ "Failed to execute 'insertDTMF': The associated transceiver direction does not allow sending",
68
+ 'InvalidStateError',
69
+ );
70
+ }
71
+
72
+ // Step 6: If tones contains invalid characters, throw InvalidCharacterError
73
+ for (const ch of tones) {
74
+ if (!VALID_DTMF_CHARS.has(ch)) {
75
+ throw new DOMException(
76
+ `Failed to execute 'insertDTMF': Invalid DTMF character '${ch}'`,
77
+ 'InvalidCharacterError',
78
+ );
79
+ }
80
+ }
81
+
82
+ // Normalize a-d to uppercase
83
+ tones = tones.replace(/[a-d]/g, c => c.toUpperCase());
84
+
85
+ // Clamp duration to [40, 6000]
86
+ const d = duration ?? DEFAULT_DURATION;
87
+ this._duration = Math.max(MIN_DURATION, Math.min(MAX_DURATION, d));
88
+
89
+ // Clamp interToneGap to [30, ...]
90
+ const g = interToneGap ?? DEFAULT_INTER_TONE_GAP;
91
+ this._interToneGap = Math.max(MIN_INTER_TONE_GAP, g);
92
+
93
+ // Step 9: Set the object's toneBuffer to tones
94
+ this._toneBuffer = tones;
95
+
96
+ // Step 10: If toneBuffer was empty before, start the playout task
97
+ if (this._timerId !== null) {
98
+ clearTimeout(this._timerId);
99
+ this._timerId = null;
100
+ }
101
+ if (tones.length > 0) {
102
+ this._scheduleNextTone(0);
103
+ }
104
+ }
105
+
106
+ private _scheduleNextTone(delay: number): void {
107
+ this._timerId = setTimeout(() => {
108
+ this._timerId = null;
109
+ this._playNextTone();
110
+ }, delay);
111
+ }
112
+
113
+ private _playNextTone(): void {
114
+ if (this._toneBuffer.length === 0) {
115
+ // Spec: fire one final tonechange with empty tone
116
+ this._fireToneChange('');
117
+ return;
118
+ }
119
+
120
+ const tone = this._toneBuffer[0];
121
+ this._toneBuffer = this._toneBuffer.slice(1);
122
+
123
+ // Fire tonechange event for this tone
124
+ this._fireToneChange(tone);
125
+
126
+ // Schedule next tone after duration + interToneGap (comma = 2s delay)
127
+ const delay = tone === ',' ? COMMA_DELAY : (this._duration + this._interToneGap);
128
+ this._scheduleNextTone(delay);
129
+ }
130
+
131
+ private _fireToneChange(tone: string): void {
132
+ const ev = new RTCDTMFToneChangeEvent('tonechange', { tone });
133
+ this._ontonechange?.call(this, ev);
134
+ this.dispatchEvent(ev);
135
+ }
136
+
137
+ /** @internal — called by RTCRtpSender on cleanup */
138
+ _stop(): void {
139
+ if (this._timerId !== null) {
140
+ clearTimeout(this._timerId);
141
+ this._timerId = null;
142
+ }
143
+ this._toneBuffer = '';
144
+ this._canInsert = false;
145
+ }
146
+ }
@@ -0,0 +1,49 @@
1
+ // RTCError extends DOMException per W3C WebRTC spec §7.
2
+ //
3
+ // Reference: refs/node-datachannel/src/polyfill/RTCError.ts (MIT) +
4
+ // https://www.w3.org/TR/webrtc/#rtcerror-interface
5
+ //
6
+ // CLAUDE.md Rule 8 exception: the class below extends `DOMException`
7
+ // (a global). Seed the global before the class body is evaluated.
8
+
9
+ import '@gjsify/dom-exception/register';
10
+
11
+ export type RTCErrorDetailType =
12
+ | 'data-channel-failure'
13
+ | 'dtls-failure'
14
+ | 'fingerprint-failure'
15
+ | 'sctp-failure'
16
+ | 'sdp-syntax-error'
17
+ | 'hardware-encoder-not-available'
18
+ | 'hardware-encoder-error';
19
+
20
+ export interface RTCErrorInit {
21
+ errorDetail: RTCErrorDetailType;
22
+ sdpLineNumber?: number | null;
23
+ sctpCauseCode?: number | null;
24
+ receivedAlert?: number | null;
25
+ sentAlert?: number | null;
26
+ httpRequestStatusCode?: number | null;
27
+ }
28
+
29
+ export class RTCError extends DOMException {
30
+ readonly errorDetail: RTCErrorDetailType;
31
+ readonly sdpLineNumber: number | null;
32
+ readonly sctpCauseCode: number | null;
33
+ readonly receivedAlert: number | null;
34
+ readonly sentAlert: number | null;
35
+ readonly httpRequestStatusCode: number | null;
36
+
37
+ constructor(init: RTCErrorInit, message?: string) {
38
+ super(message ?? init.errorDetail, 'OperationError');
39
+ if (!init || !init.errorDetail) {
40
+ throw new TypeError('RTCError: errorDetail is required');
41
+ }
42
+ this.errorDetail = init.errorDetail;
43
+ this.sdpLineNumber = init.sdpLineNumber ?? null;
44
+ this.sctpCauseCode = init.sctpCauseCode ?? null;
45
+ this.receivedAlert = init.receivedAlert ?? null;
46
+ this.sentAlert = init.sentAlert ?? null;
47
+ this.httpRequestStatusCode = init.httpRequestStatusCode ?? null;
48
+ }
49
+ }