@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,144 @@
|
|
|
1
|
+
export type WebAudioSessionType = "playback" | "play-and-record";
|
|
2
|
+
|
|
3
|
+
type WebAudioSession = {
|
|
4
|
+
type: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type NavigatorWithAudioSession = Navigator & {
|
|
8
|
+
audioSession?: WebAudioSession;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type WebAudioPlaybackSessionRestoredListener = () => void;
|
|
12
|
+
|
|
13
|
+
const captureStates = new WeakMap<
|
|
14
|
+
WebAudioSession,
|
|
15
|
+
{ count: number; previousType: string }
|
|
16
|
+
>();
|
|
17
|
+
const playbackSessionRestoredListeners = new WeakMap<
|
|
18
|
+
WebAudioSession,
|
|
19
|
+
Set<WebAudioPlaybackSessionRestoredListener>
|
|
20
|
+
>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Keep Web Audio audible through the iOS Ring/Silent switch. Do not override
|
|
24
|
+
* a play-and-record session that may belong to an active microphone track.
|
|
25
|
+
*/
|
|
26
|
+
export function configureWebAudioPlaybackSession(
|
|
27
|
+
target: Navigator = navigator,
|
|
28
|
+
): boolean {
|
|
29
|
+
const audioSession = getAudioSession(target);
|
|
30
|
+
if (!audioSession) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const captureState = captureStates.get(audioSession);
|
|
35
|
+
if (captureState?.count || audioSession.type === "play-and-record") {
|
|
36
|
+
return setAudioSessionType(audioSession, "play-and-record");
|
|
37
|
+
}
|
|
38
|
+
return setAudioSessionType(audioSession, "playback");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function beginWebAudioCaptureSession(
|
|
42
|
+
target: Navigator = navigator,
|
|
43
|
+
): boolean {
|
|
44
|
+
const audioSession = getAudioSession(target);
|
|
45
|
+
if (!audioSession) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const existingState = captureStates.get(audioSession);
|
|
50
|
+
const previousType = existingState?.previousType ?? audioSession.type;
|
|
51
|
+
if (!setAudioSessionType(audioSession, "play-and-record")) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
captureStates.set(audioSession, {
|
|
56
|
+
count: (existingState?.count ?? 0) + 1,
|
|
57
|
+
previousType,
|
|
58
|
+
});
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function subscribeWebAudioPlaybackSessionRestored(
|
|
63
|
+
listener: WebAudioPlaybackSessionRestoredListener,
|
|
64
|
+
target: Navigator = navigator,
|
|
65
|
+
): () => void {
|
|
66
|
+
const audioSession = getAudioSession(target);
|
|
67
|
+
if (!audioSession) {
|
|
68
|
+
return () => {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let listeners = playbackSessionRestoredListeners.get(audioSession);
|
|
72
|
+
if (!listeners) {
|
|
73
|
+
listeners = new Set();
|
|
74
|
+
playbackSessionRestoredListeners.set(audioSession, listeners);
|
|
75
|
+
}
|
|
76
|
+
listeners.add(listener);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
listeners?.delete(listener);
|
|
80
|
+
if (listeners?.size === 0) {
|
|
81
|
+
playbackSessionRestoredListeners.delete(audioSession);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function endWebAudioCaptureSession(
|
|
87
|
+
target: Navigator = navigator,
|
|
88
|
+
): boolean {
|
|
89
|
+
const audioSession = getAudioSession(target);
|
|
90
|
+
if (!audioSession) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const captureState = captureStates.get(audioSession);
|
|
95
|
+
if (!captureState) {
|
|
96
|
+
return configureWebAudioPlaybackSession(target);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
captureState.count -= 1;
|
|
100
|
+
if (captureState.count > 0) {
|
|
101
|
+
return setAudioSessionType(audioSession, "play-and-record");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
captureStates.delete(audioSession);
|
|
105
|
+
const nextType = captureState.previousType === "play-and-record"
|
|
106
|
+
? "play-and-record"
|
|
107
|
+
: "playback";
|
|
108
|
+
const updated = setAudioSessionType(audioSession, nextType);
|
|
109
|
+
if (updated && nextType === "playback") {
|
|
110
|
+
notifyPlaybackSessionRestored(audioSession);
|
|
111
|
+
}
|
|
112
|
+
return updated;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function notifyPlaybackSessionRestored(audioSession: WebAudioSession): void {
|
|
116
|
+
const listeners = playbackSessionRestoredListeners.get(audioSession);
|
|
117
|
+
if (!listeners) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const listener of [...listeners]) {
|
|
122
|
+
try {
|
|
123
|
+
listener();
|
|
124
|
+
} catch {
|
|
125
|
+
// Playback recovery must never break microphone cleanup.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getAudioSession(target: Navigator): WebAudioSession | undefined {
|
|
131
|
+
return (target as NavigatorWithAudioSession).audioSession;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function setAudioSessionType(
|
|
135
|
+
audioSession: WebAudioSession,
|
|
136
|
+
type: WebAudioSessionType,
|
|
137
|
+
): boolean {
|
|
138
|
+
try {
|
|
139
|
+
audioSession.type = type;
|
|
140
|
+
return audioSession.type === type;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// PCM转WAV的辅助函数
|
|
2
|
+
export function pcmToWav(pcmData: Uint8Array) {
|
|
3
|
+
const wavHeader = new ArrayBuffer(44);
|
|
4
|
+
const view = new DataView(wavHeader);
|
|
5
|
+
|
|
6
|
+
/* RIFF identifier */
|
|
7
|
+
writeString(view, 0, "RIFF");
|
|
8
|
+
/* RIFF chunk length */
|
|
9
|
+
view.setUint32(4, 36 + pcmData.length, true);
|
|
10
|
+
/* RIFF type */
|
|
11
|
+
writeString(view, 8, "WAVE");
|
|
12
|
+
/* format chunk identifier */
|
|
13
|
+
writeString(view, 12, "fmt ");
|
|
14
|
+
/* format chunk length */
|
|
15
|
+
view.setUint32(16, 16, true);
|
|
16
|
+
/* sample format (raw) */
|
|
17
|
+
view.setUint16(20, 1, true);
|
|
18
|
+
/* channel count */
|
|
19
|
+
view.setUint16(22, 1, true); // mono
|
|
20
|
+
/* sample rate */
|
|
21
|
+
view.setUint32(24, 24000, true);
|
|
22
|
+
/* byte rate (sample rate * block align) */
|
|
23
|
+
view.setUint32(28, 24000 * 2, true);
|
|
24
|
+
/* block align (channel count * bytes per sample) */
|
|
25
|
+
view.setUint16(32, 2, true);
|
|
26
|
+
/* bits per sample */
|
|
27
|
+
view.setUint16(34, 16, true);
|
|
28
|
+
/* data chunk identifier */
|
|
29
|
+
writeString(view, 36, "data");
|
|
30
|
+
/* data chunk length */
|
|
31
|
+
view.setUint32(40, pcmData.length, true);
|
|
32
|
+
|
|
33
|
+
const wavData = new Uint8Array(wavHeader.byteLength + pcmData.length);
|
|
34
|
+
wavData.set(new Uint8Array(wavHeader), 0);
|
|
35
|
+
wavData.set(new Uint8Array(pcmData), wavHeader.byteLength);
|
|
36
|
+
|
|
37
|
+
return wavData.buffer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeString(view: DataView, offset: number, string: string) {
|
|
41
|
+
for (let i = 0; i < string.length; i++) {
|
|
42
|
+
view.setUint8(offset + i, string.charCodeAt(i));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { ICapabilityCheckItem, ICapabilityCheckReport } from '../types/capability';
|
|
2
|
+
|
|
3
|
+
// ==================== 同步检查(constructor 使用) ====================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 同步检查安全上下文(HTTPS / localhost)
|
|
7
|
+
*/
|
|
8
|
+
function checkSecureContextSync(): ICapabilityCheckItem {
|
|
9
|
+
const supported = window.isSecureContext;
|
|
10
|
+
return {
|
|
11
|
+
name: 'secureContext',
|
|
12
|
+
supported,
|
|
13
|
+
severity: 'fatal',
|
|
14
|
+
reason: supported ? undefined : '当前环境不是安全上下文,请使用 HTTPS 或 localhost',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 同步检查 WebGL2 支持(创建上下文)
|
|
20
|
+
*/
|
|
21
|
+
function checkWebGL2Sync(): ICapabilityCheckItem {
|
|
22
|
+
try {
|
|
23
|
+
const canvas = document.createElement('canvas');
|
|
24
|
+
const gl = canvas.getContext('webgl2', { failIfMajorPerformanceCaveat: true });
|
|
25
|
+
if (!gl) {
|
|
26
|
+
return {
|
|
27
|
+
name: 'webgl2',
|
|
28
|
+
supported: false,
|
|
29
|
+
severity: 'fatal',
|
|
30
|
+
reason: '浏览器不支持 WebGL2',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// 获取基本信息
|
|
34
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
35
|
+
const detail = {
|
|
36
|
+
version: gl.getParameter(gl.VERSION),
|
|
37
|
+
vendor: gl.getParameter(gl.VENDOR),
|
|
38
|
+
renderer: gl.getParameter(gl.RENDERER),
|
|
39
|
+
gpuVendor: debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : undefined,
|
|
40
|
+
gpuRenderer: debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : undefined,
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
name: 'webgl2',
|
|
44
|
+
supported: true,
|
|
45
|
+
severity: 'fatal',
|
|
46
|
+
detail,
|
|
47
|
+
};
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return {
|
|
50
|
+
name: 'webgl2',
|
|
51
|
+
supported: false,
|
|
52
|
+
severity: 'fatal',
|
|
53
|
+
reason: 'WebGL2 初始化失败',
|
|
54
|
+
detail: e,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 同步检查音频系统(PCM 或 MSE 任一可用即可)
|
|
61
|
+
*
|
|
62
|
+
* SDK 有两种音频播放器:
|
|
63
|
+
* 1. PCM 播放器:AudioWorklet + AudioContext(raw_audio = true)
|
|
64
|
+
* 2. MSE 播放器:MediaSource + HTMLAudioElement(raw_audio = false)
|
|
65
|
+
*
|
|
66
|
+
* 两者二选一,任一可用则 SDK 音频能力可用
|
|
67
|
+
*/
|
|
68
|
+
function checkAudioSystemSync(): ICapabilityCheckItem {
|
|
69
|
+
// PCM 路径:需要 AudioContext + AudioWorkletNode
|
|
70
|
+
const hasAudioContext = typeof (window as any).AudioContext !== 'undefined'
|
|
71
|
+
|| typeof (window as any).webkitAudioContext !== 'undefined';
|
|
72
|
+
const hasAudioWorklet = typeof (window as any).AudioWorkletNode !== 'undefined';
|
|
73
|
+
const pcmSupported = hasAudioContext && hasAudioWorklet;
|
|
74
|
+
|
|
75
|
+
// MSE 路径:需要 MediaSource + SourceBuffer
|
|
76
|
+
const hasMediaSource = typeof (window as any).MediaSource !== 'undefined';
|
|
77
|
+
const mseSupported = hasMediaSource;
|
|
78
|
+
|
|
79
|
+
const supported = pcmSupported || mseSupported;
|
|
80
|
+
|
|
81
|
+
const detail = {
|
|
82
|
+
pcm: {
|
|
83
|
+
supported: pcmSupported,
|
|
84
|
+
hasAudioContext,
|
|
85
|
+
hasAudioWorklet,
|
|
86
|
+
},
|
|
87
|
+
mse: {
|
|
88
|
+
supported: mseSupported,
|
|
89
|
+
hasMediaSource,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (!supported) {
|
|
94
|
+
return {
|
|
95
|
+
name: 'audio',
|
|
96
|
+
supported: false,
|
|
97
|
+
severity: 'fatal',
|
|
98
|
+
reason: '音频播放能力不可用:PCM(AudioWorklet)和 MSE(MediaSource)均不支持',
|
|
99
|
+
detail,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
name: 'audio',
|
|
105
|
+
supported: true,
|
|
106
|
+
severity: 'fatal',
|
|
107
|
+
detail,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 同步检查 WebCodecs API 是否存在(不探测具体编解码器)
|
|
113
|
+
*/
|
|
114
|
+
function checkWebCodecsSync(): ICapabilityCheckItem {
|
|
115
|
+
const hasVideoDecoder = typeof (window as any).VideoDecoder !== 'undefined';
|
|
116
|
+
const supported = hasVideoDecoder;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
name: 'webcodecs',
|
|
120
|
+
supported,
|
|
121
|
+
severity: 'fatal',
|
|
122
|
+
reason: supported ? undefined : '浏览器不支持 WebCodecs VideoDecoder API',
|
|
123
|
+
detail: { hasVideoDecoder },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 同步检查 Web Worker 构造函数是否存在
|
|
129
|
+
*/
|
|
130
|
+
function checkWorkerSync(): ICapabilityCheckItem {
|
|
131
|
+
const supported = typeof Worker !== 'undefined';
|
|
132
|
+
return {
|
|
133
|
+
name: 'worker',
|
|
134
|
+
supported,
|
|
135
|
+
severity: 'fatal',
|
|
136
|
+
reason: supported ? undefined : '浏览器不支持 Web Worker',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 同步检查所有浏览器能力(用于 constructor 快速校验)
|
|
142
|
+
*
|
|
143
|
+
* 检查项:
|
|
144
|
+
* - secureContext:安全上下文(HTTPS / localhost)
|
|
145
|
+
* - webgl2:WebGL2 渲染能力
|
|
146
|
+
* - audio:音频播放能力(PCM 或 MSE 任一可用即可)
|
|
147
|
+
* - webcodecs:WebCodecs API 存在性
|
|
148
|
+
* - worker:Web Worker 支持
|
|
149
|
+
*
|
|
150
|
+
* 音频检查逻辑:SDK 有两种音频播放器(PCM 和 MSE),只需其中一种可用即可
|
|
151
|
+
* - PCM 播放器依赖:AudioContext + AudioWorkletNode
|
|
152
|
+
* - MSE 播放器依赖:MediaSource
|
|
153
|
+
* - 两者都不可用时,音频能力检查失败
|
|
154
|
+
*/
|
|
155
|
+
export function checkCapabilitySync(audioOnlyMode = false): ICapabilityCheckReport {
|
|
156
|
+
const items: ICapabilityCheckItem[] = [
|
|
157
|
+
checkSecureContextSync(),
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
if (audioOnlyMode) {
|
|
161
|
+
// audioOnlyMode: only check secureContext, audio, and worker
|
|
162
|
+
items.push(checkAudioSystemSync());
|
|
163
|
+
items.push(checkWorkerSync());
|
|
164
|
+
} else {
|
|
165
|
+
// Full mode: check all capabilities
|
|
166
|
+
items.push(
|
|
167
|
+
checkWebGL2Sync(),
|
|
168
|
+
checkAudioSystemSync(),
|
|
169
|
+
checkWebCodecsSync(),
|
|
170
|
+
checkWorkerSync(),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fatalCount = items.filter(i => i.severity === 'fatal' && !i.supported).length;
|
|
175
|
+
const warnCount = items.filter(i => i.severity === 'warn' && !i.supported).length;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
supported: fatalCount === 0,
|
|
179
|
+
fatalCount,
|
|
180
|
+
warnCount,
|
|
181
|
+
items,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ==================== 异步检查(init 详细探测使用) ====================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 异步检查 WebCodecs 详细编解码器支持
|
|
189
|
+
*/
|
|
190
|
+
async function checkWebCodecsDetailed(): Promise<ICapabilityCheckItem> {
|
|
191
|
+
try {
|
|
192
|
+
const hasVideoDecoder = typeof (window as any).VideoDecoder !== 'undefined';
|
|
193
|
+
const supported = hasVideoDecoder;
|
|
194
|
+
|
|
195
|
+
if (!supported) {
|
|
196
|
+
return {
|
|
197
|
+
name: 'webcodecs',
|
|
198
|
+
supported: false,
|
|
199
|
+
severity: 'fatal',
|
|
200
|
+
reason: '浏览器不支持 WebCodecs VideoDecoder API',
|
|
201
|
+
detail: { hasVideoDecoder },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 探测具体编解码器支持
|
|
206
|
+
let codecSupport: Record<string, boolean> = {};
|
|
207
|
+
const testCodecs = [
|
|
208
|
+
'video/mp4; codecs="avc1.42E01E"', // H.264 Baseline
|
|
209
|
+
'video/mp4; codecs="avc1.4D001F"', // H.264 Main
|
|
210
|
+
'video/mp4; codecs="avc1.64001F"', // H.264 High
|
|
211
|
+
];
|
|
212
|
+
for (const codec of testCodecs) {
|
|
213
|
+
try {
|
|
214
|
+
const result = await (window as any).VideoDecoder.isConfigSupported({
|
|
215
|
+
codec,
|
|
216
|
+
});
|
|
217
|
+
codecSupport[codec] = result.supported;
|
|
218
|
+
} catch {
|
|
219
|
+
codecSupport[codec] = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
name: 'webcodecs',
|
|
225
|
+
supported: true,
|
|
226
|
+
severity: 'fatal',
|
|
227
|
+
detail: { hasVideoDecoder, codecSupport },
|
|
228
|
+
};
|
|
229
|
+
} catch (e) {
|
|
230
|
+
return {
|
|
231
|
+
name: 'webcodecs',
|
|
232
|
+
supported: false,
|
|
233
|
+
severity: 'fatal',
|
|
234
|
+
reason: 'WebCodecs 探测失败',
|
|
235
|
+
detail: e,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 异步检查 Worker 实际执行能力
|
|
242
|
+
*/
|
|
243
|
+
const WORKER_CHECK_TIMEOUT_MS = 3000;
|
|
244
|
+
const WORKER_CHECK_MAX_ATTEMPTS = 2;
|
|
245
|
+
|
|
246
|
+
type WorkerProbeResult =
|
|
247
|
+
| { status: 'supported' }
|
|
248
|
+
| { status: 'timeout' }
|
|
249
|
+
| { status: 'failed'; reason: string; detail?: unknown };
|
|
250
|
+
|
|
251
|
+
function probeWorkerExecution(): Promise<WorkerProbeResult> {
|
|
252
|
+
return new Promise((resolve) => {
|
|
253
|
+
let worker: Worker | null = null;
|
|
254
|
+
let url: string | null = null;
|
|
255
|
+
let settled = false;
|
|
256
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
257
|
+
|
|
258
|
+
const finish = (result: WorkerProbeResult) => {
|
|
259
|
+
if (settled) return;
|
|
260
|
+
settled = true;
|
|
261
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
262
|
+
worker?.terminate();
|
|
263
|
+
if (url) URL.revokeObjectURL(url);
|
|
264
|
+
resolve(result);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const blob = new Blob(['self.postMessage("ok");'], { type: 'application/javascript' });
|
|
269
|
+
url = URL.createObjectURL(blob);
|
|
270
|
+
worker = new Worker(url);
|
|
271
|
+
worker.onmessage = (event) => {
|
|
272
|
+
finish(event.data === 'ok'
|
|
273
|
+
? { status: 'supported' }
|
|
274
|
+
: { status: 'failed', reason: 'Web Worker 执行异常' });
|
|
275
|
+
};
|
|
276
|
+
worker.onerror = (event) => {
|
|
277
|
+
finish({ status: 'failed', reason: 'Web Worker 创建失败', detail: event });
|
|
278
|
+
};
|
|
279
|
+
timeout = setTimeout(() => {
|
|
280
|
+
finish({ status: 'timeout' });
|
|
281
|
+
}, WORKER_CHECK_TIMEOUT_MS);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
finish({ status: 'failed', reason: 'Web Worker 异常', detail: error });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function checkWorkerDetailed(): Promise<ICapabilityCheckItem> {
|
|
289
|
+
if (typeof Worker === 'undefined') {
|
|
290
|
+
return {
|
|
291
|
+
name: 'worker',
|
|
292
|
+
supported: false,
|
|
293
|
+
severity: 'fatal',
|
|
294
|
+
reason: '浏览器不支持 Web Worker',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (let attempt = 1; attempt <= WORKER_CHECK_MAX_ATTEMPTS; attempt += 1) {
|
|
299
|
+
const result = await probeWorkerExecution();
|
|
300
|
+
if (result.status === 'supported') {
|
|
301
|
+
return {
|
|
302
|
+
name: 'worker',
|
|
303
|
+
supported: true,
|
|
304
|
+
severity: 'fatal',
|
|
305
|
+
detail: { attempts: attempt },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (result.status === 'failed') {
|
|
309
|
+
return {
|
|
310
|
+
name: 'worker',
|
|
311
|
+
supported: false,
|
|
312
|
+
severity: 'fatal',
|
|
313
|
+
reason: result.reason,
|
|
314
|
+
detail: result.detail,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// iOS/WebKit can suspend or delay a Blob Worker while the page is being
|
|
320
|
+
// inspected, backgrounded, or recovering from a reload. The Worker API is
|
|
321
|
+
// present and no explicit error was raised, so a timing-only probe must not
|
|
322
|
+
// reject the entire SDK initialization. Real worker creation still reports
|
|
323
|
+
// its own fatal error later if the capability is genuinely unavailable.
|
|
324
|
+
return {
|
|
325
|
+
name: 'worker',
|
|
326
|
+
supported: false,
|
|
327
|
+
severity: 'warn',
|
|
328
|
+
reason: `Web Worker 创建超时(已重试 ${WORKER_CHECK_MAX_ATTEMPTS} 次)`,
|
|
329
|
+
detail: { attempts: WORKER_CHECK_MAX_ATTEMPTS },
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 异步检查所有浏览器能力(详细版,用于 init 阶段详细探测)
|
|
335
|
+
*
|
|
336
|
+
* 包含更详细的编解码器探测和 Worker 执行测试
|
|
337
|
+
*/
|
|
338
|
+
export async function checkCapability(audioOnlyMode = false): Promise<ICapabilityCheckReport> {
|
|
339
|
+
// 先执行同步检查
|
|
340
|
+
const syncReport = checkCapabilitySync(audioOnlyMode);
|
|
341
|
+
|
|
342
|
+
if (audioOnlyMode) {
|
|
343
|
+
// audioOnlyMode: only detailed check worker
|
|
344
|
+
const detailedItems = await Promise.all([
|
|
345
|
+
Promise.resolve(syncReport.items.find(i => i.name === 'secureContext')!),
|
|
346
|
+
Promise.resolve(syncReport.items.find(i => i.name === 'audio')!),
|
|
347
|
+
checkWorkerDetailed(),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
const fatalCount = detailedItems.filter(i => i.severity === 'fatal' && !i.supported).length;
|
|
351
|
+
const warnCount = detailedItems.filter(i => i.severity === 'warn' && !i.supported).length;
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
supported: fatalCount === 0,
|
|
355
|
+
fatalCount,
|
|
356
|
+
warnCount,
|
|
357
|
+
items: detailedItems,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 替换 webcodecs 和 worker 为详细版本
|
|
362
|
+
const detailedItems = await Promise.all([
|
|
363
|
+
Promise.resolve(syncReport.items.find(i => i.name === 'secureContext')!),
|
|
364
|
+
Promise.resolve(syncReport.items.find(i => i.name === 'webgl2')!),
|
|
365
|
+
Promise.resolve(syncReport.items.find(i => i.name === 'audio')!),
|
|
366
|
+
checkWebCodecsDetailed(),
|
|
367
|
+
checkWorkerDetailed(),
|
|
368
|
+
]);
|
|
369
|
+
|
|
370
|
+
const fatalCount = detailedItems.filter(i => i.severity === 'fatal' && !i.supported).length;
|
|
371
|
+
const warnCount = detailedItems.filter(i => i.severity === 'warn' && !i.supported).length;
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
supported: fatalCount === 0,
|
|
375
|
+
fatalCount,
|
|
376
|
+
warnCount,
|
|
377
|
+
items: detailedItems,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import md5 from 'blueimp-md5';
|
|
2
|
+
|
|
3
|
+
function encodeWithMd5(s: string): string {
|
|
4
|
+
return md5(s);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function escapeNonAsciiForPythonJson(value: string): string {
|
|
8
|
+
return value.replace(/[^\u0000-\u007E]/g, character => {
|
|
9
|
+
return `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function compareUnicodeCodePoints(left: string, right: string): number {
|
|
14
|
+
const leftPoints = Array.from(left, character => character.codePointAt(0)!);
|
|
15
|
+
const rightPoints = Array.from(right, character => character.codePointAt(0)!);
|
|
16
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
17
|
+
|
|
18
|
+
for (let index = 0; index < length; index += 1) {
|
|
19
|
+
if (leftPoints[index] !== rightPoints[index]) {
|
|
20
|
+
return leftPoints[index] - rightPoints[index];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return leftPoints.length - rightPoints.length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Headers {
|
|
28
|
+
[key: string]: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseUrlWithoutBase(url: string): { isAbsolute: boolean; pathWithQuery: string } {
|
|
32
|
+
try {
|
|
33
|
+
// 尝试直接解析为绝对 URL(不含基准)
|
|
34
|
+
const urlObj = new URL(url);
|
|
35
|
+
// 若是绝对 URL,返回标记+完整路径+查询
|
|
36
|
+
return {
|
|
37
|
+
isAbsolute: true,
|
|
38
|
+
pathWithQuery: urlObj.pathname + urlObj.search
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
// 解析失败,视为相对路径(path+query)
|
|
42
|
+
// 处理开头可能的斜杠,确保统一格式(如 "path" → "/path")
|
|
43
|
+
let path = url.trim();
|
|
44
|
+
if (!path.startsWith('/')) {
|
|
45
|
+
path = '/' + path;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
isAbsolute: false,
|
|
49
|
+
pathWithQuery: path
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function headersNeedSign(
|
|
55
|
+
ak: string,
|
|
56
|
+
sk: string,
|
|
57
|
+
method: string,
|
|
58
|
+
url: string,
|
|
59
|
+
data: Record<string, any>
|
|
60
|
+
): { headers: Headers; data: Record<string, any> } {
|
|
61
|
+
const headers: Headers = {};
|
|
62
|
+
const t = Math.floor(Date.now() / 1000);
|
|
63
|
+
const urlPathQuery = parseUrlWithoutBase(url).pathWithQuery;
|
|
64
|
+
|
|
65
|
+
// 递归排序对象的所有 key(包括嵌套对象)
|
|
66
|
+
function sortObjectKeys(obj: any): any {
|
|
67
|
+
if (obj === null || obj === undefined) {
|
|
68
|
+
return obj;
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(obj)) {
|
|
71
|
+
return obj.map(item => sortObjectKeys(item));
|
|
72
|
+
}
|
|
73
|
+
if (typeof obj !== 'object') {
|
|
74
|
+
return obj;
|
|
75
|
+
}
|
|
76
|
+
const sortedKeys = Object.keys(obj).sort(compareUnicodeCodePoints);
|
|
77
|
+
const sortedObj: Record<string, any> = {};
|
|
78
|
+
for (const key of sortedKeys) {
|
|
79
|
+
sortedObj[key] = sortObjectKeys(obj[key]);
|
|
80
|
+
}
|
|
81
|
+
return sortedObj;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 对象按 key 排序并去除空格
|
|
85
|
+
const sortedData = sortObjectKeys(data);
|
|
86
|
+
|
|
87
|
+
// 对齐 Python json.dumps(..., sort_keys=True) 的默认 ensure_ascii=True 行为。
|
|
88
|
+
const dataStr = escapeNonAsciiForPythonJson(JSON.stringify(sortedData)).replace(/ /g, '');
|
|
89
|
+
|
|
90
|
+
const oriSign = `${urlPathQuery.toLowerCase()}${method.toLowerCase()}${dataStr}${sk}${t}`;
|
|
91
|
+
const sign = encodeWithMd5(oriSign);
|
|
92
|
+
|
|
93
|
+
headers['X-APP-ID'] = ak;
|
|
94
|
+
headers['X-TOKEN'] = sign;
|
|
95
|
+
headers['X-TIMESTAMP'] = t.toString();
|
|
96
|
+
return {
|
|
97
|
+
headers,
|
|
98
|
+
data: sortedData,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// translate numpy.float16 tobytes() to Float32Array
|
|
2
|
+
const halfToFloat = (h: number) => {
|
|
3
|
+
const s = (h & 0x8000) >> 15;
|
|
4
|
+
let e = (h & 0x7C00) >> 10;
|
|
5
|
+
let f = h & 0x03FF;
|
|
6
|
+
|
|
7
|
+
if (e === 0) {
|
|
8
|
+
// subnormal
|
|
9
|
+
return ((-1) ** s) * (f / 0x400) * 2 ** (-14);
|
|
10
|
+
} else if (e === 0x1F) {
|
|
11
|
+
// ±Inf or NaN
|
|
12
|
+
return f ? NaN : ((-1) ** s) * Infinity;
|
|
13
|
+
} else {
|
|
14
|
+
// normal
|
|
15
|
+
return ((-1) ** s) * (1 + f / 0x400) * 2 ** (e - 15);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const translateBufferToFloatArray = (buf: any) => {
|
|
20
|
+
const count = buf.byteLength / 2;
|
|
21
|
+
const floats = new Float32Array(count);
|
|
22
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < count; i++) {
|
|
25
|
+
const h = dv.getUint16(i * 2, true); // true = little‑endian
|
|
26
|
+
floats[i] = halfToFloat(h);
|
|
27
|
+
}
|
|
28
|
+
return Array.from(floats)
|
|
29
|
+
}
|