whepts 1.0.2 → 1.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/dist/core/codec.d.ts +11 -0
- package/dist/core/connection.d.ts +24 -0
- package/dist/core/http.d.ts +16 -0
- package/dist/core/track.d.ts +12 -0
- package/dist/errors.d.ts +3 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/whep.d.ts +8 -98
- package/package.json +11 -12
- package/.vscode/settings.json +0 -38
- package/QWEN.md +0 -118
- package/dist/utils/observer.d.ts +0 -8
- package/src/errors.ts +0 -27
- package/src/index.ts +0 -6
- package/src/utils/flow-check.ts +0 -73
- package/src/utils/observer.ts +0 -28
- package/src/utils/sdp.ts +0 -253
- package/src/utils/webrtc.ts +0 -123
- package/src/whep.ts +0 -404
package/src/utils/sdp.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/** Type for parsed offer data */
|
|
2
|
-
export interface ParsedOffer {
|
|
3
|
-
iceUfrag: string
|
|
4
|
-
icePwd: string
|
|
5
|
-
medias: string[]
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* SDP processing utilities
|
|
10
|
-
*/
|
|
11
|
-
export class SdpUtils {
|
|
12
|
-
/**
|
|
13
|
-
* Parse an offer SDP into iceUfrag, icePwd, and medias.
|
|
14
|
-
*/
|
|
15
|
-
static parseOffer(sdp: string): ParsedOffer {
|
|
16
|
-
const ret: ParsedOffer = {
|
|
17
|
-
iceUfrag: '',
|
|
18
|
-
icePwd: '',
|
|
19
|
-
medias: [],
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
for (const line of sdp.split('\r\n')) {
|
|
23
|
-
if (line.startsWith('m=')) {
|
|
24
|
-
ret.medias.push(line.slice('m='.length))
|
|
25
|
-
}
|
|
26
|
-
else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {
|
|
27
|
-
ret.iceUfrag = line.slice('a=ice-ufrag:'.length)
|
|
28
|
-
}
|
|
29
|
-
else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) {
|
|
30
|
-
ret.icePwd = line.slice('a=ice-pwd:'.length)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return ret
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Reserve a payload type.
|
|
39
|
-
*/
|
|
40
|
-
static reservePayloadType(payloadTypes: string[]): string {
|
|
41
|
-
// everything is valid between 30 and 127, except for interval between 64 and 95
|
|
42
|
-
// https://chromium.googlesource.com/external/webrtc/+/refs/heads/master/call/payload_type.h#29
|
|
43
|
-
for (let i = 30; i <= 127; i++) {
|
|
44
|
-
if ((i <= 63 || i >= 96) && !payloadTypes.includes(i.toString())) {
|
|
45
|
-
const pl = i.toString()
|
|
46
|
-
payloadTypes.push(pl)
|
|
47
|
-
return pl
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
throw new Error('unable to find a free payload type')
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Enable stereo PCMA/PCMU codecs.
|
|
55
|
-
*/
|
|
56
|
-
static enableStereoPcmau(payloadTypes: string[], section: string): string {
|
|
57
|
-
const lines = section.split('\r\n')
|
|
58
|
-
|
|
59
|
-
let payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
60
|
-
lines[0] += ` ${payloadType}`
|
|
61
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMU/8000/2`)
|
|
62
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
63
|
-
|
|
64
|
-
payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
65
|
-
lines[0] += ` ${payloadType}`
|
|
66
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMA/8000/2`)
|
|
67
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
68
|
-
|
|
69
|
-
return lines.join('\r\n')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Enable multichannel Opus codec.
|
|
74
|
-
*/
|
|
75
|
-
static enableMultichannelOpus(payloadTypes: string[], section: string): string {
|
|
76
|
-
const lines = section.split('\r\n')
|
|
77
|
-
|
|
78
|
-
let payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
79
|
-
lines[0] += ` ${payloadType}`
|
|
80
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/3`)
|
|
81
|
-
lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`)
|
|
82
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
83
|
-
|
|
84
|
-
payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
85
|
-
lines[0] += ` ${payloadType}`
|
|
86
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/4`)
|
|
87
|
-
lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`)
|
|
88
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
89
|
-
|
|
90
|
-
payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
91
|
-
lines[0] += ` ${payloadType}`
|
|
92
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/5`)
|
|
93
|
-
lines.splice(
|
|
94
|
-
lines.length - 1,
|
|
95
|
-
0,
|
|
96
|
-
`a=fmtp:${payloadType} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`,
|
|
97
|
-
)
|
|
98
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
99
|
-
|
|
100
|
-
payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
101
|
-
lines[0] += ` ${payloadType}`
|
|
102
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/6`)
|
|
103
|
-
lines.splice(
|
|
104
|
-
lines.length - 1,
|
|
105
|
-
0,
|
|
106
|
-
`a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`,
|
|
107
|
-
)
|
|
108
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
109
|
-
|
|
110
|
-
payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
111
|
-
lines[0] += ` ${payloadType}`
|
|
112
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/7`)
|
|
113
|
-
lines.splice(
|
|
114
|
-
lines.length - 1,
|
|
115
|
-
0,
|
|
116
|
-
`a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`,
|
|
117
|
-
)
|
|
118
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
119
|
-
|
|
120
|
-
payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
121
|
-
lines[0] += ` ${payloadType}`
|
|
122
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/8`)
|
|
123
|
-
lines.splice(
|
|
124
|
-
lines.length - 1,
|
|
125
|
-
0,
|
|
126
|
-
`a=fmtp:${payloadType} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`,
|
|
127
|
-
)
|
|
128
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
129
|
-
|
|
130
|
-
return lines.join('\r\n')
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Enable L16 codec.
|
|
135
|
-
*/
|
|
136
|
-
static enableL16(payloadTypes: string[], section: string): string {
|
|
137
|
-
const lines = section.split('\r\n')
|
|
138
|
-
|
|
139
|
-
let payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
140
|
-
lines[0] += ` ${payloadType}`
|
|
141
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/8000/2`)
|
|
142
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
143
|
-
|
|
144
|
-
payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
145
|
-
lines[0] += ` ${payloadType}`
|
|
146
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/16000/2`)
|
|
147
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
148
|
-
|
|
149
|
-
payloadType = SdpUtils.reservePayloadType(payloadTypes)
|
|
150
|
-
lines[0] += ` ${payloadType}`
|
|
151
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/48000/2`)
|
|
152
|
-
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`)
|
|
153
|
-
|
|
154
|
-
return lines.join('\r\n')
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Enable stereo Opus codec.
|
|
159
|
-
*/
|
|
160
|
-
static enableStereoOpus(section: string): string {
|
|
161
|
-
let opusPayloadFormat = ''
|
|
162
|
-
const lines = section.split('\r\n')
|
|
163
|
-
|
|
164
|
-
for (let i = 0; i < lines.length; i++) {
|
|
165
|
-
if (lines[i].startsWith('a=rtpmap:') && lines[i].toLowerCase().includes('opus/')) {
|
|
166
|
-
opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0]
|
|
167
|
-
break
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (opusPayloadFormat === '') {
|
|
172
|
-
return section
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
for (let i = 0; i < lines.length; i++) {
|
|
176
|
-
if (lines[i].startsWith(`a=fmtp:${opusPayloadFormat} `)) {
|
|
177
|
-
if (!lines[i].includes('stereo')) {
|
|
178
|
-
lines[i] += ';stereo=1'
|
|
179
|
-
}
|
|
180
|
-
if (!lines[i].includes('sprop-stereo')) {
|
|
181
|
-
lines[i] += ';sprop-stereo=1'
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return lines.join('\r\n')
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Edit an offer SDP to enable non-advertised codecs.
|
|
191
|
-
*/
|
|
192
|
-
static editOffer(sdp: string, nonAdvertisedCodecs: string[]): string {
|
|
193
|
-
const sections = sdp.split('m=')
|
|
194
|
-
|
|
195
|
-
const payloadTypes = sections
|
|
196
|
-
.slice(1)
|
|
197
|
-
.map(s => s.split('\r\n')[0].split(' ').slice(3))
|
|
198
|
-
.reduce((prev, cur) => [...prev, ...cur], [])
|
|
199
|
-
|
|
200
|
-
for (let i = 1; i < sections.length; i++) {
|
|
201
|
-
if (sections[i].startsWith('audio')) {
|
|
202
|
-
sections[i] = SdpUtils.enableStereoOpus(sections[i])
|
|
203
|
-
|
|
204
|
-
if (nonAdvertisedCodecs.includes('pcma/8000/2')) {
|
|
205
|
-
sections[i] = SdpUtils.enableStereoPcmau(payloadTypes, sections[i])
|
|
206
|
-
}
|
|
207
|
-
if (nonAdvertisedCodecs.includes('multiopus/48000/6')) {
|
|
208
|
-
sections[i] = SdpUtils.enableMultichannelOpus(payloadTypes, sections[i])
|
|
209
|
-
}
|
|
210
|
-
if (nonAdvertisedCodecs.includes('L16/48000/2')) {
|
|
211
|
-
sections[i] = SdpUtils.enableL16(payloadTypes, sections[i])
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
break
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return sections.join('m=')
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Generate an SDP fragment.
|
|
223
|
-
*/
|
|
224
|
-
static generateSdpFragment(od: ParsedOffer, candidates: RTCIceCandidate[]): string {
|
|
225
|
-
const candidatesByMedia: Record<number, RTCIceCandidate[]> = {}
|
|
226
|
-
for (const candidate of candidates) {
|
|
227
|
-
const mid = candidate.sdpMLineIndex
|
|
228
|
-
if (mid) {
|
|
229
|
-
if (candidatesByMedia[mid] === undefined) {
|
|
230
|
-
candidatesByMedia[mid] = []
|
|
231
|
-
}
|
|
232
|
-
candidatesByMedia[mid].push(candidate)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
let frag = `a=ice-ufrag:${od.iceUfrag}\r\n` + `a=ice-pwd:${od.icePwd}\r\n`
|
|
237
|
-
|
|
238
|
-
let mid = 0
|
|
239
|
-
|
|
240
|
-
for (const media of od.medias) {
|
|
241
|
-
if (candidatesByMedia[mid] !== undefined) {
|
|
242
|
-
frag += `m=${media}\r\n` + `a=mid:${mid}\r\n`
|
|
243
|
-
|
|
244
|
-
for (const candidate of candidatesByMedia[mid]) {
|
|
245
|
-
frag += `a=${candidate.candidate}\r\n`
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
mid++
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return frag
|
|
252
|
-
}
|
|
253
|
-
}
|
package/src/utils/webrtc.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { ErrorTypes, WebRTCError } from '~/errors'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* WebRTC utilities
|
|
5
|
-
*/
|
|
6
|
-
export class WebRtcUtils {
|
|
7
|
-
/**
|
|
8
|
-
* Check if the browser supports a non-advertised codec.
|
|
9
|
-
*/
|
|
10
|
-
static async supportsNonAdvertisedCodec(codec: string, fmtp?: string): Promise<boolean> {
|
|
11
|
-
return new Promise((resolve) => {
|
|
12
|
-
const pc = new RTCPeerConnection({ iceServers: [] })
|
|
13
|
-
const mediaType = 'audio'
|
|
14
|
-
let payloadType = ''
|
|
15
|
-
|
|
16
|
-
pc.addTransceiver(mediaType, { direction: 'recvonly' })
|
|
17
|
-
pc.createOffer()
|
|
18
|
-
.then((offer) => {
|
|
19
|
-
if (!offer.sdp) {
|
|
20
|
-
throw new Error('SDP not present')
|
|
21
|
-
}
|
|
22
|
-
if (offer.sdp.includes(` ${codec}`)) {
|
|
23
|
-
// codec is advertised, there's no need to add it manually
|
|
24
|
-
throw new Error('already present')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const sections = offer.sdp.split(`m=${mediaType}`)
|
|
28
|
-
|
|
29
|
-
const payloadTypes = sections
|
|
30
|
-
.slice(1)
|
|
31
|
-
.map(s => s.split('\r\n')[0].split(' ').slice(3))
|
|
32
|
-
.reduce((prev, cur) => [...prev, ...cur], [])
|
|
33
|
-
payloadType = WebRtcUtils.reservePayloadType(payloadTypes)
|
|
34
|
-
|
|
35
|
-
const lines = sections[1].split('\r\n')
|
|
36
|
-
lines[0] += ` ${payloadType}`
|
|
37
|
-
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} ${codec}`)
|
|
38
|
-
if (fmtp !== undefined) {
|
|
39
|
-
lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} ${fmtp}`)
|
|
40
|
-
}
|
|
41
|
-
sections[1] = lines.join('\r\n')
|
|
42
|
-
offer.sdp = sections.join(`m=${mediaType}`)
|
|
43
|
-
return pc.setLocalDescription(offer)
|
|
44
|
-
})
|
|
45
|
-
.then(() =>
|
|
46
|
-
pc.setRemoteDescription(
|
|
47
|
-
new RTCSessionDescription({
|
|
48
|
-
type: 'answer',
|
|
49
|
-
sdp:
|
|
50
|
-
`v=0\r\n`
|
|
51
|
-
+ `o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n`
|
|
52
|
-
+ `s=-\r\n`
|
|
53
|
-
+ `t=0 0\r\n`
|
|
54
|
-
+ `a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n`
|
|
55
|
-
+ `m=${mediaType} 9 UDP/TLS/RTP/SAVPF ${payloadType}\r\n`
|
|
56
|
-
+ `c=IN IP4 0.0.0.0\r\n`
|
|
57
|
-
+ `a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n`
|
|
58
|
-
+ `a=ice-ufrag:29e036dc\r\n`
|
|
59
|
-
+ `a=sendonly\r\n`
|
|
60
|
-
+ `a=rtcp-mux\r\n`
|
|
61
|
-
+ `a=rtpmap:${payloadType} ${codec}\r\n${fmtp !== undefined ? `a=fmtp:${payloadType} ${fmtp}\r\n` : ''}`,
|
|
62
|
-
}),
|
|
63
|
-
),
|
|
64
|
-
)
|
|
65
|
-
.then(() => resolve(true))
|
|
66
|
-
.catch(() => resolve(false))
|
|
67
|
-
.finally(() => pc.close())
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Unquote a credential string.
|
|
73
|
-
*/
|
|
74
|
-
static unquoteCredential(v: string): string {
|
|
75
|
-
return JSON.parse(`"${v}"`)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Convert Link header to iceServers array.
|
|
80
|
-
*/
|
|
81
|
-
static linkToIceServers(links: string | null): RTCIceServer[] {
|
|
82
|
-
if (links) {
|
|
83
|
-
return links.split(', ').map((link) => {
|
|
84
|
-
const m = link.match(
|
|
85
|
-
/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i,
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
if (!m) {
|
|
89
|
-
throw new WebRTCError(ErrorTypes.SIGNAL_ERROR, 'Invalid ICE server link format')
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const ret: RTCIceServer = {
|
|
93
|
-
urls: [m[1]],
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (m[3]) {
|
|
97
|
-
ret.username = WebRtcUtils.unquoteCredential(m[3])
|
|
98
|
-
ret.credential = WebRtcUtils.unquoteCredential(m[4])
|
|
99
|
-
ret.credentialType = 'password'
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return ret
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
return []
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Reserve a payload type.
|
|
110
|
-
*/
|
|
111
|
-
static reservePayloadType(payloadTypes: string[]): string {
|
|
112
|
-
// everything is valid between 30 and 127, except for interval between 64 and 95
|
|
113
|
-
// https://chromium.googlesource.com/external/webrtc/+/refs/heads/master/call/payload_type.h#29
|
|
114
|
-
for (let i = 30; i <= 127; i++) {
|
|
115
|
-
if ((i <= 63 || i >= 96) && !payloadTypes.includes(i.toString())) {
|
|
116
|
-
const pl = i.toString()
|
|
117
|
-
payloadTypes.push(pl)
|
|
118
|
-
return pl
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
throw new Error('unable to find a free payload type')
|
|
122
|
-
}
|
|
123
|
-
}
|