@xmov/avatar 2.4.0 → 2.5.0-alpha.1

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 (157) hide show
  1. package/README.md +34 -6
  2. package/package-dist/agent/avatar.cjs +1 -0
  3. package/package-dist/agent/avatar.modern.js +1 -0
  4. package/package-dist/agent/avatar.module.js +1 -0
  5. package/package-dist/agent/avatar.umd.js +1 -0
  6. package/package-dist/agent/index.d.ts +42 -0
  7. package/{dist/agent/types.d.ts → package-dist/agent/public-types.d.ts} +115 -116
  8. package/package-dist/index.cjs +3 -0
  9. package/package-dist/index.d.ts +34 -0
  10. package/package-dist/index.modern.js +1 -0
  11. package/package-dist/index.module.js +1 -0
  12. package/package-dist/index.umd.js +14 -0
  13. package/package-dist/public-types.d.ts +205 -0
  14. package/package.json +41 -33
  15. package/dist/agent/avatar.cjs +0 -1
  16. package/dist/agent/avatar.modern.js +0 -1
  17. package/dist/agent/avatar.module.js +0 -1
  18. package/dist/agent/avatar.umd.js +0 -1
  19. package/dist/agent/index.d.ts +0 -137
  20. package/dist/baseRender/AudioRenderer.d.ts +0 -46
  21. package/dist/baseRender/AudioWorklet.d.ts +0 -78
  22. package/dist/baseRender/AvatarRenderer.d.ts +0 -225
  23. package/dist/baseRender/MSEAudioPlayer.d.ts +0 -51
  24. package/dist/baseRender/UIRenderer.d.ts +0 -39
  25. package/dist/baseRender/pcm-audio-processor.d.ts +0 -63
  26. package/dist/control/APIForwarder.d.ts +0 -23
  27. package/dist/control/DataCacheQueue.d.ts +0 -69
  28. package/dist/control/EventDispatcher.d.ts +0 -35
  29. package/dist/control/RenderScheduler.d.ts +0 -171
  30. package/dist/control/SaveAndDownload.d.ts +0 -14
  31. package/dist/control/StreamingClient.d.ts +0 -33
  32. package/dist/control/VideoPlayer.d.ts +0 -47
  33. package/dist/control/ttsa.d.ts +0 -125
  34. package/dist/encoding/media-recorder-encoder.d.ts +0 -33
  35. package/dist/encoding/pcm-webm-encoder.d.ts +0 -13
  36. package/dist/encoding/webcodec-opus-encoder.d.ts +0 -41
  37. package/dist/encoding/webm-muxer.d.ts +0 -90
  38. package/dist/index.cjs +0 -2
  39. package/dist/index.cjs.map +0 -1
  40. package/dist/index.d.ts +0 -251
  41. package/dist/index.modern.js +0 -2
  42. package/dist/index.modern.js.map +0 -1
  43. package/dist/index.module.js +0 -2
  44. package/dist/index.module.js.map +0 -1
  45. package/dist/index.umd.js +0 -2
  46. package/dist/index.umd.js.map +0 -1
  47. package/dist/modules/Composition.d.ts +0 -28
  48. package/dist/modules/ResourceManager.d.ts +0 -299
  49. package/dist/modules/TrackRenderer/base-track.d.ts +0 -4
  50. package/dist/modules/TrackRenderer/index.d.ts +0 -12
  51. package/dist/modules/TrackRenderer/render-implements.d.ts +0 -19
  52. package/dist/modules/TrackRenderer/track-pic.d.ts +0 -8
  53. package/dist/modules/TrackRenderer/track-subtitle.d.ts +0 -8
  54. package/dist/modules/cache-manager.d.ts +0 -5
  55. package/dist/modules/decoder.d.ts +0 -84
  56. package/dist/modules/error-handle.d.ts +0 -17
  57. package/dist/modules/network.d.ts +0 -21
  58. package/dist/modules/sentry-reporter.d.ts +0 -58
  59. package/dist/proto/face_data_pb.d.ts +0 -0
  60. package/dist/types/capability.d.ts +0 -15
  61. package/dist/types/error.d.ts +0 -70
  62. package/dist/types/event.d.ts +0 -25
  63. package/dist/types/frame-data.d.ts +0 -107
  64. package/dist/types/index.d.ts +0 -196
  65. package/dist/types/render.d.ts +0 -18
  66. package/dist/utils/DataInterface.d.ts +0 -113
  67. package/dist/utils/GLDevice.d.ts +0 -46
  68. package/dist/utils/GLPipeline.d.ts +0 -84
  69. package/dist/utils/GLPipelineDebugTools.d.ts +0 -5
  70. package/dist/utils/Math.d.ts +0 -21
  71. package/dist/utils/__tests__/capability-checker.test.d.ts +0 -1
  72. package/dist/utils/audio-session.d.ts +0 -11
  73. package/dist/utils/audio.d.ts +0 -1
  74. package/dist/utils/capability-checker.d.ts +0 -28
  75. package/dist/utils/encodeToken.d.ts +0 -14
  76. package/dist/utils/face.d.ts +0 -1
  77. package/dist/utils/float32-decoder.d.ts +0 -26
  78. package/dist/utils/fpsTracker.d.ts +0 -27
  79. package/dist/utils/index.d.ts +0 -22
  80. package/dist/utils/logger.d.ts +0 -9
  81. package/dist/utils/media-recorder-timestamp.d.ts +0 -15
  82. package/dist/utils/pcmAnalyzer.d.ts +0 -40
  83. package/dist/utils/perfermance.d.ts +0 -15
  84. package/dist/utils/request.d.ts +0 -20
  85. package/dist/utils/requestAnimateFrames.d.ts +0 -31
  86. package/dist/utils/time.d.ts +0 -9
  87. package/dist/utils/video-probe-asset.d.ts +0 -28
  88. package/dist/view/DebugOverlay.d.ts +0 -54
  89. package/dist/worker/streaming-video.d.ts +0 -1
  90. package/src/baseRender/AudioRenderer.ts +0 -328
  91. package/src/baseRender/AudioWorklet.ts +0 -776
  92. package/src/baseRender/AvatarRenderer.ts +0 -1550
  93. package/src/baseRender/MSEAudioPlayer.ts +0 -254
  94. package/src/baseRender/UIRenderer.ts +0 -195
  95. package/src/baseRender/pcm-audio-processor.js +0 -267
  96. package/src/control/APIForwarder.ts +0 -67
  97. package/src/control/DataCacheQueue.ts +0 -372
  98. package/src/control/EventDispatcher.ts +0 -106
  99. package/src/control/RenderScheduler.ts +0 -1000
  100. package/src/control/SaveAndDownload.js +0 -77
  101. package/src/control/StreamingClient.ts +0 -138
  102. package/src/control/VideoPlayer.ts +0 -306
  103. package/src/control/ttsa.ts +0 -754
  104. package/src/encoding/media-recorder-encoder.ts +0 -175
  105. package/src/encoding/pcm-webm-encoder.ts +0 -72
  106. package/src/encoding/webcodec-opus-encoder.ts +0 -258
  107. package/src/encoding/webm-muxer.ts +0 -337
  108. package/src/global.d.ts +0 -112
  109. package/src/index.ts +0 -2053
  110. package/src/modules/Composition.ts +0 -79
  111. package/src/modules/ResourceManager.ts +0 -926
  112. package/src/modules/TrackRenderer/base-track.ts +0 -7
  113. package/src/modules/TrackRenderer/index.ts +0 -40
  114. package/src/modules/TrackRenderer/render-implements.ts +0 -269
  115. package/src/modules/TrackRenderer/track-pic.ts +0 -19
  116. package/src/modules/TrackRenderer/track-subtitle.ts +0 -16
  117. package/src/modules/cache-manager.ts +0 -10
  118. package/src/modules/decoder.ts +0 -548
  119. package/src/modules/error-handle.ts +0 -20
  120. package/src/modules/network.ts +0 -71
  121. package/src/modules/sentry-reporter.ts +0 -171
  122. package/src/proto/face_data.proto +0 -45
  123. package/src/proto/face_data_pb.js +0 -1297
  124. package/src/proto/protobuf.min.js +0 -8
  125. package/src/types/capability.ts +0 -24
  126. package/src/types/error.ts +0 -211
  127. package/src/types/event.ts +0 -38
  128. package/src/types/frame-data.ts +0 -121
  129. package/src/types/index.ts +0 -220
  130. package/src/types/render.ts +0 -18
  131. package/src/utils/DataInterface.ts +0 -746
  132. package/src/utils/GLDevice.ts +0 -286
  133. package/src/utils/GLPipeline.ts +0 -1293
  134. package/src/utils/GLPipelineDebugTools.ts +0 -174
  135. package/src/utils/Math.ts +0 -266
  136. package/src/utils/__tests__/capability-checker.test.ts +0 -159
  137. package/src/utils/audio-session.ts +0 -144
  138. package/src/utils/audio.ts +0 -44
  139. package/src/utils/blueimp-md5.d.ts +0 -3
  140. package/src/utils/capability-checker.ts +0 -762
  141. package/src/utils/encodeToken.ts +0 -115
  142. package/src/utils/face.ts +0 -29
  143. package/src/utils/float32-decoder.js +0 -130
  144. package/src/utils/fpsTracker.ts +0 -100
  145. package/src/utils/index.ts +0 -182
  146. package/src/utils/logger.js +0 -36
  147. package/src/utils/media-recorder-timestamp.ts +0 -79
  148. package/src/utils/pcmAnalyzer.ts +0 -154
  149. package/src/utils/perfermance.ts +0 -141
  150. package/src/utils/request.ts +0 -107
  151. package/src/utils/requestAnimateFrames.ts +0 -195
  152. package/src/utils/time.ts +0 -32
  153. package/src/utils/video-probe-asset.ts +0 -99
  154. package/src/view/DebugOverlay.ts +0 -409
  155. package/src/worker/streaming-video.ts +0 -4647
  156. /package/{dist → package-dist}/agent/index.cjs +0 -0
  157. /package/{dist → package-dist}/agent/index.umd.js +0 -0
