whepts 1.0.0 → 1.0.2

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/QWEN.md ADDED
@@ -0,0 +1,118 @@
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
package/README.md CHANGED
@@ -24,13 +24,13 @@ WebRTC WHEP (WebRTC HTTP Egress Protocol) 是一种用于从 WebRTC 服务器获
24
24
  ## 安装
25
25
 
26
26
  ```bash
27
- npm install
27
+ npm i whepts
28
28
  ```
29
29
 
30
30
 
31
31
 
32
32
  ```bash
33
- pnpm install
33
+ pnpm add whepts
34
34
  ```
35
35
 
36
36
  ## 使用方法
@@ -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,4 @@
1
- /**
2
- * Configuration interface for WebRTCWhep.
3
- */
4
- export interface Conf {
5
- /** Absolute URL of the WHEP endpoint */
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 { ErrorTypes, WebRTCError } from './errors';
2
+ import WebRTCWhep, { Conf } from './whep';
3
+ export { Conf, ErrorTypes, WebRTCError };
4
+ 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{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)}}class r{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 r=0;s.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.onError(new t(e.NETWORK_ERROR,"data stream interruption"))}}class n{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 r=n.reservePayloadType(e);return s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} PCMU/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${r} transport-cc`),r=n.reservePayloadType(e),s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} PCMA/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${r} transport-cc`),s.join("\r\n")}static enableMultichannelOpus(e,t){const s=t.split("\r\n");let r=n.reservePayloadType(e);return s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} multiopus/48000/3`),s.splice(s.length-1,0,`a=fmtp:${r} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`),s.splice(s.length-1,0,`a=rtcp-fb:${r} transport-cc`),r=n.reservePayloadType(e),s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} multiopus/48000/4`),s.splice(s.length-1,0,`a=fmtp:${r} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${r} transport-cc`),r=n.reservePayloadType(e),s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} multiopus/48000/5`),s.splice(s.length-1,0,`a=fmtp:${r} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${r} transport-cc`),r=n.reservePayloadType(e),s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} multiopus/48000/6`),s.splice(s.length-1,0,`a=fmtp:${r} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${r} transport-cc`),r=n.reservePayloadType(e),s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} multiopus/48000/7`),s.splice(s.length-1,0,`a=fmtp:${r} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${r} transport-cc`),r=n.reservePayloadType(e),s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} multiopus/48000/8`),s.splice(s.length-1,0,`a=fmtp:${r} 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:${r} transport-cc`),s.join("\r\n")}static enableL16(e,t){const s=t.split("\r\n");let r=n.reservePayloadType(e);return s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} L16/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${r} transport-cc`),r=n.reservePayloadType(e),s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} L16/16000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${r} transport-cc`),r=n.reservePayloadType(e),s[0]+=` ${r}`,s.splice(s.length-1,0,`a=rtpmap:${r} L16/48000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${r} 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="),r=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]=n.enableStereoOpus(s[e]),t.includes("pcma/8000/2")&&(s[e]=n.enableStereoPcmau(r,s[e])),t.includes("multiopus/48000/6")&&(s[e]=n.enableMultichannelOpus(r,s[e])),t.includes("L16/48000/2")&&(s[e]=n.enableL16(r,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}}class i{static async supportsNonAdvertisedCodec(e,t){return new Promise(s=>{const r=new RTCPeerConnection({iceServers:[]}),n="audio";let a="";r.addTransceiver(n,{direction:"recvonly"}),r.createOffer().then(s=>{if(!s.sdp)throw new Error("SDP not present");if(s.sdp.includes(` ${e}`))throw new Error("already present");const o=s.sdp.split(`m=${n}`),c=o.slice(1).map(e=>e.split("\r\n")[0].split(" ").slice(3)).reduce((e,t)=>[...e,...t],[]);a=i.reservePayloadType(c);const h=o[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}`),o[1]=h.join("\r\n"),s.sdp=o.join(`m=${n}`),r.setLocalDescription(s)}).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=${n} 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(()=>s(!0)).catch(()=>s(!1)).finally(()=>r.close())})}static unquoteCredential(e){return JSON.parse(`"${e}"`)}static linkToIceServers(s){return s?s.split(", ").map(s=>{const r=s.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);if(!r)throw new t(e.SIGNAL_ERROR,"Invalid ICE server link format");const n={urls:[r[1]]};return r[3]&&(n.username=i.unquoteCredential(r[3]),n.credential=i.unquoteCredential(r[4]),n.credentialType="password"),n}):[]}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 a{constructor(e){this.retryPause=2e3,this.queuedCandidates=[],this.nonAdvertisedCodecs=[],this.conf=e,this.state="getting_codecs",this.observer=new s,this.flowCheck=new r({interval:5e3,onError:e=>this.handleError(e)}),this.getNonAdvertisedCodecs()}get isRunning(){return"running"===this.state}close(){this.state="closed",this.pc?.close(),this.observer.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.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),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)))}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=>i.supportsNonAdvertisedCodec(e[0],e[1]).then(t=>!!t&&e[0]))).then(e=>e.filter(e=>!1!==e)).then(s=>{if("getting_codecs"!==this.state)throw new t(e.STATE_ERROR,"closed");this.nonAdvertisedCodecs=s,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=>i.linkToIceServers(e.headers.get("Link")))}async setupPeerConnection(s){if("running"!==this.state)throw new t(e.STATE_ERROR,"closed");const r=new RTCPeerConnection({iceServers:s,sdpSemantics:"unified-plan"});this.pc=r,this.flowCheck.setPeerConnection(r);const i="recvonly";return r.addTransceiver("video",{direction:i}),r.addTransceiver("audio",{direction:i}),r.onicecandidate=e=>this.onLocalCandidate(e),r.onconnectionstatechange=()=>this.onConnectionState(),r.ontrack=e=>this.onTrack(e),r.createOffer().then(s=>{if(!s.sdp)throw new t(e.SIGNAL_ERROR,"Failed to create offer SDP");return s.sdp=n.editOffer(s.sdp,this.nonAdvertisedCodecs),this.offerData=n.parseOffer(s.sdp),r.setLocalDescription(s).then(()=>s.sdp)})}sendOffer(s){if("running"!==this.state)throw new t(e.STATE_ERROR,"closed");return fetch(this.conf.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 r=s.headers.get("Location");return r&&(this.sessionUrl=new URL(r,this.conf.url).toString()),s.text()})}setAnswer(s){if("running"!==this.state)throw new t(e.STATE_ERROR,"closed");return this.pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:s})).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(s){this.sessionUrl&&this.offerData&&fetch(this.sessionUrl,{method:"PATCH",headers:{"Content-Type":"application/trickle-ice-sdpfrag","If-Match":"*"},body:n.generateSdpFragment(this.offerData,s)}).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.handleError(e))}onConnectionState(){"running"===this.state&&this.pc&&("failed"===this.pc.connectionState||"closed"===this.pc.connectionState?this.handleError(new t(e.OTHER_ERROR,"peer connection closed")):"connected"===this.pc.connectionState&&this.flowCheck.start())}onTrack(e){this.stream=e.streams[0],this.observer.start(this.conf.container,e=>{e?this.resume():this.pause()})}get paused(){return null===this.conf.container.srcObject}pause(){this.conf.container.srcObject=null}resume(){this.stream&&this.paused&&(this.conf.container.srcObject=this.stream)}}export{e as ErrorTypes,t as WebRTCError,a as default};
@@ -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,117 @@
1
+ import { 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
+ /** Extend RTCConfiguration to include experimental properties */
22
+ declare global {
23
+ interface RTCConfiguration {
24
+ sdpSemantics?: 'plan-b' | 'unified-plan';
25
+ }
26
+ interface RTCIceServer {
27
+ credentialType?: 'password';
28
+ }
29
+ }
30
+ /** WebRTC/WHEP reader. */
31
+ export default class WebRTCWhep {
32
+ private retryPause;
33
+ private conf;
34
+ private state;
35
+ private restartTimeout?;
36
+ private pc?;
37
+ private offerData?;
38
+ private sessionUrl?;
39
+ private queuedCandidates;
40
+ private nonAdvertisedCodecs;
41
+ private observer;
42
+ private stream?;
43
+ private flowCheck;
44
+ /**
45
+ * Create a WebRTCWhep.
46
+ * @param {Conf} conf - Configuration.
47
+ */
48
+ constructor(conf: Conf);
49
+ /**
50
+ * 媒体是否正常
51
+ */
52
+ get isRunning(): boolean;
53
+ /**
54
+ * Close the reader and all its resources.
55
+ */
56
+ close(): void;
57
+ /**
58
+ * Handle errors.
59
+ */
60
+ private handleError;
61
+ /**
62
+ * Get non-advertised codecs.
63
+ */
64
+ private getNonAdvertisedCodecs;
65
+ /**
66
+ * Start the WebRTC session.
67
+ */
68
+ private start;
69
+ /**
70
+ * Generate an authorization header.
71
+ */
72
+ private authHeader;
73
+ /**
74
+ * Request ICE servers from the endpoint.
75
+ */
76
+ private requestICEServers;
77
+ /**
78
+ * Setup a peer connection.
79
+ */
80
+ private setupPeerConnection;
81
+ /**
82
+ * Send an offer to the endpoint.
83
+ */
84
+ private sendOffer;
85
+ /**
86
+ * Set a remote answer.
87
+ */
88
+ private setAnswer;
89
+ /**
90
+ * Handle local ICE candidates.
91
+ */
92
+ private onLocalCandidate;
93
+ /**
94
+ * Send local ICE candidates to the endpoint.
95
+ */
96
+ private sendLocalCandidates;
97
+ /**
98
+ * Handle peer connection state changes.
99
+ */
100
+ private onConnectionState;
101
+ /**
102
+ * Handle incoming tracks.
103
+ */
104
+ private onTrack;
105
+ /**
106
+ * 流是否为空
107
+ */
108
+ get paused(): boolean;
109
+ /**
110
+ * 暂停播放
111
+ */
112
+ pause(): void;
113
+ /**
114
+ * 恢复播放
115
+ */
116
+ resume(): void;
117
+ }
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "whepts",
3
3
  "type": "module",
4
- "version": "1.0.0",
5
- "packageManager": "pnpm@10.26.1",
6
- "description": "",
4
+ "version": "1.0.2",
5
+ "packageManager": "pnpm@10.26.2",
6
+ "description": "基于 mediamtx 的 WebRTC WHEP 播放器,支持 ZLM 和 Mediamtx 的播放地址",
7
7
  "author": "mapleafgo",
8
8
  "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+ssh://git@github.com/mapleafgo/whepts.git"
12
+ },
9
13
  "keywords": [
10
14
  "webrtc",
11
15
  "whep",
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
+ }