@gjsify/webrtc 0.4.0 → 0.4.4
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.
- package/package.json +73 -70
- package/src/get-user-media.ts +0 -131
- package/src/gst-enum-maps.ts +0 -125
- package/src/gst-init.ts +0 -49
- package/src/gst-stats-parser.ts +0 -137
- package/src/gst-utils.ts +0 -41
- package/src/index.ts +0 -104
- package/src/internal/gst-types.ts +0 -122
- package/src/media-device-info.ts +0 -33
- package/src/media-devices.ts +0 -191
- package/src/media-stream-track.ts +0 -159
- package/src/media-stream.ts +0 -96
- package/src/register/data-channel.ts +0 -11
- package/src/register/error.ts +0 -11
- package/src/register/media-devices.ts +0 -10
- package/src/register/media.ts +0 -15
- package/src/register/peer-connection.ts +0 -20
- package/src/register.spec.ts +0 -55
- package/src/register.ts +0 -10
- package/src/rtc-certificate.ts +0 -110
- package/src/rtc-data-channel.ts +0 -283
- package/src/rtc-dtls-transport.ts +0 -48
- package/src/rtc-dtmf-sender.ts +0 -146
- package/src/rtc-error.ts +0 -49
- package/src/rtc-events.ts +0 -64
- package/src/rtc-ice-candidate.ts +0 -115
- package/src/rtc-ice-transport.ts +0 -104
- package/src/rtc-peer-connection.ts +0 -1039
- package/src/rtc-rtp-receiver.ts +0 -122
- package/src/rtc-rtp-sender.ts +0 -471
- package/src/rtc-rtp-transceiver.ts +0 -131
- package/src/rtc-sctp-transport.ts +0 -48
- package/src/rtc-session-description.ts +0 -64
- package/src/rtc-stats-report.ts +0 -39
- package/src/rtc-track-event.ts +0 -45
- package/src/rtp-capabilities.ts +0 -48
- package/src/tee-multiplexer.ts +0 -75
- package/src/test.mts +0 -11
- package/src/webrtc.spec.ts +0 -1186
- package/src/wpt-helpers.ts +0 -156
- package/src/wpt-media.spec.ts +0 -1154
- package/src/wpt.spec.ts +0 -1136
- package/tsconfig.json +0 -36
- package/tsconfig.tsbuildinfo +0 -1
package/src/media-stream.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
// W3C MediaStream for GJS.
|
|
2
|
-
//
|
|
3
|
-
// Pure-JS collection container for MediaStreamTrack instances.
|
|
4
|
-
// No GStreamer pipeline integration — that is Phase 2.5.
|
|
5
|
-
//
|
|
6
|
-
// Reference: refs/node-gst-webrtc/src/media/MediaStream.ts (ISC)
|
|
7
|
-
// Reference: W3C MediaStream spec
|
|
8
|
-
|
|
9
|
-
import '@gjsify/dom-events/register/event-target';
|
|
10
|
-
|
|
11
|
-
import GLib from 'gi://GLib?version=2.0';
|
|
12
|
-
|
|
13
|
-
import { MediaStreamTrack } from './media-stream-track.js';
|
|
14
|
-
|
|
15
|
-
export class MediaStream extends EventTarget {
|
|
16
|
-
readonly id: string;
|
|
17
|
-
private _tracks = new Map<string, MediaStreamTrack>();
|
|
18
|
-
|
|
19
|
-
private _onaddtrack: ((ev: Event) => void) | null = null;
|
|
20
|
-
private _onremovetrack: ((ev: Event) => void) | null = null;
|
|
21
|
-
|
|
22
|
-
constructor(streamOrTracks?: MediaStream | MediaStreamTrack[]) {
|
|
23
|
-
super();
|
|
24
|
-
this.id = GLib.uuid_string_random();
|
|
25
|
-
|
|
26
|
-
if (streamOrTracks instanceof MediaStream) {
|
|
27
|
-
for (const track of streamOrTracks.getTracks()) {
|
|
28
|
-
this._tracks.set(track.id, track.clone());
|
|
29
|
-
}
|
|
30
|
-
} else if (Array.isArray(streamOrTracks)) {
|
|
31
|
-
for (const track of streamOrTracks) {
|
|
32
|
-
this._tracks.set(track.id, track);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
get active(): boolean {
|
|
38
|
-
for (const track of this._tracks.values()) {
|
|
39
|
-
if (track.readyState === 'live') return true;
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
get onaddtrack(): ((ev: Event) => void) | null { return this._onaddtrack; }
|
|
45
|
-
set onaddtrack(v: ((ev: Event) => void) | null) { this._onaddtrack = v; }
|
|
46
|
-
get onremovetrack(): ((ev: Event) => void) | null { return this._onremovetrack; }
|
|
47
|
-
set onremovetrack(v: ((ev: Event) => void) | null) { this._onremovetrack = v; }
|
|
48
|
-
|
|
49
|
-
getTracks(): MediaStreamTrack[] {
|
|
50
|
-
return [...this._tracks.values()];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
getAudioTracks(): MediaStreamTrack[] {
|
|
54
|
-
return this.getTracks().filter((t) => t.kind === 'audio');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
getVideoTracks(): MediaStreamTrack[] {
|
|
58
|
-
return this.getTracks().filter((t) => t.kind === 'video');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
getTrackById(id: string): MediaStreamTrack | null {
|
|
62
|
-
return this._tracks.get(id) ?? null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
addTrack(track: MediaStreamTrack): void {
|
|
66
|
-
if (this._tracks.has(track.id)) return;
|
|
67
|
-
this._tracks.set(track.id, track);
|
|
68
|
-
const ev = new MediaStreamTrackEvent('addtrack', { track });
|
|
69
|
-
this._onaddtrack?.call(this, ev);
|
|
70
|
-
this.dispatchEvent(ev);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
removeTrack(track: MediaStreamTrack): void {
|
|
74
|
-
if (!this._tracks.delete(track.id)) return;
|
|
75
|
-
const ev = new MediaStreamTrackEvent('removetrack', { track });
|
|
76
|
-
this._onremovetrack?.call(this, ev);
|
|
77
|
-
this.dispatchEvent(ev);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
clone(): MediaStream {
|
|
81
|
-
return new MediaStream(this);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface MediaStreamTrackEventInit extends EventInit {
|
|
86
|
-
track: MediaStreamTrack;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export class MediaStreamTrackEvent extends Event {
|
|
90
|
-
readonly track: MediaStreamTrack;
|
|
91
|
-
|
|
92
|
-
constructor(type: string, init: MediaStreamTrackEventInit) {
|
|
93
|
-
super(type, init);
|
|
94
|
-
this.track = init.track;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Registers: RTCDataChannel, RTCDataChannelEvent.
|
|
2
|
-
|
|
3
|
-
import { RTCDataChannel } from '../rtc-data-channel.js';
|
|
4
|
-
import { RTCDataChannelEvent } from '../rtc-events.js';
|
|
5
|
-
|
|
6
|
-
if (typeof (globalThis as any).RTCDataChannel === 'undefined') {
|
|
7
|
-
(globalThis as any).RTCDataChannel = RTCDataChannel;
|
|
8
|
-
}
|
|
9
|
-
if (typeof (globalThis as any).RTCDataChannelEvent === 'undefined') {
|
|
10
|
-
(globalThis as any).RTCDataChannelEvent = RTCDataChannelEvent;
|
|
11
|
-
}
|
package/src/register/error.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Registers: RTCError, RTCErrorEvent.
|
|
2
|
-
|
|
3
|
-
import { RTCError } from '../rtc-error.js';
|
|
4
|
-
import { RTCErrorEvent } from '../rtc-events.js';
|
|
5
|
-
|
|
6
|
-
if (typeof (globalThis as any).RTCError === 'undefined') {
|
|
7
|
-
(globalThis as any).RTCError = RTCError;
|
|
8
|
-
}
|
|
9
|
-
if (typeof (globalThis as any).RTCErrorEvent === 'undefined') {
|
|
10
|
-
(globalThis as any).RTCErrorEvent = RTCErrorEvent;
|
|
11
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Register navigator.mediaDevices on globalThis for GJS.
|
|
2
|
-
|
|
3
|
-
import { MediaDevices } from '../media-devices.js';
|
|
4
|
-
|
|
5
|
-
if (typeof (globalThis as any).navigator === 'undefined') {
|
|
6
|
-
(globalThis as any).navigator = {} as any;
|
|
7
|
-
}
|
|
8
|
-
if (typeof (globalThis as any).navigator.mediaDevices === 'undefined') {
|
|
9
|
-
(globalThis as any).navigator.mediaDevices = new MediaDevices();
|
|
10
|
-
}
|
package/src/register/media.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Registers: MediaStream, MediaStreamTrack, RTCTrackEvent.
|
|
2
|
-
|
|
3
|
-
import { MediaStream } from '../media-stream.js';
|
|
4
|
-
import { MediaStreamTrack } from '../media-stream-track.js';
|
|
5
|
-
import { RTCTrackEvent } from '../rtc-track-event.js';
|
|
6
|
-
|
|
7
|
-
if (typeof (globalThis as any).MediaStream === 'undefined') {
|
|
8
|
-
(globalThis as any).MediaStream = MediaStream;
|
|
9
|
-
}
|
|
10
|
-
if (typeof (globalThis as any).MediaStreamTrack === 'undefined') {
|
|
11
|
-
(globalThis as any).MediaStreamTrack = MediaStreamTrack;
|
|
12
|
-
}
|
|
13
|
-
if (typeof (globalThis as any).RTCTrackEvent === 'undefined') {
|
|
14
|
-
(globalThis as any).RTCTrackEvent = RTCTrackEvent;
|
|
15
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// Registers: RTCPeerConnection, RTCSessionDescription, RTCIceCandidate,
|
|
2
|
-
// RTCPeerConnectionIceEvent.
|
|
3
|
-
|
|
4
|
-
import { RTCPeerConnection } from '../rtc-peer-connection.js';
|
|
5
|
-
import { RTCSessionDescription } from '../rtc-session-description.js';
|
|
6
|
-
import { RTCIceCandidate } from '../rtc-ice-candidate.js';
|
|
7
|
-
import { RTCPeerConnectionIceEvent } from '../rtc-events.js';
|
|
8
|
-
|
|
9
|
-
if (typeof (globalThis as any).RTCPeerConnection === 'undefined') {
|
|
10
|
-
(globalThis as any).RTCPeerConnection = RTCPeerConnection;
|
|
11
|
-
}
|
|
12
|
-
if (typeof (globalThis as any).RTCSessionDescription === 'undefined') {
|
|
13
|
-
(globalThis as any).RTCSessionDescription = RTCSessionDescription;
|
|
14
|
-
}
|
|
15
|
-
if (typeof (globalThis as any).RTCIceCandidate === 'undefined') {
|
|
16
|
-
(globalThis as any).RTCIceCandidate = RTCIceCandidate;
|
|
17
|
-
}
|
|
18
|
-
if (typeof (globalThis as any).RTCPeerConnectionIceEvent === 'undefined') {
|
|
19
|
-
(globalThis as any).RTCPeerConnectionIceEvent = RTCPeerConnectionIceEvent;
|
|
20
|
-
}
|
package/src/register.spec.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// WebRTC globals registration tests — verifies the granular /register
|
|
2
|
-
// subpaths wire each identifier onto globalThis.
|
|
3
|
-
//
|
|
4
|
-
// These tests must run AFTER importing the register subpaths — in `test.mts`
|
|
5
|
-
// the register module is imported alongside the spec, so the assertions below
|
|
6
|
-
// run post-registration.
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect } from '@gjsify/unit';
|
|
9
|
-
|
|
10
|
-
import '@gjsify/webrtc/register';
|
|
11
|
-
|
|
12
|
-
export default async () => {
|
|
13
|
-
await describe('@gjsify/webrtc/register', async () => {
|
|
14
|
-
await it('registers RTCPeerConnection', async () => {
|
|
15
|
-
expect(typeof (globalThis as any).RTCPeerConnection).toBe('function');
|
|
16
|
-
});
|
|
17
|
-
await it('registers RTCSessionDescription', async () => {
|
|
18
|
-
expect(typeof (globalThis as any).RTCSessionDescription).toBe('function');
|
|
19
|
-
});
|
|
20
|
-
await it('registers RTCIceCandidate', async () => {
|
|
21
|
-
expect(typeof (globalThis as any).RTCIceCandidate).toBe('function');
|
|
22
|
-
});
|
|
23
|
-
await it('registers RTCPeerConnectionIceEvent', async () => {
|
|
24
|
-
expect(typeof (globalThis as any).RTCPeerConnectionIceEvent).toBe('function');
|
|
25
|
-
});
|
|
26
|
-
await it('registers RTCDataChannel', async () => {
|
|
27
|
-
expect(typeof (globalThis as any).RTCDataChannel).toBe('function');
|
|
28
|
-
});
|
|
29
|
-
await it('registers RTCDataChannelEvent', async () => {
|
|
30
|
-
expect(typeof (globalThis as any).RTCDataChannelEvent).toBe('function');
|
|
31
|
-
});
|
|
32
|
-
await it('registers RTCError', async () => {
|
|
33
|
-
expect(typeof (globalThis as any).RTCError).toBe('function');
|
|
34
|
-
});
|
|
35
|
-
await it('registers RTCErrorEvent', async () => {
|
|
36
|
-
expect(typeof (globalThis as any).RTCErrorEvent).toBe('function');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
await it('RTCError extends DOMException', async () => {
|
|
40
|
-
const err = new (globalThis as any).RTCError({ errorDetail: 'data-channel-failure' }, 'test');
|
|
41
|
-
const DOMExceptionCtor = (globalThis as any).DOMException;
|
|
42
|
-
if (DOMExceptionCtor) {
|
|
43
|
-
expect(err instanceof DOMExceptionCtor).toBeTruthy();
|
|
44
|
-
}
|
|
45
|
-
expect(err.errorDetail).toBe('data-channel-failure');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
await it('RTCPeerConnectionIceEvent is a subclass of Event', async () => {
|
|
49
|
-
const RTCPeerConnectionIceEventCtor = (globalThis as any).RTCPeerConnectionIceEvent;
|
|
50
|
-
const ev = new RTCPeerConnectionIceEventCtor('icecandidate', { candidate: null });
|
|
51
|
-
expect(ev instanceof Event).toBeTruthy();
|
|
52
|
-
expect(ev.candidate).toBeNull();
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
};
|
package/src/register.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Catch-all side-effect module: registers all WebRTC globals on GJS.
|
|
2
|
-
// Prefer granular imports (e.g. '@gjsify/webrtc/register/data-channel')
|
|
3
|
-
// when only specific globals are needed — the --globals auto mode does this
|
|
4
|
-
// automatically.
|
|
5
|
-
|
|
6
|
-
import './register/peer-connection.js';
|
|
7
|
-
import './register/data-channel.js';
|
|
8
|
-
import './register/error.js';
|
|
9
|
-
import './register/media.js';
|
|
10
|
-
import './register/media-devices.js';
|
package/src/rtc-certificate.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
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
|
-
}
|
package/src/rtc-data-channel.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
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
|
-
import { DOMException } from '@gjsify/dom-exception';
|
|
18
|
-
import { Blob } from '@gjsify/buffer';
|
|
19
|
-
|
|
20
|
-
import { RTCError } from './rtc-error.js';
|
|
21
|
-
import { RTCErrorEvent } from './rtc-events.js';
|
|
22
|
-
|
|
23
|
-
export type RTCDataChannelState = 'connecting' | 'open' | 'closing' | 'closed';
|
|
24
|
-
export type BinaryType = 'blob' | 'arraybuffer';
|
|
25
|
-
|
|
26
|
-
// NOTE: the GstWebRTCDataChannelState C enum is 1-based (CONNECTING=1 …
|
|
27
|
-
// CLOSED=4), but the TypeScript @girs/gstwebrtc-1.0 declaration omits the
|
|
28
|
-
// explicit initialiser so the `.d.ts` mis-infers 0-based values. Map
|
|
29
|
-
// against the real runtime values.
|
|
30
|
-
const STATE_MAP: Record<number, RTCDataChannelState> = {
|
|
31
|
-
1: 'connecting',
|
|
32
|
-
2: 'open',
|
|
33
|
-
3: 'closing',
|
|
34
|
-
4: 'closed',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type EventHandler<E extends Event = Event> = ((this: RTCDataChannel, ev: E) => any) | null;
|
|
38
|
-
|
|
39
|
-
/** Convert a JS typed array / ArrayBuffer to a GLib.Bytes. */
|
|
40
|
-
function toGBytes(buffer: ArrayBuffer | ArrayBufferView): GLib.Bytes {
|
|
41
|
-
let view: Uint8Array;
|
|
42
|
-
if (ArrayBuffer.isView(buffer)) {
|
|
43
|
-
view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
44
|
-
} else {
|
|
45
|
-
view = new Uint8Array(buffer);
|
|
46
|
-
}
|
|
47
|
-
return new GLib.Bytes(view);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Convert a GLib.Bytes payload to an ArrayBuffer. */
|
|
51
|
-
function bytesToArrayBuffer(bytes: GLib.Bytes): ArrayBuffer {
|
|
52
|
-
const arr = (bytes as any).toArray?.();
|
|
53
|
-
if (arr instanceof Uint8Array) {
|
|
54
|
-
return (arr.buffer as ArrayBuffer).slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
|
|
55
|
-
}
|
|
56
|
-
const data = (bytes as any).get_data?.();
|
|
57
|
-
if (data instanceof Uint8Array) {
|
|
58
|
-
return (data.buffer as ArrayBuffer).slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
59
|
-
}
|
|
60
|
-
return new ArrayBuffer(0);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export class RTCDataChannel extends EventTarget {
|
|
64
|
-
private readonly _native: GstWebRTC.WebRTCDataChannel;
|
|
65
|
-
private readonly _bridge: DataChannelBridgeType;
|
|
66
|
-
private _binaryType: BinaryType = 'arraybuffer';
|
|
67
|
-
private _bufferedAmount = 0;
|
|
68
|
-
private _closed = false;
|
|
69
|
-
|
|
70
|
-
// `on<event>` attribute handlers — W3C requires both addEventListener and on*.
|
|
71
|
-
private _onopen: EventHandler = null;
|
|
72
|
-
private _onclose: EventHandler = null;
|
|
73
|
-
private _onerror: EventHandler<RTCErrorEvent> = null;
|
|
74
|
-
private _onmessage: EventHandler<MessageEvent> = null;
|
|
75
|
-
private _onbufferedamountlow: EventHandler = null;
|
|
76
|
-
private _onclosing: EventHandler = null;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @internal
|
|
80
|
-
* Accepts either a raw GstWebRTCDataChannel (for locally-created channels)
|
|
81
|
-
* or a pre-made DataChannelBridge (for remotely-originated channels that
|
|
82
|
-
* the WebrtcbinBridge already wrapped on the streaming thread to avoid
|
|
83
|
-
* missing early messages).
|
|
84
|
-
*/
|
|
85
|
-
constructor(source: GstWebRTC.WebRTCDataChannel | DataChannelBridgeType) {
|
|
86
|
-
super();
|
|
87
|
-
if ((source as DataChannelBridgeType).channel !== undefined && (source as any).dispose_bridge) {
|
|
88
|
-
this._bridge = source as DataChannelBridgeType;
|
|
89
|
-
this._native = this._bridge.channel as unknown as GstWebRTC.WebRTCDataChannel;
|
|
90
|
-
} else {
|
|
91
|
-
this._native = source as GstWebRTC.WebRTCDataChannel;
|
|
92
|
-
this._bridge = new (DataChannelBridge as any)({ channel: this._native });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this._bridge.connect('opened', () => this._handleOpen());
|
|
96
|
-
this._bridge.connect('closed', () => this._handleClose());
|
|
97
|
-
this._bridge.connect('error-occurred', (_b, message) => this._handleError(message));
|
|
98
|
-
this._bridge.connect('message-string', (_b, data) => this._handleString(data));
|
|
99
|
-
this._bridge.connect('message-data', (_b, data) => this._handleData(data));
|
|
100
|
-
this._bridge.connect('buffered-amount-low', () => this._handleBufferedAmountLow());
|
|
101
|
-
this._bridge.connect('ready-state-changed', () => this._handleReadyStateChange());
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---- Properties --------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
get label(): string { return this._native.label; }
|
|
107
|
-
get ordered(): boolean { return this._native.ordered; }
|
|
108
|
-
get protocol(): string { return this._native.protocol; }
|
|
109
|
-
get negotiated(): boolean { return this._native.negotiated; }
|
|
110
|
-
get id(): number | null { return this._native.id >= 0 ? this._native.id : null; }
|
|
111
|
-
|
|
112
|
-
get maxPacketLifeTime(): number | null {
|
|
113
|
-
const v = this._native.max_packet_lifetime;
|
|
114
|
-
return v >= 0 ? v : null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
get maxRetransmits(): number | null {
|
|
118
|
-
const v = this._native.max_retransmits;
|
|
119
|
-
return v >= 0 ? v : null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
get readyState(): RTCDataChannelState {
|
|
123
|
-
if (this._closed) return 'closed';
|
|
124
|
-
return STATE_MAP[this._native.ready_state as number] ?? 'connecting';
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
get bufferedAmount(): number {
|
|
128
|
-
try {
|
|
129
|
-
return Number(this._native.buffered_amount) || this._bufferedAmount;
|
|
130
|
-
} catch {
|
|
131
|
-
return this._bufferedAmount;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
get bufferedAmountLowThreshold(): number {
|
|
136
|
-
return Number(this._native.buffered_amount_low_threshold) || 0;
|
|
137
|
-
}
|
|
138
|
-
set bufferedAmountLowThreshold(v: number) {
|
|
139
|
-
this._native.buffered_amount_low_threshold = v;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
get binaryType(): BinaryType { return this._binaryType; }
|
|
143
|
-
set binaryType(v: BinaryType) {
|
|
144
|
-
// W3C §6.2 (and WPT RTCDataChannel-binaryType tests): invalid
|
|
145
|
-
// values must be silently ignored — keep the previous value.
|
|
146
|
-
// See: refs/wpt/webrtc/RTCDataChannel-binaryType.window.js
|
|
147
|
-
if (v !== 'arraybuffer' && v !== 'blob') return;
|
|
148
|
-
// Blob is always available — we import it from @gjsify/buffer (GJS)
|
|
149
|
-
// or use the native one (Node), so the formerly gated `if (!Blob)`
|
|
150
|
-
// branch that threw NotSupportedError is no longer reachable.
|
|
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
|
-
throw new DOMException(
|
|
175
|
-
`RTCDataChannel.send: readyState is '${state}', expected 'open'`,
|
|
176
|
-
'InvalidStateError',
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (typeof data === 'string') {
|
|
181
|
-
this._native.send_string(data);
|
|
182
|
-
this._bufferedAmount += new TextEncoder().encode(data).byteLength;
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (data instanceof Blob) {
|
|
187
|
-
const blob = data;
|
|
188
|
-
blob.arrayBuffer().then((buf) => {
|
|
189
|
-
try {
|
|
190
|
-
this._native.send_data(toGBytes(buf));
|
|
191
|
-
this._bufferedAmount += buf.byteLength;
|
|
192
|
-
} catch {
|
|
193
|
-
/* channel may have closed mid-flight */
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (ArrayBuffer.isView(data)) {
|
|
200
|
-
const bytes = toGBytes(data);
|
|
201
|
-
this._native.send_data(bytes);
|
|
202
|
-
this._bufferedAmount += data.byteLength;
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (data instanceof ArrayBuffer) {
|
|
206
|
-
const bytes = toGBytes(data);
|
|
207
|
-
this._native.send_data(bytes);
|
|
208
|
-
this._bufferedAmount += data.byteLength;
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
throw new TypeError('RTCDataChannel.send: unsupported data type');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
close(): void {
|
|
216
|
-
if (this._closed) return;
|
|
217
|
-
try { this._native.close(); } catch { /* ignore */ }
|
|
218
|
-
this._disconnectSignals();
|
|
219
|
-
this._closed = true;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** @internal */
|
|
223
|
-
_disconnectSignals(): void {
|
|
224
|
-
try { this._bridge.dispose_bridge(); } catch { /* ignore */ }
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ---- Signal → event translators ---------------------------------------
|
|
228
|
-
// Already running on the main context (DataChannelBridge did the hop).
|
|
229
|
-
|
|
230
|
-
private _handleOpen(): void {
|
|
231
|
-
const ev = new Event('open');
|
|
232
|
-
this._onopen?.call(this, ev);
|
|
233
|
-
this.dispatchEvent(ev);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private _handleClose(): void {
|
|
237
|
-
this._closed = true;
|
|
238
|
-
const ev = new Event('close');
|
|
239
|
-
this._onclose?.call(this, ev);
|
|
240
|
-
this.dispatchEvent(ev);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
private _handleError(message: string): void {
|
|
244
|
-
const rtcErr = new RTCError(
|
|
245
|
-
{ errorDetail: 'data-channel-failure' },
|
|
246
|
-
message || 'RTCDataChannel error',
|
|
247
|
-
);
|
|
248
|
-
const ev = new RTCErrorEvent('error', { error: rtcErr });
|
|
249
|
-
this._onerror?.call(this, ev);
|
|
250
|
-
this.dispatchEvent(ev);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private _handleString(data: string): void {
|
|
254
|
-
const ev = new MessageEvent('message', { data });
|
|
255
|
-
this._onmessage?.call(this, ev);
|
|
256
|
-
this.dispatchEvent(ev);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private _handleData(bytes: GLib.Bytes): void {
|
|
260
|
-
if (!bytes) return;
|
|
261
|
-
const buf = bytesToArrayBuffer(bytes);
|
|
262
|
-
const data: ArrayBuffer | Blob =
|
|
263
|
-
this._binaryType === 'blob' ? new Blob([buf]) : buf;
|
|
264
|
-
const ev = new MessageEvent('message', { data });
|
|
265
|
-
this._onmessage?.call(this, ev);
|
|
266
|
-
this.dispatchEvent(ev);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
private _handleBufferedAmountLow(): void {
|
|
270
|
-
this._bufferedAmount = Number(this._native.buffered_amount) || 0;
|
|
271
|
-
const ev = new Event('bufferedamountlow');
|
|
272
|
-
this._onbufferedamountlow?.call(this, ev);
|
|
273
|
-
this.dispatchEvent(ev);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private _handleReadyStateChange(): void {
|
|
277
|
-
if (this.readyState === 'closing') {
|
|
278
|
-
const ev = new Event('closing');
|
|
279
|
-
this._onclosing?.call(this, ev);
|
|
280
|
-
this.dispatchEvent(ev);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
}
|