node-rtc-connection 1.0.18 → 2.0.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/README.md +94 -85
- package/dist/index.cjs +20 -5421
- package/dist/index.mjs +25 -5413
- package/dist/types/crypto/der.d.ts +107 -0
- package/dist/types/crypto/x509.d.ts +56 -0
- package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
- package/dist/types/dtls/RTCCertificate.d.ts +163 -0
- package/dist/types/dtls/cipher.d.ts +81 -0
- package/dist/types/dtls/connection.d.ts +81 -0
- package/dist/types/dtls/prf.d.ts +29 -0
- package/dist/types/dtls/protocol.d.ts +127 -0
- package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
- package/dist/types/foundation/RTCError.d.ts +152 -0
- package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
- package/dist/types/ice/ice-agent.d.ts +154 -0
- package/dist/types/ice/stun-message.d.ts +92 -0
- package/dist/types/index.d.ts +29 -0
- package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
- package/dist/types/sctp/association.d.ts +77 -0
- package/dist/types/sctp/chunks.d.ts +200 -0
- package/dist/types/sctp/crc32c.d.ts +24 -0
- package/dist/types/sctp/datachannel-manager.d.ts +51 -0
- package/dist/types/sctp/dcep.d.ts +56 -0
- package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
- package/dist/types/sdp/sdp-utils.d.ts +103 -0
- package/dist/types/stun/stun-client.d.ts +119 -0
- package/dist/types/transport-stack.d.ts +68 -0
- package/package.json +26 -21
- package/src/crypto/der.ts +205 -0
- package/src/crypto/x509.ts +146 -0
- package/src/datachannel/RTCDataChannel.ts +388 -0
- package/src/dtls/RTCCertificate.ts +396 -0
- package/src/dtls/cipher.ts +198 -0
- package/src/dtls/connection.ts +974 -0
- package/src/dtls/prf.ts +62 -0
- package/src/dtls/protocol.ts +204 -0
- package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
- package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
- package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
- package/src/ice/ice-agent.ts +609 -0
- package/src/ice/stun-message.ts +260 -0
- package/src/index.ts +72 -0
- package/src/peerconnection/RTCPeerConnection.ts +430 -0
- package/src/sctp/association.ts +523 -0
- package/src/sctp/chunks.ts +350 -0
- package/src/sctp/crc32c.ts +57 -0
- package/src/sctp/datachannel-manager.ts +187 -0
- package/src/sctp/dcep.ts +94 -0
- package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
- package/src/sdp/sdp-utils.ts +229 -0
- package/src/stun/stun-client.ts +936 -0
- package/src/transport-stack.ts +165 -0
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/src/datachannel/RTCDataChannel.js +0 -354
- package/src/dtls/RTCCertificate.js +0 -310
- package/src/dtls/RTCDtlsTransport.js +0 -247
- package/src/ice/RTCIceTransport.js +0 -998
- package/src/index.d.ts +0 -400
- package/src/index.js +0 -92
- package/src/network/network-transport.js +0 -478
- package/src/peerconnection/RTCPeerConnection.js +0 -851
- package/src/sctp/RTCSctpTransport.js +0 -253
- package/src/sdp/sdp-utils.js +0 -224
- package/src/stun/stun-client.js +0 -643
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file x509.ts
|
|
3
|
+
* @description Self-signed X.509 v3 certificate generation for WebRTC DTLS.
|
|
4
|
+
* @module crypto/x509
|
|
5
|
+
*
|
|
6
|
+
* WebRTC peers authenticate by self-signed certificate. The SDP carries
|
|
7
|
+
* a=fingerprint as the hash of the DER-encoded certificate (RFC 8122), which
|
|
8
|
+
* the peer verifies against the certificate presented during the DTLS
|
|
9
|
+
* handshake. Node has no certificate builder, so we assemble a minimal but
|
|
10
|
+
* spec-valid ECDSA P-256 / ecdsa-with-SHA256 certificate by hand.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
import * as der from './der';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options for {@link generateSelfSigned}.
|
|
20
|
+
*/
|
|
21
|
+
export interface GenerateSelfSignedOptions {
|
|
22
|
+
/** CN; WebRTC uses a random value. */
|
|
23
|
+
commonName?: string;
|
|
24
|
+
/** Validity period in days. */
|
|
25
|
+
days?: number;
|
|
26
|
+
/** Override start time (default: now - 1 day). */
|
|
27
|
+
notBefore?: Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Result of {@link generateSelfSigned}.
|
|
32
|
+
*/
|
|
33
|
+
export interface SelfSignedCertificate {
|
|
34
|
+
/** DER-encoded certificate. */
|
|
35
|
+
certDer: Buffer;
|
|
36
|
+
privateKey: crypto.KeyObject;
|
|
37
|
+
publicKey: crypto.KeyObject;
|
|
38
|
+
notBefore: Date;
|
|
39
|
+
notAfter: Date;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// OIDs used in the certificate.
|
|
43
|
+
export const OID = Object.freeze({
|
|
44
|
+
ecPublicKey: '1.2.840.10045.2.1',
|
|
45
|
+
prime256v1: '1.2.840.10045.3.1.7',
|
|
46
|
+
ecdsaWithSHA256: '1.2.840.10045.4.3.2',
|
|
47
|
+
commonName: '2.5.4.3',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a Name with a single CN RDN.
|
|
52
|
+
* @param {string} cn
|
|
53
|
+
* @returns {Buffer}
|
|
54
|
+
*/
|
|
55
|
+
function buildName(cn: string): Buffer {
|
|
56
|
+
const attr = der.encodeSequence([
|
|
57
|
+
der.encodeOID(OID.commonName),
|
|
58
|
+
der.encodeUTF8String(cn),
|
|
59
|
+
]);
|
|
60
|
+
const rdn = der.encodeSet([attr]);
|
|
61
|
+
return der.encodeSequence([rdn]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The AlgorithmIdentifier for ecdsa-with-SHA256 (no parameters).
|
|
66
|
+
* @returns {Buffer}
|
|
67
|
+
*/
|
|
68
|
+
function ecdsaWithSHA256AlgId(): Buffer {
|
|
69
|
+
return der.encodeSequence([der.encodeOID(OID.ecdsaWithSHA256)]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a self-signed ECDSA P-256 certificate.
|
|
74
|
+
*
|
|
75
|
+
* @param {GenerateSelfSignedOptions} [options]
|
|
76
|
+
* @returns {SelfSignedCertificate}
|
|
77
|
+
*/
|
|
78
|
+
export function generateSelfSigned(
|
|
79
|
+
options: GenerateSelfSignedOptions = {}
|
|
80
|
+
): SelfSignedCertificate {
|
|
81
|
+
const commonName =
|
|
82
|
+
options.commonName || `WebRTC-${crypto.randomBytes(8).toString('hex')}`;
|
|
83
|
+
const days = options.days || 30;
|
|
84
|
+
|
|
85
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
|
|
86
|
+
namedCurve: 'prime256v1',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Node exports a complete SubjectPublicKeyInfo in DER — reuse verbatim.
|
|
90
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' }) as Buffer;
|
|
91
|
+
|
|
92
|
+
// Validity. Start one day in the past to tolerate clock skew between peers.
|
|
93
|
+
const notBefore =
|
|
94
|
+
options.notBefore || new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
95
|
+
const notAfter = new Date(notBefore.getTime() + days * 24 * 60 * 60 * 1000);
|
|
96
|
+
|
|
97
|
+
// Serial number: positive 20-byte random (high bit cleared via encoder).
|
|
98
|
+
const serial = crypto.randomBytes(20);
|
|
99
|
+
serial[0]! &= 0x7f;
|
|
100
|
+
if (serial[0] === 0) serial[0] = 0x01;
|
|
101
|
+
|
|
102
|
+
const name = buildName(commonName);
|
|
103
|
+
|
|
104
|
+
// TBSCertificate (X.509 v3).
|
|
105
|
+
const tbs = der.encodeSequence([
|
|
106
|
+
der.encodeExplicit(0, der.encodeInteger(2)), // version v3 (value 2)
|
|
107
|
+
der.encodeIntegerFromBuffer(serial), // serialNumber
|
|
108
|
+
ecdsaWithSHA256AlgId(), // signature algorithm
|
|
109
|
+
name, // issuer (== subject, self-signed)
|
|
110
|
+
der.encodeSequence([der.encodeTime(notBefore), der.encodeTime(notAfter)]),
|
|
111
|
+
name, // subject
|
|
112
|
+
spki, // subjectPublicKeyInfo
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
// Sign the TBS. Node returns a DER ECDSA-Sig-Value (SEQUENCE { r, s }).
|
|
116
|
+
const signature = crypto.sign('sha256', tbs, privateKey);
|
|
117
|
+
|
|
118
|
+
const certDer = der.encodeSequence([
|
|
119
|
+
tbs,
|
|
120
|
+
ecdsaWithSHA256AlgId(),
|
|
121
|
+
der.encodeBitString(signature),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
return { certDer, privateKey, publicKey, notBefore, notAfter };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compute the certificate fingerprint as used in SDP a=fingerprint (RFC 8122):
|
|
129
|
+
* hash over the DER-encoded certificate, uppercase hex, colon-separated.
|
|
130
|
+
*
|
|
131
|
+
* @param {Buffer} certDer
|
|
132
|
+
* @param {string} [algorithm='sha-256'] - 'sha-256' | 'sha-384' | 'sha-512'
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
export function fingerprint(
|
|
136
|
+
certDer: Buffer,
|
|
137
|
+
algorithm: string = 'sha-256'
|
|
138
|
+
): string {
|
|
139
|
+
const nodeAlgo = algorithm.replace('-', '').toLowerCase(); // sha-256 -> sha256
|
|
140
|
+
const digest = crypto
|
|
141
|
+
.createHash(nodeAlgo)
|
|
142
|
+
.update(certDer)
|
|
143
|
+
.digest('hex')
|
|
144
|
+
.toUpperCase();
|
|
145
|
+
return digest.match(/.{2}/g)!.join(':');
|
|
146
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RTCDataChannel.ts
|
|
3
|
+
* @description WebRTC DataChannel implementation for peer-to-peer data transfer.
|
|
4
|
+
* @module datachannel/RTCDataChannel
|
|
5
|
+
*
|
|
6
|
+
* Implements the W3C RTCDataChannel interface
|
|
7
|
+
* (https://www.w3.org/TR/webrtc/#rtcdatachannel).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* RTCDataChannelState - Current state of the data channel
|
|
14
|
+
* @readonly
|
|
15
|
+
* @enum {string}
|
|
16
|
+
*/
|
|
17
|
+
export const RTCDataChannelState = Object.freeze({
|
|
18
|
+
CONNECTING: 'connecting',
|
|
19
|
+
OPEN: 'open',
|
|
20
|
+
CLOSING: 'closing',
|
|
21
|
+
CLOSED: 'closed'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type RTCDataChannelReadyState = 'connecting' | 'open' | 'closing' | 'closed';
|
|
25
|
+
type RTCDataChannelBinaryType = 'arraybuffer' | 'blob';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Package-internal events that wire an RTCDataChannel to the SCTP transport.
|
|
29
|
+
* They are keyed by Symbol so they never collide with — or leak into — the
|
|
30
|
+
* public event surface ('open'/'message'/'close'/'error'/'bufferedamountlow').
|
|
31
|
+
* The SCTP data-channel manager and the channel communicate purely by emitting
|
|
32
|
+
* these on the channel's own EventEmitter:
|
|
33
|
+
*
|
|
34
|
+
* - SEND channel → transport: outbound frame `(data: Buffer, isBinary: boolean)`
|
|
35
|
+
* - RECEIVE transport → channel: inbound frame `(data: Buffer, isBinary: boolean)`
|
|
36
|
+
* - OPEN transport → channel: transition the channel to 'open'
|
|
37
|
+
* - SET_ID transport → channel: assign the SCTP stream id `(id: number)`
|
|
38
|
+
*/
|
|
39
|
+
export const RTCDataChannelEvents = Object.freeze({
|
|
40
|
+
SEND: Symbol('rtcdatachannel:send'),
|
|
41
|
+
RECEIVE: Symbol('rtcdatachannel:receive'),
|
|
42
|
+
OPEN: Symbol('rtcdatachannel:open'),
|
|
43
|
+
SET_ID: Symbol('rtcdatachannel:setId'),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* RTCDataChannelInit - Configuration for creating a data channel
|
|
48
|
+
* @typedef {Object} RTCDataChannelInit
|
|
49
|
+
* @property {boolean} [ordered=true] - Whether messages must arrive in order
|
|
50
|
+
* @property {number} [maxPacketLifeTime] - Maximum packet lifetime in milliseconds
|
|
51
|
+
* @property {number} [maxRetransmits] - Maximum number of retransmissions
|
|
52
|
+
* @property {string} [protocol=''] - Subprotocol name
|
|
53
|
+
* @property {boolean} [negotiated=false] - Whether channel was negotiated out-of-band
|
|
54
|
+
* @property {number} [id] - Channel ID (required if negotiated is true)
|
|
55
|
+
*/
|
|
56
|
+
export interface RTCDataChannelInit {
|
|
57
|
+
ordered?: boolean;
|
|
58
|
+
maxPacketLifeTime?: number;
|
|
59
|
+
maxRetransmits?: number;
|
|
60
|
+
protocol?: string;
|
|
61
|
+
negotiated?: boolean;
|
|
62
|
+
id?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @class RTCDataChannel
|
|
67
|
+
* @extends EventEmitter
|
|
68
|
+
* @description Represents a bidirectional data channel between peers.
|
|
69
|
+
* Provides reliable or unreliable data transfer with configurable ordering.
|
|
70
|
+
*
|
|
71
|
+
* Events:
|
|
72
|
+
* - 'open': Fired when the channel opens
|
|
73
|
+
* - 'message': Fired when a message is received
|
|
74
|
+
* - 'bufferedamountlow': Fired when bufferedAmount drops below threshold
|
|
75
|
+
* - 'error': Fired when an error occurs
|
|
76
|
+
* - 'closing': Fired when the channel is closing
|
|
77
|
+
* - 'close': Fired when the channel closes
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* const dataChannel = peerConnection.createDataChannel('myChannel', {
|
|
81
|
+
* ordered: true,
|
|
82
|
+
* maxRetransmits: 3
|
|
83
|
+
* });
|
|
84
|
+
*
|
|
85
|
+
* dataChannel.on('open', () => {
|
|
86
|
+
* console.log('Channel opened');
|
|
87
|
+
* dataChannel.send('Hello!');
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* dataChannel.on('message', (event) => {
|
|
91
|
+
* console.log('Received:', event.data);
|
|
92
|
+
* });
|
|
93
|
+
*/
|
|
94
|
+
export class RTCDataChannel extends EventEmitter {
|
|
95
|
+
#label: string;
|
|
96
|
+
#ordered: boolean;
|
|
97
|
+
#maxPacketLifeTime: number | null;
|
|
98
|
+
#maxRetransmits: number | null;
|
|
99
|
+
#protocol: string;
|
|
100
|
+
#negotiated: boolean;
|
|
101
|
+
#id: number | null;
|
|
102
|
+
#readyState: RTCDataChannelReadyState;
|
|
103
|
+
#bufferedAmount: number;
|
|
104
|
+
#bufferedAmountLowThreshold: number;
|
|
105
|
+
#binaryType: RTCDataChannelBinaryType;
|
|
106
|
+
/** Whether a transport is listening for outbound SEND events. */
|
|
107
|
+
#connected: boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create an RTCDataChannel instance.
|
|
111
|
+
* @param {string} label - Channel label
|
|
112
|
+
* @param {RTCDataChannelInit} [init] - Channel configuration
|
|
113
|
+
*/
|
|
114
|
+
constructor(label: string, init: RTCDataChannelInit = {}) {
|
|
115
|
+
super();
|
|
116
|
+
|
|
117
|
+
if (typeof label !== 'string') {
|
|
118
|
+
throw new TypeError('label must be a string');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Channel configuration
|
|
122
|
+
this.#label = label;
|
|
123
|
+
this.#ordered = init.ordered !== undefined ? init.ordered : true;
|
|
124
|
+
this.#maxPacketLifeTime = init.maxPacketLifeTime || null;
|
|
125
|
+
this.#maxRetransmits = init.maxRetransmits || null;
|
|
126
|
+
this.#protocol = init.protocol || '';
|
|
127
|
+
this.#negotiated = init.negotiated || false;
|
|
128
|
+
this.#id = init.id !== undefined ? init.id : null;
|
|
129
|
+
|
|
130
|
+
// State
|
|
131
|
+
this.#readyState = RTCDataChannelState.CONNECTING as RTCDataChannelReadyState;
|
|
132
|
+
this.#bufferedAmount = 0;
|
|
133
|
+
this.#bufferedAmountLowThreshold = 0;
|
|
134
|
+
this.#binaryType = 'arraybuffer'; // or 'blob'
|
|
135
|
+
this.#connected = false;
|
|
136
|
+
|
|
137
|
+
// Transport drives the channel via internal (Symbol-keyed) events.
|
|
138
|
+
this.on(RTCDataChannelEvents.SET_ID, (id: number) => { this.#id = id; });
|
|
139
|
+
this.on(RTCDataChannelEvents.OPEN, () => {
|
|
140
|
+
this.#connected = true;
|
|
141
|
+
this.#setState(RTCDataChannelState.OPEN as RTCDataChannelReadyState);
|
|
142
|
+
});
|
|
143
|
+
this.on(RTCDataChannelEvents.RECEIVE, (data: Buffer, isBinary: boolean) => {
|
|
144
|
+
this.#receiveMessage(data, isBinary);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the channel label.
|
|
150
|
+
* @returns {string} Channel label
|
|
151
|
+
*/
|
|
152
|
+
get label(): string {
|
|
153
|
+
return this.#label;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if messages are delivered in order.
|
|
158
|
+
* @returns {boolean} True if ordered
|
|
159
|
+
*/
|
|
160
|
+
get ordered(): boolean {
|
|
161
|
+
return this.#ordered;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get the maximum packet lifetime in milliseconds.
|
|
166
|
+
* @returns {number|null} Maximum lifetime or null if not set
|
|
167
|
+
*/
|
|
168
|
+
get maxPacketLifeTime(): number | null {
|
|
169
|
+
return this.#maxPacketLifeTime;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the maximum number of retransmissions.
|
|
174
|
+
* @returns {number|null} Maximum retransmits or null if not set
|
|
175
|
+
*/
|
|
176
|
+
get maxRetransmits(): number | null {
|
|
177
|
+
return this.#maxRetransmits;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get the subprotocol name.
|
|
182
|
+
* @returns {string} Protocol name
|
|
183
|
+
*/
|
|
184
|
+
get protocol(): string {
|
|
185
|
+
return this.#protocol;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if the channel was negotiated out-of-band.
|
|
190
|
+
* @returns {boolean} True if negotiated
|
|
191
|
+
*/
|
|
192
|
+
get negotiated(): boolean {
|
|
193
|
+
return this.#negotiated;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get the channel ID.
|
|
198
|
+
* @returns {number|null} Channel ID or null if not assigned
|
|
199
|
+
*/
|
|
200
|
+
get id(): number | null {
|
|
201
|
+
return this.#id;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get the current state of the channel.
|
|
206
|
+
* @returns {string} Channel state
|
|
207
|
+
*/
|
|
208
|
+
get readyState(): RTCDataChannelReadyState {
|
|
209
|
+
return this.#readyState;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get the number of bytes queued to send.
|
|
214
|
+
* @returns {number} Buffered amount in bytes
|
|
215
|
+
*/
|
|
216
|
+
get bufferedAmount(): number {
|
|
217
|
+
return this.#bufferedAmount;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get the threshold for bufferedamountlow event.
|
|
222
|
+
* @returns {number} Threshold in bytes
|
|
223
|
+
*/
|
|
224
|
+
get bufferedAmountLowThreshold(): number {
|
|
225
|
+
return this.#bufferedAmountLowThreshold;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Set the threshold for bufferedamountlow event.
|
|
230
|
+
* @param {number} value - Threshold in bytes
|
|
231
|
+
*/
|
|
232
|
+
set bufferedAmountLowThreshold(value: number) {
|
|
233
|
+
this.#bufferedAmountLowThreshold = value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get the binary data type.
|
|
238
|
+
* @returns {string} 'arraybuffer' or 'blob'
|
|
239
|
+
*/
|
|
240
|
+
get binaryType(): RTCDataChannelBinaryType {
|
|
241
|
+
return this.#binaryType;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Set the binary data type.
|
|
246
|
+
* @param {string} value - 'arraybuffer' or 'blob'
|
|
247
|
+
* @throws {TypeError} If value is invalid
|
|
248
|
+
*/
|
|
249
|
+
set binaryType(value: RTCDataChannelBinaryType) {
|
|
250
|
+
if (value !== 'arraybuffer' && value !== 'blob') {
|
|
251
|
+
throw new TypeError('binaryType must be "arraybuffer" or "blob"');
|
|
252
|
+
}
|
|
253
|
+
this.#binaryType = value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check if the channel is reliable (deprecated).
|
|
258
|
+
* @returns {boolean} True if ordered and no packet lifetime/retransmit limits
|
|
259
|
+
* @deprecated Use ordered, maxPacketLifeTime, and maxRetransmits instead
|
|
260
|
+
*/
|
|
261
|
+
get reliable(): boolean {
|
|
262
|
+
return this.#ordered &&
|
|
263
|
+
this.#maxPacketLifeTime === null &&
|
|
264
|
+
this.#maxRetransmits === null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Send a message through the channel.
|
|
269
|
+
* @param {string|ArrayBuffer|ArrayBufferView|Blob} data - Data to send
|
|
270
|
+
* @throws {Error} If channel is not open or data is invalid
|
|
271
|
+
*/
|
|
272
|
+
send(data: string | ArrayBuffer | ArrayBufferView | Buffer): void {
|
|
273
|
+
if (this.#readyState !== RTCDataChannelState.OPEN) {
|
|
274
|
+
throw new Error('RTCDataChannel.readyState is not "open"');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let dataToSend: Buffer;
|
|
278
|
+
let byteLength = 0;
|
|
279
|
+
let isBinary: boolean;
|
|
280
|
+
|
|
281
|
+
if (typeof data === 'string') {
|
|
282
|
+
dataToSend = Buffer.from(data, 'utf8');
|
|
283
|
+
byteLength = dataToSend.length;
|
|
284
|
+
isBinary = false;
|
|
285
|
+
} else if (data instanceof ArrayBuffer) {
|
|
286
|
+
dataToSend = Buffer.from(data);
|
|
287
|
+
byteLength = data.byteLength;
|
|
288
|
+
isBinary = true;
|
|
289
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
290
|
+
dataToSend = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
291
|
+
byteLength = data.byteLength;
|
|
292
|
+
isBinary = true;
|
|
293
|
+
} else if (Buffer.isBuffer(data)) {
|
|
294
|
+
dataToSend = data as Buffer;
|
|
295
|
+
byteLength = (data as Buffer).length;
|
|
296
|
+
isBinary = true;
|
|
297
|
+
} else if (data && typeof (data as { arrayBuffer?: unknown }).arrayBuffer === 'function') {
|
|
298
|
+
// Blob-like object
|
|
299
|
+
throw new Error('Blob sending not yet implemented');
|
|
300
|
+
} else {
|
|
301
|
+
throw new TypeError('Invalid data type');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Emit the outbound frame for the transport to carry. The isBinary flag
|
|
305
|
+
// lets the peer reconstruct the right JS type; binary is transmitted as raw
|
|
306
|
+
// bytes (no JSON), avoiding corruption of Buffer/ArrayBuffer payloads.
|
|
307
|
+
if (!this.#connected) {
|
|
308
|
+
throw new Error('Data channel not connected to a transport');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Update buffered amount, then decrement once the transport has taken it.
|
|
312
|
+
this.#bufferedAmount += byteLength;
|
|
313
|
+
try {
|
|
314
|
+
this.emit(RTCDataChannelEvents.SEND, dataToSend, isBinary);
|
|
315
|
+
this.#bufferedAmount = Math.max(0, this.#bufferedAmount - byteLength);
|
|
316
|
+
this.#emitBufferedAmountLow();
|
|
317
|
+
} catch (err) {
|
|
318
|
+
this.#bufferedAmount = Math.max(0, this.#bufferedAmount - byteLength);
|
|
319
|
+
this.emit('error', err);
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Emit bufferedamountlow if appropriate. */
|
|
325
|
+
#emitBufferedAmountLow(): void {
|
|
326
|
+
if (this.#bufferedAmount <= this.#bufferedAmountLowThreshold) {
|
|
327
|
+
this.emit('bufferedamountlow');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Close the data channel.
|
|
333
|
+
*/
|
|
334
|
+
close(): void {
|
|
335
|
+
if (this.#readyState === RTCDataChannelState.CLOSING ||
|
|
336
|
+
this.#readyState === RTCDataChannelState.CLOSED) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.#setState(RTCDataChannelState.CLOSING as RTCDataChannelReadyState);
|
|
341
|
+
|
|
342
|
+
// Transition to closed asynchronously
|
|
343
|
+
setImmediate(() => {
|
|
344
|
+
if (this.#readyState === RTCDataChannelState.CLOSING) {
|
|
345
|
+
this.#setState(RTCDataChannelState.CLOSED as RTCDataChannelReadyState);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Set the channel state and emit the matching lifecycle event. */
|
|
351
|
+
#setState(newState: RTCDataChannelReadyState): void {
|
|
352
|
+
const oldState = this.#readyState;
|
|
353
|
+
if (oldState === newState) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.#readyState = newState;
|
|
358
|
+
|
|
359
|
+
// Emit state-specific events
|
|
360
|
+
if (newState === RTCDataChannelState.OPEN) {
|
|
361
|
+
this.emit('open');
|
|
362
|
+
} else if (newState === RTCDataChannelState.CLOSING) {
|
|
363
|
+
this.emit('closing');
|
|
364
|
+
} else if (newState === RTCDataChannelState.CLOSED) {
|
|
365
|
+
this.emit('close');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Deliver a received message to listeners.
|
|
371
|
+
*
|
|
372
|
+
* Mirrors the browser RTCDataChannel: text frames surface as a string;
|
|
373
|
+
* binary frames surface as an ArrayBuffer (binaryType 'arraybuffer') or a
|
|
374
|
+
* Node Buffer (binaryType 'blob', which we approximate with Buffer since
|
|
375
|
+
* Node has no Blob in older runtimes).
|
|
376
|
+
*/
|
|
377
|
+
#receiveMessage(data: Buffer, isBinary: boolean): void {
|
|
378
|
+
let payload: string | ArrayBuffer | Buffer;
|
|
379
|
+
if (!isBinary) {
|
|
380
|
+
payload = data.toString('utf8');
|
|
381
|
+
} else if (this.#binaryType === 'arraybuffer') {
|
|
382
|
+
payload = (data.buffer as ArrayBuffer).slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
383
|
+
} else {
|
|
384
|
+
payload = data;
|
|
385
|
+
}
|
|
386
|
+
this.emit('message', { data: payload });
|
|
387
|
+
}
|
|
388
|
+
}
|