@xmov/avatar 2.0.0-alpha.38
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/README.md +139 -0
- package/dist/agent/__tests__/agent.test.d.ts +1 -0
- package/dist/agent/audio-debug.d.ts +3 -0
- package/dist/agent/audio-uplink.d.ts +62 -0
- package/dist/agent/avatar.cjs +2 -0
- package/dist/agent/avatar.cjs.map +1 -0
- package/dist/agent/avatar.modern.js +2 -0
- package/dist/agent/avatar.modern.js.map +1 -0
- package/dist/agent/avatar.module.js +2 -0
- package/dist/agent/avatar.module.js.map +1 -0
- package/dist/agent/avatar.umd.js +2 -0
- package/dist/agent/avatar.umd.js.map +1 -0
- package/dist/agent/e2e-client.d.ts +32 -0
- package/dist/agent/fixed-audio-track.d.ts +48 -0
- package/dist/agent/index.cjs +5 -0
- package/dist/agent/index.d.ts +104 -0
- package/dist/agent/index.umd.js +10 -0
- package/dist/agent/microphone.d.ts +49 -0
- package/dist/agent/types.d.ts +153 -0
- package/dist/baseRender/AudioRenderer.d.ts +46 -0
- package/dist/baseRender/AudioWorklet.d.ts +78 -0
- package/dist/baseRender/AvatarRenderer.d.ts +226 -0
- package/dist/baseRender/MSEAudioPlayer.d.ts +51 -0
- package/dist/baseRender/UIRenderer.d.ts +39 -0
- package/dist/baseRender/pcm-audio-processor.d.ts +63 -0
- package/dist/control/APIForwarder.d.ts +23 -0
- package/dist/control/DataCacheQueue.d.ts +69 -0
- package/dist/control/EventDispatcher.d.ts +35 -0
- package/dist/control/RenderScheduler.d.ts +162 -0
- package/dist/control/SaveAndDownload.d.ts +14 -0
- package/dist/control/StreamingClient.d.ts +33 -0
- package/dist/control/VideoPlayer.d.ts +47 -0
- package/dist/control/ttsa.d.ts +111 -0
- package/dist/encoding/media-recorder-encoder.d.ts +33 -0
- package/dist/encoding/pcm-webm-encoder.d.ts +13 -0
- package/dist/encoding/webcodec-opus-encoder.d.ts +41 -0
- package/dist/encoding/webm-muxer.d.ts +90 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +231 -0
- package/dist/index.modern.js +2 -0
- package/dist/index.modern.js.map +1 -0
- package/dist/index.module.js +2 -0
- package/dist/index.module.js.map +1 -0
- package/dist/index.umd.js +2 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/modules/Composition.d.ts +28 -0
- package/dist/modules/ResourceManager.d.ts +295 -0
- package/dist/modules/TrackRenderer/base-track.d.ts +4 -0
- package/dist/modules/TrackRenderer/index.d.ts +12 -0
- package/dist/modules/TrackRenderer/render-implements.d.ts +19 -0
- package/dist/modules/TrackRenderer/track-pic.d.ts +8 -0
- package/dist/modules/TrackRenderer/track-subtitle.d.ts +8 -0
- package/dist/modules/cache-manager.d.ts +5 -0
- package/dist/modules/decoder.d.ts +84 -0
- package/dist/modules/error-handle.d.ts +17 -0
- package/dist/modules/network.d.ts +21 -0
- package/dist/proto/face_data_pb.d.ts +0 -0
- package/dist/types/capability.d.ts +15 -0
- package/dist/types/error.d.ts +70 -0
- package/dist/types/event.d.ts +25 -0
- package/dist/types/frame-data.d.ts +107 -0
- package/dist/types/index.d.ts +187 -0
- package/dist/types/render.d.ts +18 -0
- package/dist/utils/DataInterface.d.ts +113 -0
- package/dist/utils/GLDevice.d.ts +46 -0
- package/dist/utils/GLPipeline.d.ts +84 -0
- package/dist/utils/GLPipelineDebugTools.d.ts +5 -0
- package/dist/utils/Math.d.ts +21 -0
- package/dist/utils/__tests__/capability-checker.test.d.ts +1 -0
- package/dist/utils/audio-session.d.ts +11 -0
- package/dist/utils/audio.d.ts +1 -0
- package/dist/utils/capability-checker.d.ts +23 -0
- package/dist/utils/encodeToken.d.ts +7 -0
- package/dist/utils/face.d.ts +1 -0
- package/dist/utils/float32-decoder.d.ts +26 -0
- package/dist/utils/fpsTracker.d.ts +27 -0
- package/dist/utils/index.d.ts +22 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/media-recorder-timestamp.d.ts +15 -0
- package/dist/utils/pcmAnalyzer.d.ts +40 -0
- package/dist/utils/perfermance.d.ts +15 -0
- package/dist/utils/request.d.ts +19 -0
- package/dist/utils/requestAnimateFrames.d.ts +31 -0
- package/dist/utils/time.d.ts +9 -0
- package/dist/view/DebugOverlay.d.ts +54 -0
- package/dist/worker/streaming-video.d.ts +1 -0
- package/package.json +82 -0
- package/src/agent/__tests__/agent.test.ts +3787 -0
- package/src/agent/audio-debug.ts +36 -0
- package/src/agent/audio-uplink.ts +392 -0
- package/src/agent/e2e-client.ts +215 -0
- package/src/agent/fixed-audio-track.ts +547 -0
- package/src/agent/index.ts +1393 -0
- package/src/agent/microphone.ts +534 -0
- package/src/agent/types.ts +197 -0
- package/src/baseRender/AudioRenderer.ts +317 -0
- package/src/baseRender/AudioWorklet.ts +776 -0
- package/src/baseRender/AvatarRenderer.ts +1559 -0
- package/src/baseRender/MSEAudioPlayer.ts +243 -0
- package/src/baseRender/UIRenderer.ts +195 -0
- package/src/baseRender/pcm-audio-processor.js +267 -0
- package/src/control/APIForwarder.ts +67 -0
- package/src/control/DataCacheQueue.ts +372 -0
- package/src/control/EventDispatcher.ts +106 -0
- package/src/control/RenderScheduler.ts +945 -0
- package/src/control/SaveAndDownload.js +77 -0
- package/src/control/StreamingClient.ts +138 -0
- package/src/control/VideoPlayer.ts +306 -0
- package/src/control/ttsa.ts +676 -0
- package/src/encoding/media-recorder-encoder.ts +175 -0
- package/src/encoding/pcm-webm-encoder.ts +72 -0
- package/src/encoding/webcodec-opus-encoder.ts +258 -0
- package/src/encoding/webm-muxer.ts +337 -0
- package/src/global.d.ts +105 -0
- package/src/index.ts +1920 -0
- package/src/modules/Composition.ts +79 -0
- package/src/modules/ResourceManager.ts +899 -0
- package/src/modules/TrackRenderer/base-track.ts +7 -0
- package/src/modules/TrackRenderer/index.ts +40 -0
- package/src/modules/TrackRenderer/render-implements.ts +269 -0
- package/src/modules/TrackRenderer/track-pic.ts +19 -0
- package/src/modules/TrackRenderer/track-subtitle.ts +16 -0
- package/src/modules/cache-manager.ts +10 -0
- package/src/modules/decoder.ts +515 -0
- package/src/modules/error-handle.ts +20 -0
- package/src/modules/network.ts +71 -0
- package/src/proto/face_data.proto +45 -0
- package/src/proto/face_data_pb.js +1297 -0
- package/src/proto/protobuf.min.js +8 -0
- package/src/types/capability.ts +23 -0
- package/src/types/error.ts +211 -0
- package/src/types/event.ts +38 -0
- package/src/types/frame-data.ts +121 -0
- package/src/types/index.ts +207 -0
- package/src/types/render.ts +18 -0
- package/src/utils/DataInterface.ts +746 -0
- package/src/utils/GLDevice.ts +286 -0
- package/src/utils/GLPipeline.ts +1293 -0
- package/src/utils/GLPipelineDebugTools.ts +174 -0
- package/src/utils/Math.ts +266 -0
- package/src/utils/__tests__/capability-checker.test.ts +159 -0
- package/src/utils/audio-session.ts +144 -0
- package/src/utils/audio.ts +44 -0
- package/src/utils/blueimp-md5.d.ts +3 -0
- package/src/utils/capability-checker.ts +379 -0
- package/src/utils/encodeToken.ts +100 -0
- package/src/utils/face.ts +29 -0
- package/src/utils/float32-decoder.js +130 -0
- package/src/utils/fpsTracker.ts +100 -0
- package/src/utils/index.ts +182 -0
- package/src/utils/logger.js +36 -0
- package/src/utils/media-recorder-timestamp.ts +79 -0
- package/src/utils/pcmAnalyzer.ts +154 -0
- package/src/utils/perfermance.ts +141 -0
- package/src/utils/request.ts +95 -0
- package/src/utils/requestAnimateFrames.ts +195 -0
- package/src/utils/time.ts +32 -0
- package/src/view/DebugOverlay.ts +404 -0
- package/src/worker/streaming-video.ts +4647 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 字符串 → Uint8Array 字节流(工具函数,处理 2 字节对齐)
|
|
3
|
+
* @param base64Str 服务端返回的 Base64 编码字符串(int16 字节流 Base64 后)
|
|
4
|
+
* @returns Uint8Array 字节流(2 字节/元素,对应 int16)
|
|
5
|
+
*/
|
|
6
|
+
function base64ToUint8Array(base64Str) {
|
|
7
|
+
if (!base64Str || base64Str.trim() === '') {
|
|
8
|
+
return new Uint8Array(0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const binaryStr = atob(base64Str);
|
|
12
|
+
const uint8Array = new Uint8Array(binaryStr.length);
|
|
13
|
+
for (let i = 0; i < binaryStr.length; i++) {
|
|
14
|
+
uint8Array[i] = binaryStr.charCodeAt(i);
|
|
15
|
+
}
|
|
16
|
+
return uint8Array;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 2 字节小端序 → int16 数值(兼容服务端 np.clip 范围:-32767~32767)
|
|
21
|
+
* @param byte0 小端序低位字节
|
|
22
|
+
* @param byte1 小端序高位字节
|
|
23
|
+
* @returns 有符号 int16 数值(严格限制在 -32767~32767,与服务端一致)
|
|
24
|
+
*/
|
|
25
|
+
function twoByteToInt16(byte0, byte1) {
|
|
26
|
+
// 小端序合并为无符号 16 位整数
|
|
27
|
+
const uint16 = (byte0 << 0) | (byte1 << 8);
|
|
28
|
+
// 无符号 → 有符号 int16(自动处理负数)
|
|
29
|
+
let int16Value = uint16 > 32767 ? uint16 - 65536 : uint16;
|
|
30
|
+
// 兼容服务端 np.clip:强制限制范围(防止异常数据超出)
|
|
31
|
+
return Math.max(-32767, Math.min(32767, int16Value));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 服务端 floats_to_scaled_int16_bytes 反向函数:Base64 → float32 数组
|
|
36
|
+
* 核心逻辑:Base64→int16字节流→int16数值(兼容round+clip)→÷0x7FFF→float32
|
|
37
|
+
* @param base64Str 服务端返回的 Base64 编码字符串
|
|
38
|
+
* @returns float32 精度的浮点数数组(与服务端原始 floats 一致)
|
|
39
|
+
*/
|
|
40
|
+
export function scaledInt16BytesToFloat32(base64Str) {
|
|
41
|
+
// 步骤1:Base64 → Uint8Array 字节流
|
|
42
|
+
const uint8Array = base64ToUint8Array(base64Str);
|
|
43
|
+
|
|
44
|
+
// 校验:int16 占 2 字节,必须 2 字节对齐
|
|
45
|
+
if (uint8Array.length === 0 || uint8Array.length % 2 !== 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SCALE_FACTOR = 0x7FFF; // 与服务端一致:32767
|
|
50
|
+
const length = uint8Array.length / 2; // 计算 int16 元素个数
|
|
51
|
+
|
|
52
|
+
// 批量处理:用 Float32Array 确保 float32 精度(与服务端输入对齐)
|
|
53
|
+
const buffer = new ArrayBuffer(4 * length);
|
|
54
|
+
const float32View = new Float32Array(buffer);
|
|
55
|
+
|
|
56
|
+
// 步骤2:遍历解析+反量化
|
|
57
|
+
for (let i = 0; i < length; i++) {
|
|
58
|
+
const byteIndex = i * 2;
|
|
59
|
+
const byte0 = uint8Array[byteIndex]; // 小端序低位字节
|
|
60
|
+
const byte1 = uint8Array[byteIndex + 1]; // 小端序高位字节
|
|
61
|
+
|
|
62
|
+
// 步骤2.1:解析 int16(兼容服务端 round+clip)
|
|
63
|
+
const int16Value = twoByteToInt16(byte0, byte1);
|
|
64
|
+
|
|
65
|
+
// 步骤2.2:反量化(除以缩放因子,还原原始浮点数)
|
|
66
|
+
const floatValue = int16Value / SCALE_FACTOR;
|
|
67
|
+
|
|
68
|
+
// 步骤2.3:强制 float32 精度(消除 JS float64 多余精度)
|
|
69
|
+
float32View[i] = floatValue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 转为普通数组返回(方便业务使用)
|
|
73
|
+
return Array.from(float32View);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 可选:带平滑滤波的版本(解决“脸色闪”问题,优化视觉效果)
|
|
78
|
+
* @param base64Str 服务端 Base64 字符串
|
|
79
|
+
* @param lastData 上一帧数据(用于平滑)
|
|
80
|
+
* @param alpha 平滑系数(0~1,默认0.2,越大响应越快,越小越平滑)
|
|
81
|
+
* @returns 平滑后的 float32 数组
|
|
82
|
+
*/
|
|
83
|
+
export function scaledInt16BytesToSmoothFloat32(base64Str, lastData = [], alpha = 0.2) {
|
|
84
|
+
const rawData = scaledInt16BytesToFloat32(base64Str);
|
|
85
|
+
// 首次无历史数据,直接返回原始数据
|
|
86
|
+
if (lastData.length !== rawData.length) return rawData;
|
|
87
|
+
|
|
88
|
+
// 一阶低通滤波:current = alpha*raw + (1-alpha)*last
|
|
89
|
+
return rawData.map((val, idx) => {
|
|
90
|
+
return alpha * val + (1 - alpha) * lastData[idx];
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 解析后端传输的 float32 字节流为普通数组
|
|
97
|
+
* @param {string|ArrayBuffer|Uint8Array} bytes - 后端传来的二进制数据(注意格式兼容)
|
|
98
|
+
* @returns {number[]} 解析后的浮点数组(空数据返回空数组)
|
|
99
|
+
*/
|
|
100
|
+
/**
|
|
101
|
+
* 从 Uint8Array 解析出 float32 数组(核心修复版)
|
|
102
|
+
* @param {Uint8Array} uint8Arr - 后端传来的 Uint8Array 原始字节
|
|
103
|
+
* @returns {number[]} 解析后的浮点数组
|
|
104
|
+
*/
|
|
105
|
+
export function parseUint8ToFloat32(uint8Arr) {
|
|
106
|
+
// 空值处理
|
|
107
|
+
if (!uint8Arr || uint8Arr.length === 0) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
// 步骤1:校验总字节数是否为 4 的倍数(float32 每个元素占 4 字节)
|
|
113
|
+
if (uint8Arr.length % 4 !== 0) {
|
|
114
|
+
window.avatarSDKLogger.error('Uint8Array长度不是4的倍数,无法解析为float32数组');
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 步骤2:创建新的 ArrayBuffer 并拷贝字节(强制偏移量为 0)
|
|
119
|
+
// 核心:不管原偏移量是多少,拷贝后新数组偏移量=0,必然满足 4 的倍数
|
|
120
|
+
const alignedBuffer = new ArrayBuffer(uint8Arr.length);
|
|
121
|
+
const alignedUint8 = new Uint8Array(alignedBuffer);
|
|
122
|
+
alignedUint8.set(uint8Arr); // 把原始字节拷贝到新数组
|
|
123
|
+
|
|
124
|
+
// 步骤3:基于对齐的 ArrayBuffer 创建 Float32Array
|
|
125
|
+
const float32Arr = new Float32Array(alignedBuffer);
|
|
126
|
+
|
|
127
|
+
// 步骤4:转普通数组返回
|
|
128
|
+
const result = Array.from(float32Arr);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export interface FPSStats {
|
|
2
|
+
current: number;
|
|
3
|
+
average: number;
|
|
4
|
+
min: number;
|
|
5
|
+
max: number;
|
|
6
|
+
droppedFrames: number;
|
|
7
|
+
totalFrames: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class FPSTracker {
|
|
11
|
+
private frameTimestamps: number[] = [];
|
|
12
|
+
private maxSamples: number;
|
|
13
|
+
private lastFrameTime: number = 0;
|
|
14
|
+
private frameCount: number = 0;
|
|
15
|
+
private lastUpdateTime: number = 0;
|
|
16
|
+
private currentFPS: number = 0;
|
|
17
|
+
private totalFrames: number = 0;
|
|
18
|
+
private totalTime: number = 0;
|
|
19
|
+
private droppedFrames: number = 0;
|
|
20
|
+
private totalRenderedFrames: number = 0;
|
|
21
|
+
|
|
22
|
+
constructor(maxSamples: number = 60) {
|
|
23
|
+
this.maxSamples = maxSamples;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
tick(rendered: boolean = true): void {
|
|
27
|
+
const now = performance.now();
|
|
28
|
+
|
|
29
|
+
if (this.lastFrameTime > 0) {
|
|
30
|
+
const delta = now - this.lastFrameTime;
|
|
31
|
+
this.frameTimestamps.push(delta);
|
|
32
|
+
|
|
33
|
+
if (this.frameTimestamps.length > this.maxSamples) {
|
|
34
|
+
this.frameTimestamps.shift();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.totalFrames++;
|
|
38
|
+
this.totalTime += delta;
|
|
39
|
+
|
|
40
|
+
if (rendered) {
|
|
41
|
+
this.frameCount++;
|
|
42
|
+
this.totalRenderedFrames++;
|
|
43
|
+
} else {
|
|
44
|
+
this.droppedFrames++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (now - this.lastUpdateTime >= 1000) {
|
|
48
|
+
this.currentFPS = Math.round(this.frameCount * 1000 / (now - this.lastUpdateTime));
|
|
49
|
+
this.frameCount = 0;
|
|
50
|
+
this.lastUpdateTime = now;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.lastFrameTime = now;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getStats(): FPSStats {
|
|
58
|
+
if (this.frameTimestamps.length === 0) {
|
|
59
|
+
return { current: 0, average: 0, min: 0, max: 0, droppedFrames: 0, totalFrames: 0 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const avgDelta = this.frameTimestamps.reduce((a, b) => a + b, 0) / this.frameTimestamps.length;
|
|
63
|
+
const minDelta = Math.min(...this.frameTimestamps);
|
|
64
|
+
const maxDelta = Math.max(...this.frameTimestamps);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
current: this.currentFPS,
|
|
68
|
+
average: this.frameTimestamps.length > 0 ? Math.round(1000 / avgDelta) : 0,
|
|
69
|
+
min: minDelta > 0 ? Math.round(1000 / maxDelta) : 0,
|
|
70
|
+
max: maxDelta > 0 ? Math.round(1000 / minDelta) : 0,
|
|
71
|
+
droppedFrames: this.droppedFrames,
|
|
72
|
+
totalFrames: this.totalFrames,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getCurrentFPS(): number {
|
|
77
|
+
return this.currentFPS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getAverageFPS(): number {
|
|
81
|
+
if (this.totalTime === 0) return 0;
|
|
82
|
+
return Math.round(this.totalFrames * 1000 / this.totalTime);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
reset(): void {
|
|
86
|
+
this.frameTimestamps = [];
|
|
87
|
+
this.lastFrameTime = 0;
|
|
88
|
+
this.frameCount = 0;
|
|
89
|
+
this.lastUpdateTime = 0;
|
|
90
|
+
this.currentFPS = 0;
|
|
91
|
+
this.totalFrames = 0;
|
|
92
|
+
this.totalTime = 0;
|
|
93
|
+
this.droppedFrames = 0;
|
|
94
|
+
this.totalRenderedFrames = 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getDroppedFrames(): number {
|
|
98
|
+
return this.droppedFrames;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
const DefineENV = ENV
|
|
3
|
+
export function isDEV() {
|
|
4
|
+
return DefineENV === "development";
|
|
5
|
+
}
|
|
6
|
+
export function isSupportMES() {
|
|
7
|
+
const ua = navigator.userAgent;
|
|
8
|
+
const vendor = navigator.vendor;
|
|
9
|
+
|
|
10
|
+
// 检查 userAgent 中是否包含 Chrome
|
|
11
|
+
const hasChrome = /Chrome/.test(ua);
|
|
12
|
+
|
|
13
|
+
// 检查 vendor 是否为 Google Inc.
|
|
14
|
+
const isGoogleVendor = /Google Inc/.test(vendor);
|
|
15
|
+
|
|
16
|
+
// Edge(基于 Chromium 的新版 Edge 也包含 Chrome 标识)
|
|
17
|
+
const isEdge = /Edg/.test(ua);
|
|
18
|
+
|
|
19
|
+
// MediaSource 支持性检查(可选)
|
|
20
|
+
const hasMediaSource = typeof (window as any).MediaSource !== "undefined";
|
|
21
|
+
|
|
22
|
+
return hasChrome && (isGoogleVendor || isEdge) && hasMediaSource;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getStyleStr(styles: Record<string, any>) {
|
|
26
|
+
return Object.entries(styles).map(([key, value]) => {
|
|
27
|
+
// 处理zIndex等数字属性转字符串
|
|
28
|
+
const val = typeof value === 'number' ? value.toString() : value;
|
|
29
|
+
// 驼峰转短横线(如zIndex → z-index)
|
|
30
|
+
const cssKey = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
31
|
+
return `${cssKey}:${val};`;
|
|
32
|
+
}).join('');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function updateCanvasXOffset(canvas, offset_PX) {
|
|
36
|
+
// 1. 获取当前 transform 样式
|
|
37
|
+
const currentTransform = canvas.style.transform || '';
|
|
38
|
+
|
|
39
|
+
// 2. 正则匹配现有 translateX 的值(提取数字)
|
|
40
|
+
const translateXRegex = /translateX\((-?\d+(\.\d+)?)px\)/;
|
|
41
|
+
|
|
42
|
+
// 3. 计算新的偏移量(初始为0,匹配到则取原有值)
|
|
43
|
+
|
|
44
|
+
// 4. 移除原有 translateX,拼接新值(保留其他 transform 效果)
|
|
45
|
+
const newTransform = currentTransform
|
|
46
|
+
.replace(translateXRegex, '') // 移除旧的 translateX
|
|
47
|
+
.trim() + ` translateX(${offset_PX}px)`; // 添加新的 translateX
|
|
48
|
+
|
|
49
|
+
// 5. 应用新样式(去除多余空格)
|
|
50
|
+
canvas.style.transform = newTransform.replace(/\s+/g, ' ').trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 生成设备指纹
|
|
54
|
+
export function generateFingerprint() {
|
|
55
|
+
const screenSize = `${screen.width}x${screen.height}`;
|
|
56
|
+
const userAgent = navigator.userAgent;
|
|
57
|
+
const language = navigator.language;
|
|
58
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
59
|
+
const fingerprint = encodeURIComponent(`${screenSize}|${userAgent}|${language}|${timezone}`);
|
|
60
|
+
return btoa(fingerprint);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 获取设备品牌及型号及版本号
|
|
64
|
+
export function getDeviceInfo() {
|
|
65
|
+
const ua = navigator.userAgent;
|
|
66
|
+
let deviceBrand = 'Unknown Brand';
|
|
67
|
+
let deviceModel = 'Unknown Model';
|
|
68
|
+
let deviceVersion = 'Unknown Version';
|
|
69
|
+
let browserType = 'Unknown Browser';
|
|
70
|
+
let browserVersion = 'Unknown Version';
|
|
71
|
+
|
|
72
|
+
// 获取操作系统版本
|
|
73
|
+
const windowsMatch = ua.match(/Windows NT (\d+\.\d+)/);
|
|
74
|
+
const androidMatch = ua.match(/Android (\d+\.\d+(\.\d+)?)/);
|
|
75
|
+
const iosMatch = ua.match(/OS (\d+_\d+(\._\d+)?) like Mac OS X/);
|
|
76
|
+
|
|
77
|
+
if (windowsMatch) {
|
|
78
|
+
deviceVersion = `Windows NT ${windowsMatch[1]}`;
|
|
79
|
+
} else if (androidMatch) {
|
|
80
|
+
deviceVersion = `Android ${androidMatch[1]}`;
|
|
81
|
+
} else if (iosMatch) {
|
|
82
|
+
deviceVersion = `iOS ${iosMatch[1].replace(/_/g, '.')}`;
|
|
83
|
+
} else if (ua.includes('Macintosh')) {
|
|
84
|
+
deviceVersion = 'Macintosh';
|
|
85
|
+
} else if (ua.includes('Linux')) {
|
|
86
|
+
deviceVersion = 'Linux';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 匹配品牌 - 更准确的匹配逻辑
|
|
90
|
+
// 首先检查苹果设备
|
|
91
|
+
if (ua.includes('iPhone') || ua.includes('iPad') || ua.includes('iPod')) {
|
|
92
|
+
deviceBrand = 'Apple';
|
|
93
|
+
if (ua.includes('iPhone')) deviceModel = 'iPhone';
|
|
94
|
+
else if (ua.includes('iPad')) deviceModel = 'iPad';
|
|
95
|
+
else if (ua.includes('iPod')) deviceModel = 'iPod';
|
|
96
|
+
}
|
|
97
|
+
// 检查 Android 设备
|
|
98
|
+
else if (ua.includes('Android')) {
|
|
99
|
+
deviceBrand = 'Android';
|
|
100
|
+
deviceModel = 'Android';
|
|
101
|
+
}
|
|
102
|
+
// 检查 Windows 设备
|
|
103
|
+
else if (ua.includes('Windows')) {
|
|
104
|
+
deviceBrand = 'Windows';
|
|
105
|
+
if (ua.includes('Windows Phone')) deviceModel = 'Windows Phone';
|
|
106
|
+
else deviceModel = 'Windows Desktop';
|
|
107
|
+
}
|
|
108
|
+
// 检查 Linux 设备
|
|
109
|
+
else if (ua.includes('Linux')) {
|
|
110
|
+
deviceBrand = 'Linux';
|
|
111
|
+
deviceModel = 'Linux';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 匹配浏览器类型和版本
|
|
115
|
+
const chromeMatch = ua.match(/Chrome\/(\d+\.\d+\.\d+\.\d+)/);
|
|
116
|
+
const safariMatch = ua.match(/Version\/(\d+\.\d+(\.\d+)?) Safari/);
|
|
117
|
+
const firefoxMatch = ua.match(/Firefox\/(\d+\.\d+(\.\d+)?)\b/);
|
|
118
|
+
const edgeMatch = ua.match(/Edg\/(\d+\.\d+\.\d+\.\d+)/);
|
|
119
|
+
const operaMatch = ua.match(/Opera\/(\d+\.\d+)/);
|
|
120
|
+
const ieMatch = ua.match(/MSIE (\d+\.\d+)/);
|
|
121
|
+
|
|
122
|
+
if (chromeMatch) {
|
|
123
|
+
browserType = 'Chrome';
|
|
124
|
+
browserVersion = chromeMatch[1];
|
|
125
|
+
} else if (safariMatch) {
|
|
126
|
+
browserType = 'Safari';
|
|
127
|
+
browserVersion = safariMatch[1];
|
|
128
|
+
} else if (firefoxMatch) {
|
|
129
|
+
browserType = 'Firefox';
|
|
130
|
+
browserVersion = firefoxMatch[1];
|
|
131
|
+
} else if (edgeMatch) {
|
|
132
|
+
browserType = 'Edge';
|
|
133
|
+
browserVersion = edgeMatch[1];
|
|
134
|
+
} else if (operaMatch) {
|
|
135
|
+
browserType = 'Opera';
|
|
136
|
+
browserVersion = operaMatch[1];
|
|
137
|
+
} else if (ieMatch) {
|
|
138
|
+
browserType = 'IE';
|
|
139
|
+
browserVersion = ieMatch[1];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
device_id: generateFingerprint(),
|
|
144
|
+
device_brand: deviceBrand,
|
|
145
|
+
device_model: deviceModel,
|
|
146
|
+
os_version: deviceVersion,
|
|
147
|
+
browser: browserType,
|
|
148
|
+
browser_version: browserVersion,
|
|
149
|
+
browser_ua: ua,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------- 扩展:支持小数的版本(可选) ----------------------
|
|
154
|
+
/**
|
|
155
|
+
* 校验scale格式(支持小数)并返回合法值
|
|
156
|
+
* @param {unknown} scale - 待校验的scale值
|
|
157
|
+
* @param {string} tag - 日志标签
|
|
158
|
+
* @returns {number | string} 合法值,非法返回1
|
|
159
|
+
*/
|
|
160
|
+
export function validateScaleWithDecimal(scale: unknown, tag: string): number | string {
|
|
161
|
+
const validVhRegex = /^\d+(\.\d+)?vh$/; // 支持 "1.5vh"、"10vh"
|
|
162
|
+
const validNumberStrRegex = /^\d+(\.\d+)?$/; // 支持 "2.8"、"5"
|
|
163
|
+
|
|
164
|
+
if (typeof scale === 'number') {
|
|
165
|
+
return scale;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof scale === 'string' && (validVhRegex.test(scale) || validNumberStrRegex.test(scale))) {
|
|
169
|
+
return scale;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
(window as any).avatarSDKLogger?.warn(
|
|
173
|
+
tag,
|
|
174
|
+
'scale 必须是数字、带小数的数字字符串或者"xxxvh"格式(xxx为数字/小数)'
|
|
175
|
+
);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function isharmony() {
|
|
180
|
+
const userAgent = (navigator.userAgent || "").toLowerCase();
|
|
181
|
+
return /openharmony|arkweb/.test(userAgent);
|
|
182
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// logger.js - 改进的单例日志系统
|
|
2
|
+
let isLoggingEnabled = false;
|
|
3
|
+
|
|
4
|
+
function executeLog(consoleMethod, ...args) {
|
|
5
|
+
// 边界防护:1. 开关关闭 2. 控制台方法不存在(兼容异常环境)
|
|
6
|
+
if (!isLoggingEnabled || typeof consoleMethod !== 'function') {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
// 统一拼接前缀,执行原生控制台方法
|
|
10
|
+
consoleMethod( ...args);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sdkLog = {
|
|
14
|
+
log: (...args) => executeLog(console.log, ...args),
|
|
15
|
+
info: (...args) => executeLog(console.info, ...args),
|
|
16
|
+
warn: (...args) => executeLog(console.warn, ...args),
|
|
17
|
+
error: (...args) => executeLog(console.error, ...args)
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// 对外暴露的配置方法
|
|
22
|
+
function setLoggingEnabled(enabled) {
|
|
23
|
+
isLoggingEnabled = enabled;
|
|
24
|
+
// 可选:添加日志说明当前日志状态变化
|
|
25
|
+
if (!enabled) {
|
|
26
|
+
console.log('[AVATAR SDK] version:', VERSION, '日志已禁用');
|
|
27
|
+
} else {
|
|
28
|
+
console.log('[AVATAR SDK] version:', VERSION, '日志已启用');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 定义完整的日志对象
|
|
33
|
+
window.avatarSDKLogger = {
|
|
34
|
+
...sdkLog,
|
|
35
|
+
setEnabled: setLoggingEnabled
|
|
36
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
type EpochClock = () => number;
|
|
2
|
+
|
|
3
|
+
function monotonicEpochNow() {
|
|
4
|
+
const clock = globalThis.performance;
|
|
5
|
+
if (
|
|
6
|
+
clock
|
|
7
|
+
&& Number.isFinite(clock.timeOrigin)
|
|
8
|
+
&& typeof clock.now === "function"
|
|
9
|
+
) {
|
|
10
|
+
return clock.timeOrigin + clock.now();
|
|
11
|
+
}
|
|
12
|
+
return Date.now();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Normalizes browser-specific BlobEvent.timecode units to epoch milliseconds. */
|
|
16
|
+
export class MediaRecorderTimestamp {
|
|
17
|
+
private readonly startedAt: number;
|
|
18
|
+
private readonly now: EpochClock;
|
|
19
|
+
private timecodeScale: 1 | 1000 | null = null;
|
|
20
|
+
private firstTimecode: number | null = null;
|
|
21
|
+
private lastTimecode: number | null = null;
|
|
22
|
+
private lastObservedAt: number | null = null;
|
|
23
|
+
private lastTimestamp: number | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly chunkMs: number,
|
|
27
|
+
now: EpochClock = monotonicEpochNow,
|
|
28
|
+
) {
|
|
29
|
+
this.now = now;
|
|
30
|
+
this.startedAt = now();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
resolve(timecode: number): number {
|
|
34
|
+
const observedAt = this.now();
|
|
35
|
+
const validTimecode = Number.isFinite(timecode) && timecode >= 0;
|
|
36
|
+
|
|
37
|
+
if (validTimecode && this.firstTimecode === null) {
|
|
38
|
+
this.firstTimecode = timecode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
validTimecode
|
|
43
|
+
&& this.timecodeScale === null
|
|
44
|
+
&& this.lastTimecode !== null
|
|
45
|
+
&& timecode > this.lastTimecode
|
|
46
|
+
) {
|
|
47
|
+
const timecodeDelta = timecode - this.lastTimecode;
|
|
48
|
+
const observedDelta = this.lastObservedAt === null ? 0 : observedAt - this.lastObservedAt;
|
|
49
|
+
const expectedDelta = observedDelta >= this.chunkMs / 4 ? observedDelta : this.chunkMs;
|
|
50
|
+
const millisecondError = Math.abs(timecodeDelta - expectedDelta);
|
|
51
|
+
const secondError = Math.abs(timecodeDelta * 1000 - expectedDelta);
|
|
52
|
+
this.timecodeScale = secondError < millisecondError ? 1000 : 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let timestamp = observedAt - this.chunkMs;
|
|
56
|
+
const relativeTimecode = validTimecode && this.firstTimecode !== null
|
|
57
|
+
? timecode - this.firstTimecode
|
|
58
|
+
: null;
|
|
59
|
+
if (
|
|
60
|
+
relativeTimecode !== null
|
|
61
|
+
&& relativeTimecode >= 0
|
|
62
|
+
&& (this.timecodeScale !== null || relativeTimecode === 0)
|
|
63
|
+
) {
|
|
64
|
+
timestamp = this.startedAt + relativeTimecode * (this.timecodeScale ?? 1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (this.lastTimestamp !== null) {
|
|
68
|
+
// A non-empty MediaRecorder Blob must not collapse into an already-used
|
|
69
|
+
// AU bucket when a browser repeats or resets its timecode.
|
|
70
|
+
timestamp = Math.max(timestamp, this.lastTimestamp + this.chunkMs);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const resolved = Math.max(Math.round(this.startedAt), Math.round(timestamp));
|
|
74
|
+
this.lastTimecode = validTimecode ? timecode : this.lastTimecode;
|
|
75
|
+
this.lastObservedAt = observedAt;
|
|
76
|
+
this.lastTimestamp = resolved;
|
|
77
|
+
return resolved;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PCM 音频参数分析工具
|
|
3
|
+
* 用于检测和分析 PCM 音频数据的基本参数
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PCMAudioParams {
|
|
7
|
+
sampleRate: number; // 采样率 (Hz)
|
|
8
|
+
channels: number; // 声道数 (1=单声道, 2=双声道)
|
|
9
|
+
bitDepth: number; // 位深度 (8, 16, 24, 32)
|
|
10
|
+
format: string; // 格式 (S16LE, S24LE, F32LE 等)
|
|
11
|
+
encoding: string; // 编码方式 (PCM)
|
|
12
|
+
bytesPerSample: number; // 每个采样点的字节数
|
|
13
|
+
sampleCount?: number; // 样本数量(如果提供了数据)
|
|
14
|
+
duration?: number; // 时长(秒)(如果提供了数据)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 分析 PCM 音频数据的基本参数
|
|
19
|
+
* @param pcmData PCM 音频数据(Uint8Array)
|
|
20
|
+
* @param knownSampleRate 已知的采样率(可选,如果不提供则使用默认值 24000)
|
|
21
|
+
* @returns PCM 音频参数
|
|
22
|
+
*/
|
|
23
|
+
export function analyzePCMParams(
|
|
24
|
+
pcmData?: Uint8Array,
|
|
25
|
+
knownSampleRate?: number
|
|
26
|
+
): PCMAudioParams {
|
|
27
|
+
// 根据代码中的配置,PCM 音频使用固定参数
|
|
28
|
+
const sampleRate = knownSampleRate || 24000;
|
|
29
|
+
const channels = 1; // 单声道
|
|
30
|
+
const bitDepth = 16; // 16位
|
|
31
|
+
const format = 'S16LE'; // Signed 16-bit Little Endian
|
|
32
|
+
const encoding = 'PCM'; // 未压缩的 PCM 编码
|
|
33
|
+
const bytesPerSample = 2; // 16位 = 2字节/采样点
|
|
34
|
+
|
|
35
|
+
let sampleCount: number | undefined;
|
|
36
|
+
let duration: number | undefined;
|
|
37
|
+
|
|
38
|
+
if (pcmData) {
|
|
39
|
+
// 计算样本数量
|
|
40
|
+
sampleCount = Math.floor(pcmData.length / bytesPerSample);
|
|
41
|
+
// 计算时长(秒)
|
|
42
|
+
duration = sampleCount / sampleRate;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
sampleRate,
|
|
47
|
+
channels,
|
|
48
|
+
bitDepth,
|
|
49
|
+
format,
|
|
50
|
+
encoding,
|
|
51
|
+
bytesPerSample,
|
|
52
|
+
sampleCount,
|
|
53
|
+
duration,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 检测 PCM 数据的可能参数(通过分析数据特征)
|
|
59
|
+
* 注意:这种方法只能提供推测,最准确的参数应该从服务器配置中获取
|
|
60
|
+
* @param pcmData PCM 音频数据
|
|
61
|
+
* @returns 检测到的参数(可能不准确)
|
|
62
|
+
*/
|
|
63
|
+
export function detectPCMParams(pcmData: Uint8Array): Partial<PCMAudioParams> {
|
|
64
|
+
const dataLength = pcmData.length;
|
|
65
|
+
|
|
66
|
+
// 检测位深度和字节序
|
|
67
|
+
// 由于数据是 16 位,每个样本占 2 字节
|
|
68
|
+
const bytesPerSample = 2; // 根据代码,固定为 2 字节(16位)
|
|
69
|
+
const bitDepth = 16;
|
|
70
|
+
|
|
71
|
+
// 检测声道数(通过数据长度和样本对齐)
|
|
72
|
+
// 如果数据长度是 2 的倍数,可能是单声道
|
|
73
|
+
// 如果数据长度是 4 的倍数,可能是双声道
|
|
74
|
+
let channels = 1;
|
|
75
|
+
if (dataLength % 4 === 0) {
|
|
76
|
+
// 可能是双声道(2 通道 × 2 字节 = 4 字节/样本)
|
|
77
|
+
// 但根据代码,这里固定为单声道
|
|
78
|
+
channels = 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 尝试检测采样率(这个很难准确检测,只能推测)
|
|
82
|
+
// 如果知道数据时长和样本数,可以反推采样率
|
|
83
|
+
const sampleCount = Math.floor(dataLength / bytesPerSample);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
channels,
|
|
87
|
+
bitDepth,
|
|
88
|
+
format: 'S16LE', // 根据代码,固定为 S16LE
|
|
89
|
+
encoding: 'PCM',
|
|
90
|
+
bytesPerSample,
|
|
91
|
+
sampleCount,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 格式化显示 PCM 参数
|
|
97
|
+
* @param params PCM 音频参数
|
|
98
|
+
* @returns 格式化的字符串
|
|
99
|
+
*/
|
|
100
|
+
export function formatPCMParams(params: PCMAudioParams): string {
|
|
101
|
+
const parts = [
|
|
102
|
+
`采样率: ${params.sampleRate} Hz`,
|
|
103
|
+
`声道: ${params.channels === 1 ? '单声道 (Mono)' : `双声道 (Stereo, ${params.channels} channels)`}`,
|
|
104
|
+
`位深度: ${params.bitDepth} bit`,
|
|
105
|
+
`格式: ${params.format}`,
|
|
106
|
+
`编码: ${params.encoding}`,
|
|
107
|
+
`每样本字节数: ${params.bytesPerSample} bytes`,
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
if (params.sampleCount !== undefined) {
|
|
111
|
+
parts.push(`样本数: ${params.sampleCount.toLocaleString()}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (params.duration !== undefined) {
|
|
115
|
+
parts.push(`时长: ${params.duration.toFixed(3)} 秒 (${(params.duration * 1000).toFixed(0)} ms)`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return parts.join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 从音频渲染器获取当前 PCM 配置
|
|
123
|
+
* @param audioRenderer 音频渲染器实例
|
|
124
|
+
* @returns PCM 音频参数
|
|
125
|
+
*/
|
|
126
|
+
export function getPCMParamsFromRenderer(audioRenderer: any): PCMAudioParams {
|
|
127
|
+
// 尝试从 AudioWorklet 或 Audio 播放器获取配置
|
|
128
|
+
let sampleRate = 24000; // 默认值
|
|
129
|
+
let channels = 1;
|
|
130
|
+
let bitDepth = 16;
|
|
131
|
+
let format = 'S16LE';
|
|
132
|
+
let bytesPerSample = 2;
|
|
133
|
+
|
|
134
|
+
// 检查 AudioWorklet
|
|
135
|
+
if (audioRenderer?.audio?.audioCtx) {
|
|
136
|
+
sampleRate = audioRenderer.audio.audioCtx.sampleRate || 24000;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 检查旧的 Audio 播放器
|
|
140
|
+
if (audioRenderer?.audio?.SAMPLE_RATE) {
|
|
141
|
+
sampleRate = audioRenderer.audio.SAMPLE_RATE;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 根据代码,PCM 音频固定使用这些参数
|
|
145
|
+
return {
|
|
146
|
+
sampleRate,
|
|
147
|
+
channels,
|
|
148
|
+
bitDepth,
|
|
149
|
+
format,
|
|
150
|
+
encoding: 'PCM',
|
|
151
|
+
bytesPerSample,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|