whepts 1.0.2 → 1.0.3
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/.claude/settings.local.json +9 -0
- package/AGENTS.md +92 -0
- 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/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 +8 -9
- package/src/core/codec.ts +33 -0
- package/src/core/connection.ts +103 -0
- package/src/core/http.ts +93 -0
- package/src/core/track.ts +51 -0
- package/src/index.ts +3 -2
- package/src/types.ts +37 -0
- package/src/whep.ts +76 -305
- package/QWEN.md +0 -118
- package/dist/utils/observer.d.ts +0 -8
- package/src/utils/observer.ts +0 -28
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class TrackManager {
|
|
2
|
+
private stream?: MediaStream
|
|
3
|
+
private observer?: IntersectionObserver
|
|
4
|
+
|
|
5
|
+
constructor(private container: HTMLMediaElement) {}
|
|
6
|
+
|
|
7
|
+
onTrack(evt: RTCTrackEvent): void {
|
|
8
|
+
this.stream = evt.streams[0]
|
|
9
|
+
|
|
10
|
+
// 停止之前的观察器
|
|
11
|
+
this.stopObserver()
|
|
12
|
+
|
|
13
|
+
// 创建新的可见性观察器,自动处理暂停/恢复
|
|
14
|
+
this.observer = new IntersectionObserver(
|
|
15
|
+
([entry]) => {
|
|
16
|
+
if (entry.isIntersecting)
|
|
17
|
+
this.resume()
|
|
18
|
+
else
|
|
19
|
+
this.pause()
|
|
20
|
+
},
|
|
21
|
+
{ threshold: 0.5 },
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
this.observer.observe(this.container)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private stopObserver(): void {
|
|
28
|
+
if (this.observer) {
|
|
29
|
+
this.observer.disconnect()
|
|
30
|
+
this.observer = undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get paused(): boolean {
|
|
35
|
+
return this.container.srcObject === null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pause(): void {
|
|
39
|
+
this.container.srcObject = null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resume(): void {
|
|
43
|
+
if (this.stream && this.paused)
|
|
44
|
+
this.container.srcObject = this.stream
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
stop(): void {
|
|
48
|
+
this.stopObserver()
|
|
49
|
+
this.stream = undefined
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { Conf, State } from './types'
|
|
1
2
|
import { ErrorTypes, WebRTCError } from './errors'
|
|
2
|
-
import WebRTCWhep
|
|
3
|
+
import WebRTCWhep from './whep'
|
|
3
4
|
|
|
4
|
-
export { Conf, ErrorTypes, WebRTCError }
|
|
5
|
+
export { Conf, ErrorTypes, State, WebRTCError }
|
|
5
6
|
|
|
6
7
|
export default WebRTCWhep
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { WebRTCError } from './errors'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration interface for WebRTCWhep.
|
|
5
|
+
*/
|
|
6
|
+
export interface Conf {
|
|
7
|
+
/** Absolute URL of the WHEP endpoint */
|
|
8
|
+
url: string
|
|
9
|
+
/** Media player container */
|
|
10
|
+
container: HTMLMediaElement
|
|
11
|
+
/** Username for authentication */
|
|
12
|
+
user?: string
|
|
13
|
+
/** Password for authentication */
|
|
14
|
+
pass?: string
|
|
15
|
+
/** Token for authentication */
|
|
16
|
+
token?: string
|
|
17
|
+
/** ice server list */
|
|
18
|
+
iceServers?: RTCIceServer[]
|
|
19
|
+
/** Called when there's an error */
|
|
20
|
+
onError?: (err: WebRTCError) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* State type for WebRTCWhep.
|
|
25
|
+
*/
|
|
26
|
+
export type State = 'getting_codecs' | 'running' | 'restarting' | 'closed' | 'failed'
|
|
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
|
+
}
|
package/src/whep.ts
CHANGED
|
@@ -1,95 +1,81 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
1
|
+
import type { Conf, State } from './types'
|
|
2
|
+
import { CodecDetector } from './core/codec'
|
|
3
|
+
import { ConnectionManager } from './core/connection'
|
|
4
|
+
import { HttpClient } from './core/http'
|
|
5
|
+
import { TrackManager } from './core/track'
|
|
3
6
|
import { ErrorTypes, WebRTCError } from './errors'
|
|
4
7
|
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
8
|
|
|
39
9
|
/** WebRTC/WHEP reader. */
|
|
40
10
|
export default class WebRTCWhep {
|
|
41
11
|
private retryPause: number = 2000
|
|
42
12
|
private conf: Conf
|
|
43
|
-
private state:
|
|
13
|
+
private state: State
|
|
44
14
|
private restartTimeout?: NodeJS.Timeout
|
|
45
|
-
private pc?: RTCPeerConnection
|
|
46
|
-
private offerData?: ParsedOffer
|
|
47
15
|
private sessionUrl?: string
|
|
48
16
|
private queuedCandidates: RTCIceCandidate[] = []
|
|
49
17
|
private nonAdvertisedCodecs: string[] = []
|
|
50
|
-
private observer: VisibilityObserver
|
|
51
|
-
private stream?: MediaStream
|
|
52
18
|
private flowCheck: FlowCheck
|
|
19
|
+
private httpClient: HttpClient
|
|
20
|
+
private connectionManager: ConnectionManager
|
|
21
|
+
private trackManager: TrackManager
|
|
22
|
+
private codecDetector: CodecDetector
|
|
53
23
|
|
|
54
|
-
/**
|
|
55
|
-
* Create a WebRTCWhep.
|
|
56
|
-
* @param {Conf} conf - Configuration.
|
|
57
|
-
*/
|
|
58
24
|
constructor(conf: Conf) {
|
|
59
25
|
this.conf = conf
|
|
60
26
|
this.state = 'getting_codecs'
|
|
61
|
-
|
|
27
|
+
|
|
62
28
|
this.flowCheck = new FlowCheck({
|
|
63
29
|
interval: 5000,
|
|
64
30
|
onError: (err: WebRTCError) => this.handleError(err),
|
|
65
31
|
})
|
|
66
|
-
|
|
32
|
+
|
|
33
|
+
this.httpClient = new HttpClient(
|
|
34
|
+
this.conf,
|
|
35
|
+
() => this.state,
|
|
36
|
+
err => this.handleError(err),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
this.connectionManager = new ConnectionManager(
|
|
40
|
+
() => this.state,
|
|
41
|
+
{
|
|
42
|
+
onCandidate: candidate => this.handleCandidate(candidate),
|
|
43
|
+
onTrack: (evt) => {
|
|
44
|
+
this.trackManager.onTrack(evt)
|
|
45
|
+
this.flowCheck.start()
|
|
46
|
+
},
|
|
47
|
+
onError: err => this.handleError(err),
|
|
48
|
+
},
|
|
49
|
+
this.nonAdvertisedCodecs,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
this.trackManager = new TrackManager(this.conf.container)
|
|
53
|
+
|
|
54
|
+
this.codecDetector = new CodecDetector(
|
|
55
|
+
() => this.state,
|
|
56
|
+
{
|
|
57
|
+
onCodecsDetected: codecs => this.handleCodecsDetected(codecs),
|
|
58
|
+
onError: err => this.handleError(err),
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
this.codecDetector.detect()
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
/**
|
|
70
|
-
* 媒体是否正常
|
|
71
|
-
*/
|
|
72
65
|
get isRunning(): boolean {
|
|
73
66
|
return this.state === 'running'
|
|
74
67
|
}
|
|
75
68
|
|
|
76
|
-
/**
|
|
77
|
-
* Close the reader and all its resources.
|
|
78
|
-
*/
|
|
79
69
|
close(): void {
|
|
80
70
|
this.state = 'closed'
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
this.observer.stop()
|
|
71
|
+
this.connectionManager.close()
|
|
72
|
+
this.trackManager.stop()
|
|
84
73
|
this.flowCheck.stop()
|
|
85
74
|
if (this.restartTimeout) {
|
|
86
75
|
clearTimeout(this.restartTimeout)
|
|
87
76
|
}
|
|
88
77
|
}
|
|
89
78
|
|
|
90
|
-
/**
|
|
91
|
-
* Handle errors.
|
|
92
|
-
*/
|
|
93
79
|
private handleError(err: Error | WebRTCError): void {
|
|
94
80
|
this.flowCheck.stop()
|
|
95
81
|
|
|
@@ -100,10 +86,7 @@ export default class WebRTCWhep {
|
|
|
100
86
|
this.state = 'failed'
|
|
101
87
|
}
|
|
102
88
|
else if (this.state === 'running') {
|
|
103
|
-
this.
|
|
104
|
-
this.pc = undefined
|
|
105
|
-
|
|
106
|
-
this.offerData = undefined
|
|
89
|
+
this.connectionManager.close()
|
|
107
90
|
this.queuedCandidates = []
|
|
108
91
|
|
|
109
92
|
if (this.sessionUrl) {
|
|
@@ -134,271 +117,59 @@ export default class WebRTCWhep {
|
|
|
134
117
|
}
|
|
135
118
|
}
|
|
136
119
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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))
|
|
120
|
+
private handleCodecsDetected(codecs: string[]): void {
|
|
121
|
+
this.nonAdvertisedCodecs = codecs
|
|
122
|
+
this.state = 'running'
|
|
123
|
+
this.start()
|
|
159
124
|
}
|
|
160
125
|
|
|
161
|
-
/**
|
|
162
|
-
* Start the WebRTC session.
|
|
163
|
-
*/
|
|
164
126
|
private start(): void {
|
|
165
|
-
this.requestICEServers()
|
|
166
|
-
.then(iceServers => this.setupPeerConnection(iceServers))
|
|
167
|
-
.then(offer => this.sendOffer(offer))
|
|
168
|
-
.then(answer => this.
|
|
127
|
+
this.httpClient.requestICEServers()
|
|
128
|
+
.then(iceServers => this.connectionManager.setupPeerConnection(iceServers))
|
|
129
|
+
.then(offer => this.httpClient.sendOffer(offer))
|
|
130
|
+
.then(({ sessionUrl, answer }) => this.handleOfferResponse(sessionUrl, answer))
|
|
169
131
|
.catch(err => this.handleError(err))
|
|
170
132
|
}
|
|
171
133
|
|
|
172
|
-
|
|
173
|
-
|
|
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 })
|
|
134
|
+
private handleOfferResponse(sessionUrl: string | undefined, answer: string): Promise<void> {
|
|
135
|
+
if (sessionUrl)
|
|
136
|
+
this.sessionUrl = sessionUrl
|
|
221
137
|
|
|
222
|
-
|
|
223
|
-
|
|
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') {
|
|
138
|
+
return this.connectionManager.setAnswer(answer).then(() => {
|
|
139
|
+
if (this.state !== 'running')
|
|
293
140
|
return
|
|
294
|
-
}
|
|
295
141
|
|
|
296
142
|
if (this.queuedCandidates.length !== 0) {
|
|
297
|
-
this.
|
|
298
|
-
this.
|
|
143
|
+
const offerData = this.connectionManager.getOfferData()
|
|
144
|
+
if (offerData && this.sessionUrl) {
|
|
145
|
+
this.httpClient.sendLocalCandidates(this.sessionUrl, offerData, this.queuedCandidates)
|
|
146
|
+
this.queuedCandidates = []
|
|
147
|
+
}
|
|
299
148
|
}
|
|
300
149
|
})
|
|
301
150
|
}
|
|
302
151
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (evt.candidate) {
|
|
312
|
-
if (this.sessionUrl) {
|
|
313
|
-
this.sendLocalCandidates([evt.candidate])
|
|
152
|
+
private handleCandidate(candidate: RTCIceCandidate): void {
|
|
153
|
+
if (this.sessionUrl) {
|
|
154
|
+
const offerData = this.connectionManager.getOfferData()
|
|
155
|
+
if (offerData) {
|
|
156
|
+
this.httpClient.sendLocalCandidates(this.sessionUrl, offerData, [candidate])
|
|
314
157
|
}
|
|
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
158
|
}
|
|
357
|
-
|
|
358
|
-
|
|
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()
|
|
159
|
+
else {
|
|
160
|
+
this.queuedCandidates.push(candidate)
|
|
367
161
|
}
|
|
368
162
|
}
|
|
369
163
|
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
164
|
+
get paused(): boolean {
|
|
165
|
+
return this.trackManager.paused
|
|
388
166
|
}
|
|
389
167
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
*/
|
|
393
|
-
pause() {
|
|
394
|
-
this.conf.container.srcObject = null
|
|
168
|
+
pause(): void {
|
|
169
|
+
this.trackManager.pause()
|
|
395
170
|
}
|
|
396
171
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
*/
|
|
400
|
-
resume() {
|
|
401
|
-
if (this.stream && this.paused)
|
|
402
|
-
this.conf.container.srcObject = this.stream
|
|
172
|
+
resume(): void {
|
|
173
|
+
this.trackManager.resume()
|
|
403
174
|
}
|
|
404
175
|
}
|
package/QWEN.md
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# QWEN.md - WhepTS Player 项目上下文
|
|
2
|
-
|
|
3
|
-
## 项目概述
|
|
4
|
-
|
|
5
|
-
**WhepTS Player** 是一个基于 [mediamtx](https://github.com/bluenviron/mediamtx) 的 WebRTC WHEP (WebRTC HTTP Egress Protocol) 播放器,支持 ZLMediaKit 和 Mediamtx 的播放地址。该项目使用 TypeScript 编写,提供低延迟的实时音视频流播放功能。
|
|
6
|
-
|
|
7
|
-
### 核心功能
|
|
8
|
-
|
|
9
|
-
- 支持 ZLMediaKit 和 Mediamtx 的 WHEP 播放地址
|
|
10
|
-
- 基于 WebRTC 的低延迟播放
|
|
11
|
-
- 类型安全的 TypeScript 实现
|
|
12
|
-
- 事件监听和错误处理机制
|
|
13
|
-
- 可配置的连接参数
|
|
14
|
-
- 自动断流检测与重连功能
|
|
15
|
-
- 支持多种音视频编解码器
|
|
16
|
-
- 支持仅可视区域播放控制(通过 `VisibilityObserver`)
|
|
17
|
-
- 在 Chrome 上根据硬件支持 G711 和 H265 编解码器
|
|
18
|
-
|
|
19
|
-
## 技术栈与依赖
|
|
20
|
-
|
|
21
|
-
- **语言**: TypeScript
|
|
22
|
-
- **构建工具**: Rollup
|
|
23
|
-
- **包管理器**: pnpm
|
|
24
|
-
- **代码规范**: ESLint (使用 @antfu/eslint-config)
|
|
25
|
-
- **核心 API**: WebRTC API
|
|
26
|
-
- **开发依赖**:
|
|
27
|
-
- `@antfu/eslint-config`
|
|
28
|
-
- `@rollup/plugin-commonjs`
|
|
29
|
-
- `@rollup/plugin-eslint`
|
|
30
|
-
- `@rollup/plugin-terser`
|
|
31
|
-
- `@rollup/plugin-typescript`
|
|
32
|
-
- `@types/node`
|
|
33
|
-
- `rollup`
|
|
34
|
-
- `rollup-plugin-delete`
|
|
35
|
-
- `tslib`
|
|
36
|
-
- `typescript`
|
|
37
|
-
|
|
38
|
-
## 项目结构
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
src/
|
|
42
|
-
├── index.ts # 主要的 WebRTCWhep 类实现
|
|
43
|
-
└── utils/
|
|
44
|
-
└── observer.ts # 可见性监测工具
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## 主要代码文件
|
|
48
|
-
|
|
49
|
-
### `src/index.ts`
|
|
50
|
-
|
|
51
|
-
这是项目的核心文件,实现了 `WebRTCWhep` 类,负责处理 WebRTC 连接、WHEP 协议交互、媒体流处理等主要功能。关键特性包括:
|
|
52
|
-
|
|
53
|
-
- **配置接口**: `Conf` 接口定义了播放器的配置选项,包括 WHEP 服务器地址、媒体容器、认证信息、ICE 服务器配置等。
|
|
54
|
-
- **错误处理**: 定义了 `WebRTCError` 类和 `ErrorTypes` 常量,用于处理不同类型的错误(信令、状态、网络、媒体等)。
|
|
55
|
-
- **连接管理**: 实现了与 WHEP 服务器的连接、发送 offer、接收 answer、处理 ICE 候选等流程。
|
|
56
|
-
- **编解码器支持**: 包含了对非标准编解码器(如 PCMA/PCMU、multiopus、L16 等)的支持逻辑。
|
|
57
|
-
- **断流检测**: 通过 `getStats()` 监控接收字节数,实现断流检测和自动重连。
|
|
58
|
-
- **可见性控制**: 与 `VisibilityObserver` 集成,实现仅在元素可见时播放的功能。
|
|
59
|
-
|
|
60
|
-
### `src/utils/observer.ts`
|
|
61
|
-
|
|
62
|
-
实现了 `VisibilityObserver` 类,使用 `IntersectionObserver` API 来监测媒体元素的可见性,以便在元素进入或离开视口时暂停或恢复播放。
|
|
63
|
-
|
|
64
|
-
## 构建与开发
|
|
65
|
-
|
|
66
|
-
### 构建命令
|
|
67
|
-
|
|
68
|
-
- `npm run build` - 生产环境构建(压缩代码)
|
|
69
|
-
- `npm run build:debug` - 开发环境构建(保留源映射)
|
|
70
|
-
|
|
71
|
-
### 开发命令
|
|
72
|
-
|
|
73
|
-
- `npm run dev` - 开发模式(如果 package.json 中有定义)
|
|
74
|
-
- `npm run lint` - 代码检查
|
|
75
|
-
- `npm run lint:fix` - 自动修复代码问题
|
|
76
|
-
|
|
77
|
-
### 构建配置
|
|
78
|
-
|
|
79
|
-
- **Rollup**: 使用 `rollup.config.ts` 进行构建配置,输出格式为 ES 模块,目标文件为 `dist/index.js`。
|
|
80
|
-
- **TypeScript**: 使用 `tsconfig.json` 进行编译配置,目标为 ES2020,启用严格模式和类型检查。
|
|
81
|
-
- **ESLint**: 使用 `eslint.config.ts` 进行代码规范检查,基于 @antfu/eslint-config。
|
|
82
|
-
|
|
83
|
-
## API 使用
|
|
84
|
-
|
|
85
|
-
### WebRTCWhep 类
|
|
86
|
-
|
|
87
|
-
#### 构造函数
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
WebRTCWhep(conf: Conf)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
- `conf`: 配置对象,包含 `url`、`container`、`user`、`pass`、`token`、`iceServers`、`onError` 等属性。
|
|
94
|
-
|
|
95
|
-
#### 属性
|
|
96
|
-
|
|
97
|
-
- `isRunning`: 检查流是否正在运行
|
|
98
|
-
- `paused`: 检查播放器是否已暂停
|
|
99
|
-
|
|
100
|
-
#### 方法
|
|
101
|
-
|
|
102
|
-
- `close()`: 关闭播放器和所有资源
|
|
103
|
-
- `pause()`: 暂停播放
|
|
104
|
-
- `resume()`: 恢复播放
|
|
105
|
-
|
|
106
|
-
## 开发约定
|
|
107
|
-
|
|
108
|
-
- 代码使用 TypeScript 编写,遵循严格的类型检查。
|
|
109
|
-
- 使用 ESLint 进行代码风格和质量检查。
|
|
110
|
-
- 使用 Rollup 进行模块打包。
|
|
111
|
-
- 代码结构清晰,核心逻辑集中在 `src/index.ts`,工具类在 `src/utils/` 目录下。
|
|
112
|
-
- 错误处理采用自定义的 `WebRTCError` 类,便于区分不同类型的错误。
|
|
113
|
-
- 支持自动断流检测和重连,提高播放稳定性。
|
|
114
|
-
- 支持可见性控制,优化资源使用。
|
|
115
|
-
|
|
116
|
-
## 许可证
|
|
117
|
-
|
|
118
|
-
MIT
|