@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,899 @@
|
|
|
1
|
+
import Pako from 'pako'
|
|
2
|
+
import { IAvatarOptions, INetworkInfo, Layout, WalkConfig } from "../types/index";
|
|
3
|
+
import request, { XMLRequest } from "../utils/request";
|
|
4
|
+
import {
|
|
5
|
+
IBRAnimationGeneratorCharInfo_NN,
|
|
6
|
+
} from "../utils/DataInterface";
|
|
7
|
+
import XmovAvatar from "../index";
|
|
8
|
+
import { EErrorCode } from "../types/error";
|
|
9
|
+
import { headersNeedSign } from "../utils/encodeToken";
|
|
10
|
+
// import { isDEV } from "../utils";
|
|
11
|
+
import CacheManager from './cache-manager';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 资源处理模块,加载并存储资源
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type TOptions = Pick<
|
|
18
|
+
IAvatarOptions,
|
|
19
|
+
"appId" | "appSecret" | "gatewayServer" | 'cacheServer' | "sdkInstance" | "config" | "headers" | "gateway_type" | "tag" |
|
|
20
|
+
"asr_id" | "llm_id" | "session_speak_req_id"
|
|
21
|
+
> & {
|
|
22
|
+
sessionRequestData?: Record<string, unknown>
|
|
23
|
+
onNetworkInfo(quality: INetworkInfo): void
|
|
24
|
+
onStartSessionWarning?: (message: Object) => void
|
|
25
|
+
};
|
|
26
|
+
export type TDownloadProgress = (progress: number) => void;
|
|
27
|
+
export interface IOfflineIdle {
|
|
28
|
+
asf: number
|
|
29
|
+
aef: number
|
|
30
|
+
n: string
|
|
31
|
+
}
|
|
32
|
+
export interface IResumeParams {
|
|
33
|
+
client_frame: number
|
|
34
|
+
current_ani: string
|
|
35
|
+
current_ani_frame: number
|
|
36
|
+
next_state: string
|
|
37
|
+
}
|
|
38
|
+
export interface ISessionResponse {
|
|
39
|
+
session_id: string;
|
|
40
|
+
room: string;
|
|
41
|
+
token: string;
|
|
42
|
+
socket_io_url: string;
|
|
43
|
+
e2e_resp?: {
|
|
44
|
+
ws_url?: string;
|
|
45
|
+
e2e_token?: string;
|
|
46
|
+
monitor_ws_url?: string;
|
|
47
|
+
features?: {
|
|
48
|
+
speech_frontend?: {
|
|
49
|
+
enable_aec?: boolean;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
};
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
features?: {
|
|
56
|
+
speech_frontend?: {
|
|
57
|
+
enable_aec?: boolean;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
};
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
};
|
|
62
|
+
session_start_time: string;
|
|
63
|
+
resource_pack: {
|
|
64
|
+
enable_robot_expression: boolean;
|
|
65
|
+
body_data_dir: string;
|
|
66
|
+
face_ani_char_data: string;
|
|
67
|
+
face_ani_preload_data: string;
|
|
68
|
+
offline_idle: IOfflineIdle[],
|
|
69
|
+
interpolate_joints: [number, number][]
|
|
70
|
+
};
|
|
71
|
+
reconnect_timeout: number;
|
|
72
|
+
reconnect_client_timeout: number;
|
|
73
|
+
config: {
|
|
74
|
+
background_img: string;
|
|
75
|
+
frame_rate: number;
|
|
76
|
+
look_name: string;
|
|
77
|
+
tts_vcn_id: string;
|
|
78
|
+
sta_face_id: string;
|
|
79
|
+
mp_service_id: string;
|
|
80
|
+
raw_audio: boolean;
|
|
81
|
+
resolution: {
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
},
|
|
85
|
+
init_events: [
|
|
86
|
+
{
|
|
87
|
+
type: string;
|
|
88
|
+
data: {
|
|
89
|
+
image: string;
|
|
90
|
+
axis_id: number;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
fx?: {
|
|
95
|
+
transform?: {
|
|
96
|
+
offsetX: number;
|
|
97
|
+
offsetY: number;
|
|
98
|
+
scaleX: number;
|
|
99
|
+
scaleY: number;
|
|
100
|
+
};
|
|
101
|
+
// 阴影效果
|
|
102
|
+
shadow?: {
|
|
103
|
+
// [x, y, z],阴影投射的基准点
|
|
104
|
+
refPos: number[];
|
|
105
|
+
// 光源位置
|
|
106
|
+
light: number[];
|
|
107
|
+
// 光源类型:"point"(点光源)| "directional"(平行光)
|
|
108
|
+
lightType: string;
|
|
109
|
+
// 阴影模糊半径(越大越柔和)
|
|
110
|
+
blurRadius: number;
|
|
111
|
+
// 阴影颜色
|
|
112
|
+
tint: number[];
|
|
113
|
+
// 阴影强度(0.0 = 无阴影,1.0 = 最深)
|
|
114
|
+
intensity: number;
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export default class ResourceManager {
|
|
120
|
+
private TAG = "[ResourceManager]";
|
|
121
|
+
options: TOptions;
|
|
122
|
+
sdk: XmovAvatar;
|
|
123
|
+
session_id?: string;
|
|
124
|
+
mouthShapeLib: any;
|
|
125
|
+
offlineIdle: IOfflineIdle[] = []
|
|
126
|
+
offlineCache = new Map<string, {
|
|
127
|
+
data: ArrayBuffer;
|
|
128
|
+
}>();
|
|
129
|
+
networkInfos: INetworkInfo[] = [];
|
|
130
|
+
private bg: HTMLImageElement | null = null;
|
|
131
|
+
private destroyed = false;
|
|
132
|
+
public resource_pack: {
|
|
133
|
+
enable_robot_expression: boolean;
|
|
134
|
+
body_data_dir: string;
|
|
135
|
+
face_ani_char_data: string;
|
|
136
|
+
face_ani_preload_data: string;
|
|
137
|
+
blendshape_map?: number[][];
|
|
138
|
+
interpolate_joints: [number, number][]
|
|
139
|
+
}
|
|
140
|
+
progress = 0; // start session 以后之后资源下载进度
|
|
141
|
+
|
|
142
|
+
config: {
|
|
143
|
+
background_img: string;
|
|
144
|
+
frame_rate: number;
|
|
145
|
+
look_name: string;
|
|
146
|
+
tts_vcn_id: string;
|
|
147
|
+
sta_face_id: string;
|
|
148
|
+
mp_service_id: string;
|
|
149
|
+
raw_audio: boolean;
|
|
150
|
+
resolution: {
|
|
151
|
+
width: number;
|
|
152
|
+
height: number;
|
|
153
|
+
},
|
|
154
|
+
init_events: [
|
|
155
|
+
{
|
|
156
|
+
type: string;
|
|
157
|
+
"x_location"?: number;//控件锚点信息,控件左上角在屏幕上的横向位置。范围0-1
|
|
158
|
+
"y_location"?: number;//控件锚点信息,控件左上角在屏幕上的纵向位置。范围0-1
|
|
159
|
+
width?: number;//控件锚点信息,控件在屏幕上的宽度。范围0-1
|
|
160
|
+
height: number;//控件锚点信息,控件在屏幕上的高度。范围0-1
|
|
161
|
+
speech_id: number;
|
|
162
|
+
data?: {
|
|
163
|
+
image: string;
|
|
164
|
+
axis_id: number;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
framedata_proto_version: number;
|
|
169
|
+
layout: Layout;
|
|
170
|
+
walk_config: WalkConfig;
|
|
171
|
+
fx?: {
|
|
172
|
+
transform?: {
|
|
173
|
+
offsetX: number;
|
|
174
|
+
offsetY: number;
|
|
175
|
+
scaleX: number;
|
|
176
|
+
scaleY: number;
|
|
177
|
+
};
|
|
178
|
+
// 阴影效果
|
|
179
|
+
shadow?: {
|
|
180
|
+
// [x, y, z],阴影投射的基准点
|
|
181
|
+
refPos: number[];
|
|
182
|
+
// 光源位置
|
|
183
|
+
light: number[];
|
|
184
|
+
// 光源类型:"point"(点光源)| "directional"(平行光)
|
|
185
|
+
lightType: string;
|
|
186
|
+
// 阴影模糊半径(越大越柔和)
|
|
187
|
+
blurRadius: number;
|
|
188
|
+
// 阴影颜色
|
|
189
|
+
tint: number[];
|
|
190
|
+
// 阴影强度(0.0 = 无阴影,1.0 = 最深)
|
|
191
|
+
intensity: number;
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// 视频缓存相关
|
|
197
|
+
private videoCache = new Map<string, {
|
|
198
|
+
data: ArrayBuffer;
|
|
199
|
+
lastAccessTime: number;
|
|
200
|
+
accessCount: number;
|
|
201
|
+
}>();
|
|
202
|
+
private maxCacheSize = 10 * 1024 * 1024; // 5MB 默认缓存大小
|
|
203
|
+
private maxCacheEntries = 20; // 最大缓存条目数
|
|
204
|
+
private cacheCleanupInterval: any = null;
|
|
205
|
+
private isProcessingVideo = false; // 防止在视频处理过程中清理缓存
|
|
206
|
+
private cacheServer?: CacheManager;
|
|
207
|
+
private startSessionTimer: any = null;
|
|
208
|
+
|
|
209
|
+
// 新增:正在下载的视频跟踪
|
|
210
|
+
private downloadingVideos = new Map<string, Promise<ArrayBuffer | undefined>>();
|
|
211
|
+
first_load = true;
|
|
212
|
+
constructor(options: TOptions) {
|
|
213
|
+
this.options = options;
|
|
214
|
+
this.sdk = options.sdkInstance as XmovAvatar;
|
|
215
|
+
this.mouthShapeLib = {
|
|
216
|
+
char_info: null,
|
|
217
|
+
};
|
|
218
|
+
this.config = options.config || {};
|
|
219
|
+
this.resource_pack = {
|
|
220
|
+
enable_robot_expression: false,
|
|
221
|
+
body_data_dir: "",
|
|
222
|
+
face_ani_char_data: "",
|
|
223
|
+
face_ani_preload_data: "",
|
|
224
|
+
interpolate_joints: [[4, 3]]
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (options.cacheServer) {
|
|
228
|
+
this.cacheServer = new CacheManager(options.cacheServer);
|
|
229
|
+
}
|
|
230
|
+
// 启动缓存清理定时器
|
|
231
|
+
this.startCacheCleanup();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public getAppInfo() {
|
|
235
|
+
return {
|
|
236
|
+
appId: this.options.appId
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 加载背景图片
|
|
242
|
+
* @returns Promise<void>
|
|
243
|
+
*/
|
|
244
|
+
getBackgroundImage(): Promise<void> {
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
try {
|
|
247
|
+
const img = new Image();
|
|
248
|
+
// 设置跨域属性
|
|
249
|
+
img.crossOrigin = "anonymous";
|
|
250
|
+
img.src = this.config?.background_img || "";
|
|
251
|
+
if (!this.config?.background_img) return;
|
|
252
|
+
img.onload = () => {
|
|
253
|
+
this.bg = img;
|
|
254
|
+
resolve();
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
img.onerror = (error) => {
|
|
258
|
+
this.sdk.onMessage({
|
|
259
|
+
code: EErrorCode.BACKGROUND_IMAGE_LOAD_ERROR,
|
|
260
|
+
message: `Error: 背景图片加载失败`,
|
|
261
|
+
e: { error, img },
|
|
262
|
+
});
|
|
263
|
+
this.bg = null;
|
|
264
|
+
resolve();
|
|
265
|
+
};
|
|
266
|
+
} catch (error) {
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getIsEnableRobotExpression() {
|
|
272
|
+
return this.resource_pack?.enable_robot_expression
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 获取背景图片
|
|
277
|
+
* @returns HTMLImageElement | null
|
|
278
|
+
*/
|
|
279
|
+
getBackgroundImageElement(): HTMLImageElement | null {
|
|
280
|
+
return this.bg;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getConfig() {
|
|
284
|
+
return this.config;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async load(onDownloadProgress?: TDownloadProgress) {
|
|
288
|
+
this.destroyed = false; // 重置销毁状态,允许重新加载
|
|
289
|
+
onDownloadProgress?.(this.progress);
|
|
290
|
+
if (this.startSessionTimer) {
|
|
291
|
+
clearInterval(this.startSessionTimer);
|
|
292
|
+
this.startSessionTimer = null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.startSessionTimer = setInterval(() => {
|
|
296
|
+
if (this.destroyed || this.progress >= 14) {
|
|
297
|
+
clearInterval(this.startSessionTimer);
|
|
298
|
+
this.startSessionTimer = null;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.progress += 1;
|
|
302
|
+
onDownloadProgress?.(this.progress);
|
|
303
|
+
}, 100);
|
|
304
|
+
// http 请求算 10 进度
|
|
305
|
+
const result = await this.startSession();
|
|
306
|
+
// 如果在 startSession 过程中被销毁了,直接返回,避免继续加载资源
|
|
307
|
+
if (this.destroyed) {
|
|
308
|
+
(window as any).avatarSDKLogger.warn(this.TAG, "sdk destroyed during load, aborting after startSession");
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
if (!result) {
|
|
312
|
+
clearInterval(this.startSessionTimer);
|
|
313
|
+
this.startSessionTimer = null;
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
clearInterval(this.startSessionTimer);
|
|
318
|
+
this.startSessionTimer = null;
|
|
319
|
+
this.progress = 15;
|
|
320
|
+
onDownloadProgress?.(this.progress);
|
|
321
|
+
const res = result as unknown as ISessionResponse;
|
|
322
|
+
this.session_id = res?.session_id;
|
|
323
|
+
this.resource_pack = res.resource_pack;
|
|
324
|
+
this.config = res.config as any;
|
|
325
|
+
this.offlineIdle = res.resource_pack?.offline_idle || []
|
|
326
|
+
if (this.resource_pack) {
|
|
327
|
+
await this.loadMouthShapeLib(onDownloadProgress);
|
|
328
|
+
this.getBackgroundImage();
|
|
329
|
+
if (!this.mouthShapeLib?.char_info && res?.resource_pack?.face_ani_char_data) {
|
|
330
|
+
return null
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return res;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async startSession() {
|
|
337
|
+
try {
|
|
338
|
+
const result = await this._startSession();
|
|
339
|
+
if (result?.verify_res) {
|
|
340
|
+
this.options.onStartSessionWarning?.(result.verify_res);
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
_startSession() {
|
|
348
|
+
if (!navigator.onLine) {
|
|
349
|
+
this.sdk.onMessage({
|
|
350
|
+
code: EErrorCode.NETWORK_BREAK,
|
|
351
|
+
message: '没有网络,请联网后重试~',
|
|
352
|
+
});
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const url = `${this.options.gatewayServer}`;
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
const data = {
|
|
360
|
+
tag: this.options.tag,
|
|
361
|
+
gateway_type: this.options.gateway_type,
|
|
362
|
+
...(this.options.asr_id !== undefined ? { asr_id: this.options.asr_id } : {}),
|
|
363
|
+
...(this.options.llm_id !== undefined ? { llm_id: this.options.llm_id } : {}),
|
|
364
|
+
...(this.options.session_speak_req_id !== undefined
|
|
365
|
+
? { session_speak_req_id: this.options.session_speak_req_id }
|
|
366
|
+
: {}),
|
|
367
|
+
...(this.options.sessionRequestData ?? {}),
|
|
368
|
+
config: {
|
|
369
|
+
...this.config,
|
|
370
|
+
framedata_proto_version: 2,
|
|
371
|
+
walk_version: 3,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const { headers, data: signedData } = headersNeedSign(this.options.appId, this.options.appSecret, "POST", url, data);
|
|
375
|
+
return request(url, { method: "POST", data: signedData, headers: { ...headers, ...this.options.headers } })
|
|
376
|
+
.then(response => response.json())
|
|
377
|
+
.then(res => {
|
|
378
|
+
if (!res?.data?.resource_pack) {
|
|
379
|
+
this.sdk.onMessage({
|
|
380
|
+
code: res?.error_code,
|
|
381
|
+
message: `Error: ${res?.error_code}, ${res?.error_reason}`,
|
|
382
|
+
});
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
return res?.data;
|
|
386
|
+
}).catch(error => {
|
|
387
|
+
this.sdk.onMessage({
|
|
388
|
+
code: EErrorCode.START_SESSION_ERROR,
|
|
389
|
+
message: `Error: 启动会话失败`,
|
|
390
|
+
e: error,
|
|
391
|
+
});
|
|
392
|
+
return null;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async stopSession(stop_reason: string) {
|
|
397
|
+
try {
|
|
398
|
+
const url = `${this.options.gatewayServer}`;
|
|
399
|
+
if (!this.session_id) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const data = {
|
|
403
|
+
session_id: this.session_id,
|
|
404
|
+
stop_reason,
|
|
405
|
+
}
|
|
406
|
+
const { headers, data: signedData } = headersNeedSign(this.options.appId, this.options.appSecret, "DELETE", url, data);
|
|
407
|
+
const response = await request(url, { method: "DELETE", data: signedData, headers: { ...headers, ...this.options.headers } });
|
|
408
|
+
return await response.json();
|
|
409
|
+
} catch (error) {
|
|
410
|
+
this.sdk.onMessage({
|
|
411
|
+
code: EErrorCode.STOP_SESSION_ERROR,
|
|
412
|
+
message: `Error: 停止会话失败`,
|
|
413
|
+
e: error,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* 加载表情数据
|
|
420
|
+
*/
|
|
421
|
+
async loadMouthShapeLib(onDownloadProgress?: TDownloadProgress) {
|
|
422
|
+
try {
|
|
423
|
+
if (!this.resource_pack.face_ani_char_data) {
|
|
424
|
+
this.progress += 60;
|
|
425
|
+
onDownloadProgress?.(this.progress);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// 优先加载 .gz,失败则回退 bin
|
|
429
|
+
let char_data: ArrayBuffer | null = null;
|
|
430
|
+
let gzError: any = null;
|
|
431
|
+
let finalUrl = '';
|
|
432
|
+
let fileSize = 0;
|
|
433
|
+
let startTime = 0;
|
|
434
|
+
let preTime = 0;
|
|
435
|
+
let preLoaded = 0;
|
|
436
|
+
|
|
437
|
+
const createProgressHandler = () => (progress: number, e?: ProgressEvent) => {
|
|
438
|
+
if (!e) return;
|
|
439
|
+
const now = performance.now();
|
|
440
|
+
const deltaTime = now - preTime;
|
|
441
|
+
const deltaLoaded = e.loaded - preLoaded;
|
|
442
|
+
|
|
443
|
+
const speedMbps = (deltaTime > 0 && deltaLoaded > 0)
|
|
444
|
+
? (deltaLoaded * 8) / (deltaTime / 1000) / 1000000
|
|
445
|
+
: 0;
|
|
446
|
+
|
|
447
|
+
this.options.onNetworkInfo?.({
|
|
448
|
+
rtt: deltaTime,
|
|
449
|
+
downlink: speedMbps,
|
|
450
|
+
networkStatus: navigator.onLine,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
preLoaded = e.loaded;
|
|
454
|
+
preTime = now;
|
|
455
|
+
onDownloadProgress?.(this.progress + progress * 0.65);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const gzUrl = `${this.resource_pack.face_ani_char_data}.gz`;
|
|
460
|
+
finalUrl = gzUrl;
|
|
461
|
+
startTime = performance.now();
|
|
462
|
+
preTime = startTime;
|
|
463
|
+
preLoaded = 0;
|
|
464
|
+
|
|
465
|
+
const [, gz_data] = await XMLRequest({
|
|
466
|
+
url: gzUrl,
|
|
467
|
+
onProgress: createProgressHandler(),
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const decompressed = Pako.ungzip(new Uint8Array(gz_data as ArrayBuffer));
|
|
471
|
+
const ab = new ArrayBuffer(decompressed.byteLength);
|
|
472
|
+
new Uint8Array(ab).set(decompressed);
|
|
473
|
+
char_data = ab;
|
|
474
|
+
} catch (e) {
|
|
475
|
+
gzError = e;
|
|
476
|
+
char_data = null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!char_data) {
|
|
480
|
+
try {
|
|
481
|
+
const binUrl = this.resource_pack.face_ani_char_data;
|
|
482
|
+
finalUrl = binUrl;
|
|
483
|
+
startTime = performance.now();
|
|
484
|
+
preTime = startTime;
|
|
485
|
+
preLoaded = 0;
|
|
486
|
+
|
|
487
|
+
const [, bin_data] = await XMLRequest({
|
|
488
|
+
url: binUrl,
|
|
489
|
+
onProgress: createProgressHandler(),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
char_data = bin_data as ArrayBuffer;
|
|
493
|
+
} catch (e) {
|
|
494
|
+
if (this.first_load) {
|
|
495
|
+
this.first_load = false;
|
|
496
|
+
this.stopSession("char_bin_load_error");
|
|
497
|
+
}
|
|
498
|
+
this.sdk.onMessage?.({
|
|
499
|
+
code: EErrorCode.FACE_BIN_LOAD_ERROR,
|
|
500
|
+
message: "表情数据加载失败(.gz 和 bin 均失败)",
|
|
501
|
+
e: { gzError, binError: e },
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const totalCostMs = performance.now() - startTime;
|
|
508
|
+
const totalSpeedMbps = fileSize > 0 && totalCostMs > 0
|
|
509
|
+
? (fileSize * 8) / (totalCostMs / 1000) / 1000000
|
|
510
|
+
: 0;
|
|
511
|
+
|
|
512
|
+
this.options.onNetworkInfo?.({
|
|
513
|
+
rtt: totalCostMs,
|
|
514
|
+
downlink: totalSpeedMbps,
|
|
515
|
+
networkStatus: navigator.onLine,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
this.mouthShapeLib = {
|
|
519
|
+
char_info: new IBRAnimationGeneratorCharInfo_NN(char_data, {
|
|
520
|
+
blendshapeMap: this.resource_pack.blendshape_map || [],
|
|
521
|
+
}),
|
|
522
|
+
};
|
|
523
|
+
this.progress += 65;
|
|
524
|
+
onDownloadProgress?.(this.progress);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (this.first_load) {
|
|
527
|
+
this.first_load = false;
|
|
528
|
+
this.stopSession("char_bin_load_error");
|
|
529
|
+
}
|
|
530
|
+
this.sdk.onMessage?.({
|
|
531
|
+
code: EErrorCode.FACE_BIN_LOAD_ERROR,
|
|
532
|
+
message: "表情数据加载失败,请检查文件是否正确",
|
|
533
|
+
e: error,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
getMouthShapeLib() {
|
|
539
|
+
return this.mouthShapeLib;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
getVideoUrl(name: string) {
|
|
543
|
+
if (!this.resource_pack.body_data_dir) {
|
|
544
|
+
return "";
|
|
545
|
+
}
|
|
546
|
+
if (!name) {
|
|
547
|
+
if (this.first_load) {
|
|
548
|
+
this.first_load = false;
|
|
549
|
+
this.stopSession("no_video_name_error");
|
|
550
|
+
}
|
|
551
|
+
this.sdk.onMessage({
|
|
552
|
+
code: EErrorCode.INVALID_BODY_NAME,
|
|
553
|
+
message: `Error: 视频地址获取失败`,
|
|
554
|
+
e: name,
|
|
555
|
+
});
|
|
556
|
+
return "";
|
|
557
|
+
}
|
|
558
|
+
const url = `${this.resource_pack.body_data_dir}${name}.mp4`
|
|
559
|
+
if (this.cacheServer) {
|
|
560
|
+
return this.cacheServer?.getVideo(url);
|
|
561
|
+
}
|
|
562
|
+
return url
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* 预加载视频
|
|
567
|
+
* @param name 视频名称
|
|
568
|
+
* @returns Promise<ArrayBuffer>
|
|
569
|
+
*/
|
|
570
|
+
async preloadVideo(name: string) {
|
|
571
|
+
if (this.videoCache.has(name)) return;
|
|
572
|
+
return this.loadVideo(name);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* 加载视频
|
|
576
|
+
* @param name 视频名称
|
|
577
|
+
* @returns Promise<ArrayBuffer>
|
|
578
|
+
*/
|
|
579
|
+
async loadVideo(name: string) {
|
|
580
|
+
// 检查缓存
|
|
581
|
+
const cacheEntry = this.videoCache.get(name) || this.offlineCache.get(name);
|
|
582
|
+
if (cacheEntry) {
|
|
583
|
+
if (cacheEntry.data) {
|
|
584
|
+
this.updateCacheAccess(name);
|
|
585
|
+
return cacheEntry.data;
|
|
586
|
+
} else {
|
|
587
|
+
// 缓存条目存在但数据为空,删除这个无效条目
|
|
588
|
+
(window as any).avatarSDKLogger.warn(this.TAG, `发现无效缓存条目: ${name},正在删除`);
|
|
589
|
+
this.videoCache.delete(name);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 检查是否正在下载
|
|
594
|
+
if (this.downloadingVideos.has(name)) {
|
|
595
|
+
try {
|
|
596
|
+
const result = await this.downloadingVideos.get(name);
|
|
597
|
+
return result;
|
|
598
|
+
} catch (error) {
|
|
599
|
+
// 如果下载失败,从下载列表中移除
|
|
600
|
+
this.downloadingVideos.delete(name);
|
|
601
|
+
(window as any).avatarSDKLogger.warn(this.TAG, `${name} 视频下载失败,从下载列表移除`);
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// 开始下载
|
|
607
|
+
this.isProcessingVideo = true;
|
|
608
|
+
const downloadPromise = this.downloadVideo(name);
|
|
609
|
+
this.downloadingVideos.set(name, downloadPromise);
|
|
610
|
+
try {
|
|
611
|
+
const result = await downloadPromise;
|
|
612
|
+
return result;
|
|
613
|
+
} finally {
|
|
614
|
+
// 下载完成后从下载列表中移除
|
|
615
|
+
this.downloadingVideos.delete(name);
|
|
616
|
+
this.isProcessingVideo = false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* 实际下载视频的方法
|
|
622
|
+
* @param name 视频名称
|
|
623
|
+
* @returns Promise<ArrayBuffer>
|
|
624
|
+
*/
|
|
625
|
+
private async downloadVideo(name: string): Promise<ArrayBuffer | undefined> {
|
|
626
|
+
const url = this.getVideoUrl(name);
|
|
627
|
+
if (!url) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const { response, arrayBuffer } = await this._fetchVideo(url);
|
|
632
|
+
if (!response.ok) {
|
|
633
|
+
if (this.first_load) {
|
|
634
|
+
this.first_load = false;
|
|
635
|
+
this.stopSession("load_video_error");
|
|
636
|
+
}
|
|
637
|
+
this.sdk.onMessage({
|
|
638
|
+
code: EErrorCode.VIDEO_DOWNLOAD_ERROR,
|
|
639
|
+
message: `Error: ${name} 视频下载失败`,
|
|
640
|
+
e: { name, url, res: response },
|
|
641
|
+
});
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 将下载的数据添加到缓存
|
|
646
|
+
this.addToCache(name, arrayBuffer);
|
|
647
|
+
|
|
648
|
+
return arrayBuffer;
|
|
649
|
+
} catch (error) {
|
|
650
|
+
this.sdk.onMessage({
|
|
651
|
+
code: EErrorCode.VIDEO_DOWNLOAD_ERROR,
|
|
652
|
+
message: `Error: ${name} 视频下载失败`,
|
|
653
|
+
e: { name, url, error },
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private async _fetchVideo(url: string) {
|
|
660
|
+
if (this.networkInfos.length >= 4) {
|
|
661
|
+
const avgRtt = this.networkInfos.reduce((sum, curr) => sum + curr.rtt, 0) / this.networkInfos.length;
|
|
662
|
+
const avgDownlink = this.networkInfos.reduce((sum, curr) => sum + curr.downlink, 0) / this.networkInfos.length;
|
|
663
|
+
this.options.onNetworkInfo?.({
|
|
664
|
+
rtt: avgRtt,
|
|
665
|
+
downlink: avgDownlink,
|
|
666
|
+
networkStatus: navigator.onLine,
|
|
667
|
+
})
|
|
668
|
+
this.networkInfos.length = 0
|
|
669
|
+
}
|
|
670
|
+
const startTime = performance.now();
|
|
671
|
+
const response = await fetch(url)
|
|
672
|
+
const totalSize = parseFloat(response.headers.get('Content-Length') || '0');
|
|
673
|
+
const endTime = performance.now();
|
|
674
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
675
|
+
const rtt = endTime - startTime; // 毫秒
|
|
676
|
+
const downlink = (totalSize * 8) / (rtt / 1000 * 1000000); // Mbps
|
|
677
|
+
|
|
678
|
+
const networkInfo = {
|
|
679
|
+
rtt,
|
|
680
|
+
downlink,
|
|
681
|
+
networkStatus: navigator.onLine,
|
|
682
|
+
};
|
|
683
|
+
this.networkInfos.push(networkInfo)
|
|
684
|
+
return {
|
|
685
|
+
response,
|
|
686
|
+
arrayBuffer,
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* 添加数据到缓存
|
|
692
|
+
*/
|
|
693
|
+
private addToCache(key: string, data: ArrayBuffer) {
|
|
694
|
+
const now = Date.now();
|
|
695
|
+
|
|
696
|
+
// 检查缓存大小限制
|
|
697
|
+
let totalSize = 0;
|
|
698
|
+
for (const value of this.videoCache.values()) {
|
|
699
|
+
totalSize += value.data.byteLength;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// 如果添加这个数据会超过限制,先清理一些缓存
|
|
703
|
+
if (totalSize + data.byteLength > this.maxCacheSize || this.videoCache.size >= this.maxCacheEntries) {
|
|
704
|
+
this.cleanupCache();
|
|
705
|
+
|
|
706
|
+
// 重新计算大小
|
|
707
|
+
totalSize = 0;
|
|
708
|
+
for (const value of this.videoCache.values()) {
|
|
709
|
+
totalSize += value.data.byteLength;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// 如果仍然超过限制,且单个文件太大,不缓存这个数据
|
|
713
|
+
if (totalSize + data.byteLength > this.maxCacheSize && data.byteLength > this.maxCacheSize * 0.5) {
|
|
714
|
+
(window as any).avatarSDKLogger.warn(this.TAG, `视频文件过大(${(data.byteLength / 1024 / 1024).toFixed(2)}MB),跳过缓存: ${key}`);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// 如果缓存空间仍然不足,清理更多缓存
|
|
719
|
+
if (totalSize + data.byteLength > this.maxCacheSize) {
|
|
720
|
+
// 强制清理到50%容量
|
|
721
|
+
const targetSize = this.maxCacheSize * 0.5;
|
|
722
|
+
const sortedEntries = Array.from(this.videoCache.entries())
|
|
723
|
+
.sort((a, b) => a[1].lastAccessTime - b[1].lastAccessTime);
|
|
724
|
+
|
|
725
|
+
for (const [cacheKey, cacheValue] of sortedEntries) {
|
|
726
|
+
if (totalSize <= targetSize) break;
|
|
727
|
+
this.videoCache.delete(cacheKey);
|
|
728
|
+
totalSize -= cacheValue.data.byteLength;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// 添加到缓存
|
|
734
|
+
this.videoCache.set(key, {
|
|
735
|
+
data: data,
|
|
736
|
+
lastAccessTime: now,
|
|
737
|
+
accessCount: 1
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* 启动缓存清理定时器
|
|
743
|
+
*/
|
|
744
|
+
private startCacheCleanup() {
|
|
745
|
+
// 每5分钟清理一次缓存
|
|
746
|
+
this.cacheCleanupInterval = setInterval(() => {
|
|
747
|
+
this.cleanupCache();
|
|
748
|
+
}, 5 * 60 * 1000);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* 清理缓存
|
|
753
|
+
*/
|
|
754
|
+
private cleanupCache() {
|
|
755
|
+
// 如果正在处理视频,跳过清理
|
|
756
|
+
if (this.isProcessingVideo) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const now = Date.now();
|
|
761
|
+
const inactiveThreshold = 10 * 60 * 1000; // 10分钟不活跃的缓存将被清理
|
|
762
|
+
|
|
763
|
+
// 按最后访问时间排序
|
|
764
|
+
const sortedEntries = Array.from(this.videoCache.entries())
|
|
765
|
+
.sort((a, b) => a[1].lastAccessTime - b[1].lastAccessTime);
|
|
766
|
+
|
|
767
|
+
let totalSize = 0;
|
|
768
|
+
const entriesToRemove: string[] = [];
|
|
769
|
+
|
|
770
|
+
// 计算当前缓存总大小
|
|
771
|
+
for (const [_, value] of this.videoCache.entries()) {
|
|
772
|
+
totalSize += value.data.byteLength;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
// 清理不活跃的缓存(但保留最近访问的)
|
|
777
|
+
for (const [key, value] of sortedEntries) {
|
|
778
|
+
if (now - value.lastAccessTime > inactiveThreshold) {
|
|
779
|
+
entriesToRemove.push(key);
|
|
780
|
+
totalSize -= value.data.byteLength;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 如果缓存仍然过大,清理最旧的条目(但至少保留2个条目)
|
|
785
|
+
if (totalSize > this.maxCacheSize || this.videoCache.size > this.maxCacheEntries) {
|
|
786
|
+
for (const [key, value] of sortedEntries) {
|
|
787
|
+
if (!entriesToRemove.includes(key)) {
|
|
788
|
+
entriesToRemove.push(key);
|
|
789
|
+
totalSize -= value.data.byteLength;
|
|
790
|
+
|
|
791
|
+
// 确保至少保留2个缓存条目,防止完全清空
|
|
792
|
+
if ((this.videoCache.size - entriesToRemove.length <= 2) ||
|
|
793
|
+
(totalSize <= this.maxCacheSize * 0.8 && this.videoCache.size - entriesToRemove.length <= this.maxCacheEntries * 0.8)) {
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// 执行清理
|
|
801
|
+
for (const key of entriesToRemove) {
|
|
802
|
+
this.videoCache.delete(key);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* 更新缓存访问信息
|
|
808
|
+
*/
|
|
809
|
+
private updateCacheAccess(key: string) {
|
|
810
|
+
const cacheEntry = this.videoCache.get(key);
|
|
811
|
+
if (cacheEntry) {
|
|
812
|
+
cacheEntry.lastAccessTime = Date.now();
|
|
813
|
+
cacheEntry.accessCount++;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* 清空所有缓存
|
|
819
|
+
*/
|
|
820
|
+
clearAllCache() {
|
|
821
|
+
this.videoCache.clear();
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** 获取离线 idle */
|
|
825
|
+
_getOfflineIdle() {
|
|
826
|
+
return this.offlineIdle
|
|
827
|
+
}
|
|
828
|
+
/** 获取上一次 session_id */
|
|
829
|
+
_getSessionId() {
|
|
830
|
+
return this.session_id || ''
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** 离线重连 */
|
|
834
|
+
async _reload() {
|
|
835
|
+
const result = await this.startSession();
|
|
836
|
+
if (!result) {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
const res = result as unknown as ISessionResponse;
|
|
840
|
+
this.session_id = res?.session_id;
|
|
841
|
+
this.resource_pack = res.resource_pack;
|
|
842
|
+
this.config = res.config as any;
|
|
843
|
+
this.offlineIdle = res.resource_pack?.offline_idle || []
|
|
844
|
+
if (this.resource_pack) {
|
|
845
|
+
this.getBackgroundImage();
|
|
846
|
+
}
|
|
847
|
+
return res;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
public async _setOfflineCache() {
|
|
851
|
+
if (!this.offlineIdle || this.offlineIdle.length === 0) {
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
this.offlineCache.clear()
|
|
855
|
+
const names = this.offlineIdle.map($i => $i.n)
|
|
856
|
+
for (const name of names) {
|
|
857
|
+
try {
|
|
858
|
+
await this._runOfflineCache(name)
|
|
859
|
+
} catch (error) {
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
private async _runOfflineCache(name: string) {
|
|
864
|
+
const url = this.getVideoUrl(name)
|
|
865
|
+
if (!url) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
return this._fetchVideo(url)
|
|
869
|
+
.then(({ arrayBuffer }) => {
|
|
870
|
+
this.offlineCache.set(name, {
|
|
871
|
+
data: arrayBuffer,
|
|
872
|
+
})
|
|
873
|
+
})
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* 销毁资源管理器
|
|
878
|
+
*/
|
|
879
|
+
destroy() {
|
|
880
|
+
this.destroyed = true;
|
|
881
|
+
// 清理定时器
|
|
882
|
+
if (this.cacheCleanupInterval) {
|
|
883
|
+
clearInterval(this.cacheCleanupInterval);
|
|
884
|
+
this.cacheCleanupInterval = null;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// 清理缓存
|
|
888
|
+
this.clearAllCache();
|
|
889
|
+
|
|
890
|
+
// 清理下载跟踪
|
|
891
|
+
this.downloadingVideos.clear();
|
|
892
|
+
|
|
893
|
+
// 清理背景图片
|
|
894
|
+
if (this.bg) {
|
|
895
|
+
this.bg = null;
|
|
896
|
+
}
|
|
897
|
+
this.progress = 0
|
|
898
|
+
}
|
|
899
|
+
}
|