@@ -1,77 +0,0 @@
1
-
2
- class SaveAndDownload {
3
- constructor(fileName = "avatarData.json", enabled = false) {
4
- this.fileName = fileName;
5
- this.enabled = enabled; // 功能开关
6
- this.data = {}; // 存储JSON数据
7
- this.downloadButton = null;
8
- if (enabled) {
9
- this.initDownloadButton();
10
- }
11
- }
12
-
13
- // 初始化下载按钮
14
- initDownloadButton() {
15
- if (!this.enabled) return;
16
-
17
- this.downloadButton = document.createElement("a");
18
- this.downloadButton.style.position = "absolute";
19
- this.downloadButton.style.left = "0";
20
- this.downloadButton.style.bottom = "30px";
21
- this.downloadButton.style.background = "#4CAF50";
22
- this.downloadButton.style.color = "white";
23
- this.downloadButton.style.fontSize = "12px";
24
- this.downloadButton.style.padding = "10px";
25
- this.downloadButton.style.borderRadius = "4px";
26
- this.downloadButton.style.textDecoration = "none";
27
- this.downloadButton.style.zIndex = "1000";
28
- this.downloadButton.style.cursor = "pointer";
29
- this.downloadButton.innerHTML = "下载数据提交开发回放";
30
- this.downloadButton.addEventListener("click", () => {
31
- this.download();
32
- });
33
- document.body.appendChild(this.downloadButton);
34
- }
35
-
36
- // 写入多个字段
37
- writeFields(fields) {
38
- if (!this.enabled) return;
39
- Object.assign(this.data, fields);
40
- }
41
-
42
- // 追加多个项目到数组字段
43
- appendMultipleToArray(fieldName, items) {
44
- if (!this.enabled) return;
45
- if (!this.data[fieldName]) {
46
- this.data[fieldName] = [];
47
- }
48
- this.data[fieldName].push(...items);
49
- }
50
-
51
- // 下载文件
52
- download() {
53
- if (!this.enabled) return;
54
- const jsonContent = this.generateJSONContent();
55
- this.downloadFile(jsonContent, this.fileName, "application/json");
56
- }
57
-
58
- // 生成JSON文件内容
59
- generateJSONContent() {
60
- return JSON.stringify(this.data, null, 2);
61
- }
62
-
63
- // 下载文件的具体实现
64
- downloadFile(content, fileName, contentType) {
65
- if (!this.enabled) return;
66
- const blob = new Blob([content], { type: contentType });
67
- const url = URL.createObjectURL(blob);
68
-
69
- this.downloadButton.href = url;
70
- this.downloadButton.download = fileName;
71
- this.downloadButton.scrollIntoView({ behavior: "smooth", block: "center" });
72
-
73
- this.downloadButton.dataset.objectUrl = url;
74
- }
75
- }
76
-
77
- export default SaveAndDownload;
@@ -1,138 +0,0 @@
1
- /**
2
- * StreamingClient - 薄客户端模式下的 WebSocket 连接管理器
3
- * 负责与 Streaming Server 建立连接,管理消息路由、心跳和重连。
4
- */
5
-
6
- export interface StreamingClientOptions {
7
- url: string;
8
- onReady?: () => void;
9
- onOffer?: (sdp: string) => void;
10
- onIce?: (candidate: RTCIceCandidateInit) => void;
11
- onEventProxy?: (eventType: string, data: any, serverFrame?: number) => void;
12
- onError?: (error: string) => void;
13
- onDisconnect?: () => void;
14
- }
15
-
16
- export class StreamingClient {
17
- private ws: WebSocket | null = null;
18
- private options: StreamingClientOptions;
19
- private retryCount = 0;
20
- private maxRetry = 27; // 3轮 × 9次
21
- private retryTimer: number = -1;
22
- private destroyed = false;
23
- private connected = false;
24
-
25
- constructor(options: StreamingClientOptions) {
26
- this.options = options;
27
- }
28
-
29
- connect() {
30
- if (this.destroyed) return;
31
-
32
- try {
33
- this.ws = new WebSocket(this.options.url);
34
- } catch (e) {
35
- console.error('[StreamingClient] 创建 WebSocket 失败:', e);
36
- this.scheduleRetry();
37
- return;
38
- }
39
-
40
- this.ws.onopen = () => {
41
- this.connected = true;
42
- this.retryCount = 0;
43
- console.log('[StreamingClient] 已连接');
44
- // 以 client 角色加入
45
- this.send({ type: 'join', role: 'client' });
46
- };
47
-
48
- this.ws.onmessage = (e) => {
49
- let msg: any;
50
- try {
51
- msg = JSON.parse(e.data);
52
- } catch {
53
- return;
54
- }
55
- this.handleMessage(msg);
56
- };
57
-
58
- this.ws.onclose = () => {
59
- this.connected = false;
60
- if (!this.destroyed) {
61
- this.options.onDisconnect?.();
62
- this.scheduleRetry();
63
- }
64
- };
65
-
66
- this.ws.onerror = () => {
67
- this.options.onError?.('WebSocket 连接错误');
68
- };
69
- }
70
-
71
- private handleMessage(msg: any) {
72
- switch (msg.type) {
73
- case 'ready':
74
- this.options.onReady?.();
75
- break;
76
- case 'offer':
77
- this.options.onOffer?.(msg.sdp);
78
- break;
79
- case 'answer':
80
- // Client 不发送 offer,不会收到 answer
81
- break;
82
- case 'ice':
83
- this.options.onIce?.(msg.candidate);
84
- break;
85
- case 'event_proxy':
86
- this.options.onEventProxy?.(msg.event_type, msg.data, msg.server_frame);
87
- break;
88
- case 'error':
89
- this.options.onError?.(msg.message || '未知错误');
90
- break;
91
- }
92
- }
93
-
94
- send(msg: object) {
95
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
96
- this.ws.send(JSON.stringify(msg));
97
- }
98
- }
99
-
100
- /**
101
- * 发送 API 调用
102
- */
103
- sendApiCall(method: string, params: any[] = []) {
104
- this.send({ type: 'api_call', method, params });
105
- }
106
-
107
- private scheduleRetry() {
108
- if (this.destroyed) return;
109
- if (this.retryCount >= this.maxRetry) {
110
- console.warn('[StreamingClient] 重试次数已达上限');
111
- this.options.onError?.('连接重试次数已达上限');
112
- return;
113
- }
114
-
115
- const delay = this.retryCount === 0 ? 1000 : Math.pow(2, Math.min(this.retryCount, 6)) * 1000;
116
- this.retryCount++;
117
- this.retryTimer = window.setTimeout(() => {
118
- this.connect();
119
- }, delay);
120
- }
121
-
122
- isConnected() {
123
- return this.connected;
124
- }
125
-
126
- destroy() {
127
- this.destroyed = true;
128
- if (this.retryTimer !== -1) {
129
- clearTimeout(this.retryTimer);
130
- this.retryTimer = -1;
131
- }
132
- if (this.ws) {
133
- this.ws.close();
134
- this.ws = null;
135
- }
136
- this.connected = false;
137
- }
138
- }
@@ -1,306 +0,0 @@
1
- /**
2
- * VideoPlayer - 薄客户端模式下的 WebRTC 收流与播放管理
3
- * 支持两种模式:
4
- * - 普通模式:直接 <video> 播放
5
- * - Alpha Mask 模式:canvas 渲染 + 双路流 alpha 合成(透明背景)
6
- */
7
-
8
- import { StreamingClient } from './StreamingClient';
9
-
10
- export interface VideoPlayerOptions {
11
- container: HTMLElement;
12
- streamingClient: StreamingClient;
13
- useAlphaMask?: boolean; // 是否启用 alpha mask 透明背景
14
- onConnected?: () => void;
15
- onDisconnected?: () => void;
16
- }
17
-
18
- export class VideoPlayer {
19
- private video: HTMLVideoElement | null = null;
20
- private canvas: HTMLCanvasElement | null = null;
21
- private canvasCtx: CanvasRenderingContext2D | null = null;
22
- private pc: RTCPeerConnection | null = null;
23
- private stream: MediaStream | null = null;
24
- private options: VideoPlayerOptions;
25
- private destroyed = false;
26
- private animFrameId = 0;
27
- // alpha 处理用的临时 canvas
28
- private tempCanvas: HTMLCanvasElement | null = null;
29
- private tempCtx: CanvasRenderingContext2D | null = null;
30
-
31
- constructor(options: VideoPlayerOptions) {
32
- this.options = options;
33
- if (options.useAlphaMask) {
34
- this.createCanvasElement();
35
- } else {
36
- this.createVideoElement();
37
- }
38
- }
39
-
40
- private createVideoElement() {
41
- const video = document.createElement('video');
42
- video.autoplay = true;
43
- video.playsInline = true;
44
- video.muted = true; // 先静音,等音频轨到达后取消
45
- video.style.width = '100%';
46
- video.style.height = '100%';
47
- video.style.objectFit = 'contain';
48
- video.style.background = 'transparent';
49
- this.video = video;
50
- this.options.container.appendChild(video);
51
- }
52
-
53
- private createCanvasElement() {
54
- const canvas = document.createElement('canvas');
55
- canvas.style.width = '100%';
56
- canvas.style.height = '100%';
57
- canvas.style.objectFit = 'contain';
58
- canvas.style.background = 'transparent';
59
- this.canvas = canvas;
60
- this.canvasCtx = canvas.getContext('2d', { willReadFrequently: true });
61
-
62
- // 用于 alpha 合成的临时 canvas
63
- this.tempCanvas = document.createElement('canvas');
64
- this.tempCtx = this.tempCanvas.getContext('2d', { willReadFrequently: true });
65
-
66
- this.options.container.appendChild(canvas);
67
-
68
- // 创建隐藏的 video 元素用于接收 WebRTC 流
69
- // 注意:不能用 display:none,某些浏览器不解码。用绝对定位 + 1px 尺寸
70
- const video = document.createElement('video');
71
- video.autoplay = true;
72
- video.playsInline = true;
73
- video.muted = true;
74
- video.style.position = 'absolute';
75
- video.style.width = '1px';
76
- video.style.height = '1px';
77
- video.style.opacity = '0';
78
- video.style.pointerEvents = 'none';
79
- video.style.zIndex = '-1';
80
- this.video = video;
81
- this.options.container.appendChild(video);
82
- }
83
-
84
- /**
85
- * 处理远端 SDP offer,创建 answer 并通过 StreamingClient 回传
86
- */
87
- async handleOffer(sdp: string) {
88
- this.destroyPeer();
89
-
90
- this.pc = new RTCPeerConnection({
91
- iceServers: [
92
- { urls: 'stun:stun.l.google.com:19302' },
93
- { urls: 'stun:stun1.l.google.com:19302' },
94
- // Open Relay 免费 TURN 服务器(跨网络 NAT 穿透)
95
- { urls: 'turn:openrelay.metered.ca:80', username: 'openrelayproject', credential: 'openrelayproject' },
96
- { urls: 'turn:openrelay.metered.ca:443', username: 'openrelayproject', credential: 'openrelayproject' },
97
- { urls: 'turns:openrelay.metered.ca:443', username: 'openrelayproject', credential: 'openrelayproject' },
98
- ]
99
- });
100
-
101
- this.pc.onicecandidate = (e) => {
102
- if (e.candidate) {
103
- this.options.streamingClient.send({
104
- type: 'ice',
105
- candidate: e.candidate.toJSON(),
106
- });
107
- }
108
- };
109
-
110
- this.pc.ontrack = (e) => {
111
- console.log('[VideoPlayer] 收到轨道:', e.track.kind, e.track.id);
112
- if (!this.stream) {
113
- this.stream = new MediaStream();
114
- if (this.video) {
115
- this.video.srcObject = this.stream;
116
- }
117
- }
118
- this.stream.addTrack(e.track);
119
-
120
- // 音频轨到达后取消静音并尝试播放
121
- if (e.track.kind === 'audio' && this.video) {
122
- this.video.muted = false;
123
- this.video.play().catch(() => {});
124
- }
125
-
126
- // 视频轨到达后,如果启用 alpha mask,开始渲染循环
127
- if (e.track.kind === 'video' && this.options.useAlphaMask) {
128
- console.log('[VideoPlayer] Alpha Mask 模式:启动 canvas 渲染循环');
129
- this.startAlphaRendering();
130
- }
131
- };
132
-
133
- this.pc.onconnectionstatechange = () => {
134
- if (!this.pc) return;
135
- console.log('[VideoPlayer] WebRTC 状态:', this.pc.connectionState);
136
- if (this.pc.connectionState === 'connected') {
137
- this.options.onConnected?.();
138
- } else if (this.pc.connectionState === 'disconnected' || this.pc.connectionState === 'failed') {
139
- this.options.onDisconnected?.();
140
- }
141
- };
142
-
143
- await this.pc.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp }));
144
- const answer = await this.pc.createAnswer();
145
- await this.pc.setLocalDescription(answer);
146
-
147
- this.options.streamingClient.send({ type: 'answer', sdp: answer.sdp });
148
- }
149
-
150
- /**
151
- * Alpha Mask 渲染循环
152
- * 将双路流(RGB + Alpha)合成为带透明度的画面
153
- */
154
- private startAlphaRendering() {
155
- if (!this.video || !this.canvas || !this.canvasCtx || !this.tempCanvas || !this.tempCtx) return;
156
-
157
- const video = this.video;
158
- const canvas = this.canvas;
159
- const ctx = this.canvasCtx;
160
- const tempCanvas = this.tempCanvas;
161
- const tempCtx = this.tempCtx;
162
-
163
- let lastWidth = 0;
164
- let lastHeight = 0;
165
- let frameCount = 0;
166
- let debugInterval: ReturnType<typeof setInterval> | null = null;
167
-
168
- // 启动调试日志:每 2 秒打印一次状态
169
- debugInterval = setInterval(() => {
170
- if (this.destroyed) {
171
- if (debugInterval) clearInterval(debugInterval);
172
- return;
173
- }
174
- console.log(`[VideoPlayer] Alpha 渲染调试: frames=${frameCount}, videoReadyState=${video.readyState}, videoSize=${video.videoWidth}x${video.videoHeight}, currentTime=${video.currentTime.toFixed(2)}s, paused=${video.paused}`);
175
- }, 2000);
176
-
177
- const render = () => {
178
- if (this.destroyed) {
179
- if (debugInterval) clearInterval(debugInterval);
180
- return;
181
- }
182
-
183
- // 视频数据就绪
184
- if (video.readyState >= 2 && video.videoWidth > 0) {
185
- const vw = video.videoWidth;
186
- const vh = video.videoHeight;
187
-
188
- // 双路流:宽度是原始 2x,左半 RGB,右半 Alpha
189
- const realW = vw / 2;
190
- const realH = vh;
191
-
192
- // 尺寸变化时更新 canvas 大小
193
- if (vw !== lastWidth || vh !== lastHeight) {
194
- canvas.width = realW;
195
- canvas.height = realH;
196
- tempCanvas.width = vw; // 需要容纳完整视频帧(RGB + Alpha)
197
- tempCanvas.height = realH;
198
- lastWidth = vw;
199
- lastHeight = vh;
200
- console.log(`[VideoPlayer] Alpha Mask: 检测到双流 ${vw}x${vh} → 原始 ${realW}x${realH}`);
201
- }
202
-
203
- // 快照:先将整个视频帧绘制到临时 canvas,避免两次 drawImage(video) 之间视频切帧
204
- tempCtx.clearRect(0, 0, realW * 2, realH);
205
- tempCtx.drawImage(video, 0, 0);
206
-
207
- // 1. 从快照绘制左半 RGB 到输出 canvas
208
- ctx.clearRect(0, 0, realW, realH);
209
- ctx.drawImage(tempCanvas, 0, 0, realW, realH, 0, 0, realW, realH);
210
-
211
- // 2. 从同一快照读取右半 Alpha 灰度图
212
- // 3. 读取像素数据并合成 alpha
213
- try {
214
- const rgbData = ctx.getImageData(0, 0, realW, realH);
215
- const alphaData = tempCtx.getImageData(realW, 0, realW, realH);
216
- const rgb = rgbData.data;
217
- const alpha = alphaData.data;
218
-
219
- // 首帧调试:检查像素数据
220
- if (frameCount === 0) {
221
- let nonZeroRGB = 0;
222
- let nonZeroAlpha = 0;
223
- const sampleSize = Math.min(1000, rgb.length / 4);
224
- for (let i = 0; i < sampleSize * 4; i += 4) {
225
- if (rgb[i] > 0 || rgb[i+1] > 0 || rgb[i+2] > 0) nonZeroRGB++;
226
- if (alpha[i] > 0) nonZeroAlpha++;
227
- }
228
- console.log(`[VideoPlayer] 首帧像素检查: RGB非零=${nonZeroRGB}/${sampleSize}, Alpha非零=${nonZeroAlpha}/${sampleSize}`);
229
- if (nonZeroRGB === 0) {
230
- console.warn(`[VideoPlayer] 警告: RGB 全黑!视频可能没有内容`);
231
- }
232
- }
233
-
234
- // 将 alpha 灰度(R 通道)写入 RGB 的 alpha 通道
235
- for (let i = 0; i < rgb.length; i += 4) {
236
- rgb[i + 3] = alpha[i]; // alpha[i] = R 通道 = 灰度值 = 原始 alpha
237
- }
238
-
239
- ctx.putImageData(rgbData, 0, 0);
240
- frameCount++;
241
- } catch (e) {
242
- // getImageData/putImageData 可能因 tainted canvas 失败
243
- // 降级为直接绘制(无透明背景)
244
- console.warn(`[VideoPlayer] Alpha 合成失败,降级为直接绘制:`, e);
245
- ctx.drawImage(video, 0, 0, realW, realH, 0, 0, realW, realH);
246
- frameCount++;
247
- }
248
- }
249
-
250
- this.animFrameId = requestAnimationFrame(render);
251
- };
252
-
253
- this.animFrameId = requestAnimationFrame(render);
254
- }
255
-
256
- /**
257
- * 添加 ICE candidate
258
- */
259
- async addIceCandidate(candidate: RTCIceCandidateInit) {
260
- if (this.pc) {
261
- await this.pc.addIceCandidate(candidate).catch((e) => {
262
- console.warn('[VideoPlayer] addIceCandidate 失败:', e);
263
- });
264
- }
265
- }
266
-
267
- getVideoElement(): HTMLVideoElement | null {
268
- return this.video;
269
- }
270
-
271
- getCanvasElement(): HTMLCanvasElement | null {
272
- return this.canvas;
273
- }
274
-
275
- isConnected(): boolean {
276
- return this.pc?.connectionState === 'connected';
277
- }
278
-
279
- private destroyPeer() {
280
- if (this.pc) {
281
- this.pc.close();
282
- this.pc = null;
283
- }
284
- this.stream = null;
285
- if (this.animFrameId) {
286
- cancelAnimationFrame(this.animFrameId);
287
- this.animFrameId = 0;
288
- }
289
- }
290
-
291
- destroy() {
292
- this.destroyed = true;
293
- this.destroyPeer();
294
- if (this.video && this.video.parentNode) {
295
- this.video.parentNode.removeChild(this.video);
296
- }
297
- if (this.canvas && this.canvas.parentNode) {
298
- this.canvas.parentNode.removeChild(this.canvas);
299
- }
300
- this.video = null;
301
- this.canvas = null;
302
- this.canvasCtx = null;
303
- this.tempCanvas = null;
304
- this.tempCtx = null;
305
- }
306
- }