yjz-web-sdk 1.0.9-beta.5 → 1.0.10

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 (43) hide show
  1. package/lib/components/RemotePlayer/index.vue.d.ts +1 -1
  2. package/lib/composables/useRemoteVideo.d.ts +1 -1
  3. package/lib/util/WasmUtil.d.ts +1 -2
  4. package/lib/yjz-web-sdk.js +68 -36
  5. package/package.json +5 -4
  6. package/src/assets/icon/circle.svg +1 -0
  7. package/src/assets/icon/triangle.svg +1 -0
  8. package/src/assets/wasm/h264-atomic.wasm +0 -0
  9. package/src/assets/wasm/h264-simd.wasm +0 -0
  10. package/src/components/RemotePlayer/index.vue +170 -0
  11. package/src/components/RemotePlayer/type.ts +11 -0
  12. package/src/composables/useCursorStyle.ts +15 -0
  13. package/src/composables/useKeyboardControl.ts +32 -0
  14. package/src/composables/useMouseTouchControl.ts +158 -0
  15. package/src/composables/useRemoteVideo.ts +248 -0
  16. package/src/composables/useResizeObserver.ts +27 -0
  17. package/src/core/WebRTCSdk.ts +561 -0
  18. package/src/core/data/MessageType.ts +70 -0
  19. package/src/core/data/TurnType.ts +25 -0
  20. package/src/core/data/WebRtcError.ts +93 -0
  21. package/src/core/data/WebrtcDataType.ts +354 -0
  22. package/src/core/groupctrl/GroupCtrlSocketManager.ts +94 -0
  23. package/src/core/groupctrl/SdkController.ts +96 -0
  24. package/src/core/rtc/WebRTCClient.ts +862 -0
  25. package/src/core/rtc/WebRTCConfig.ts +86 -0
  26. package/src/core/rtc/WebRtcNegotiate.ts +164 -0
  27. package/src/core/signal/SignalingClient.ts +221 -0
  28. package/src/core/util/FileTypeUtils.ts +75 -0
  29. package/src/core/util/KeyCodeUtil.ts +162 -0
  30. package/src/core/util/Logger.ts +83 -0
  31. package/src/core/util/MapCache.ts +135 -0
  32. package/src/core/util/ScreenControlUtil.ts +174 -0
  33. package/src/core/util/TurnTestUtil.ts +123 -0
  34. package/src/env.d.ts +30 -0
  35. package/src/index.ts +61 -0
  36. package/src/render/Canvas2DRenderer.ts +38 -0
  37. package/src/render/WebGLRenderer.ts +150 -0
  38. package/src/render/WebGPURenderer.ts +194 -0
  39. package/src/types/index.ts +15 -0
  40. package/src/types/webgpu.d.ts +1158 -0
  41. package/src/util/WasmUtil.ts +291 -0
  42. package/src/worker/worker.ts +292 -0
  43. package/lib/worker/worker.js +0 -12598
