@wetspace/wetrtc 3.0.1 → 4.0.0

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.
Files changed (139) hide show
  1. package/README.md +156 -27
  2. package/dist/es/__test__/codec-preference.test.js +36 -0
  3. package/dist/es/__test__/codec-preference.test.js.map +1 -0
  4. package/dist/es/__test__/data-manager.test.js +60 -0
  5. package/dist/es/__test__/data-manager.test.js.map +1 -0
  6. package/dist/es/__test__/fsm.test.js +33 -0
  7. package/dist/es/__test__/fsm.test.js.map +1 -0
  8. package/dist/es/__test__/media-manager.test.js +41 -0
  9. package/dist/es/__test__/media-manager.test.js.map +1 -0
  10. package/dist/es/__test__/signal-manager.test.js +100 -0
  11. package/dist/es/__test__/signal-manager.test.js.map +1 -0
  12. package/dist/es/__test__/wetrtc-lifecycle.test.js +152 -0
  13. package/dist/es/__test__/wetrtc-lifecycle.test.js.map +1 -0
  14. package/dist/es/data/data-manager.d.ts +37 -0
  15. package/dist/es/data/data-manager.d.ts.map +1 -0
  16. package/dist/es/data/data-manager.js +282 -0
  17. package/dist/es/data/data-manager.js.map +1 -0
  18. package/dist/es/data/types.d.ts +34 -0
  19. package/dist/es/data/types.d.ts.map +1 -0
  20. package/dist/es/data/types.js +0 -0
  21. package/dist/es/disposable.d.ts +12 -0
  22. package/dist/es/disposable.d.ts.map +1 -0
  23. package/dist/es/disposable.js +36 -0
  24. package/dist/es/disposable.js.map +1 -0
  25. package/dist/es/fsm.d.ts +26 -0
  26. package/dist/es/fsm.d.ts.map +1 -0
  27. package/dist/es/fsm.js +63 -0
  28. package/dist/es/fsm.js.map +1 -0
  29. package/dist/es/index.d.ts +22 -0
  30. package/dist/es/index.d.ts.map +1 -0
  31. package/dist/es/index.js +48 -0
  32. package/dist/es/index.js.map +1 -0
  33. package/dist/es/media/audio-encoding.d.ts +10 -0
  34. package/dist/es/media/audio-encoding.d.ts.map +1 -0
  35. package/dist/es/media/audio-encoding.js +41 -0
  36. package/dist/es/media/audio-encoding.js.map +1 -0
  37. package/dist/es/media/codec-preference.d.ts +11 -0
  38. package/dist/es/media/codec-preference.d.ts.map +1 -0
  39. package/dist/es/media/codec-preference.js +77 -0
  40. package/dist/es/media/codec-preference.js.map +1 -0
  41. package/dist/es/media/encoding-utils.d.ts +2 -0
  42. package/dist/es/media/encoding-utils.d.ts.map +1 -0
  43. package/dist/es/media/encoding-utils.js +8 -0
  44. package/dist/es/media/encoding-utils.js.map +1 -0
  45. package/dist/es/media/media-manager.d.ts +39 -0
  46. package/dist/es/media/media-manager.d.ts.map +1 -0
  47. package/dist/es/media/media-manager.js +121 -0
  48. package/dist/es/media/media-manager.js.map +1 -0
  49. package/dist/es/media/types.d.ts +25 -0
  50. package/dist/es/media/types.d.ts.map +1 -0
  51. package/dist/es/media/types.js +0 -0
  52. package/dist/es/media/video-encoding.d.ts +12 -0
  53. package/dist/es/media/video-encoding.d.ts.map +1 -0
  54. package/dist/es/media/video-encoding.js +60 -0
  55. package/dist/es/media/video-encoding.js.map +1 -0
  56. package/dist/es/signal/signal-manager.d.ts +45 -0
  57. package/dist/es/signal/signal-manager.d.ts.map +1 -0
  58. package/dist/es/signal/signal-manager.js +250 -0
  59. package/dist/es/signal/signal-manager.js.map +1 -0
  60. package/dist/es/signal/types.d.ts +26 -0
  61. package/dist/es/signal/types.d.ts.map +1 -0
  62. package/dist/es/signal/types.js +8 -0
  63. package/dist/es/signal/types.js.map +1 -0
  64. package/dist/es/stats/stats-monitor.d.ts +32 -0
  65. package/dist/es/stats/stats-monitor.d.ts.map +1 -0
  66. package/dist/es/stats/stats-monitor.js +191 -0
  67. package/dist/es/stats/stats-monitor.js.map +1 -0
  68. package/dist/es/stats/types.d.ts +33 -0
  69. package/dist/es/stats/types.d.ts.map +1 -0
  70. package/dist/es/stats/types.js +0 -0
  71. package/dist/es/utils/types.d.ts +46 -0
  72. package/dist/es/utils/types.d.ts.map +1 -0
  73. package/dist/es/utils/types.js +80 -0
  74. package/dist/es/utils/types.js.map +1 -0
  75. package/dist/es/wetrtc.d.ts +92 -0
  76. package/dist/es/wetrtc.d.ts.map +1 -0
  77. package/dist/es/wetrtc.js +403 -0
  78. package/dist/es/wetrtc.js.map +1 -0
  79. package/dist/lib/__test__/codec-preference.test.js +34 -0
  80. package/dist/lib/__test__/codec-preference.test.js.map +1 -0
  81. package/dist/lib/__test__/data-manager.test.js +61 -0
  82. package/dist/lib/__test__/data-manager.test.js.map +1 -0
  83. package/dist/lib/__test__/fsm.test.js +34 -0
  84. package/dist/lib/__test__/fsm.test.js.map +1 -0
  85. package/dist/lib/__test__/media-manager.test.js +42 -0
  86. package/dist/lib/__test__/media-manager.test.js.map +1 -0
  87. package/dist/lib/__test__/signal-manager.test.js +101 -0
  88. package/dist/lib/__test__/signal-manager.test.js.map +1 -0
  89. package/dist/lib/__test__/wetrtc-lifecycle.test.js +153 -0
  90. package/dist/lib/__test__/wetrtc-lifecycle.test.js.map +1 -0
  91. package/dist/lib/data/data-manager.js +306 -0
  92. package/dist/lib/data/data-manager.js.map +1 -0
  93. package/dist/lib/data/types.js +18 -0
  94. package/dist/lib/data/types.js.map +1 -0
  95. package/dist/lib/disposable.js +60 -0
  96. package/dist/lib/disposable.js.map +1 -0
  97. package/dist/lib/fsm.js +87 -0
  98. package/dist/lib/fsm.js.map +1 -0
  99. package/dist/lib/index.js +75 -0
  100. package/dist/lib/index.js.map +1 -0
  101. package/dist/lib/media/audio-encoding.js +66 -0
  102. package/dist/lib/media/audio-encoding.js.map +1 -0
  103. package/dist/lib/media/codec-preference.js +106 -0
  104. package/dist/lib/media/codec-preference.js.map +1 -0
  105. package/dist/lib/media/encoding-utils.js +32 -0
  106. package/dist/lib/media/encoding-utils.js.map +1 -0
  107. package/dist/lib/media/media-manager.js +145 -0
  108. package/dist/lib/media/media-manager.js.map +1 -0
  109. package/dist/lib/media/types.js +18 -0
  110. package/dist/lib/media/types.js.map +1 -0
  111. package/dist/lib/media/video-encoding.js +87 -0
  112. package/dist/lib/media/video-encoding.js.map +1 -0
  113. package/dist/lib/signal/signal-manager.js +274 -0
  114. package/dist/lib/signal/signal-manager.js.map +1 -0
  115. package/dist/lib/signal/types.js +32 -0
  116. package/dist/lib/signal/types.js.map +1 -0
  117. package/dist/lib/stats/stats-monitor.js +215 -0
  118. package/dist/lib/stats/stats-monitor.js.map +1 -0
  119. package/dist/lib/stats/types.js +18 -0
  120. package/dist/lib/stats/types.js.map +1 -0
  121. package/dist/lib/utils/types.js +108 -0
  122. package/dist/lib/utils/types.js.map +1 -0
  123. package/dist/lib/wetrtc.js +415 -0
  124. package/dist/lib/wetrtc.js.map +1 -0
  125. package/package.json +38 -43
  126. package/es/core/constant.d.ts +0 -6
  127. package/es/core/hook.d.ts +0 -31
  128. package/es/core/index.d.ts +0 -39
  129. package/es/index.d.ts +0 -6
  130. package/es/index.js +0 -1
  131. package/es/libs/index.d.ts +0 -41
  132. package/es/libs/record.d.ts +0 -8
  133. package/lib/core/constant.d.ts +0 -6
  134. package/lib/core/hook.d.ts +0 -31
  135. package/lib/core/index.d.ts +0 -39
  136. package/lib/index.d.ts +0 -6
  137. package/lib/index.js +0 -1
  138. package/lib/libs/index.d.ts +0 -41
  139. package/lib/libs/record.d.ts +0 -8
