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/whep.ts
DELETED
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
import type { ParsedOffer } from './utils/sdp'
|
|
2
|
-
import VisibilityObserver from '~/utils/observer'
|
|
3
|
-
import { ErrorTypes, WebRTCError } from './errors'
|
|
4
|
-
import { FlowCheck } from './utils/flow-check'
|
|
5
|
-
import { SdpUtils } from './utils/sdp'
|
|
6
|
-
import { WebRtcUtils } from './utils/webrtc'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Configuration interface for WebRTCWhep.
|
|
10
|
-
*/
|
|
11
|
-
export interface Conf {
|
|
12
|
-
/** Absolute URL of the WHEP endpoint */
|
|
13
|
-
url: string
|
|
14
|
-
/** Media player container */
|
|
15
|
-
container: HTMLMediaElement
|
|
16
|
-
/** Username for authentication */
|
|
17
|
-
user?: string
|
|
18
|
-
/** Password for authentication */
|
|
19
|
-
pass?: string
|
|
20
|
-
/** Token for authentication */
|
|
21
|
-
token?: string
|
|
22
|
-
/** ice server list */
|
|
23
|
-
iceServers?: RTCIceServer[]
|
|
24
|
-
/** Called when there's an error */
|
|
25
|
-
onError?: (err: WebRTCError) => void
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Extend RTCConfiguration to include experimental properties */
|
|
29
|
-
declare global {
|
|
30
|
-
interface RTCConfiguration {
|
|
31
|
-
sdpSemantics?: 'plan-b' | 'unified-plan'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface RTCIceServer {
|
|
35
|
-
credentialType?: 'password'
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** WebRTC/WHEP reader. */
|
|
40
|
-
export default class WebRTCWhep {
|
|
41
|
-
private retryPause: number = 2000
|
|
42
|
-
private conf: Conf
|
|
43
|
-
private state: 'getting_codecs' | 'running' | 'restarting' | 'closed' | 'failed'
|
|
44
|
-
private restartTimeout?: NodeJS.Timeout
|
|
45
|
-
private pc?: RTCPeerConnection
|
|
46
|
-
private offerData?: ParsedOffer
|
|
47
|
-
private sessionUrl?: string
|
|
48
|
-
private queuedCandidates: RTCIceCandidate[] = []
|
|
49
|
-
private nonAdvertisedCodecs: string[] = []
|
|
50
|
-
private observer: VisibilityObserver
|
|
51
|
-
private stream?: MediaStream
|
|
52
|
-
private flowCheck: FlowCheck
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Create a WebRTCWhep.
|
|
56
|
-
* @param {Conf} conf - Configuration.
|
|
57
|
-
*/
|
|
58
|
-
constructor(conf: Conf) {
|
|
59
|
-
this.conf = conf
|
|
60
|
-
this.state = 'getting_codecs'
|
|
61
|
-
this.observer = new VisibilityObserver()
|
|
62
|
-
this.flowCheck = new FlowCheck({
|
|
63
|
-
interval: 5000,
|
|
64
|
-
onError: (err: WebRTCError) => this.handleError(err),
|
|
65
|
-
})
|
|
66
|
-
this.getNonAdvertisedCodecs()
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 媒体是否正常
|
|
71
|
-
*/
|
|
72
|
-
get isRunning(): boolean {
|
|
73
|
-
return this.state === 'running'
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Close the reader and all its resources.
|
|
78
|
-
*/
|
|
79
|
-
close(): void {
|
|
80
|
-
this.state = 'closed'
|
|
81
|
-
this.pc?.close()
|
|
82
|
-
|
|
83
|
-
this.observer.stop()
|
|
84
|
-
this.flowCheck.stop()
|
|
85
|
-
if (this.restartTimeout) {
|
|
86
|
-
clearTimeout(this.restartTimeout)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Handle errors.
|
|
92
|
-
*/
|
|
93
|
-
private handleError(err: Error | WebRTCError): void {
|
|
94
|
-
this.flowCheck.stop()
|
|
95
|
-
|
|
96
|
-
if (this.state === 'getting_codecs') {
|
|
97
|
-
this.state = 'failed'
|
|
98
|
-
}
|
|
99
|
-
else if (err instanceof WebRTCError && err.type === ErrorTypes.SIGNAL_ERROR) {
|
|
100
|
-
this.state = 'failed'
|
|
101
|
-
}
|
|
102
|
-
else if (this.state === 'running') {
|
|
103
|
-
this.pc?.close()
|
|
104
|
-
this.pc = undefined
|
|
105
|
-
|
|
106
|
-
this.offerData = undefined
|
|
107
|
-
this.queuedCandidates = []
|
|
108
|
-
|
|
109
|
-
if (this.sessionUrl) {
|
|
110
|
-
fetch(this.sessionUrl, {
|
|
111
|
-
method: 'DELETE',
|
|
112
|
-
})
|
|
113
|
-
this.sessionUrl = undefined
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
this.state = 'restarting'
|
|
117
|
-
|
|
118
|
-
this.restartTimeout = setTimeout(() => {
|
|
119
|
-
this.restartTimeout = undefined
|
|
120
|
-
this.state = 'running'
|
|
121
|
-
this.start()
|
|
122
|
-
}, this.retryPause)
|
|
123
|
-
|
|
124
|
-
err.message = `${err.message}, retrying in some seconds`
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (this.conf.onError) {
|
|
128
|
-
if (err instanceof WebRTCError) {
|
|
129
|
-
this.conf.onError(err)
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
this.conf.onError(new WebRTCError(ErrorTypes.OTHER_ERROR, err.message))
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Get non-advertised codecs.
|
|
139
|
-
*/
|
|
140
|
-
private getNonAdvertisedCodecs(): void {
|
|
141
|
-
Promise.all(
|
|
142
|
-
[
|
|
143
|
-
['pcma/8000/2'],
|
|
144
|
-
['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'],
|
|
145
|
-
['L16/48000/2'],
|
|
146
|
-
].map(c => WebRtcUtils.supportsNonAdvertisedCodec(c[0], c[1]).then(r => (r ? c[0] : false))),
|
|
147
|
-
)
|
|
148
|
-
.then(c => c.filter(e => e !== false))
|
|
149
|
-
.then((codecs) => {
|
|
150
|
-
if (this.state !== 'getting_codecs') {
|
|
151
|
-
throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed')
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
this.nonAdvertisedCodecs = codecs as string[]
|
|
155
|
-
this.state = 'running'
|
|
156
|
-
this.start()
|
|
157
|
-
})
|
|
158
|
-
.catch(err => this.handleError(err))
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Start the WebRTC session.
|
|
163
|
-
*/
|
|
164
|
-
private start(): void {
|
|
165
|
-
this.requestICEServers()
|
|
166
|
-
.then(iceServers => this.setupPeerConnection(iceServers))
|
|
167
|
-
.then(offer => this.sendOffer(offer))
|
|
168
|
-
.then(answer => this.setAnswer(answer))
|
|
169
|
-
.catch(err => this.handleError(err))
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Generate an authorization header.
|
|
174
|
-
*/
|
|
175
|
-
private authHeader(): Record<string, string> {
|
|
176
|
-
if (this.conf.user && this.conf.user !== '') {
|
|
177
|
-
const credentials = btoa(`${this.conf.user}:${this.conf.pass}`)
|
|
178
|
-
return { Authorization: `Basic ${credentials}` }
|
|
179
|
-
}
|
|
180
|
-
if (this.conf.token && this.conf.token !== '') {
|
|
181
|
-
return { Authorization: `Bearer ${this.conf.token}` }
|
|
182
|
-
}
|
|
183
|
-
return {}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Request ICE servers from the endpoint.
|
|
188
|
-
*/
|
|
189
|
-
private async requestICEServers(): Promise<RTCIceServer[]> {
|
|
190
|
-
if (this.conf.iceServers && this.conf.iceServers.length > 0) {
|
|
191
|
-
return this.conf.iceServers
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return fetch(this.conf.url, {
|
|
195
|
-
method: 'OPTIONS',
|
|
196
|
-
headers: {
|
|
197
|
-
...this.authHeader(),
|
|
198
|
-
},
|
|
199
|
-
}).then(res => WebRtcUtils.linkToIceServers(res.headers.get('Link')))
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Setup a peer connection.
|
|
204
|
-
*/
|
|
205
|
-
private async setupPeerConnection(iceServers: RTCIceServer[]): Promise<string> {
|
|
206
|
-
if (this.state !== 'running') {
|
|
207
|
-
throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed')
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const pc = new RTCPeerConnection({
|
|
211
|
-
iceServers,
|
|
212
|
-
// https://webrtc.org/getting-started/unified-plan-transition-guide
|
|
213
|
-
sdpSemantics: 'unified-plan',
|
|
214
|
-
})
|
|
215
|
-
this.pc = pc
|
|
216
|
-
this.flowCheck.setPeerConnection(pc)
|
|
217
|
-
|
|
218
|
-
const direction: RTCRtpTransceiverDirection = 'recvonly'
|
|
219
|
-
pc.addTransceiver('video', { direction })
|
|
220
|
-
pc.addTransceiver('audio', { direction })
|
|
221
|
-
|
|
222
|
-
pc.onicecandidate = (evt: RTCPeerConnectionIceEvent) => this.onLocalCandidate(evt)
|
|
223
|
-
pc.onconnectionstatechange = () => this.onConnectionState()
|
|
224
|
-
pc.ontrack = (evt: RTCTrackEvent) => this.onTrack(evt)
|
|
225
|
-
|
|
226
|
-
return pc.createOffer().then((offer) => {
|
|
227
|
-
if (!offer.sdp) {
|
|
228
|
-
throw new WebRTCError(ErrorTypes.SIGNAL_ERROR, 'Failed to create offer SDP')
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
offer.sdp = SdpUtils.editOffer(offer.sdp, this.nonAdvertisedCodecs)
|
|
232
|
-
this.offerData = SdpUtils.parseOffer(offer.sdp)
|
|
233
|
-
|
|
234
|
-
return pc.setLocalDescription(offer).then(() => offer.sdp!)
|
|
235
|
-
})
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Send an offer to the endpoint.
|
|
240
|
-
*/
|
|
241
|
-
private sendOffer(offer: string): Promise<string> {
|
|
242
|
-
if (this.state !== 'running') {
|
|
243
|
-
throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed')
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return fetch(this.conf.url, {
|
|
247
|
-
method: 'POST',
|
|
248
|
-
headers: {
|
|
249
|
-
...this.authHeader(),
|
|
250
|
-
'Content-Type': 'application/sdp',
|
|
251
|
-
},
|
|
252
|
-
body: offer,
|
|
253
|
-
}).then((res) => {
|
|
254
|
-
switch (res.status) {
|
|
255
|
-
case 201:
|
|
256
|
-
break
|
|
257
|
-
case 404:
|
|
258
|
-
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, 'stream not found')
|
|
259
|
-
case 406:
|
|
260
|
-
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, 'stream not supported')
|
|
261
|
-
case 400:
|
|
262
|
-
return res.json().then((e: { error: string }) => {
|
|
263
|
-
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, e.error)
|
|
264
|
-
})
|
|
265
|
-
default:
|
|
266
|
-
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, `bad status code ${res.status}`)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const location = res.headers.get('Location')
|
|
270
|
-
if (location) {
|
|
271
|
-
this.sessionUrl = new URL(location, this.conf.url).toString()
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return res.text()
|
|
275
|
-
})
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Set a remote answer.
|
|
280
|
-
*/
|
|
281
|
-
private setAnswer(answer: string): Promise<void> {
|
|
282
|
-
if (this.state !== 'running') {
|
|
283
|
-
throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed')
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return this.pc!.setRemoteDescription(
|
|
287
|
-
new RTCSessionDescription({
|
|
288
|
-
type: 'answer',
|
|
289
|
-
sdp: answer,
|
|
290
|
-
}),
|
|
291
|
-
).then(() => {
|
|
292
|
-
if (this.state !== 'running') {
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (this.queuedCandidates.length !== 0) {
|
|
297
|
-
this.sendLocalCandidates(this.queuedCandidates)
|
|
298
|
-
this.queuedCandidates = []
|
|
299
|
-
}
|
|
300
|
-
})
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Handle local ICE candidates.
|
|
305
|
-
*/
|
|
306
|
-
private onLocalCandidate(evt: RTCPeerConnectionIceEvent): void {
|
|
307
|
-
if (this.state !== 'running') {
|
|
308
|
-
return
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (evt.candidate) {
|
|
312
|
-
if (this.sessionUrl) {
|
|
313
|
-
this.sendLocalCandidates([evt.candidate])
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
this.queuedCandidates.push(evt.candidate)
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Send local ICE candidates to the endpoint.
|
|
323
|
-
*/
|
|
324
|
-
private sendLocalCandidates(candidates: RTCIceCandidate[]): void {
|
|
325
|
-
if (!this.sessionUrl || !this.offerData) {
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
fetch(this.sessionUrl, {
|
|
330
|
-
method: 'PATCH',
|
|
331
|
-
headers: {
|
|
332
|
-
'Content-Type': 'application/trickle-ice-sdpfrag',
|
|
333
|
-
'If-Match': '*',
|
|
334
|
-
},
|
|
335
|
-
body: SdpUtils.generateSdpFragment(this.offerData, candidates),
|
|
336
|
-
})
|
|
337
|
-
.then((res) => {
|
|
338
|
-
switch (res.status) {
|
|
339
|
-
case 204:
|
|
340
|
-
break
|
|
341
|
-
case 404:
|
|
342
|
-
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, 'stream not found')
|
|
343
|
-
default:
|
|
344
|
-
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, `bad status code ${res.status}`)
|
|
345
|
-
}
|
|
346
|
-
})
|
|
347
|
-
.catch(err => this.handleError(err))
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Handle peer connection state changes.
|
|
352
|
-
*/
|
|
353
|
-
private onConnectionState(): void {
|
|
354
|
-
if (this.state !== 'running' || !this.pc) {
|
|
355
|
-
return
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// "closed" can arrive before "failed" and without
|
|
359
|
-
// the close() method being called at all.
|
|
360
|
-
// It happens when the other peer sends a termination
|
|
361
|
-
// message like a DTLS CloseNotify.
|
|
362
|
-
if (this.pc.connectionState === 'failed' || this.pc.connectionState === 'closed') {
|
|
363
|
-
this.handleError(new WebRTCError(ErrorTypes.OTHER_ERROR, 'peer connection closed'))
|
|
364
|
-
}
|
|
365
|
-
else if (this.pc.connectionState === 'connected') {
|
|
366
|
-
this.flowCheck.start()
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Handle incoming tracks.
|
|
372
|
-
*/
|
|
373
|
-
private onTrack(evt: RTCTrackEvent): void {
|
|
374
|
-
this.stream = evt.streams[0]
|
|
375
|
-
this.observer.start(this.conf.container, (isIntersecting) => {
|
|
376
|
-
if (isIntersecting)
|
|
377
|
-
this.resume()
|
|
378
|
-
else
|
|
379
|
-
this.pause()
|
|
380
|
-
})
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* 流是否为空
|
|
385
|
-
*/
|
|
386
|
-
get paused() {
|
|
387
|
-
return this.conf.container.srcObject === null
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* 暂停播放
|
|
392
|
-
*/
|
|
393
|
-
pause() {
|
|
394
|
-
this.conf.container.srcObject = null
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* 恢复播放
|
|
399
|
-
*/
|
|
400
|
-
resume() {
|
|
401
|
-
if (this.stream && this.paused)
|
|
402
|
-
this.conf.container.srcObject = this.stream
|
|
403
|
-
}
|
|
404
|
-
}
|