whepts 1.0.1 → 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/errors.d.ts +21 -0
- package/dist/index.d.ts +5 -196
- package/dist/index.js +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/utils/flow-check.d.ts +29 -0
- package/dist/utils/sdp.d.ts +43 -0
- package/dist/utils/webrtc.d.ts +21 -0
- package/dist/whep.d.ts +27 -0
- package/package.json +13 -10
- 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/errors.ts +27 -0
- package/src/index.ts +5 -824
- package/src/types.ts +37 -0
- package/src/utils/flow-check.ts +73 -0
- package/src/utils/sdp.ts +253 -0
- package/src/utils/webrtc.ts +123 -0
- package/src/whep.ts +175 -0
- package/dist/utils/observer.d.ts +0 -8
- package/src/utils/observer.ts +0 -28
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { State } from '~/types'
|
|
2
|
+
import { ErrorTypes, WebRTCError } from '~/errors'
|
|
3
|
+
import { WebRtcUtils } from '../utils/webrtc'
|
|
4
|
+
|
|
5
|
+
export interface CodecDetectorCallbacks {
|
|
6
|
+
onCodecsDetected: (codecs: string[]) => void
|
|
7
|
+
onError: (err: Error) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class CodecDetector {
|
|
11
|
+
constructor(
|
|
12
|
+
private getState: () => State,
|
|
13
|
+
private callbacks: CodecDetectorCallbacks,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
detect(): void {
|
|
17
|
+
Promise.all(
|
|
18
|
+
[
|
|
19
|
+
['pcma/8000/2'],
|
|
20
|
+
['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'],
|
|
21
|
+
['L16/48000/2'],
|
|
22
|
+
].map(c => WebRtcUtils.supportsNonAdvertisedCodec(c[0], c[1]).then(r => (r ? c[0] : false))),
|
|
23
|
+
)
|
|
24
|
+
.then(c => c.filter(e => e !== false))
|
|
25
|
+
.then((codecs) => {
|
|
26
|
+
if (this.getState() !== 'getting_codecs')
|
|
27
|
+
throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed')
|
|
28
|
+
|
|
29
|
+
this.callbacks.onCodecsDetected(codecs as string[])
|
|
30
|
+
})
|
|
31
|
+
.catch(err => this.callbacks.onError(err))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ParsedOffer } from '../utils/sdp'
|
|
2
|
+
import type { State } from '~/types'
|
|
3
|
+
import { ErrorTypes, WebRTCError } from '~/errors'
|
|
4
|
+
import { SdpUtils } from '../utils/sdp'
|
|
5
|
+
|
|
6
|
+
export interface ConnectionManagerCallbacks {
|
|
7
|
+
onCandidate: (candidate: RTCIceCandidate) => void
|
|
8
|
+
onTrack: (evt: RTCTrackEvent) => void
|
|
9
|
+
onError: (err: WebRTCError) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ConnectionManager {
|
|
13
|
+
private pc?: RTCPeerConnection
|
|
14
|
+
private offerData?: ParsedOffer
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private getState: () => State,
|
|
18
|
+
private callbacks: ConnectionManagerCallbacks,
|
|
19
|
+
private nonAdvertisedCodecs: string[] = [],
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
async setupPeerConnection(iceServers: RTCIceServer[]): Promise<string> {
|
|
23
|
+
if (this.getState() !== 'running')
|
|
24
|
+
throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed')
|
|
25
|
+
|
|
26
|
+
const pc = new RTCPeerConnection({
|
|
27
|
+
iceServers,
|
|
28
|
+
sdpSemantics: 'unified-plan',
|
|
29
|
+
})
|
|
30
|
+
this.pc = pc
|
|
31
|
+
|
|
32
|
+
const direction: RTCRtpTransceiverDirection = 'recvonly'
|
|
33
|
+
pc.addTransceiver('video', { direction })
|
|
34
|
+
pc.addTransceiver('audio', { direction })
|
|
35
|
+
|
|
36
|
+
pc.onicecandidate = (evt: RTCPeerConnectionIceEvent) => this.onLocalCandidate(evt)
|
|
37
|
+
pc.onconnectionstatechange = () => this.onConnectionState()
|
|
38
|
+
pc.oniceconnectionstatechange = () => this.onIceConnectionState()
|
|
39
|
+
pc.ontrack = (evt: RTCTrackEvent) => this.callbacks.onTrack(evt)
|
|
40
|
+
|
|
41
|
+
return pc.createOffer().then((offer) => {
|
|
42
|
+
if (!offer.sdp)
|
|
43
|
+
throw new WebRTCError(ErrorTypes.SIGNAL_ERROR, 'Failed to create offer SDP')
|
|
44
|
+
|
|
45
|
+
offer.sdp = SdpUtils.editOffer(offer.sdp, this.nonAdvertisedCodecs)
|
|
46
|
+
this.offerData = SdpUtils.parseOffer(offer.sdp)
|
|
47
|
+
|
|
48
|
+
return pc.setLocalDescription(offer).then(() => offer.sdp!)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async setAnswer(answer: string): Promise<void> {
|
|
53
|
+
if (this.getState() !== 'running')
|
|
54
|
+
throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed')
|
|
55
|
+
|
|
56
|
+
return this.pc!.setRemoteDescription(
|
|
57
|
+
new RTCSessionDescription({
|
|
58
|
+
type: 'answer',
|
|
59
|
+
sdp: answer,
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getPeerConnection(): RTCPeerConnection | undefined {
|
|
65
|
+
return this.pc
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getOfferData(): ParsedOffer | undefined {
|
|
69
|
+
return this.offerData
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
close(): void {
|
|
73
|
+
this.pc?.close()
|
|
74
|
+
this.pc = undefined
|
|
75
|
+
this.offerData = undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private onLocalCandidate(evt: RTCPeerConnectionIceEvent): void {
|
|
79
|
+
if (this.getState() !== 'running')
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if (evt.candidate)
|
|
83
|
+
this.callbacks.onCandidate(evt.candidate)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private onConnectionState(): void {
|
|
87
|
+
if (this.getState() !== 'running' || !this.pc)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if (this.pc.connectionState === 'failed' || this.pc.connectionState === 'closed')
|
|
91
|
+
this.callbacks.onError(new WebRTCError(ErrorTypes.OTHER_ERROR, 'peer connection closed'))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private onIceConnectionState(): void {
|
|
95
|
+
if (this.getState() !== 'running' || !this.pc)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if (this.pc.iceConnectionState === 'failed') {
|
|
99
|
+
console.warn('ICE connection failed')
|
|
100
|
+
this.pc.restartIce()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/core/http.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ParsedOffer } from '../utils/sdp'
|
|
2
|
+
import type { Conf, State } from '~/types'
|
|
3
|
+
import { ErrorTypes, WebRTCError } from '~/errors'
|
|
4
|
+
import { SdpUtils } from '../utils/sdp'
|
|
5
|
+
import { WebRtcUtils } from '../utils/webrtc'
|
|
6
|
+
|
|
7
|
+
export class HttpClient {
|
|
8
|
+
constructor(
|
|
9
|
+
private config: Conf,
|
|
10
|
+
private getState: () => State,
|
|
11
|
+
private onError: (err: Error | WebRTCError) => void,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
private authHeader(): Record<string, string> {
|
|
15
|
+
if (this.config.user && this.config.user !== '') {
|
|
16
|
+
const credentials = btoa(`${this.config.user}:${this.config.pass}`)
|
|
17
|
+
return { Authorization: `Basic ${credentials}` }
|
|
18
|
+
}
|
|
19
|
+
if (this.config.token && this.config.token !== '') {
|
|
20
|
+
return { Authorization: `Bearer ${this.config.token}` }
|
|
21
|
+
}
|
|
22
|
+
return {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async requestICEServers(): Promise<RTCIceServer[]> {
|
|
26
|
+
if (this.config.iceServers && this.config.iceServers.length > 0)
|
|
27
|
+
return this.config.iceServers
|
|
28
|
+
|
|
29
|
+
return fetch(this.config.url, {
|
|
30
|
+
method: 'OPTIONS',
|
|
31
|
+
headers: {
|
|
32
|
+
...this.authHeader(),
|
|
33
|
+
},
|
|
34
|
+
}).then(res => WebRtcUtils.linkToIceServers(res.headers.get('Link')))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async sendOffer(offer: string): Promise<{ sessionUrl?: string, answer: string }> {
|
|
38
|
+
if (this.getState() !== 'running')
|
|
39
|
+
throw new WebRTCError(ErrorTypes.STATE_ERROR, 'closed')
|
|
40
|
+
|
|
41
|
+
return fetch(this.config.url, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
...this.authHeader(),
|
|
45
|
+
'Content-Type': 'application/sdp',
|
|
46
|
+
},
|
|
47
|
+
body: offer,
|
|
48
|
+
}).then((res) => {
|
|
49
|
+
switch (res.status) {
|
|
50
|
+
case 201:
|
|
51
|
+
break
|
|
52
|
+
case 404:
|
|
53
|
+
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, 'stream not found')
|
|
54
|
+
case 406:
|
|
55
|
+
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, 'stream not supported')
|
|
56
|
+
case 400:
|
|
57
|
+
return res.json().then((e: { error: string }) => {
|
|
58
|
+
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, e.error)
|
|
59
|
+
})
|
|
60
|
+
default:
|
|
61
|
+
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, `bad status code ${res.status}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const location = res.headers.get('Location')
|
|
65
|
+
const sessionUrl = location
|
|
66
|
+
? new URL(location, this.config.url).toString()
|
|
67
|
+
: undefined
|
|
68
|
+
return res.text().then(answer => ({ sessionUrl, answer }))
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
sendLocalCandidates(sessionUrl: string, offerData: ParsedOffer, candidates: RTCIceCandidate[]): void {
|
|
73
|
+
fetch(sessionUrl, {
|
|
74
|
+
method: 'PATCH',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/trickle-ice-sdpfrag',
|
|
77
|
+
'If-Match': '*',
|
|
78
|
+
},
|
|
79
|
+
body: SdpUtils.generateSdpFragment(offerData, candidates),
|
|
80
|
+
})
|
|
81
|
+
.then((res) => {
|
|
82
|
+
switch (res.status) {
|
|
83
|
+
case 204:
|
|
84
|
+
break
|
|
85
|
+
case 404:
|
|
86
|
+
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, 'stream not found')
|
|
87
|
+
default:
|
|
88
|
+
throw new WebRTCError(ErrorTypes.NETWORK_ERROR, `bad status code ${res.status}`)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
.catch(err => this.onError(err))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -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/errors.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 错误类型
|
|
3
|
+
*/
|
|
4
|
+
export type ErrorType = string
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 错误类型常量
|
|
8
|
+
*/
|
|
9
|
+
export const ErrorTypes = {
|
|
10
|
+
SIGNAL_ERROR: 'SignalError', // 信令异常
|
|
11
|
+
STATE_ERROR: 'StateError', // 状态异常
|
|
12
|
+
NETWORK_ERROR: 'NetworkError',
|
|
13
|
+
MEDIA_ERROR: 'MediaError',
|
|
14
|
+
OTHER_ERROR: 'OtherError',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 错误
|
|
19
|
+
*/
|
|
20
|
+
export class WebRTCError extends Error {
|
|
21
|
+
type: ErrorType
|
|
22
|
+
|
|
23
|
+
constructor(type: ErrorType, message: string, options?: ErrorOptions) {
|
|
24
|
+
super(message, options)
|
|
25
|
+
this.type = type
|
|
26
|
+
}
|
|
27
|
+
}
|