package/README.md CHANGED
@@ -1,27 +1,156 @@
1
- # WetRTC
2
-
3
- 本工具库是基于 WebRTC 的进一步封装,简化了其 WebRTC 中生涩的 API 的使用. 如果要考虑到兼容性,推荐引入`webrtc-adapter`这个适配库
4
-
5
- 适配库的安装:
6
-
7
- `npm install webrtc-adapter`
8
-
9
- [适配库的使用文档地址](https://github.com/webrtchacks/adapter#readme)
10
-
11
- Electron 应用中是不需要使用适配库的,高版本的 Electron 对该部分支持非常到位
12
-
13
- ## 1. 下载安装
14
-
15
- `npm i @wetspace/wetrtc -S` `yarn add @wetspace/wetrtc`
16
-
17
- ## 2. 源码地址
18
-
19
- [地址](https://gitee.com/wetspace/wetrtc)
20
-
21
- ## 3. [Chrome 对 RTC 的测试指标显示地址](chrome://webrtc-internals/)
22
-
23
- 该地址就是当你开启了 WebRTC 的音视频传输,它就能显示一些通讯中的基本信息,可用于某些功能的调试
24
-
25
- ## 测试体验工具(音视频通讯)
26
-
27
- 下载地址:[windows 版本]()
1
+ # @wetspace/wetrtc
2
+
3
+ 框架无关的 WebRTC 连接库 简化 SDP/ICE 协商、媒体管理与 DataChannel,信令传输由你自选。
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@wetspace/wetrtc)](https://www.npmjs.com/package/@wetspace/wetrtc)
6
+ [![license](https://img.shields.io/npm/l/@wetspace/wetrtc)](./LICENSE)
7
+
8
+ ## 特性
9
+
10
+ - **框架无关** — 不绑定 React / Vue / Angular,浏览器、Electron、Node.js 均可使用
11
+ - **信令解耦** 实现 `SignalChannel` 即可接入 WebSocket、Socket.IO、HTTP 长轮询等
12
+ - **完整子系统** — 媒体捕获、DataChannel、连接统计与诊断
13
+ - **Perfect Negotiation** — 内置 `polite` / `impolite` 模式,避免 glare 冲突
14
+ - **低延迟屏幕共享** — H.264 / Opus 偏好、编码参数与 playout delay 调优
15
+ - **自动重连** 可配置指数退避,ICE 断开后自动恢复
16
+ - **TypeScript** — 完整类型定义与强类型事件
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ npm install @wetspace/wetrtc
22
+ # or
23
+ pnpm add @wetspace/wetrtc
24
+ # or
25
+ yarn add @wetspace/wetrtc
26
+ ```
27
+
28
+ ### 浏览器兼容性
29
+
30
+ 若需兼容旧版浏览器,建议额外安装 [webrtc-adapter](https://github.com/webrtchacks/adapter):
31
+
32
+ ```bash
33
+ npm install webrtc-adapter
34
+ ```
35
+
36
+ 并在入口最早处引入:
37
+
38
+ ```ts
39
+ import 'webrtc-adapter'
40
+ ```
41
+
42
+ > **Electron** 应用通常无需 adapter,较新版本 Chromium 对 WebRTC 支持已足够完善。
43
+
44
+ ## 快速开始
45
+
46
+ WetRTC 需要一个**信令通道**交换 SDP 与 ICE。你只需实现 `SignalChannel` 接口:
47
+
48
+ ```ts
49
+ import type { SignalChannel } from '@wetspace/wetrtc'
50
+
51
+ const ws = new WebSocket('wss://signal.example.com')
52
+
53
+ export const signal: SignalChannel = {
54
+ async send(data) {
55
+ ws.send(JSON.stringify(data))
56
+ },
57
+ onMessage(handler) {
58
+ const listener = (e: MessageEvent) => handler(JSON.parse(e.data))
59
+ ws.addEventListener('message', listener)
60
+ return () => ws.removeEventListener('message', listener)
61
+ },
62
+ }
63
+ ```
64
+
65
+ 创建连接并监听远端媒体:
66
+
67
+ ```ts
68
+ import { WetRTC } from '@wetspace/wetrtc'
69
+
70
+ const rtc = new WetRTC({
71
+ signal,
72
+ direction: 'sendrecv',
73
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
74
+ })
75
+
76
+ rtc.on('track', (ev) => {
77
+ videoEl.srcObject = ev.streams[0]
78
+ })
79
+
80
+ rtc.on('statechange', (state) => {
81
+ console.log('connection:', state)
82
+ })
83
+
84
+ await rtc.connect()
85
+
86
+ // 使用完毕后释放
87
+ rtc.dispose()
88
+ ```
89
+
90
+ ## 连接方向
91
+
92
+ | 方向 | 说明 |
93
+ |------|------|
94
+ | `sendrecv` | 双向(默认),可发送和接收媒体 |
95
+ | `sendonly` | 仅发送,如屏幕共享主机 |
96
+ | `recvonly` | 仅接收,如手机观看端 |
97
+
98
+ ## 屏幕共享示例
99
+
100
+ ```ts
101
+ const rtc = new WetRTC({
102
+ signal,
103
+ direction: 'sendonly',
104
+ preferredVideoCodec: 'h264',
105
+ videoEncoding: {
106
+ maxBitrate: 4_000_000,
107
+ maxFramerate: 30,
108
+ },
109
+ })
110
+
111
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true })
112
+ for (const track of stream.getTracks()) {
113
+ rtc.peerConnection.addTrack(track, stream)
114
+ }
115
+
116
+ await rtc.connect()
117
+ ```
118
+
119
+ ## 主要导出
120
+
121
+ | 导出 | 说明 |
122
+ |------|------|
123
+ | `WetRTC` | 主入口,门面类 |
124
+ | `SignalManager` | 信令子系统(高级用法) |
125
+ | `MediaManager` | 媒体设备管理 |
126
+ | `DataManager` | DataChannel 与文件传输 |
127
+ | `StatsMonitor` | 连接质量统计 |
128
+ | `applyH264CodecPreference` 等 | 编解码器与编码调优工具 |
129
+
130
+ 完整 API 见[在线文档](https://www.wetspace.top/docs/wetrtc/)。
131
+
132
+ ## 文档
133
+
134
+ - [快速开始](https://www.wetspace.top/docs/wetrtc/guide/getting-started)
135
+ - [核心概念](https://www.wetspace.top/docs/wetrtc/guide/core-concepts)
136
+ - [信令](https://www.wetspace.top/docs/wetrtc/guide/signaling)
137
+ - [低延迟屏幕共享](https://www.wetspace.top/docs/wetrtc/guide/low-latency-screenshare)
138
+ - [WET 扩展屏](https://www.wetspace.top/docs/wetrtc/wet-screen/) — 基于本库的产品示例
139
+
140
+ ## 开发
141
+
142
+ ```bash
143
+ pnpm install
144
+ pnpm build # 输出到 dist/
145
+ pnpm test
146
+ ```
147
+
148
+ ## 相关链接
149
+
150
+ - **npm**: https://www.npmjs.com/package/@wetspace/wetrtc
151
+ - **源码**: https://gitee.com/wetspace/wetrtc
152
+ - **文档**: https://www.wetspace.top/docs/wetrtc/
153
+
154
+ ## License
155
+
156
+ MIT — 详见仓库根目录 [LICENSE](https://gitee.com/wetspace/wetrtc/blob/master/LICENSE)。
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ sortVideoCodecsH264First,
4
+ sortAudioCodecsOpusFirst
5
+ } from "../media/codec-preference";
6
+ describe("sortVideoCodecsH264First", () => {
7
+ it("places H264 codecs before others", () => {
8
+ const codecs = [
9
+ { mimeType: "video/VP8", clockRate: 9e4, channels: void 0, sdpFmtpLine: void 0 },
10
+ { mimeType: "video/H264", clockRate: 9e4, channels: void 0, sdpFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0" },
11
+ { mimeType: "video/VP9", clockRate: 9e4, channels: void 0, sdpFmtpLine: void 0 },
12
+ { mimeType: "video/H264", clockRate: 9e4, channels: void 0, sdpFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1" }
13
+ ];
14
+ const sorted = sortVideoCodecsH264First(codecs);
15
+ expect(sorted[0].mimeType).toBe("video/H264");
16
+ expect(sorted[0].sdpFmtpLine).toContain("packetization-mode=1");
17
+ expect(sorted[1].mimeType).toBe("video/H264");
18
+ expect(sorted[2].mimeType).toBe("video/VP8");
19
+ expect(sorted[3].mimeType).toBe("video/VP9");
20
+ });
21
+ });
22
+ describe("sortAudioCodecsOpusFirst", () => {
23
+ it("places Opus codecs before others", () => {
24
+ const codecs = [
25
+ { mimeType: "audio/PCMU", clockRate: 8e3, channels: 1, sdpFmtpLine: void 0 },
26
+ { mimeType: "audio/opus", clockRate: 48e3, channels: 2, sdpFmtpLine: "minptime=10;useinbandfec=1" },
27
+ { mimeType: "audio/PCMA", clockRate: 8e3, channels: 1, sdpFmtpLine: void 0 }
28
+ ];
29
+ const sorted = sortAudioCodecsOpusFirst(codecs);
30
+ expect(sorted[0].mimeType).toBe("audio/opus");
31
+ expect(sorted[1].mimeType).toBe("audio/PCMU");
32
+ expect(sorted[2].mimeType).toBe("audio/PCMA");
33
+ });
34
+ });
35
+
36
+ //# sourceMappingURL=codec-preference.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"mappings":"AAAA,SAAS,UAAU,QAAQ,UAAU;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAEP,SAAS,4BAA4B,MAAM;AACzC,KAAG,oCAAoC,MAAM;AAC3C,UAAM,SAAS;AAAA,MACb,EAAE,UAAU,aAAa,WAAW,KAAO,UAAU,QAAW,aAAa,OAAU;AAAA,MACvF,EAAE,UAAU,cAAc,WAAW,KAAO,UAAU,QAAW,aAAa,iDAAiD;AAAA,MAC/H,EAAE,UAAU,aAAa,WAAW,KAAO,UAAU,QAAW,aAAa,OAAU;AAAA,MACvF,EAAE,UAAU,cAAc,WAAW,KAAO,UAAU,QAAW,aAAa,iDAAiD;AAAA,IACjI;AAEA,UAAM,SAAS,yBAAyB,MAAM;AAE9C,WAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,YAAY;AAC5C,WAAO,OAAO,CAAC,EAAE,WAAW,EAAE,UAAU,sBAAsB;AAC9D,WAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,YAAY;AAC5C,WAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,WAAW;AAC3C,WAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,WAAW;AAAA,EAC7C,CAAC;AACH,CAAC;AAED,SAAS,4BAA4B,MAAM;AACzC,KAAG,oCAAoC,MAAM;AAC3C,UAAM,SAAS;AAAA,MACb,EAAE,UAAU,cAAc,WAAW,KAAM,UAAU,GAAG,aAAa,OAAU;AAAA,MAC/E,EAAE,UAAU,cAAc,WAAW,MAAO,UAAU,GAAG,aAAa,6BAA6B;AAAA,MACnG,EAAE,UAAU,cAAc,WAAW,KAAM,UAAU,GAAG,aAAa,OAAU;AAAA,IACjF;AAEA,UAAM,SAAS,yBAAyB,MAAM;AAE9C,WAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,YAAY;AAC5C,WAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,YAAY;AAC5C,WAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,YAAY;AAAA,EAC9C,CAAC;AACH,CAAC","names":[],"ignoreList":[],"sources":["../../../src/__test__/codec-preference.test.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest';\r\nimport {\r\n sortVideoCodecsH264First,\r\n sortAudioCodecsOpusFirst,\r\n type VideoRtpCodec,\r\n type AudioRtpCodec,\r\n} from '../media/codec-preference';\r\n\r\ndescribe('sortVideoCodecsH264First', () => {\r\n it('places H264 codecs before others', () => {\r\n const codecs = [\r\n { mimeType: 'video/VP8', clockRate: 90000, channels: undefined, sdpFmtpLine: undefined },\r\n { mimeType: 'video/H264', clockRate: 90000, channels: undefined, sdpFmtpLine: 'level-asymmetry-allowed=1;packetization-mode=0' },\r\n { mimeType: 'video/VP9', clockRate: 90000, channels: undefined, sdpFmtpLine: undefined },\r\n { mimeType: 'video/H264', clockRate: 90000, channels: undefined, sdpFmtpLine: 'level-asymmetry-allowed=1;packetization-mode=1' },\r\n ] as VideoRtpCodec[];\r\n\r\n const sorted = sortVideoCodecsH264First(codecs);\r\n\r\n expect(sorted[0].mimeType).toBe('video/H264');\r\n expect(sorted[0].sdpFmtpLine).toContain('packetization-mode=1');\r\n expect(sorted[1].mimeType).toBe('video/H264');\r\n expect(sorted[2].mimeType).toBe('video/VP8');\r\n expect(sorted[3].mimeType).toBe('video/VP9');\r\n });\r\n});\r\n\r\ndescribe('sortAudioCodecsOpusFirst', () => {\r\n it('places Opus codecs before others', () => {\r\n const codecs = [\r\n { mimeType: 'audio/PCMU', clockRate: 8000, channels: 1, sdpFmtpLine: undefined },\r\n { mimeType: 'audio/opus', clockRate: 48000, channels: 2, sdpFmtpLine: 'minptime=10;useinbandfec=1' },\r\n { mimeType: 'audio/PCMA', clockRate: 8000, channels: 1, sdpFmtpLine: undefined },\r\n ] as AudioRtpCodec[];\r\n\r\n const sorted = sortAudioCodecsOpusFirst(codecs);\r\n\r\n expect(sorted[0].mimeType).toBe('audio/opus');\r\n expect(sorted[1].mimeType).toBe('audio/PCMU');\r\n expect(sorted[2].mimeType).toBe('audio/PCMA');\r\n });\r\n});\r\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IiIsIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZXMiOltdLCJzb3VyY2VzQ29udGVudCI6W119"]}
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { DataManager } from "../data/data-manager";
3
+ function createChannel(label = "file-test") {
4
+ const listeners = /* @__PURE__ */ new Map();
5
+ return {
6
+ label,
7
+ readyState: "open",
8
+ bufferedAmount: 0,
9
+ bufferedAmountLowThreshold: 0,
10
+ send: vi.fn(),
11
+ close: vi.fn(),
12
+ addEventListener: vi.fn((type, fn) => {
13
+ if (!listeners.has(type))
14
+ listeners.set(type, /* @__PURE__ */ new Set());
15
+ listeners.get(type).add(fn);
16
+ }),
17
+ removeEventListener: vi.fn((type, fn) => {
18
+ listeners.get(type)?.delete(fn);
19
+ }),
20
+ onmessage: null,
21
+ onopen: null,
22
+ onclose: null,
23
+ onerror: null
24
+ };
25
+ }
26
+ function createPc(channel) {
27
+ return {
28
+ createDataChannel: vi.fn(() => channel)
29
+ };
30
+ }
31
+ describe("DataManager", () => {
32
+ it("receives a complete file", async () => {
33
+ const channel = createChannel();
34
+ const manager = new DataManager(createPc(channel));
35
+ const handler = vi.fn();
36
+ manager.onFile(handler);
37
+ manager.createChannel("file-test");
38
+ channel.onmessage?.({ data: JSON.stringify({
39
+ type: "file-meta",
40
+ name: "hello.txt",
41
+ size: 5,
42
+ chunks: 1,
43
+ mimeType: "text/plain"
44
+ }) });
45
+ channel.onmessage?.({ data: new Blob(["hello"]) });
46
+ channel.onmessage?.({ data: JSON.stringify({ type: "file-complete", name: "hello.txt" }) });
47
+ expect(handler).toHaveBeenCalledTimes(1);
48
+ const [meta, blob] = handler.mock.calls[0];
49
+ expect(meta).toEqual({ name: "hello.txt", size: 5, type: "text/plain" });
50
+ expect(await blob.text()).toBe("hello");
51
+ });
52
+ it("rejects sendFile when file exceeds maxFileSize", async () => {
53
+ const channel = createChannel();
54
+ const manager = new DataManager(createPc(channel));
55
+ const file = new File(["too large"], "large.txt");
56
+ await expect(manager.sendFile(file, { maxFileSize: 1 })).rejects.toThrow(/maxFileSize/);
57
+ });
58
+ });
59
+
60
+ //# sourceMappingURL=data-manager.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"mappings":"AAAA,SAAS,UAAU,QAAQ,IAAI,UAAU;AACzC,SAAS,mBAAmB;AAE5B,SAAS,cAAc,QAAQ,aAA6B;AAC1D,QAAM,YAAY,oBAAI,IAA2C;AACjE,SAAO;AAAA,IACL;AAAA,IACA,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,4BAA4B;AAAA,IAC5B,MAAM,GAAG,GAAG;AAAA,IACZ,OAAO,GAAG,GAAG;AAAA,IACb,kBAAkB,GAAG,GAAG,CAAC,MAAc,OAAiC;AACtE,UAAI,CAAC,UAAU,IAAI,IAAI;AAAG,kBAAU,IAAI,MAAM,oBAAI,IAAI,CAAC;AACvD,gBAAU,IAAI,IAAI,EAAG,IAAI,EAAE;AAAA,IAC7B,CAAC;AAAA,IACD,qBAAqB,GAAG,GAAG,CAAC,MAAc,OAAiC;AACzE,gBAAU,IAAI,IAAI,GAAG,OAAO,EAAE;AAAA,IAChC,CAAC;AAAA,IACD,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AACF;AAEA,SAAS,SAAS,SAA4C;AAC5D,SAAO;AAAA,IACL,mBAAmB,GAAG,GAAG,MAAM,OAAO;AAAA,EACxC;AACF;AAEA,SAAS,eAAe,MAAM;AAC5B,KAAG,4BAA4B,YAAY;AACzC,UAAM,UAAU,cAAc;AAC9B,UAAM,UAAU,IAAI,YAAY,SAAS,OAAO,CAAC;AACjD,UAAM,UAAU,GAAG,GAAG;AAEtB,YAAQ,OAAO,OAAO;AACtB,YAAQ,cAAc,WAAW;AAEjC,YAAQ,YAAY,EAAE,MAAM,KAAK,UAAU;AAAA,MACzC,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,UAAU;AAAA,IACZ,CAAC,EAAE,CAAiB;AACpB,YAAQ,YAAY,EAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAiB;AACjE,YAAQ,YAAY,EAAE,MAAM,KAAK,UAAU,EAAE,MAAM,iBAAiB,MAAM,YAAY,CAAC,EAAE,CAAiB;AAE1G,WAAO,OAAO,EAAE,sBAAsB,CAAC;AACvC,UAAM,CAAC,MAAM,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC;AACzC,WAAO,IAAI,EAAE,QAAQ,EAAE,MAAM,aAAa,MAAM,GAAG,MAAM,aAAa,CAAC;AACvE,WAAO,MAAM,KAAK,KAAK,CAAC,EAAE,KAAK,OAAO;AAAA,EACxC,CAAC;AAED,KAAG,kDAAkD,YAAY;AAC/D,UAAM,UAAU,cAAc;AAC9B,UAAM,UAAU,IAAI,YAAY,SAAS,OAAO,CAAC;AACjD,UAAM,OAAO,IAAI,KAAK,CAAC,WAAW,GAAG,WAAW;AAEhD,UAAM,OAAO,QAAQ,SAAS,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC,EAAE,QAAQ,QAAQ,aAAa;AAAA,EACxF,CAAC;AACH,CAAC","names":[],"ignoreList":[],"sources":["../../../src/__test__/data-manager.test.ts"],"sourcesContent":["import { describe, expect, it, vi } from 'vitest';\r\nimport { DataManager } from '../data/data-manager';\r\n\r\nfunction createChannel(label = 'file-test'): RTCDataChannel {\r\n const listeners = new Map<string, Set<(...args: any[]) => void>>();\r\n return {\r\n label,\r\n readyState: 'open',\r\n bufferedAmount: 0,\r\n bufferedAmountLowThreshold: 0,\r\n send: vi.fn(),\r\n close: vi.fn(),\r\n addEventListener: vi.fn((type: string, fn: (...args: any[]) => void) => {\r\n if (!listeners.has(type)) listeners.set(type, new Set());\r\n listeners.get(type)!.add(fn);\r\n }),\r\n removeEventListener: vi.fn((type: string, fn: (...args: any[]) => void) => {\r\n listeners.get(type)?.delete(fn);\r\n }),\r\n onmessage: null,\r\n onopen: null,\r\n onclose: null,\r\n onerror: null,\r\n } as unknown as RTCDataChannel;\r\n}\r\n\r\nfunction createPc(channel: RTCDataChannel): RTCPeerConnection {\r\n return {\r\n createDataChannel: vi.fn(() => channel),\r\n } as unknown as RTCPeerConnection;\r\n}\r\n\r\ndescribe('DataManager', () => {\r\n it('receives a complete file', async () => {\r\n const channel = createChannel();\r\n const manager = new DataManager(createPc(channel));\r\n const handler = vi.fn();\r\n\r\n manager.onFile(handler);\r\n manager.createChannel('file-test');\r\n\r\n channel.onmessage?.({ data: JSON.stringify({\r\n type: 'file-meta',\r\n name: 'hello.txt',\r\n size: 5,\r\n chunks: 1,\r\n mimeType: 'text/plain',\r\n }) } as MessageEvent);\r\n channel.onmessage?.({ data: new Blob(['hello']) } as MessageEvent);\r\n channel.onmessage?.({ data: JSON.stringify({ type: 'file-complete', name: 'hello.txt' }) } as MessageEvent);\r\n\r\n expect(handler).toHaveBeenCalledTimes(1);\r\n const [meta, blob] = handler.mock.calls[0];\r\n expect(meta).toEqual({ name: 'hello.txt', size: 5, type: 'text/plain' });\r\n expect(await blob.text()).toBe('hello');\r\n });\r\n\r\n it('rejects sendFile when file exceeds maxFileSize', async () => {\r\n const channel = createChannel();\r\n const manager = new DataManager(createPc(channel));\r\n const file = new File(['too large'], 'large.txt');\r\n\r\n await expect(manager.sendFile(file, { maxFileSize: 1 })).rejects.toThrow(/maxFileSize/);\r\n });\r\n});\r\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IiIsIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZXMiOltdLCJzb3VyY2VzQ29udGVudCI6W119"]}
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { ConnectionStateMachine } from "../fsm";
3
+ describe("ConnectionStateMachine", () => {
4
+ it("starts in idle", () => {
5
+ const fsm = new ConnectionStateMachine();
6
+ expect(fsm.state).toBe("idle");
7
+ });
8
+ it("allows idle → signaling → connecting → connected", () => {
9
+ const fsm = new ConnectionStateMachine();
10
+ fsm.transition("signaling");
11
+ fsm.transition("connecting");
12
+ fsm.transition("connected");
13
+ expect(fsm.state).toBe("connected");
14
+ });
15
+ it("throws on illegal transition idle → connected", () => {
16
+ const fsm = new ConnectionStateMachine();
17
+ expect(() => fsm.transition("connected")).toThrow(/Invalid state transition/);
18
+ });
19
+ it("force sets state without validation", () => {
20
+ const fsm = new ConnectionStateMachine();
21
+ fsm.force("connected");
22
+ expect(fsm.state).toBe("connected");
23
+ });
24
+ it("failed can return to idle", () => {
25
+ const fsm = new ConnectionStateMachine();
26
+ fsm.transition("signaling");
27
+ fsm.transition("failed");
28
+ fsm.transition("idle");
29
+ expect(fsm.state).toBe("idle");
30
+ });
31
+ });
32
+
33
+ //# sourceMappingURL=fsm.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"mappings":"AAAA,SAAS,UAAU,IAAI,cAAc;AACrC,SAAS,8BAA8B;AAEvC,SAAS,0BAA0B,MAAM;AACvC,KAAG,kBAAkB,MAAM;AACzB,UAAM,MAAM,IAAI,uBAAuB;AACvC,WAAO,IAAI,KAAK,EAAE,KAAK,MAAM;AAAA,EAC/B,CAAC;AAED,KAAG,oDAAoD,MAAM;AAC3D,UAAM,MAAM,IAAI,uBAAuB;AACvC,QAAI,WAAW,WAAW;AAC1B,QAAI,WAAW,YAAY;AAC3B,QAAI,WAAW,WAAW;AAC1B,WAAO,IAAI,KAAK,EAAE,KAAK,WAAW;AAAA,EACpC,CAAC;AAED,KAAG,iDAAiD,MAAM;AACxD,UAAM,MAAM,IAAI,uBAAuB;AACvC,WAAO,MAAM,IAAI,WAAW,WAAW,CAAC,EAAE,QAAQ,0BAA0B;AAAA,EAC9E,CAAC;AAED,KAAG,uCAAuC,MAAM;AAC9C,UAAM,MAAM,IAAI,uBAAuB;AACvC,QAAI,MAAM,WAAW;AACrB,WAAO,IAAI,KAAK,EAAE,KAAK,WAAW;AAAA,EACpC,CAAC;AAED,KAAG,6BAA6B,MAAM;AACpC,UAAM,MAAM,IAAI,uBAAuB;AACvC,QAAI,WAAW,WAAW;AAC1B,QAAI,WAAW,QAAQ;AACvB,QAAI,WAAW,MAAM;AACrB,WAAO,IAAI,KAAK,EAAE,KAAK,MAAM;AAAA,EAC/B,CAAC;AACH,CAAC","names":[],"ignoreList":[],"sources":["../../../src/__test__/fsm.test.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest';\r\nimport { ConnectionStateMachine } from '../fsm';\r\n\r\ndescribe('ConnectionStateMachine', () => {\r\n it('starts in idle', () => {\r\n const fsm = new ConnectionStateMachine();\r\n expect(fsm.state).toBe('idle');\r\n });\r\n\r\n it('allows idle → signaling → connecting → connected', () => {\r\n const fsm = new ConnectionStateMachine();\r\n fsm.transition('signaling');\r\n fsm.transition('connecting');\r\n fsm.transition('connected');\r\n expect(fsm.state).toBe('connected');\r\n });\r\n\r\n it('throws on illegal transition idle → connected', () => {\r\n const fsm = new ConnectionStateMachine();\r\n expect(() => fsm.transition('connected')).toThrow(/Invalid state transition/);\r\n });\r\n\r\n it('force sets state without validation', () => {\r\n const fsm = new ConnectionStateMachine();\r\n fsm.force('connected');\r\n expect(fsm.state).toBe('connected');\r\n });\r\n\r\n it('failed can return to idle', () => {\r\n const fsm = new ConnectionStateMachine();\r\n fsm.transition('signaling');\r\n fsm.transition('failed');\r\n fsm.transition('idle');\r\n expect(fsm.state).toBe('idle');\r\n });\r\n});\r\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IiIsIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZXMiOltdLCJzb3VyY2VzQ29udGVudCI6W119"]}
@@ -0,0 +1,41 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { MediaManager } from "../media/media-manager";
3
+ describe("MediaManager", () => {
4
+ afterEach(() => {
5
+ vi.unstubAllGlobals();
6
+ });
7
+ it("maps display options to getDisplayMedia constraints", async () => {
8
+ const stream = {};
9
+ const getDisplayMedia = vi.fn().mockResolvedValue(stream);
10
+ vi.stubGlobal("navigator", {
11
+ mediaDevices: {
12
+ getDisplayMedia,
13
+ getUserMedia: vi.fn()
14
+ }
15
+ });
16
+ await MediaManager.getDisplayMedia({
17
+ width: 1920,
18
+ height: 1080,
19
+ frameRate: 30,
20
+ audio: true
21
+ });
22
+ expect(getDisplayMedia).toHaveBeenCalledWith({
23
+ video: { width: 1920, height: 1080, frameRate: 30 },
24
+ audio: true
25
+ });
26
+ });
27
+ it("instance getDisplayMedia delegates to static implementation", async () => {
28
+ const stream = {};
29
+ const getDisplayMedia = vi.fn().mockResolvedValue(stream);
30
+ vi.stubGlobal("navigator", {
31
+ mediaDevices: {
32
+ getDisplayMedia,
33
+ getUserMedia: vi.fn()
34
+ }
35
+ });
36
+ const media = new MediaManager();
37
+ await expect(media.getDisplayMedia({ audio: false })).resolves.toBe(stream);
38
+ });
39
+ });
40
+
41
+ //# sourceMappingURL=media-manager.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"mappings":"AAAA,SAAS,WAAW,UAAU,QAAQ,IAAI,UAAU;AACpD,SAAS,oBAAoB;AAE7B,SAAS,gBAAgB,MAAM;AAC7B,YAAU,MAAM;AACd,OAAG,iBAAiB;AAAA,EACtB,CAAC;AAED,KAAG,uDAAuD,YAAY;AACpE,UAAM,SAAS,CAAC;AAChB,UAAM,kBAAkB,GAAG,GAAG,EAAE,kBAAkB,MAAM;AACxD,OAAG,WAAW,aAAa;AAAA,MACzB,cAAc;AAAA,QACZ;AAAA,QACA,cAAc,GAAG,GAAG;AAAA,MACtB;AAAA,IACF,CAAC;AAED,UAAM,aAAa,gBAAgB;AAAA,MACjC,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,OAAO;AAAA,IACT,CAAC;AAED,WAAO,eAAe,EAAE,qBAAqB;AAAA,MAC3C,OAAO,EAAE,OAAO,MAAM,QAAQ,MAAM,WAAW,GAAG;AAAA,MAClD,OAAO;AAAA,IACT,CAAC;AAAA,EACH,CAAC;AAED,KAAG,+DAA+D,YAAY;AAC5E,UAAM,SAAS,CAAC;AAChB,UAAM,kBAAkB,GAAG,GAAG,EAAE,kBAAkB,MAAM;AACxD,OAAG,WAAW,aAAa;AAAA,MACzB,cAAc;AAAA,QACZ;AAAA,QACA,cAAc,GAAG,GAAG;AAAA,MACtB;AAAA,IACF,CAAC;AAED,UAAM,QAAQ,IAAI,aAAa;AAC/B,UAAM,OAAO,MAAM,gBAAgB,EAAE,OAAO,MAAM,CAAC,CAAC,EAAE,SAAS,KAAK,MAAM;AAAA,EAC5E,CAAC;AACH,CAAC","names":[],"ignoreList":[],"sources":["../../../src/__test__/media-manager.test.ts"],"sourcesContent":["import { afterEach, describe, expect, it, vi } from 'vitest';\r\nimport { MediaManager } from '../media/media-manager';\r\n\r\ndescribe('MediaManager', () => {\r\n afterEach(() => {\r\n vi.unstubAllGlobals();\r\n });\r\n\r\n it('maps display options to getDisplayMedia constraints', async () => {\r\n const stream = {} as MediaStream;\r\n const getDisplayMedia = vi.fn().mockResolvedValue(stream);\r\n vi.stubGlobal('navigator', {\r\n mediaDevices: {\r\n getDisplayMedia,\r\n getUserMedia: vi.fn(),\r\n },\r\n });\r\n\r\n await MediaManager.getDisplayMedia({\r\n width: 1920,\r\n height: 1080,\r\n frameRate: 30,\r\n audio: true,\r\n });\r\n\r\n expect(getDisplayMedia).toHaveBeenCalledWith({\r\n video: { width: 1920, height: 1080, frameRate: 30 },\r\n audio: true,\r\n });\r\n });\r\n\r\n it('instance getDisplayMedia delegates to static implementation', async () => {\r\n const stream = {} as MediaStream;\r\n const getDisplayMedia = vi.fn().mockResolvedValue(stream);\r\n vi.stubGlobal('navigator', {\r\n mediaDevices: {\r\n getDisplayMedia,\r\n getUserMedia: vi.fn(),\r\n },\r\n });\r\n\r\n const media = new MediaManager();\r\n await expect(media.getDisplayMedia({ audio: false })).resolves.toBe(stream);\r\n });\r\n});\r\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IiIsIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZXMiOltdLCJzb3VyY2VzQ29udGVudCI6W119"]}
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ConnectionStateMachine } from "../fsm";
3
+ import { SignalManager } from "../signal/signal-manager";
4
+ function createMockPc(overrides = {}) {
5
+ const listeners = /* @__PURE__ */ new Map();
6
+ return {
7
+ signalingState: "stable",
8
+ localDescription: null,
9
+ remoteDescription: null,
10
+ iceConnectionState: "new",
11
+ addEventListener: vi.fn((type, fn) => {
12
+ if (!listeners.has(type))
13
+ listeners.set(type, /* @__PURE__ */ new Set());
14
+ listeners.get(type).add(fn);
15
+ }),
16
+ removeEventListener: vi.fn((type, fn) => {
17
+ listeners.get(type)?.delete(fn);
18
+ }),
19
+ createOffer: vi.fn().mockResolvedValue({ type: "offer", sdp: "mock-offer" }),
20
+ createAnswer: vi.fn().mockResolvedValue({ type: "answer", sdp: "mock-answer" }),
21
+ setLocalDescription: vi.fn().mockImplementation(async (desc) => {
22
+ mock.localDescription = desc;
23
+ }),
24
+ setRemoteDescription: vi.fn().mockImplementation(async (desc) => {
25
+ mock.remoteDescription = desc;
26
+ }),
27
+ addIceCandidate: vi.fn().mockResolvedValue(void 0),
28
+ ...overrides
29
+ };
30
+ }
31
+ let mock;
32
+ function createSignalChannel() {
33
+ const handlers = [];
34
+ return {
35
+ handlers,
36
+ send: vi.fn().mockResolvedValue(void 0),
37
+ onMessage(handler) {
38
+ handlers.push(handler);
39
+ return () => {
40
+ const idx = handlers.indexOf(handler);
41
+ if (idx >= 0)
42
+ handlers.splice(idx, 1);
43
+ };
44
+ }
45
+ };
46
+ }
47
+ describe("SignalManager", () => {
48
+ let fsm;
49
+ let channel;
50
+ beforeEach(() => {
51
+ mock = createMockPc();
52
+ fsm = new ConnectionStateMachine();
53
+ channel = createSignalChannel();
54
+ });
55
+ it("createOffer sends offer and enters signaling", async () => {
56
+ const sm = new SignalManager(channel, mock, fsm, true);
57
+ await sm.createOffer();
58
+ expect(channel.send).toHaveBeenCalledWith(
59
+ expect.objectContaining({ type: "offer", sdp: "mock-offer" })
60
+ );
61
+ expect(fsm.state).toBe("signaling");
62
+ });
63
+ it("createOffer does not repeat signaling transition", async () => {
64
+ fsm.transition("signaling");
65
+ const sm = new SignalManager(channel, mock, fsm, true);
66
+ await expect(sm.createOffer()).resolves.toBeUndefined();
67
+ expect(fsm.state).toBe("signaling");
68
+ });
69
+ it("handleOffer sends answer", async () => {
70
+ const sm = new SignalManager(channel, mock, fsm, true);
71
+ await sm.handleOffer("remote-offer-sdp");
72
+ expect(mock.setRemoteDescription).toHaveBeenCalled();
73
+ expect(mock.createAnswer).toHaveBeenCalled();
74
+ expect(channel.send).toHaveBeenCalledWith(
75
+ expect.objectContaining({ type: "answer" })
76
+ );
77
+ });
78
+ it("setPeerConnection rebinds without throwing", () => {
79
+ const sm = new SignalManager(channel, mock, fsm, true);
80
+ const newPc = createMockPc();
81
+ expect(() => sm.setPeerConnection(newPc)).not.toThrow();
82
+ });
83
+ it("impolite peer ignores colliding offer", async () => {
84
+ mock = createMockPc({ signalingState: "have-local-offer" });
85
+ const sm = new SignalManager(channel, mock, fsm, false);
86
+ sm.makingOffer = true;
87
+ await sm.handleOffer("colliding-offer");
88
+ expect(mock.setRemoteDescription).not.toHaveBeenCalled();
89
+ });
90
+ it("polite peer rolls back colliding offer", async () => {
91
+ mock = createMockPc({ signalingState: "have-local-offer" });
92
+ const sm = new SignalManager(channel, mock, fsm, true);
93
+ sm.makingOffer = true;
94
+ await sm.handleOffer("colliding-offer");
95
+ expect(mock.setLocalDescription).toHaveBeenCalledWith({ type: "rollback" });
96
+ expect(mock.setRemoteDescription).toHaveBeenCalledWith({ type: "offer", sdp: "colliding-offer" });
97
+ });
98
+ });
99
+
100
+ //# sourceMappingURL=signal-manager.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"mappings":"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","names":[],"ignoreList":[],"sources":["../../../src/__test__/signal-manager.test.ts"],"sourcesContent":["import { describe, it, expect, vi, beforeEach } from 'vitest';\r\nimport { ConnectionStateMachine } from '../fsm';\r\nimport { SignalManager } from '../signal/signal-manager';\r\nimport type { SignalChannel } from '../signal/types';\r\n\r\nfunction createMockPc(overrides: Partial<RTCPeerConnection> = {}): RTCPeerConnection {\r\n const listeners = new Map<string, Set<EventListener>>();\r\n return {\r\n signalingState: 'stable',\r\n localDescription: null,\r\n remoteDescription: null,\r\n iceConnectionState: 'new',\r\n addEventListener: vi.fn((type: string, fn: EventListener) => {\r\n if (!listeners.has(type)) listeners.set(type, new Set());\r\n listeners.get(type)!.add(fn);\r\n }),\r\n removeEventListener: vi.fn((type: string, fn: EventListener) => {\r\n listeners.get(type)?.delete(fn);\r\n }),\r\n createOffer: vi.fn().mockResolvedValue({ type: 'offer', sdp: 'mock-offer' }),\r\n createAnswer: vi.fn().mockResolvedValue({ type: 'answer', sdp: 'mock-answer' }),\r\n setLocalDescription: vi.fn().mockImplementation(async (desc) => {\r\n (mock as any).localDescription = desc;\r\n }),\r\n setRemoteDescription: vi.fn().mockImplementation(async (desc) => {\r\n (mock as any).remoteDescription = desc;\r\n }),\r\n addIceCandidate: vi.fn().mockResolvedValue(undefined),\r\n ...overrides,\r\n } as unknown as RTCPeerConnection;\r\n}\r\n\r\nlet mock: RTCPeerConnection;\r\n\r\nfunction createSignalChannel(): SignalChannel & { handlers: Array<(msg: unknown) => void> } {\r\n const handlers: Array<(msg: unknown) => void> = [];\r\n return {\r\n handlers,\r\n send: vi.fn().mockResolvedValue(undefined),\r\n onMessage(handler: (msg: unknown) => void) {\r\n handlers.push(handler);\r\n return () => {\r\n const idx = handlers.indexOf(handler);\r\n if (idx >= 0) handlers.splice(idx, 1);\r\n };\r\n },\r\n };\r\n}\r\n\r\ndescribe('SignalManager', () => {\r\n let fsm: ConnectionStateMachine;\r\n let channel: ReturnType<typeof createSignalChannel>;\r\n\r\n beforeEach(() => {\r\n mock = createMockPc();\r\n fsm = new ConnectionStateMachine();\r\n channel = createSignalChannel();\r\n });\r\n\r\n it('createOffer sends offer and enters signaling', async () => {\r\n const sm = new SignalManager(channel, mock, fsm, true);\r\n await sm.createOffer();\r\n\r\n expect(channel.send).toHaveBeenCalledWith(\r\n expect.objectContaining({ type: 'offer', sdp: 'mock-offer' }),\r\n );\r\n expect(fsm.state).toBe('signaling');\r\n });\r\n\r\n it('createOffer does not repeat signaling transition', async () => {\r\n fsm.transition('signaling');\r\n const sm = new SignalManager(channel, mock, fsm, true);\r\n\r\n await expect(sm.createOffer()).resolves.toBeUndefined();\r\n expect(fsm.state).toBe('signaling');\r\n });\r\n\r\n it('handleOffer sends answer', async () => {\r\n const sm = new SignalManager(channel, mock, fsm, true);\r\n await sm.handleOffer('remote-offer-sdp');\r\n\r\n expect(mock.setRemoteDescription).toHaveBeenCalled();\r\n expect(mock.createAnswer).toHaveBeenCalled();\r\n expect(channel.send).toHaveBeenCalledWith(\r\n expect.objectContaining({ type: 'answer' }),\r\n );\r\n });\r\n\r\n it('setPeerConnection rebinds without throwing', () => {\r\n const sm = new SignalManager(channel, mock, fsm, true);\r\n const newPc = createMockPc();\r\n expect(() => sm.setPeerConnection(newPc)).not.toThrow();\r\n });\r\n\r\n it('impolite peer ignores colliding offer', async () => {\r\n mock = createMockPc({ signalingState: 'have-local-offer' as RTCSignalingState });\r\n const sm = new SignalManager(channel, mock, fsm, false);\r\n (sm as any).makingOffer = true;\r\n\r\n await sm.handleOffer('colliding-offer');\r\n\r\n expect(mock.setRemoteDescription).not.toHaveBeenCalled();\r\n });\r\n\r\n it('polite peer rolls back colliding offer', async () => {\r\n mock = createMockPc({ signalingState: 'have-local-offer' as RTCSignalingState });\r\n const sm = new SignalManager(channel, mock, fsm, true);\r\n (sm as any).makingOffer = true;\r\n\r\n await sm.handleOffer('colliding-offer');\r\n\r\n expect(mock.setLocalDescription).toHaveBeenCalledWith({ type: 'rollback' });\r\n expect(mock.setRemoteDescription).toHaveBeenCalledWith({ type: 'offer', sdp: 'colliding-offer' });\r\n });\r\n});\r\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IiIsIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZXMiOltdLCJzb3VyY2VzQ29udGVudCI6W119"]}
@@ -0,0 +1,152 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { WetRTC } from "../wetrtc";
3
+ const pcs = [];
4
+ class MockPeerConnection {
5
+ constructor() {
6
+ this.iceConnectionState = "new";
7
+ this.connectionState = "new";
8
+ this.signalingState = "stable";
9
+ this.localDescription = null;
10
+ this.remoteDescription = null;
11
+ this.senders = [];
12
+ this.transceivers = [];
13
+ this.closed = false;
14
+ this.addEventListener = vi.fn();
15
+ this.removeEventListener = vi.fn();
16
+ this.createDataChannel = vi.fn(() => ({ close: vi.fn(), readyState: "open" }));
17
+ this.createOffer = vi.fn().mockResolvedValue({ type: "offer", sdp: "offer" });
18
+ this.createAnswer = vi.fn().mockResolvedValue({ type: "answer", sdp: "answer" });
19
+ this.setLocalDescription = vi.fn().mockImplementation(async (desc) => {
20
+ this.localDescription = desc;
21
+ });
22
+ this.setRemoteDescription = vi.fn().mockImplementation(async (desc) => {
23
+ this.remoteDescription = desc;
24
+ });
25
+ this.addIceCandidate = vi.fn().mockResolvedValue(void 0);
26
+ this.getStats = vi.fn().mockResolvedValue({ forEach: vi.fn() });
27
+ this.close = vi.fn(() => {
28
+ this.closed = true;
29
+ });
30
+ this.getSenders = vi.fn(() => this.senders);
31
+ this.getTransceivers = vi.fn(() => this.transceivers);
32
+ this.addTransceiver = vi.fn((kind, init) => {
33
+ const transceiver = {
34
+ direction: init?.direction ?? "sendrecv",
35
+ sender: {},
36
+ receiver: {},
37
+ mid: null,
38
+ currentDirection: null,
39
+ stop: vi.fn()
40
+ };
41
+ this.transceivers.push(transceiver);
42
+ return transceiver;
43
+ });
44
+ this.addTrack = vi.fn((track) => {
45
+ const sender = {
46
+ track,
47
+ replaceTrack: vi.fn().mockResolvedValue(void 0)
48
+ };
49
+ const transceiver = {
50
+ direction: "sendrecv",
51
+ sender,
52
+ receiver: {},
53
+ mid: null,
54
+ currentDirection: null,
55
+ stop: vi.fn()
56
+ };
57
+ this.senders.push(sender);
58
+ this.transceivers.push(transceiver);
59
+ return sender;
60
+ });
61
+ this.removeTrack = vi.fn();
62
+ pcs.push(this);
63
+ }
64
+ }
65
+ function signal() {
66
+ return {
67
+ send: vi.fn().mockResolvedValue(void 0),
68
+ onMessage: vi.fn(() => () => {
69
+ })
70
+ };
71
+ }
72
+ function streamWithTrack(kind = "video") {
73
+ const track = {
74
+ id: `${kind}-1`,
75
+ kind,
76
+ stop: vi.fn(),
77
+ addEventListener: vi.fn()
78
+ };
79
+ const tracks = [track];
80
+ const stream = {
81
+ getTracks: () => tracks,
82
+ getVideoTracks: () => kind === "video" ? tracks : [],
83
+ addTrack: vi.fn((next) => {
84
+ if (!tracks.includes(next))
85
+ tracks.push(next);
86
+ }),
87
+ removeTrack: vi.fn((next) => {
88
+ const idx = tracks.indexOf(next);
89
+ if (idx >= 0)
90
+ tracks.splice(idx, 1);
91
+ })
92
+ };
93
+ return { stream, track };
94
+ }
95
+ describe("WetRTC lifecycle", () => {
96
+ beforeEach(() => {
97
+ pcs.length = 0;
98
+ vi.stubGlobal("RTCPeerConnection", MockPeerConnection);
99
+ });
100
+ afterEach(() => {
101
+ vi.unstubAllGlobals();
102
+ });
103
+ it("disconnect rebinds peer connection and restores local tracks", async () => {
104
+ const channel = signal();
105
+ const rtc = new WetRTC({ signal: channel, reconnect: false });
106
+ const { stream, track } = streamWithTrack();
107
+ const firstPc = rtc.peerConnection;
108
+ rtc.addTrack(track, stream);
109
+ await rtc.disconnect();
110
+ expect(channel.send).toHaveBeenCalledWith({ type: "bye" });
111
+ const nextPc = rtc.peerConnection;
112
+ expect(nextPc).not.toBe(firstPc);
113
+ expect(firstPc.close).toHaveBeenCalled();
114
+ expect(nextPc.addTrack).toHaveBeenCalledWith(track, stream);
115
+ expect(rtc.state).toBe("idle");
116
+ });
117
+ it("recvonly connect creates recvonly transceivers", async () => {
118
+ const rtc = new WetRTC({ signal: signal(), direction: "recvonly", reconnect: false });
119
+ await rtc.connect();
120
+ const pc = rtc.peerConnection;
121
+ expect(pc.addTransceiver).toHaveBeenCalledWith("video", { direction: "recvonly" });
122
+ expect(pc.addTransceiver).toHaveBeenCalledWith("audio", { direction: "recvonly" });
123
+ });
124
+ it("recvonly initiator sends offer", async () => {
125
+ const ch = signal();
126
+ const rtc = new WetRTC({
127
+ signal: ch,
128
+ direction: "recvonly",
129
+ initiator: true,
130
+ reconnect: false
131
+ });
132
+ await rtc.connect();
133
+ expect(ch.send).toHaveBeenCalledWith(
134
+ expect.objectContaining({ type: "offer" })
135
+ );
136
+ });
137
+ it("sendonly passive does not send offer", async () => {
138
+ const ch = signal();
139
+ const rtc = new WetRTC({
140
+ signal: ch,
141
+ direction: "sendonly",
142
+ initiator: false,
143
+ reconnect: false
144
+ });
145
+ await rtc.connect();
146
+ expect(ch.send).not.toHaveBeenCalledWith(
147
+ expect.objectContaining({ type: "offer" })
148
+ );
149
+ });
150
+ });
151
+
152
+ //# sourceMappingURL=wetrtc-lifecycle.test.js.map