@@ -0,0 +1,83 @@
1
+ // sdk/logger.ts
2
+
3
+ export enum LogLevel {
4
+ DEBUG = 10,
5
+ INFO = 20,
6
+ WARN = 30,
7
+ ERROR = 40,
8
+ OFF = 100,
9
+ }
10
+
11
+ // 全局控制
12
+ let globalLogLevel: LogLevel = LogLevel.DEBUG;
13
+ let globalEnable = true;
14
+ let globalNamespace = "SDK"; // 默认命名空间
15
+
16
+ export function setLogLevel(level: LogLevel) {
17
+ globalLogLevel = level;
18
+ }
19
+
20
+ export function enableLog(enable: boolean) {
21
+ globalEnable = enable;
22
+ }
23
+
24
+ export function setNamespace(ns: string) {
25
+ globalNamespace = ns;
26
+ }
27
+
28
+ // 颜色配置
29
+ const levelStyle: Record<LogLevel, string> = {
30
+ [LogLevel.DEBUG]: "color: #999",
31
+ [LogLevel.INFO]: "color: #2b90d9",
32
+ [LogLevel.WARN]: "color: #e6a23c",
33
+ [LogLevel.ERROR]: "color: #f56c6c",
34
+ [LogLevel.OFF]: "color: inherit",
35
+ };
36
+
37
+ class LoggerCore {
38
+ private canLog(level: LogLevel) {
39
+ return globalEnable && level >= globalLogLevel;
40
+ }
41
+
42
+ private log(level: LogLevel, ...args: any[]) {
43
+ // console.log(LogLevel.DEBUG===level, !this.canLog(level), level, globalLogLevel)
44
+ if (!this.canLog(level)) return;
45
+ const levelName = LogLevel[level];
46
+ const prefix = `%c[${globalNamespace}] [${levelName}]`;
47
+ const style = levelStyle[level];
48
+
49
+ switch (level) {
50
+ case LogLevel.DEBUG:
51
+ console.log(prefix, style, ...args);
52
+ break;
53
+ case LogLevel.INFO:
54
+ console.info(prefix, style, ...args);
55
+ break;
56
+ case LogLevel.WARN:
57
+ console.warn(prefix, style, ...args);
58
+ break;
59
+ case LogLevel.ERROR:
60
+ console.error(prefix, style, ...args);
61
+ break;
62
+ }
63
+ }
64
+
65
+ debug(...args: any[]) {
66
+ this.log(LogLevel.DEBUG, ...args);
67
+ }
68
+
69
+ info(...args: any[]) {
70
+ this.log(LogLevel.INFO, ...args);
71
+ }
72
+
73
+ warn(...args: any[]) {
74
+ this.log(LogLevel.WARN, ...args);
75
+ }
76
+
77
+ error(...args: any[]) {
78
+ this.log(LogLevel.ERROR, ...args);
79
+ }
80
+ }
81
+
82
+ // 导出单例(不用实例化,直接用)
83
+ export const Logger = new LoggerCore();
@@ -0,0 +1,135 @@
1
+ export class MapCache {
2
+ private key: string;
3
+ private maxSize: number;
4
+ private defaultExpire: number; // 默认过期时间(毫秒)
5
+
6
+ constructor(key: string, maxSize = 100, expireMs = 5 * 60 * 1000) {
7
+ this.key = key;
8
+ this.maxSize = maxSize;
9
+ this.defaultExpire = expireMs;
10
+ }
11
+
12
+ /** 从 localStorage 读取 Map(自动清理过期的 item) */
13
+ getMap(): Map<string, any> {
14
+ const raw = localStorage.getItem(this.key);
15
+ if (!raw) return new Map();
16
+
17
+ try {
18
+ const data = JSON.parse(raw) as Record<string, { value: any; timestamp: number; expire: number }>;
19
+ const now = Date.now();
20
+ const result = new Map<string, any>();
21
+ let changed = false;
22
+
23
+ for (const [k, obj] of Object.entries(data)) {
24
+ if (now - obj.timestamp <= obj.expire) {
25
+ result.set(k, obj.value);
26
+ } else {
27
+ changed = true; // 有过期项,需要更新存储
28
+ }
29
+ }
30
+
31
+ if (changed) this.saveMap(result);
32
+
33
+ return result;
34
+ } catch {
35
+ return new Map();
36
+ }
37
+ }
38
+
39
+ /** 保存 Map(每个值都有独立的 timestamp/expire) */
40
+ private saveMap(map: Map<string, any>, meta?: Record<string, { timestamp: number; expire: number }>) {
41
+ const obj: Record<string, { value: any; timestamp: number; expire: number }> = {};
42
+ const now = Date.now();
43
+
44
+ for (const [k, v] of map.entries()) {
45
+ if (meta && meta[k]) {
46
+ obj[k] = { value: v, timestamp: meta[k].timestamp, expire: meta[k].expire };
47
+ } else {
48
+ obj[k] = { value: v, timestamp: now, expire: this.defaultExpire };
49
+ }
50
+ }
51
+ localStorage.setItem(this.key, JSON.stringify(obj));
52
+ }
53
+
54
+ /** 设置值(支持单项自定义过期时间) */
55
+ set(key: string, value: any, expireMs?: number) {
56
+ const map = this.getMap();
57
+
58
+ // 取出旧的 meta 信息(避免覆盖其它项的 expire)
59
+ const raw = localStorage.getItem(this.key);
60
+ let meta: Record<string, { timestamp: number; expire: number }> = {};
61
+ if (raw) {
62
+ try {
63
+ meta = JSON.parse(raw);
64
+ } catch {
65
+ meta = {};
66
+ }
67
+ }
68
+
69
+ // 超出限制时删除最早的
70
+ if (map.size >= this.maxSize && !map.has(key)) {
71
+ const firstKey = map.keys().next().value;
72
+ if (typeof firstKey === "string") {
73
+ map.delete(firstKey);
74
+ delete meta[firstKey];
75
+ }
76
+ }
77
+
78
+ map.set(key, value);
79
+ meta[key] = { timestamp: Date.now(), expire: expireMs ?? this.defaultExpire };
80
+ this.saveMap(map, meta);
81
+ }
82
+
83
+ /** 获取值(单项过期会自动清除) */
84
+ get(key: string) {
85
+ const map = this.getMap();
86
+ return map.get(key);
87
+ }
88
+
89
+ /** 检查是否存在且未过期 */
90
+ has(key: string): boolean {
91
+ const raw = localStorage.getItem(this.key);
92
+ if (!raw) return false;
93
+
94
+ try {
95
+ const data = JSON.parse(raw) as Record<string, { value: any; timestamp: number; expire: number }>;
96
+ const obj = data[key];
97
+ if (!obj) return false;
98
+
99
+ const now = Date.now();
100
+ if (now - obj.timestamp <= obj.expire) {
101
+ return true;
102
+ } else {
103
+ // 过期则清除
104
+ this.delete(key);
105
+ return false;
106
+ }
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /** 删除 */
113
+ delete(key: string) {
114
+ const map = this.getMap();
115
+ map.delete(key);
116
+
117
+ const raw = localStorage.getItem(this.key);
118
+ if (raw) {
119
+ try {
120
+ const meta = JSON.parse(raw);
121
+ delete meta[key];
122
+ this.saveMap(map, meta);
123
+ } catch {
124
+ this.saveMap(map);
125
+ }
126
+ } else {
127
+ this.saveMap(map);
128
+ }
129
+ }
130
+
131
+ /** 清空 */
132
+ clear() {
133
+ localStorage.removeItem(this.key);
134
+ }
135
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 根据视图和云端流的尺寸、角度及输入坐标,转换为相应的坐标值
3
+ * @param viewWidth 视图宽度
4
+ * @param viewHeight 视图高度
5
+ * @param cloudWidth 云端流宽度
6
+ * @param cloudHeight 云端流高度
7
+ * @param viewAngle 视图旋转角度(0 或 -90)
8
+ * @param streamAngle 流旋转角度(0 或 -90)
9
+ * @param inputX 输入 X 坐标
10
+ * @param inputY 输入 Y 坐标
11
+ * @returns 转换后的坐标 [x, y],若超出有效区域则返回 null
12
+ */
13
+ export function transformCoordinate(
14
+ viewWidth: number,
15
+ viewHeight: number,
16
+ cloudWidth: number,
17
+ cloudHeight: number,
18
+ viewAngle: number,
19
+ streamAngle: number,
20
+ inputX: number,
21
+ inputY: number,
22
+ ): [number, number] | null {
23
+ let realShortWidth: number
24
+ let short: number
25
+
26
+ // 根据视图旋转角度判断短边及计算实际短边宽度
27
+ if (viewAngle === -90) {
28
+ short = viewHeight
29
+ realShortWidth = (cloudWidth * viewWidth) / cloudHeight
30
+ }
31
+ else {
32
+ short = viewWidth
33
+ realShortWidth = (cloudWidth * viewHeight) / cloudHeight
34
+ }
35
+
36
+ // 计算无效区域(边缘空白区域)及中间有效区域参数
37
+ const invalidValue = (short - realShortWidth) / 2
38
+ const mid = short / 2
39
+ const start = invalidValue
40
+ const end = short - invalidValue
41
+ const len = mid - invalidValue
42
+
43
+ if (viewAngle === -90) {
44
+ // 视图横屏
45
+ const resultY = linearTransform(invalidValue, mid, start, end, len, inputY)
46
+ if (streamAngle === -90) {
47
+ // 流横屏:X 坐标不变,Y 坐标经过线性转换
48
+ return [inputX, resultY]
49
+ }
50
+ else {
51
+ // 流竖屏:交换坐标并调整 Y 坐标
52
+ return [viewHeight - resultY, inputX]
53
+ }
54
+ }
55
+ else {
56
+ // 视图竖屏
57
+ const resultX = linearTransform(invalidValue, mid, start, end, len, inputX)
58
+ if (streamAngle === -90) {
59
+ // 流横屏:需要额外旋转坐标
60
+ const [rotX, rotY] = rotatePoint90(viewWidth, viewHeight, resultX, inputY)
61
+ return [rotX, rotY]
62
+ }
63
+ else {
64
+ // 流竖屏:X 坐标经过线性转换,Y 坐标不变
65
+ return [resultX, inputY]
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 将输入坐标转换为百分比值(相对于视图尺寸),并考虑云端流的宽高比例
72
+ * @param viewWidth 视图宽度
73
+ * @param viewHeight 视图高度
74
+ * @param cloudWidth 云端流宽度
75
+ * @param cloudHeight 云端流高度
76
+ * @param viewAngle 视图旋转角度(0 或 -90)
77
+ * @param streamAngle 流旋转角度(0 或 -90)
78
+ * @param inputX 输入 X 坐标
79
+ * @param inputY 输入 Y 坐标
80
+ * @returns [xRatio, yRatio] 百分比比例值
81
+ */
82
+ export function valueToPercentage(
83
+ viewWidth: number,
84
+ viewHeight: number,
85
+ cloudWidth: number,
86
+ cloudHeight: number,
87
+ viewAngle: number,
88
+ streamAngle: number,
89
+ inputX: number,
90
+ inputY: number,
91
+ ): [number, number] {
92
+ let xRatio: number
93
+ let yRatio: number
94
+
95
+ if (viewAngle === 0 && streamAngle === 0) {
96
+ // 容器竖屏, 流竖屏
97
+ xRatio = inputX / viewWidth
98
+ yRatio = inputY / viewHeight
99
+ }
100
+ else if (viewAngle === -90 && streamAngle === 0) {
101
+ // 容器横屏, 流竖屏
102
+ xRatio = inputX / viewHeight
103
+ yRatio = inputY / viewWidth
104
+ }
105
+ else if (viewAngle === -90 && streamAngle === -90) {
106
+ // 容器横屏, 流横屏
107
+ xRatio = (inputX / viewWidth) * (cloudHeight / cloudWidth)
108
+ yRatio = (inputY / viewHeight) * (cloudWidth / cloudHeight)
109
+ }
110
+ else {
111
+ // 容器竖屏, 流横屏
112
+ xRatio = (inputX / viewHeight) * (cloudHeight / cloudWidth)
113
+ yRatio = (inputY / viewWidth) * (cloudWidth / cloudHeight)
114
+ }
115
+
116
+ return [xRatio, yRatio]
117
+ }
118
+
119
+ /**
120
+ * 对输入坐标做线性转换
121
+ * @param invalidValue 无效区域大小(边缘空白区域)
122
+ * @param mid 中间有效区域的中点
123
+ * @param start 有效区域开始位置
124
+ * @param end 有效区域结束位置
125
+ * @param len 有效区域长度的一半
126
+ * @param input 输入坐标
127
+ * @returns 转换后的坐标值
128
+ */
129
+ function linearTransform(
130
+ invalidValue: number,
131
+ mid: number,
132
+ start: number,
133
+ end: number,
134
+ len: number,
135
+ input: number,
136
+ ): number {
137
+ if (input >= start && input <= mid - 0.1) {
138
+ return ((input - invalidValue) * mid) / len
139
+ }
140
+ else if (input === mid) {
141
+ return mid
142
+ }
143
+ else if (input >= mid + 0.1 && input <= end) {
144
+ return mid + ((input - mid) * mid) / len
145
+ }
146
+ else {
147
+ return input
148
+ }
149
+ }
150
+
151
+ /**
152
+ * 将坐标进行 90 度旋转转换(以视图中心为原点)
153
+ * @param width 视图宽度
154
+ * @param height 视图高度
155
+ * @param inputX 输入 X 坐标
156
+ * @param inputY 输入 Y 坐标
157
+ * @returns 旋转后的坐标 [x, y]
158
+ */
159
+ function rotatePoint90(
160
+ width: number,
161
+ height: number,
162
+ inputX: number,
163
+ inputY: number,
164
+ ): [number, number] {
165
+ const offsetLongSide = height / 2
166
+ const offsetShortSide = width / 2
167
+ // 将输入点平移至以视图中心为原点
168
+ const x0 = inputX - offsetShortSide
169
+ const y0 = inputY - offsetLongSide
170
+ // 旋转 90 度后,计算新坐标
171
+ const xRotated = y0 + offsetLongSide
172
+ const yRotated = -x0 + offsetShortSide
173
+ return [xRotated, yRotated]
174
+ }
@@ -0,0 +1,123 @@
1
+ import type {
2
+ PublicTurnTestResult,
3
+ TurnSelectionResult,
4
+ TurnServerConfig,
5
+ TurnTestResult,
6
+ TurnTestSummary,
7
+ } from '../data/TurnType'
8
+ import type { WebRTCConfigOptions } from '../rtc/WebRTCConfig'
9
+ import {Logger} from "./Logger";
10
+
11
+ export const testTurnServer = (turnConfig: TurnServerConfig, timeoutMs = 600): Promise<TurnTestResult> => {
12
+ return new Promise((resolve) => {
13
+ const start = performance.now()
14
+ let resolved = false
15
+
16
+ const pc = new RTCPeerConnection({
17
+ iceServers: [turnConfig],
18
+ iceTransportPolicy: 'relay', // 强制走 TURN
19
+ })
20
+
21
+ // 创建一个 data channel,触发 ICE 流程
22
+ pc.createDataChannel('test')
23
+
24
+ pc.createOffer()
25
+ .then(offer => pc.setLocalDescription(offer))
26
+ .catch(() => {
27
+ if (!resolved) {
28
+ resolved = true
29
+ pc.close()
30
+ resolve({ ...turnConfig, rtt: Infinity })
31
+ }
32
+ })
33
+
34
+ pc.onicecandidate = (event) => {
35
+ if (resolved) return
36
+
37
+ if (event.candidate && event.candidate.candidate.includes('relay')) {
38
+ const rtt = Math.trunc(performance.now() - start)
39
+ resolved = true
40
+ pc.close()
41
+ resolve({ ...turnConfig, rtt })
42
+ }
43
+
44
+ if (event.candidate === null) {
45
+ // gathering 完成但没有 relay candidate
46
+ resolved = true
47
+ pc.close()
48
+ resolve({ ...turnConfig, rtt: Infinity })
49
+ }
50
+ }
51
+
52
+ setTimeout(() => {
53
+ if (!resolved) {
54
+ resolved = true
55
+ pc.close()
56
+ resolve({ ...turnConfig, rtt: Infinity })
57
+ }
58
+ }, timeoutMs)
59
+ })
60
+ }
61
+
62
+ export const testMultipleTurnServers = async (
63
+ servers: string[],
64
+ timeoutMs = 600,
65
+ ): Promise<TurnTestSummary> => {
66
+ const turnServers: TurnServerConfig[] = servers.map(url => ({
67
+ urls: url,
68
+ username: 'yangyj',
69
+ credential: 'hb@2025@168',
70
+ }))
71
+
72
+ // 发起所有测试请求
73
+ const testPromises = turnServers.map(cfg =>
74
+ testTurnServer(cfg, timeoutMs)
75
+ .then(res => res)
76
+ .catch((err) => {
77
+ Logger.warn('警告日志:',`中继计算超时=====>`, err)
78
+ return { ...cfg, rtt: Infinity } // 用 Infinity 表示失败
79
+ }),
80
+ )
81
+
82
+ // 等待所有测试完成
83
+ const results: TurnTestResult[] = await Promise.all(testPromises)
84
+ Logger.debug('调试日志:', `信令计算结果======>`, results)
85
+ const available = results.filter(r => r.rtt !== Infinity)
86
+
87
+ if (available.length === 0) {
88
+ // 全部测试失败,返回错误或抛出异常
89
+ throw new Error('All TURN servers are unreachable or slow (RTT = Infinity).')
90
+ // 或者 return 特定结构:
91
+ }
92
+
93
+ const best = available.sort((a, b) => a.rtt - b.rtt)[0]
94
+
95
+ return {
96
+ best: best ? stripTurnAuth(best) : undefined,
97
+ all: results.map(stripTurnAuth),
98
+ }
99
+ }
100
+
101
+ const stripTurnAuth = (result: TurnTestResult): PublicTurnTestResult => {
102
+ return {
103
+ urls: result.urls,
104
+ rtt: result.rtt,
105
+ }
106
+ }
107
+
108
+ export const areTurnListsEmpty = (options: WebRTCConfigOptions): boolean => {
109
+ return (
110
+ (!options.hostTurn || options.hostTurn.length === 0)
111
+ && (!options.spareTurn || options.spareTurn.length === 0)
112
+ )
113
+ }
114
+
115
+ export const selectBestTurnServer = async (turnList: string[]): Promise<TurnSelectionResult> => {
116
+ try {
117
+ const result = await testMultipleTurnServers(turnList)
118
+ return { url: result.best?.urls, rtt: result.best?.rtt }
119
+ }
120
+ catch (e) {
121
+ return { error: (e as Error).message || '未知错误' }
122
+ }
123
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<{}, {}, any>
4
+ export default component
5
+ }
6
+
7
+ declare module '*.ts?worker&inline' {
8
+ const workerConstructor: {
9
+ new (): Worker;
10
+ };
11
+ export default workerConstructor;
12
+ }
13
+
14
+ declare module '*.js?worker&inline' {
15
+ const workerConstructor: {
16
+ new (): Worker;
17
+ };
18
+ export default workerConstructor;
19
+ }
20
+
21
+
22
+ declare module "*.wasm?url" {
23
+ const value: string;
24
+ export default value;
25
+ }
26
+
27
+ declare module "*.wasm" {
28
+ const value: string;
29
+ export default value;
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { WebRTCSdk } from './core/WebRTCSdk'
2
+ import { getKeyEventData } from "./core/util/KeyCodeUtil";
3
+ import { testMultipleTurnServers } from './core/util/TurnTestUtil'
4
+ import {transformCoordinate, valueToPercentage} from "./core/util/ScreenControlUtil";
5
+ import {
6
+ ActionType,
7
+ ChannelDataType,
8
+ ContainerDirection,
9
+ InputData,
10
+ KeyEventData,
11
+ TouchData,
12
+ WheelData,
13
+ GestureData,
14
+ type ActionCommand,
15
+ ActionCommandType,
16
+ ActionCommandEventType,
17
+ ActionCommandEventValue,
18
+ TrackEventData,
19
+ ClarityData,
20
+ } from "./core/data/WebrtcDataType";
21
+ import { type TurnServerConfig, type TurnTestResult, type TurnTestSummary } from "./core/data/TurnType";
22
+ import type { WebRTCConfigOptions} from './core/rtc/WebRTCConfig'
23
+
24
+ import {EmitType, type CameraError, type WebRtcError } from "./core/data/WebRtcError";
25
+ import RemotePlayer from './components/RemotePlayer/index.vue'
26
+
27
+ import { ConnectorType } from './core/data/MessageType'
28
+ import { GroupCtrlSocketManager } from './core/groupctrl/GroupCtrlSocketManager'
29
+
30
+
31
+ export {
32
+ WebRTCSdk,
33
+ getKeyEventData,
34
+ transformCoordinate,
35
+ valueToPercentage,
36
+ ActionType,
37
+ ChannelDataType,
38
+ InputData,
39
+ KeyEventData,
40
+ TouchData,
41
+ ContainerDirection,
42
+ EmitType,
43
+ WheelData,
44
+ GestureData,
45
+ ClarityData,
46
+ type ActionCommand,
47
+ ActionCommandType,
48
+ type WebRTCConfigOptions,
49
+ ActionCommandEventType,
50
+ ActionCommandEventValue,
51
+ RemotePlayer,
52
+ TrackEventData,
53
+ type TurnServerConfig,
54
+ type TurnTestResult,
55
+ type TurnTestSummary,
56
+ testMultipleTurnServers,
57
+ type CameraError,
58
+ type WebRtcError,
59
+ ConnectorType,
60
+ GroupCtrlSocketManager
61
+ }
@@ -0,0 +1,38 @@
1
+ export class Canvas2DRenderer {
2
+ private canvas: HTMLCanvasElement | OffscreenCanvas;
3
+ private ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;
4
+
5
+ constructor(canvas: HTMLCanvasElement | OffscreenCanvas) {
6
+ this.canvas = canvas;
7
+ const ctx = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D | null;
8
+ if (!ctx) throw new Error("2D context not available");
9
+ this.ctx = ctx;
10
+ }
11
+
12
+ render(frame: VideoFrame): void {
13
+ this.canvas.width = frame.displayWidth;
14
+ this.canvas.height = frame.displayHeight;
15
+ this.ctx.drawImage(frame, 0, 0, frame.displayWidth, frame.displayHeight);
16
+ frame.close();
17
+ }
18
+
19
+ /** 清空画布 */
20
+ clear(): void {
21
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
22
+ }
23
+
24
+ /** 完全销毁 Renderer,释放引用和上下文 */
25
+ destroy(): void {
26
+ // 清空画布
27
+ try {
28
+ this.clear();
29
+ } catch {}
30
+
31
+ // 释放引用
32
+ // @ts-ignore
33
+ this.ctx = null;
34
+
35
+ // @ts-ignore
36
+ this.canvas = null;
37
+ }
38
+ }