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
package/AGENTS.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# AGENTS.md - WhepTS Player
|
|
2
|
+
|
|
3
|
+
## Build/Lint Commands
|
|
4
|
+
|
|
5
|
+
- `npm run build` - Production build with minification
|
|
6
|
+
- `npm run build:debug` - Debug build with source maps
|
|
7
|
+
- `npm run lint` - Run ESLint to check code quality
|
|
8
|
+
- `npm run lint:fix` - Auto-fix ESLint issues
|
|
9
|
+
|
|
10
|
+
**Note**: No test framework is currently configured.
|
|
11
|
+
|
|
12
|
+
## Code Style Guidelines
|
|
13
|
+
|
|
14
|
+
### Imports
|
|
15
|
+
|
|
16
|
+
- Use `~/` alias for imports from `src/` (e.g., `import X from '~/utils/observer'`)
|
|
17
|
+
- Import types explicitly using `import type { T } from './module'`
|
|
18
|
+
- Group external dependencies first, then internal imports
|
|
19
|
+
|
|
20
|
+
### Formatting
|
|
21
|
+
|
|
22
|
+
- ESLint uses `@antfu/eslint-config` with formatters enabled
|
|
23
|
+
- Run `npm run lint:fix` before committing
|
|
24
|
+
- No manual formatting required - let ESLint handle it
|
|
25
|
+
|
|
26
|
+
### Types
|
|
27
|
+
|
|
28
|
+
- Strict mode enabled in `tsconfig.json`
|
|
29
|
+
- Use `interface` for object shapes, `type` for unions/aliases
|
|
30
|
+
- Mark optional properties with `?` (e.g., `onError?: (err: WebRTCError) => void`)
|
|
31
|
+
- Use TypeScript strictly - no `any` types
|
|
32
|
+
|
|
33
|
+
### Naming Conventions
|
|
34
|
+
|
|
35
|
+
- **Classes**: PascalCase (e.g., `WebRTCWhep`, `VisibilityObserver`)
|
|
36
|
+
- **Interfaces**: PascalCase (e.g., `Conf`, `ErrorType`)
|
|
37
|
+
- **Functions/Methods**: camelCase (e.g., `setupPeerConnection`, `handleError`)
|
|
38
|
+
- **Constants**: UPPER_SNAKE_CASE (e.g., `ErrorTypes`)
|
|
39
|
+
- **Private members**: prefix with `private` keyword
|
|
40
|
+
|
|
41
|
+
### Error Handling
|
|
42
|
+
|
|
43
|
+
- Use custom `WebRTCError` class for all errors (defined in `src/errors.ts`)
|
|
44
|
+
- Error types: `SIGNAL_ERROR`, `STATE_ERROR`, `NETWORK_ERROR`, `MEDIA_ERROR`, `OTHER_ERROR`
|
|
45
|
+
- Pattern: `throw new WebRTCError(ErrorTypes.NETWORK_ERROR, 'message')`
|
|
46
|
+
- Call `this.handleError(err)` for centralized error management
|
|
47
|
+
|
|
48
|
+
### Comments
|
|
49
|
+
|
|
50
|
+
- Use JSDoc for public APIs and class constructors (in English)
|
|
51
|
+
- Keep implementation comments concise in Chinese as established
|
|
52
|
+
- Example:
|
|
53
|
+
```typescript
|
|
54
|
+
/**
|
|
55
|
+
* Create a WebRTCWhep.
|
|
56
|
+
* @param {Conf} conf - Configuration.
|
|
57
|
+
*/
|
|
58
|
+
constructor(conf: Conf)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### File Organization
|
|
62
|
+
|
|
63
|
+
- Core logic in `src/` directory
|
|
64
|
+
- Utilities in `src/utils/` (e.g., `observer.ts`, `flow-check.ts`, `sdp.ts`)
|
|
65
|
+
- Error types in `src/errors.ts`
|
|
66
|
+
- Export main class from `src/index.ts`
|
|
67
|
+
|
|
68
|
+
### State Management
|
|
69
|
+
|
|
70
|
+
- Use union literal types for state (e.g., `'getting_codecs' | 'running' | 'restarting' | 'closed' | 'failed'`)
|
|
71
|
+
- Always check state before operations that depend on it
|
|
72
|
+
- Use getters for derived properties (e.g., `get isRunning()`)
|
|
73
|
+
|
|
74
|
+
### WebRTC Specifics
|
|
75
|
+
|
|
76
|
+
- Always use `unified-plan` SDP semantics
|
|
77
|
+
- Handle ICE candidates with queuing when session URL not ready
|
|
78
|
+
- Support non-advertised codecs (PCMA, multiopus, L16)
|
|
79
|
+
- Use `IntersectionObserver` for visibility-based playback control
|
|
80
|
+
|
|
81
|
+
## Tech Stack
|
|
82
|
+
|
|
83
|
+
- TypeScript 5.9 with strict mode
|
|
84
|
+
- Rollup for bundling (ES module output)
|
|
85
|
+
- ESLint with @antfu/eslint-config
|
|
86
|
+
- pnpm as package manager
|
|
87
|
+
|
|
88
|
+
## Before Committing
|
|
89
|
+
|
|
90
|
+
1. Run `npm run lint` and fix all issues
|
|
91
|
+
2. Build with `npm run build` to verify production build works
|
|
92
|
+
3. No test framework - manually verify WebRTC functionality
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { State } from '~/types';
|
|
2
|
+
export interface CodecDetectorCallbacks {
|
|
3
|
+
onCodecsDetected: (codecs: string[]) => void;
|
|
4
|
+
onError: (err: Error) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare class CodecDetector {
|
|
7
|
+
private getState;
|
|
8
|
+
private callbacks;
|
|
9
|
+
constructor(getState: () => State, callbacks: CodecDetectorCallbacks);
|
|
10
|
+
detect(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ParsedOffer } from '../utils/sdp';
|
|
2
|
+
import type { State } from '~/types';
|
|
3
|
+
import { WebRTCError } from '~/errors';
|
|
4
|
+
export interface ConnectionManagerCallbacks {
|
|
5
|
+
onCandidate: (candidate: RTCIceCandidate) => void;
|
|
6
|
+
onTrack: (evt: RTCTrackEvent) => void;
|
|
7
|
+
onError: (err: WebRTCError) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare class ConnectionManager {
|
|
10
|
+
private getState;
|
|
11
|
+
private callbacks;
|
|
12
|
+
private nonAdvertisedCodecs;
|
|
13
|
+
private pc?;
|
|
14
|
+
private offerData?;
|
|
15
|
+
constructor(getState: () => State, callbacks: ConnectionManagerCallbacks, nonAdvertisedCodecs?: string[]);
|
|
16
|
+
setupPeerConnection(iceServers: RTCIceServer[]): Promise<string>;
|
|
17
|
+
setAnswer(answer: string): Promise<void>;
|
|
18
|
+
getPeerConnection(): RTCPeerConnection | undefined;
|
|
19
|
+
getOfferData(): ParsedOffer | undefined;
|
|
20
|
+
close(): void;
|
|
21
|
+
private onLocalCandidate;
|
|
22
|
+
private onConnectionState;
|
|
23
|
+
private onIceConnectionState;
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ParsedOffer } from '../utils/sdp';
|
|
2
|
+
import type { Conf, State } from '~/types';
|
|
3
|
+
import { WebRTCError } from '~/errors';
|
|
4
|
+
export declare class HttpClient {
|
|
5
|
+
private config;
|
|
6
|
+
private getState;
|
|
7
|
+
private onError;
|
|
8
|
+
constructor(config: Conf, getState: () => State, onError: (err: Error | WebRTCError) => void);
|
|
9
|
+
private authHeader;
|
|
10
|
+
requestICEServers(): Promise<RTCIceServer[]>;
|
|
11
|
+
sendOffer(offer: string): Promise<{
|
|
12
|
+
sessionUrl?: string;
|
|
13
|
+
answer: string;
|
|
14
|
+
}>;
|
|
15
|
+
sendLocalCandidates(sessionUrl: string, offerData: ParsedOffer, candidates: RTCIceCandidate[]): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class TrackManager {
|
|
2
|
+
private container;
|
|
3
|
+
private stream?;
|
|
4
|
+
private observer?;
|
|
5
|
+
constructor(container: HTMLMediaElement);
|
|
6
|
+
onTrack(evt: RTCTrackEvent): void;
|
|
7
|
+
private stopObserver;
|
|
8
|
+
get paused(): boolean;
|
|
9
|
+
pause(): void;
|
|
10
|
+
resume(): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 错误类型
|
|
3
|
+
*/
|
|
4
|
+
export type ErrorType = string;
|
|
5
|
+
/**
|
|
6
|
+
* 错误类型常量
|
|
7
|
+
*/
|
|
8
|
+
export declare const ErrorTypes: {
|
|
9
|
+
SIGNAL_ERROR: string;
|
|
10
|
+
STATE_ERROR: string;
|
|
11
|
+
NETWORK_ERROR: string;
|
|
12
|
+
MEDIA_ERROR: string;
|
|
13
|
+
OTHER_ERROR: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* 错误
|
|
17
|
+
*/
|
|
18
|
+
export declare class WebRTCError extends Error {
|
|
19
|
+
type: ErrorType;
|
|
20
|
+
constructor(type: ErrorType, message: string, options?: ErrorOptions);
|
|
21
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,196 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
url: string;
|
|
7
|
-
/** Media player container */
|
|
8
|
-
container: HTMLMediaElement;
|
|
9
|
-
/** Username for authentication */
|
|
10
|
-
user?: string;
|
|
11
|
-
/** Password for authentication */
|
|
12
|
-
pass?: string;
|
|
13
|
-
/** Token for authentication */
|
|
14
|
-
token?: string;
|
|
15
|
-
/** ice server list */
|
|
16
|
-
iceServers?: RTCIceServer[];
|
|
17
|
-
/** Called when there's an error */
|
|
18
|
-
onError?: (err: WebRTCError) => void;
|
|
19
|
-
}
|
|
20
|
-
/** Extend RTCConfiguration to include experimental properties */
|
|
21
|
-
declare global {
|
|
22
|
-
interface RTCConfiguration {
|
|
23
|
-
sdpSemantics?: 'plan-b' | 'unified-plan';
|
|
24
|
-
}
|
|
25
|
-
interface RTCIceServer {
|
|
26
|
-
credentialType?: 'password';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* 错误类型
|
|
31
|
-
*/
|
|
32
|
-
export type ErrorType = string;
|
|
33
|
-
/**
|
|
34
|
-
* 错误类型常量
|
|
35
|
-
*/
|
|
36
|
-
export declare const ErrorTypes: {
|
|
37
|
-
SIGNAL_ERROR: string;
|
|
38
|
-
STATE_ERROR: string;
|
|
39
|
-
NETWORK_ERROR: string;
|
|
40
|
-
MEDIA_ERROR: string;
|
|
41
|
-
OTHER_ERROR: string;
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* 错误
|
|
45
|
-
*/
|
|
46
|
-
export declare class WebRTCError extends Error {
|
|
47
|
-
type: ErrorType;
|
|
48
|
-
constructor(type: ErrorType, message: string, options?: ErrorOptions);
|
|
49
|
-
}
|
|
50
|
-
/** WebRTC/WHEP reader. */
|
|
51
|
-
export default class WebRTCWhep {
|
|
52
|
-
private retryPause;
|
|
53
|
-
private conf;
|
|
54
|
-
private state;
|
|
55
|
-
private restartTimeout?;
|
|
56
|
-
private pc?;
|
|
57
|
-
private offerData?;
|
|
58
|
-
private sessionUrl?;
|
|
59
|
-
private queuedCandidates;
|
|
60
|
-
private nonAdvertisedCodecs;
|
|
61
|
-
private container;
|
|
62
|
-
private observer;
|
|
63
|
-
private stream?;
|
|
64
|
-
/**
|
|
65
|
-
* 断连重试参数
|
|
66
|
-
*/
|
|
67
|
-
private checkInterval;
|
|
68
|
-
private lastBytesReceived;
|
|
69
|
-
private checkTimer?;
|
|
70
|
-
/**
|
|
71
|
-
* Create a WebRTCWhep.
|
|
72
|
-
* @param {Conf} conf - Configuration.
|
|
73
|
-
*/
|
|
74
|
-
constructor(conf: Conf);
|
|
75
|
-
/**
|
|
76
|
-
* 媒体是否正常
|
|
77
|
-
*/
|
|
78
|
-
get isRunning(): boolean;
|
|
79
|
-
/**
|
|
80
|
-
* Close the reader and all its resources.
|
|
81
|
-
*/
|
|
82
|
-
close(): void;
|
|
83
|
-
/**
|
|
84
|
-
* Check if the browser supports a non-advertised codec.
|
|
85
|
-
*/
|
|
86
|
-
private static supportsNonAdvertisedCodec;
|
|
87
|
-
/**
|
|
88
|
-
* Unquote a credential string.
|
|
89
|
-
*/
|
|
90
|
-
private static unquoteCredential;
|
|
91
|
-
/**
|
|
92
|
-
* Convert Link header to iceServers array.
|
|
93
|
-
*/
|
|
94
|
-
private static linkToIceServers;
|
|
95
|
-
/**
|
|
96
|
-
* Parse an offer SDP into iceUfrag, icePwd, and medias.
|
|
97
|
-
*/
|
|
98
|
-
private static parseOffer;
|
|
99
|
-
/**
|
|
100
|
-
* Reserve a payload type.
|
|
101
|
-
*/
|
|
102
|
-
private static reservePayloadType;
|
|
103
|
-
/**
|
|
104
|
-
* Enable stereo PCMA/PCMU codecs.
|
|
105
|
-
*/
|
|
106
|
-
private static enableStereoPcmau;
|
|
107
|
-
/**
|
|
108
|
-
* Enable multichannel Opus codec.
|
|
109
|
-
*/
|
|
110
|
-
private static enableMultichannelOpus;
|
|
111
|
-
/**
|
|
112
|
-
* Enable L16 codec.
|
|
113
|
-
*/
|
|
114
|
-
private static enableL16;
|
|
115
|
-
/**
|
|
116
|
-
* Enable stereo Opus codec.
|
|
117
|
-
*/
|
|
118
|
-
private static enableStereoOpus;
|
|
119
|
-
/**
|
|
120
|
-
* Edit an offer SDP to enable non-advertised codecs.
|
|
121
|
-
*/
|
|
122
|
-
private static editOffer;
|
|
123
|
-
/**
|
|
124
|
-
* Generate an SDP fragment.
|
|
125
|
-
*/
|
|
126
|
-
private static generateSdpFragment;
|
|
127
|
-
/**
|
|
128
|
-
* Handle errors.
|
|
129
|
-
*/
|
|
130
|
-
private handleError;
|
|
131
|
-
/**
|
|
132
|
-
* Get non-advertised codecs.
|
|
133
|
-
*/
|
|
134
|
-
private getNonAdvertisedCodecs;
|
|
135
|
-
/**
|
|
136
|
-
* Start the WebRTC session.
|
|
137
|
-
*/
|
|
138
|
-
private start;
|
|
139
|
-
/**
|
|
140
|
-
* Generate an authorization header.
|
|
141
|
-
*/
|
|
142
|
-
private authHeader;
|
|
143
|
-
/**
|
|
144
|
-
* Request ICE servers from the endpoint.
|
|
145
|
-
*/
|
|
146
|
-
private requestICEServers;
|
|
147
|
-
/**
|
|
148
|
-
* Setup a peer connection.
|
|
149
|
-
*/
|
|
150
|
-
private setupPeerConnection;
|
|
151
|
-
/**
|
|
152
|
-
* Send an offer to the endpoint.
|
|
153
|
-
*/
|
|
154
|
-
private sendOffer;
|
|
155
|
-
/**
|
|
156
|
-
* Set a remote answer.
|
|
157
|
-
*/
|
|
158
|
-
private setAnswer;
|
|
159
|
-
/**
|
|
160
|
-
* Handle local ICE candidates.
|
|
161
|
-
*/
|
|
162
|
-
private onLocalCandidate;
|
|
163
|
-
/**
|
|
164
|
-
* Send local ICE candidates to the endpoint.
|
|
165
|
-
*/
|
|
166
|
-
private sendLocalCandidates;
|
|
167
|
-
/**
|
|
168
|
-
* Handle peer connection state changes.
|
|
169
|
-
*/
|
|
170
|
-
private onConnectionState;
|
|
171
|
-
/**
|
|
172
|
-
* 启动断流检测
|
|
173
|
-
*/
|
|
174
|
-
private startFlowCheck;
|
|
175
|
-
/**
|
|
176
|
-
* 停止断流检测
|
|
177
|
-
*/
|
|
178
|
-
private stopFlowCheck;
|
|
179
|
-
private checkFlowState;
|
|
180
|
-
/**
|
|
181
|
-
* Handle incoming tracks.
|
|
182
|
-
*/
|
|
183
|
-
private onTrack;
|
|
184
|
-
/**
|
|
185
|
-
* 流是否为空
|
|
186
|
-
*/
|
|
187
|
-
get paused(): boolean;
|
|
188
|
-
/**
|
|
189
|
-
* 暂停播放
|
|
190
|
-
*/
|
|
191
|
-
pause(): void;
|
|
192
|
-
/**
|
|
193
|
-
* 恢复播放
|
|
194
|
-
*/
|
|
195
|
-
resume(): void;
|
|
196
|
-
}
|
|
1
|
+
import type { Conf, State } from './types';
|
|
2
|
+
import { ErrorTypes, WebRTCError } from './errors';
|
|
3
|
+
import WebRTCWhep from './whep';
|
|
4
|
+
export { Conf, ErrorTypes, State, WebRTCError };
|
|
5
|
+
export default WebRTCWhep;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class e{start(e,t){e&&(this.stop(),this.observer=new IntersectionObserver(([e])=>t(e.isIntersecting),{threshold:.5}),this.observer.observe(e))}stop(){this.observer&&(this.observer.disconnect(),this.observer=void 0)}}const t={SIGNAL_ERROR:"SignalError",STATE_ERROR:"StateError",NETWORK_ERROR:"NetworkError",MEDIA_ERROR:"MediaError",OTHER_ERROR:"OtherError"};class s extends Error{constructor(e,t,s){super(t,s),this.type=e}}class r{constructor(t){this.retryPause=2e3,this.queuedCandidates=[],this.nonAdvertisedCodecs=[],this.checkInterval=5e3,this.lastBytesReceived=0,this.conf=t,this.state="getting_codecs",this.container=t.container,this.observer=new e,this.getNonAdvertisedCodecs()}get isRunning(){return"running"===this.state}close(){this.state="closed",this.pc?.close(),this.observer.stop(),this.stopFlowCheck(),this.restartTimeout&&clearTimeout(this.restartTimeout)}static async supportsNonAdvertisedCodec(e,n){return new Promise(i=>{const a=new RTCPeerConnection({iceServers:[]}),c="audio";let o="";a.addTransceiver(c,{direction:"recvonly"}),a.createOffer().then(i=>{if(!i.sdp)throw new s(t.SIGNAL_ERROR,"SDP not present");if(i.sdp.includes(` ${e}`))throw new s(t.SIGNAL_ERROR,"already present");const h=i.sdp.split(`m=${c}`),p=h.slice(1).map(e=>e.split("\r\n")[0].split(" ").slice(3)).reduce((e,t)=>[...e,...t],[]);o=r.reservePayloadType(p);const l=h[1].split("\r\n");return l[0]+=` ${o}`,l.splice(l.length-1,0,`a=rtpmap:${o} ${e}`),void 0!==n&&l.splice(l.length-1,0,`a=fmtp:${o} ${n}`),h[1]=l.join("\r\n"),i.sdp=h.join(`m=${c}`),a.setLocalDescription(i)}).then(()=>a.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:`v=0\r\no=- 6539324223450680508 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=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\nm=${c} 9 UDP/TLS/RTP/SAVPF ${o}\r\nc=IN IP4 0.0.0.0\r\na=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\na=ice-ufrag:29e036dc\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:${o} ${e}\r\n${void 0!==n?`a=fmtp:${o} ${n}\r\n`:""}`}))).then(()=>i(!0)).catch(()=>i(!1)).finally(()=>a.close())})}static unquoteCredential(e){return JSON.parse(`"${e}"`)}static linkToIceServers(e){return e?e.split(", ").map(e=>{const n=e.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);if(!n)throw new s(t.SIGNAL_ERROR,"Invalid ICE server link format");const i={urls:[n[1]]};return n[3]&&(i.username=r.unquoteCredential(n[3]),i.credential=r.unquoteCredential(n[4]),i.credentialType="password"),i}):[]}static parseOffer(e){const t={iceUfrag:"",icePwd:"",medias:[]};for(const s of e.split("\r\n"))s.startsWith("m=")?t.medias.push(s.slice(2)):""===t.iceUfrag&&s.startsWith("a=ice-ufrag:")?t.iceUfrag=s.slice(12):""===t.icePwd&&s.startsWith("a=ice-pwd:")&&(t.icePwd=s.slice(10));return t}static reservePayloadType(e){for(let t=30;t<=127;t++)if((t<=63||t>=96)&&!e.includes(t.toString())){const s=t.toString();return e.push(s),s}throw new s(t.SIGNAL_ERROR,"unable to find a free payload type")}static enableStereoPcmau(e,t){const s=t.split("\r\n");let n=r.reservePayloadType(e);return s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} PCMU/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} PCMA/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),s.join("\r\n")}static enableMultichannelOpus(e,t){const s=t.split("\r\n");let n=r.reservePayloadType(e);return s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/3`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/4`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/5`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/6`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/7`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/8`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),s.join("\r\n")}static enableL16(e,t){const s=t.split("\r\n");let n=r.reservePayloadType(e);return s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} L16/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} L16/16000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} L16/48000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),s.join("\r\n")}static enableStereoOpus(e){let t="";const s=e.split("\r\n");for(let e=0;e<s.length;e++)if(s[e].startsWith("a=rtpmap:")&&s[e].toLowerCase().includes("opus/")){t=s[e].slice(9).split(" ")[0];break}if(""===t)return e;for(let e=0;e<s.length;e++)s[e].startsWith(`a=fmtp:${t} `)&&(s[e].includes("stereo")||(s[e]+=";stereo=1"),s[e].includes("sprop-stereo")||(s[e]+=";sprop-stereo=1"));return s.join("\r\n")}static editOffer(e,t){const s=e.split("m="),n=s.slice(1).map(e=>e.split("\r\n")[0].split(" ").slice(3)).reduce((e,t)=>[...e,...t],[]);for(let e=1;e<s.length;e++)if(s[e].startsWith("audio")){s[e]=r.enableStereoOpus(s[e]),t.includes("pcma/8000/2")&&(s[e]=r.enableStereoPcmau(n,s[e])),t.includes("multiopus/48000/6")&&(s[e]=r.enableMultichannelOpus(n,s[e])),t.includes("L16/48000/2")&&(s[e]=r.enableL16(n,s[e]));break}return s.join("m=")}static generateSdpFragment(e,t){const s={};for(const e of t){const t=e.sdpMLineIndex;t&&(void 0===s[t]&&(s[t]=[]),s[t].push(e))}let r=`a=ice-ufrag:${e.iceUfrag}\r\na=ice-pwd:${e.icePwd}\r\n`,n=0;for(const t of e.medias){if(void 0!==s[n]){r+=`m=${t}\r\na=mid:${n}\r\n`;for(const e of s[n])r+=`a=${e.candidate}\r\n`}n++}return r}handleError(e){this.stopFlowCheck(),"getting_codecs"===this.state||e instanceof s&&e.type===t.SIGNAL_ERROR?this.state="failed":"running"===this.state&&(this.pc?.close(),this.pc=void 0,this.offerData=void 0,this.queuedCandidates=[],this.sessionUrl&&(fetch(this.sessionUrl,{method:"DELETE"}),this.sessionUrl=void 0),this.state="restarting",this.restartTimeout=setTimeout(()=>{this.restartTimeout=void 0,this.state="running",this.start()},this.retryPause),e.message=`${e.message}, retrying in some seconds`),this.conf.onError&&(e instanceof s?this.conf.onError(e):this.conf.onError(new s(t.OTHER_ERROR,e.message)))}getNonAdvertisedCodecs(){Promise.all([["pcma/8000/2"],["multiopus/48000/6","channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2"],["L16/48000/2"]].map(e=>r.supportsNonAdvertisedCodec(e[0],e[1]).then(t=>!!t&&e[0]))).then(e=>e.filter(e=>!1!==e)).then(e=>{if("getting_codecs"!==this.state)throw new s(t.STATE_ERROR,"closed");this.nonAdvertisedCodecs=e,this.state="running",this.start()}).catch(e=>this.handleError(e))}start(){this.requestICEServers().then(e=>this.setupPeerConnection(e)).then(e=>this.sendOffer(e)).then(e=>this.setAnswer(e)).catch(e=>this.handleError(e))}authHeader(){if(this.conf.user&&""!==this.conf.user){return{Authorization:`Basic ${btoa(`${this.conf.user}:${this.conf.pass}`)}`}}return this.conf.token&&""!==this.conf.token?{Authorization:`Bearer ${this.conf.token}`}:{}}async requestICEServers(){return this.conf.iceServers&&this.conf.iceServers.length>0?this.conf.iceServers:fetch(this.conf.url,{method:"OPTIONS",headers:{...this.authHeader()}}).then(e=>r.linkToIceServers(e.headers.get("Link")))}async setupPeerConnection(e){if("running"!==this.state)throw new s(t.STATE_ERROR,"closed");const n=new RTCPeerConnection({iceServers:e,sdpSemantics:"unified-plan"});this.pc=n;const i="recvonly";return n.addTransceiver("video",{direction:i}),n.addTransceiver("audio",{direction:i}),n.onicecandidate=e=>this.onLocalCandidate(e),n.onconnectionstatechange=()=>this.onConnectionState(),n.ontrack=e=>this.onTrack(e),n.createOffer().then(e=>{if(!e.sdp)throw new s(t.SIGNAL_ERROR,"Failed to create offer SDP");return e.sdp=r.editOffer(e.sdp,this.nonAdvertisedCodecs),this.offerData=r.parseOffer(e.sdp),n.setLocalDescription(e).then(()=>e.sdp)})}sendOffer(e){if("running"!==this.state)throw new s(t.STATE_ERROR,"closed");return fetch(this.conf.url,{method:"POST",headers:{...this.authHeader(),"Content-Type":"application/sdp"},body:e}).then(e=>{switch(e.status){case 201:break;case 404:throw new s(t.NETWORK_ERROR,"stream not found");case 406:throw new s(t.NETWORK_ERROR,"stream not supported");case 400:return e.json().then(e=>{throw new s(t.NETWORK_ERROR,e.error)});default:throw new s(t.NETWORK_ERROR,`bad status code ${e.status}`)}const r=e.headers.get("Location");return r&&(this.sessionUrl=new URL(r,this.conf.url).toString()),e.text()})}setAnswer(e){if("running"!==this.state)throw new s(t.STATE_ERROR,"closed");return this.pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e})).then(()=>{"running"===this.state&&0!==this.queuedCandidates.length&&(this.sendLocalCandidates(this.queuedCandidates),this.queuedCandidates=[])})}onLocalCandidate(e){"running"===this.state&&e.candidate&&(this.sessionUrl?this.sendLocalCandidates([e.candidate]):this.queuedCandidates.push(e.candidate))}sendLocalCandidates(e){this.sessionUrl&&this.offerData&&fetch(this.sessionUrl,{method:"PATCH",headers:{"Content-Type":"application/trickle-ice-sdpfrag","If-Match":"*"},body:r.generateSdpFragment(this.offerData,e)}).then(e=>{switch(e.status){case 204:break;case 404:throw new s(t.NETWORK_ERROR,"stream not found");default:throw new s(t.NETWORK_ERROR,`bad status code ${e.status}`)}}).catch(e=>this.handleError(e))}onConnectionState(){"running"===this.state&&this.pc&&("failed"===this.pc.connectionState||"closed"===this.pc.connectionState?this.handleError(new s(t.OTHER_ERROR,"peer connection closed")):"connected"===this.pc.connectionState&&this.startFlowCheck())}startFlowCheck(){this.stopFlowCheck(),this.checkTimer=setInterval(()=>this.checkFlowState(),this.checkInterval)}stopFlowCheck(){this.checkTimer&&(clearInterval(this.checkTimer),this.checkTimer=void 0)}async checkFlowState(){if(!this.pc)return;const e=await this.pc.getStats();let r=0;e.forEach(e=>{const t=e;"inbound-rtp"===e.type&&"video"===t.kind&&(r=t.bytesReceived||0)}),r!==this.lastBytesReceived||"connected"!==this.pc.connectionState?this.lastBytesReceived=r:this.handleError(new s(t.NETWORK_ERROR,"data stream interruption"))}onTrack(e){this.stream=e.streams[0],this.observer.start(this.container,e=>{e?this.resume():this.pause()})}get paused(){return null===this.container.srcObject}pause(){this.container.srcObject=null}resume(){this.stream&&this.paused&&(this.container.srcObject=this.stream)}}export{t as ErrorTypes,s as WebRTCError,r as default};
|
|
1
|
+
const e={SIGNAL_ERROR:"SignalError",STATE_ERROR:"StateError",NETWORK_ERROR:"NetworkError",MEDIA_ERROR:"MediaError",OTHER_ERROR:"OtherError"};class t extends Error{constructor(e,t,s){super(t,s),this.type=e}}class s{static async supportsNonAdvertisedCodec(e,t){return new Promise(n=>{const r=new RTCPeerConnection({iceServers:[]}),i="audio";let a="";r.addTransceiver(i,{direction:"recvonly"}),r.createOffer().then(n=>{if(!n.sdp)throw new Error("SDP not present");if(n.sdp.includes(` ${e}`))throw new Error("already present");const c=n.sdp.split(`m=${i}`),o=c.slice(1).map(e=>e.split("\r\n")[0].split(" ").slice(3)).reduce((e,t)=>[...e,...t],[]);a=s.reservePayloadType(o);const h=c[1].split("\r\n");return h[0]+=` ${a}`,h.splice(h.length-1,0,`a=rtpmap:${a} ${e}`),void 0!==t&&h.splice(h.length-1,0,`a=fmtp:${a} ${t}`),c[1]=h.join("\r\n"),n.sdp=c.join(`m=${i}`),r.setLocalDescription(n)}).then(()=>r.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:`v=0\r\no=- 6539324223450680508 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=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\nm=${i} 9 UDP/TLS/RTP/SAVPF ${a}\r\nc=IN IP4 0.0.0.0\r\na=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\na=ice-ufrag:29e036dc\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:${a} ${e}\r\n${void 0!==t?`a=fmtp:${a} ${t}\r\n`:""}`}))).then(()=>n(!0)).catch(()=>n(!1)).finally(()=>r.close())})}static unquoteCredential(e){return JSON.parse(`"${e}"`)}static linkToIceServers(n){return n?n.split(", ").map(n=>{const r=n.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);if(!r)throw new t(e.SIGNAL_ERROR,"Invalid ICE server link format");const i={urls:[r[1]]};return r[3]&&(i.username=s.unquoteCredential(r[3]),i.credential=s.unquoteCredential(r[4]),i.credentialType="password"),i}):[]}static reservePayloadType(e){for(let t=30;t<=127;t++)if((t<=63||t>=96)&&!e.includes(t.toString())){const s=t.toString();return e.push(s),s}throw new Error("unable to find a free payload type")}}class n{constructor(e,t){this.getState=e,this.callbacks=t}detect(){Promise.all([["pcma/8000/2"],["multiopus/48000/6","channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2"],["L16/48000/2"]].map(e=>s.supportsNonAdvertisedCodec(e[0],e[1]).then(t=>!!t&&e[0]))).then(e=>e.filter(e=>!1!==e)).then(s=>{if("getting_codecs"!==this.getState())throw new t(e.STATE_ERROR,"closed");this.callbacks.onCodecsDetected(s)}).catch(e=>this.callbacks.onError(e))}}class r{static parseOffer(e){const t={iceUfrag:"",icePwd:"",medias:[]};for(const s of e.split("\r\n"))s.startsWith("m=")?t.medias.push(s.slice(2)):""===t.iceUfrag&&s.startsWith("a=ice-ufrag:")?t.iceUfrag=s.slice(12):""===t.icePwd&&s.startsWith("a=ice-pwd:")&&(t.icePwd=s.slice(10));return t}static reservePayloadType(e){for(let t=30;t<=127;t++)if((t<=63||t>=96)&&!e.includes(t.toString())){const s=t.toString();return e.push(s),s}throw new Error("unable to find a free payload type")}static enableStereoPcmau(e,t){const s=t.split("\r\n");let n=r.reservePayloadType(e);return s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} PCMU/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} PCMA/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),s.join("\r\n")}static enableMultichannelOpus(e,t){const s=t.split("\r\n");let n=r.reservePayloadType(e);return s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/3`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/4`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/5`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/6`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/7`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/8`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),s.join("\r\n")}static enableL16(e,t){const s=t.split("\r\n");let n=r.reservePayloadType(e);return s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} L16/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} L16/16000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=r.reservePayloadType(e),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} L16/48000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),s.join("\r\n")}static enableStereoOpus(e){let t="";const s=e.split("\r\n");for(let e=0;e<s.length;e++)if(s[e].startsWith("a=rtpmap:")&&s[e].toLowerCase().includes("opus/")){t=s[e].slice(9).split(" ")[0];break}if(""===t)return e;for(let e=0;e<s.length;e++)s[e].startsWith(`a=fmtp:${t} `)&&(s[e].includes("stereo")||(s[e]+=";stereo=1"),s[e].includes("sprop-stereo")||(s[e]+=";sprop-stereo=1"));return s.join("\r\n")}static editOffer(e,t){const s=e.split("m="),n=s.slice(1).map(e=>e.split("\r\n")[0].split(" ").slice(3)).reduce((e,t)=>[...e,...t],[]);for(let e=1;e<s.length;e++)if(s[e].startsWith("audio")){s[e]=r.enableStereoOpus(s[e]),t.includes("pcma/8000/2")&&(s[e]=r.enableStereoPcmau(n,s[e])),t.includes("multiopus/48000/6")&&(s[e]=r.enableMultichannelOpus(n,s[e])),t.includes("L16/48000/2")&&(s[e]=r.enableL16(n,s[e]));break}return s.join("m=")}static generateSdpFragment(e,t){const s={};for(const e of t){const t=e.sdpMLineIndex;t&&(void 0===s[t]&&(s[t]=[]),s[t].push(e))}let n=`a=ice-ufrag:${e.iceUfrag}\r\na=ice-pwd:${e.icePwd}\r\n`,r=0;for(const t of e.medias){if(void 0!==s[r]){n+=`m=${t}\r\na=mid:${r}\r\n`;for(const e of s[r])n+=`a=${e.candidate}\r\n`}r++}return n}}class i{constructor(e,t,s=[]){this.getState=e,this.callbacks=t,this.nonAdvertisedCodecs=s}async setupPeerConnection(s){if("running"!==this.getState())throw new t(e.STATE_ERROR,"closed");const n=new RTCPeerConnection({iceServers:s,sdpSemantics:"unified-plan"});this.pc=n;const i="recvonly";return n.addTransceiver("video",{direction:i}),n.addTransceiver("audio",{direction:i}),n.onicecandidate=e=>this.onLocalCandidate(e),n.onconnectionstatechange=()=>this.onConnectionState(),n.oniceconnectionstatechange=()=>this.onIceConnectionState(),n.ontrack=e=>this.callbacks.onTrack(e),n.createOffer().then(s=>{if(!s.sdp)throw new t(e.SIGNAL_ERROR,"Failed to create offer SDP");return s.sdp=r.editOffer(s.sdp,this.nonAdvertisedCodecs),this.offerData=r.parseOffer(s.sdp),n.setLocalDescription(s).then(()=>s.sdp)})}async setAnswer(s){if("running"!==this.getState())throw new t(e.STATE_ERROR,"closed");return this.pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:s}))}getPeerConnection(){return this.pc}getOfferData(){return this.offerData}close(){this.pc?.close(),this.pc=void 0,this.offerData=void 0}onLocalCandidate(e){"running"===this.getState()&&e.candidate&&this.callbacks.onCandidate(e.candidate)}onConnectionState(){"running"===this.getState()&&this.pc&&("failed"!==this.pc.connectionState&&"closed"!==this.pc.connectionState||this.callbacks.onError(new t(e.OTHER_ERROR,"peer connection closed")))}onIceConnectionState(){"running"===this.getState()&&this.pc&&"failed"===this.pc.iceConnectionState&&this.pc.restartIce()}}class a{constructor(e,t,s){this.config=e,this.getState=t,this.onError=s}authHeader(){if(this.config.user&&""!==this.config.user){return{Authorization:`Basic ${btoa(`${this.config.user}:${this.config.pass}`)}`}}return this.config.token&&""!==this.config.token?{Authorization:`Bearer ${this.config.token}`}:{}}async requestICEServers(){return this.config.iceServers&&this.config.iceServers.length>0?this.config.iceServers:fetch(this.config.url,{method:"OPTIONS",headers:{...this.authHeader()}}).then(e=>s.linkToIceServers(e.headers.get("Link")))}async sendOffer(s){if("running"!==this.getState())throw new t(e.STATE_ERROR,"closed");return fetch(this.config.url,{method:"POST",headers:{...this.authHeader(),"Content-Type":"application/sdp"},body:s}).then(s=>{switch(s.status){case 201:break;case 404:throw new t(e.NETWORK_ERROR,"stream not found");case 406:throw new t(e.NETWORK_ERROR,"stream not supported");case 400:return s.json().then(s=>{throw new t(e.NETWORK_ERROR,s.error)});default:throw new t(e.NETWORK_ERROR,`bad status code ${s.status}`)}const n=s.headers.get("Location"),r=n?new URL(n,this.config.url).toString():void 0;return s.text().then(e=>({sessionUrl:r,answer:e}))})}sendLocalCandidates(s,n,i){fetch(s,{method:"PATCH",headers:{"Content-Type":"application/trickle-ice-sdpfrag","If-Match":"*"},body:r.generateSdpFragment(n,i)}).then(s=>{switch(s.status){case 204:break;case 404:throw new t(e.NETWORK_ERROR,"stream not found");default:throw new t(e.NETWORK_ERROR,`bad status code ${s.status}`)}}).catch(e=>this.onError(e))}}class c{constructor(e){this.container=e}onTrack(e){this.stream=e.streams[0],this.stopObserver(),this.observer=new IntersectionObserver(([e])=>{e.isIntersecting?this.resume():this.pause()},{threshold:.5}),this.observer.observe(this.container)}stopObserver(){this.observer&&(this.observer.disconnect(),this.observer=void 0)}get paused(){return null===this.container.srcObject}pause(){this.container.srcObject=null}resume(){this.stream&&this.paused&&(this.container.srcObject=this.stream)}stop(){this.stopObserver(),this.stream=void 0}}class o{constructor(e){this.lastBytesReceived=0,this.checkInterval=e.interval,this.onError=e.onError}setPeerConnection(e){this.pc=e}start(){this.stop(),this.checkTimer=setInterval(()=>this.checkFlowState(),this.checkInterval)}stop(){this.checkTimer&&(clearInterval(this.checkTimer),this.checkTimer=void 0)}async checkFlowState(){if(!this.pc)return;const s=await this.pc.getStats();let n=0;s.forEach(e=>{const t=e;"inbound-rtp"===e.type&&"video"===t.kind&&(n=t.bytesReceived||0)}),n!==this.lastBytesReceived||"connected"!==this.pc.connectionState?this.lastBytesReceived=n:this.onError(new t(e.NETWORK_ERROR,"data stream interruption"))}}class h{constructor(e){this.retryPause=2e3,this.queuedCandidates=[],this.nonAdvertisedCodecs=[],this.conf=e,this.state="getting_codecs",this.flowCheck=new o({interval:5e3,onError:e=>this.handleError(e)}),this.httpClient=new a(this.conf,()=>this.state,e=>this.handleError(e)),this.connectionManager=new i(()=>this.state,{onCandidate:e=>this.handleCandidate(e),onTrack:e=>{this.trackManager.onTrack(e),this.flowCheck.start()},onError:e=>this.handleError(e)},this.nonAdvertisedCodecs),this.trackManager=new c(this.conf.container),this.codecDetector=new n(()=>this.state,{onCodecsDetected:e=>this.handleCodecsDetected(e),onError:e=>this.handleError(e)}),this.codecDetector.detect()}get isRunning(){return"running"===this.state}close(){this.state="closed",this.connectionManager.close(),this.trackManager.stop(),this.flowCheck.stop(),this.restartTimeout&&clearTimeout(this.restartTimeout)}handleError(s){this.flowCheck.stop(),"getting_codecs"===this.state||s instanceof t&&s.type===e.SIGNAL_ERROR?this.state="failed":"running"===this.state&&(this.connectionManager.close(),this.queuedCandidates=[],this.sessionUrl&&(fetch(this.sessionUrl,{method:"DELETE"}),this.sessionUrl=void 0),this.state="restarting",this.restartTimeout=setTimeout(()=>{this.restartTimeout=void 0,this.state="running",this.start()},this.retryPause),s.message=`${s.message}, retrying in some seconds`),this.conf.onError&&(s instanceof t?this.conf.onError(s):this.conf.onError(new t(e.OTHER_ERROR,s.message)))}handleCodecsDetected(e){this.nonAdvertisedCodecs=e,this.state="running",this.start()}start(){this.httpClient.requestICEServers().then(e=>this.connectionManager.setupPeerConnection(e)).then(e=>this.httpClient.sendOffer(e)).then(({sessionUrl:e,answer:t})=>this.handleOfferResponse(e,t)).catch(e=>this.handleError(e))}handleOfferResponse(e,t){return e&&(this.sessionUrl=e),this.connectionManager.setAnswer(t).then(()=>{if("running"===this.state&&0!==this.queuedCandidates.length){const e=this.connectionManager.getOfferData();e&&this.sessionUrl&&(this.httpClient.sendLocalCandidates(this.sessionUrl,e,this.queuedCandidates),this.queuedCandidates=[])}})}handleCandidate(e){if(this.sessionUrl){const t=this.connectionManager.getOfferData();t&&this.httpClient.sendLocalCandidates(this.sessionUrl,t,[e])}else this.queuedCandidates.push(e)}get paused(){return this.trackManager.paused}pause(){this.trackManager.pause()}resume(){this.trackManager.resume()}}export{e as ErrorTypes,t as WebRTCError,h as default};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WebRTCError } from './errors';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration interface for WebRTCWhep.
|
|
4
|
+
*/
|
|
5
|
+
export interface Conf {
|
|
6
|
+
/** Absolute URL of the WHEP endpoint */
|
|
7
|
+
url: string;
|
|
8
|
+
/** Media player container */
|
|
9
|
+
container: HTMLMediaElement;
|
|
10
|
+
/** Username for authentication */
|
|
11
|
+
user?: string;
|
|
12
|
+
/** Password for authentication */
|
|
13
|
+
pass?: string;
|
|
14
|
+
/** Token for authentication */
|
|
15
|
+
token?: string;
|
|
16
|
+
/** ice server list */
|
|
17
|
+
iceServers?: RTCIceServer[];
|
|
18
|
+
/** Called when there's an error */
|
|
19
|
+
onError?: (err: WebRTCError) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* State type for WebRTCWhep.
|
|
23
|
+
*/
|
|
24
|
+
export type State = 'getting_codecs' | 'running' | 'restarting' | 'closed' | 'failed';
|
|
25
|
+
/** Extend RTCConfiguration to include experimental properties */
|
|
26
|
+
declare global {
|
|
27
|
+
interface RTCConfiguration {
|
|
28
|
+
sdpSemantics?: 'plan-b' | 'unified-plan';
|
|
29
|
+
}
|
|
30
|
+
interface RTCIceServer {
|
|
31
|
+
credentialType?: 'password';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { WebRTCError } from '~/errors';
|
|
2
|
+
export interface FlowCheckParams {
|
|
3
|
+
interval: number;
|
|
4
|
+
onError: (err: WebRTCError) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Flow checking logic (断流检测)
|
|
8
|
+
*/
|
|
9
|
+
export declare class FlowCheck {
|
|
10
|
+
private checkInterval;
|
|
11
|
+
private lastBytesReceived;
|
|
12
|
+
private checkTimer?;
|
|
13
|
+
private pc?;
|
|
14
|
+
private onError;
|
|
15
|
+
constructor(params: FlowCheckParams);
|
|
16
|
+
setPeerConnection(pc: RTCPeerConnection): void;
|
|
17
|
+
/**
|
|
18
|
+
* 启动断流检测
|
|
19
|
+
*/
|
|
20
|
+
start(): void;
|
|
21
|
+
/**
|
|
22
|
+
* 停止断流检测
|
|
23
|
+
*/
|
|
24
|
+
stop(): void;
|
|
25
|
+
/**
|
|
26
|
+
* 检测流状态(通过接收字节数判断是否断流)
|
|
27
|
+
*/
|
|
28
|
+
private checkFlowState;
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Type for parsed offer data */
|
|
2
|
+
export interface ParsedOffer {
|
|
3
|
+
iceUfrag: string;
|
|
4
|
+
icePwd: string;
|
|
5
|
+
medias: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* SDP processing utilities
|
|
9
|
+
*/
|
|
10
|
+
export declare class SdpUtils {
|
|
11
|
+
/**
|
|
12
|
+
* Parse an offer SDP into iceUfrag, icePwd, and medias.
|
|
13
|
+
*/
|
|
14
|
+
static parseOffer(sdp: string): ParsedOffer;
|
|
15
|
+
/**
|
|
16
|
+
* Reserve a payload type.
|
|
17
|
+
*/
|
|
18
|
+
static reservePayloadType(payloadTypes: string[]): string;
|
|
19
|
+
/**
|
|
20
|
+
* Enable stereo PCMA/PCMU codecs.
|
|
21
|
+
*/
|
|
22
|
+
static enableStereoPcmau(payloadTypes: string[], section: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Enable multichannel Opus codec.
|
|
25
|
+
*/
|
|
26
|
+
static enableMultichannelOpus(payloadTypes: string[], section: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Enable L16 codec.
|
|
29
|
+
*/
|
|
30
|
+
static enableL16(payloadTypes: string[], section: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Enable stereo Opus codec.
|
|
33
|
+
*/
|
|
34
|
+
static enableStereoOpus(section: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Edit an offer SDP to enable non-advertised codecs.
|
|
37
|
+
*/
|
|
38
|
+
static editOffer(sdp: string, nonAdvertisedCodecs: string[]): string;
|
|
39
|
+
/**
|
|
40
|
+
* Generate an SDP fragment.
|
|
41
|
+
*/
|
|
42
|
+
static generateSdpFragment(od: ParsedOffer, candidates: RTCIceCandidate[]): string;
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebRTC utilities
|
|
3
|
+
*/
|
|
4
|
+
export declare class WebRtcUtils {
|
|
5
|
+
/**
|
|
6
|
+
* Check if the browser supports a non-advertised codec.
|
|
7
|
+
*/
|
|
8
|
+
static supportsNonAdvertisedCodec(codec: string, fmtp?: string): Promise<boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* Unquote a credential string.
|
|
11
|
+
*/
|
|
12
|
+
static unquoteCredential(v: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Convert Link header to iceServers array.
|
|
15
|
+
*/
|
|
16
|
+
static linkToIceServers(links: string | null): RTCIceServer[];
|
|
17
|
+
/**
|
|
18
|
+
* Reserve a payload type.
|
|
19
|
+
*/
|
|
20
|
+
static reservePayloadType(payloadTypes: string[]): string;
|
|
21
|
+
}
|
package/dist/whep.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Conf } from './types';
|
|
2
|
+
/** WebRTC/WHEP reader. */
|
|
3
|
+
export default class WebRTCWhep {
|
|
4
|
+
private retryPause;
|
|
5
|
+
private conf;
|
|
6
|
+
private state;
|
|
7
|
+
private restartTimeout?;
|
|
8
|
+
private sessionUrl?;
|
|
9
|
+
private queuedCandidates;
|
|
10
|
+
private nonAdvertisedCodecs;
|
|
11
|
+
private flowCheck;
|
|
12
|
+
private httpClient;
|
|
13
|
+
private connectionManager;
|
|
14
|
+
private trackManager;
|
|
15
|
+
private codecDetector;
|
|
16
|
+
constructor(conf: Conf);
|
|
17
|
+
get isRunning(): boolean;
|
|
18
|
+
close(): void;
|
|
19
|
+
private handleError;
|
|
20
|
+
private handleCodecsDetected;
|
|
21
|
+
private start;
|
|
22
|
+
private handleOfferResponse;
|
|
23
|
+
private handleCandidate;
|
|
24
|
+
get paused(): boolean;
|
|
25
|
+
pause(): void;
|
|
26
|
+
resume(): void;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whepts",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
5
|
-
"
|
|
6
|
-
"description": "",
|
|
4
|
+
"version": "1.0.3",
|
|
5
|
+
"description": "基于 mediamtx 的 WebRTC WHEP 播放器,支持 ZLM 和 Mediamtx 的播放地址",
|
|
7
6
|
"author": "mapleafgo",
|
|
8
7
|
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+ssh://git@github.com/mapleafgo/whepts.git"
|
|
11
|
+
},
|
|
9
12
|
"keywords": [
|
|
10
13
|
"webrtc",
|
|
11
14
|
"whep",
|
|
@@ -22,12 +25,6 @@
|
|
|
22
25
|
],
|
|
23
26
|
"main": "dist/index.js",
|
|
24
27
|
"types": "dist/index.d.ts",
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "rollup --config --environment NODE_ENV:production",
|
|
27
|
-
"build:debug": "rollup --config",
|
|
28
|
-
"lint": "eslint",
|
|
29
|
-
"lint:fix": "eslint --fix"
|
|
30
|
-
},
|
|
31
28
|
"devDependencies": {
|
|
32
29
|
"@antfu/eslint-config": "^6.7.3",
|
|
33
30
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
@@ -41,5 +38,11 @@
|
|
|
41
38
|
"rollup-plugin-delete": "^3.0.2",
|
|
42
39
|
"tslib": "^2.8.1",
|
|
43
40
|
"typescript": "^5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "rollup --config --environment NODE_ENV:production",
|
|
44
|
+
"build:debug": "rollup --config",
|
|
45
|
+
"lint": "eslint",
|
|
46
|
+
"lint:fix": "eslint --fix"
|
|
44
47
|
}
|
|
45
|
-
}
|
|
48
|
+
}
|