@ziztechnology/dial-library 0.0.1 → 0.0.3
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 +432 -1
- package/dist/index.d.mts +263 -6
- package/dist/index.mjs +1729 -1
- package/package.json +11 -6
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,1734 @@
|
|
|
1
|
+
import { Gunzip } from "fflate";
|
|
2
|
+
//#region src/sensor/check_status.ts
|
|
3
|
+
const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
|
|
4
|
+
version: "driving-status-v1",
|
|
5
|
+
maxSampleAgeMs: 1e3,
|
|
6
|
+
rapidLongitudinalThreshold: 3,
|
|
7
|
+
longitudinalThreshold: 1,
|
|
8
|
+
turnYawThreshold: .5,
|
|
9
|
+
turnLateralThreshold: 2.5,
|
|
10
|
+
motionWindowMs: 1e3,
|
|
11
|
+
motionMinimumSamples: 5,
|
|
12
|
+
drivingMotionThreshold: .4,
|
|
13
|
+
stoppedMotionThreshold: .25,
|
|
14
|
+
stoppedGyroThreshold: .1,
|
|
15
|
+
hysteresisRatio: .75,
|
|
16
|
+
smoothingWindowSize: 3,
|
|
17
|
+
confirmationSamples: Object.freeze({
|
|
18
|
+
STOPPED: 5,
|
|
19
|
+
STEADY_DRIVING: 3,
|
|
20
|
+
ACCELERATION: 2,
|
|
21
|
+
RAPID_ACCELERATION: 1,
|
|
22
|
+
BRAKING: 2,
|
|
23
|
+
SUDDEN_BRAKING: 1,
|
|
24
|
+
LEFT_TURN: 2,
|
|
25
|
+
RIGHT_TURN: 2
|
|
26
|
+
}),
|
|
27
|
+
minimumDisplayMs: Object.freeze({
|
|
28
|
+
STOPPED: 1200,
|
|
29
|
+
STEADY_DRIVING: 1200,
|
|
30
|
+
ACCELERATION: 700,
|
|
31
|
+
RAPID_ACCELERATION: 1200,
|
|
32
|
+
BRAKING: 700,
|
|
33
|
+
SUDDEN_BRAKING: 1200,
|
|
34
|
+
LEFT_TURN: 800,
|
|
35
|
+
RIGHT_TURN: 800
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
const classifyDrivingSnapshot = (info, tuningOverrides) => {
|
|
39
|
+
const tuning = resolveDrivingStatusTuning(tuningOverrides);
|
|
40
|
+
const metrics = extractDrivingSnapshotMetrics(info, tuning);
|
|
41
|
+
return metrics === null ? null : classifyDrivingMetrics(metrics, tuning);
|
|
42
|
+
};
|
|
43
|
+
const checkRunningStatus = classifyDrivingSnapshot;
|
|
44
|
+
const resolveDrivingStatusTuning = (overrides = {}) => {
|
|
45
|
+
const tuning = {
|
|
46
|
+
...DEFAULT_DRIVING_STATUS_TUNING,
|
|
47
|
+
...overrides,
|
|
48
|
+
confirmationSamples: {
|
|
49
|
+
...DEFAULT_DRIVING_STATUS_TUNING.confirmationSamples,
|
|
50
|
+
...overrides.confirmationSamples
|
|
51
|
+
},
|
|
52
|
+
minimumDisplayMs: {
|
|
53
|
+
...DEFAULT_DRIVING_STATUS_TUNING.minimumDisplayMs,
|
|
54
|
+
...overrides.minimumDisplayMs
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
assertPositiveFinite("maxSampleAgeMs", tuning.maxSampleAgeMs);
|
|
58
|
+
assertPositiveFinite("rapidLongitudinalThreshold", tuning.rapidLongitudinalThreshold);
|
|
59
|
+
assertPositiveFinite("longitudinalThreshold", tuning.longitudinalThreshold);
|
|
60
|
+
assertPositiveFinite("turnYawThreshold", tuning.turnYawThreshold);
|
|
61
|
+
assertPositiveFinite("turnLateralThreshold", tuning.turnLateralThreshold);
|
|
62
|
+
assertPositiveFinite("motionWindowMs", tuning.motionWindowMs);
|
|
63
|
+
if (!Number.isInteger(tuning.motionMinimumSamples) || tuning.motionMinimumSamples < 1) throw new RangeError("motionMinimumSamples must be a positive integer");
|
|
64
|
+
assertPositiveFinite("drivingMotionThreshold", tuning.drivingMotionThreshold);
|
|
65
|
+
assertPositiveFinite("stoppedMotionThreshold", tuning.stoppedMotionThreshold);
|
|
66
|
+
assertPositiveFinite("stoppedGyroThreshold", tuning.stoppedGyroThreshold);
|
|
67
|
+
if (tuning.drivingMotionThreshold <= tuning.stoppedMotionThreshold) throw new RangeError("drivingMotionThreshold must be greater than stoppedMotionThreshold");
|
|
68
|
+
if (!Number.isInteger(tuning.smoothingWindowSize) || tuning.smoothingWindowSize < 1) throw new RangeError("smoothingWindowSize must be a positive integer");
|
|
69
|
+
if (tuning.rapidLongitudinalThreshold < tuning.longitudinalThreshold) throw new RangeError("rapidLongitudinalThreshold must be greater than or equal to longitudinalThreshold");
|
|
70
|
+
if (!Number.isFinite(tuning.hysteresisRatio) || tuning.hysteresisRatio <= 0 || tuning.hysteresisRatio >= 1) throw new RangeError("hysteresisRatio must be greater than 0 and less than 1");
|
|
71
|
+
for (const [status, value] of Object.entries(tuning.confirmationSamples)) if (!Number.isInteger(value) || value < 1) throw new RangeError(`confirmationSamples.${status} must be a positive integer`);
|
|
72
|
+
for (const [status, value] of Object.entries(tuning.minimumDisplayMs)) if (!Number.isFinite(value) || value < 0) throw new RangeError(`minimumDisplayMs.${status} must be a non-negative finite number`);
|
|
73
|
+
return tuning;
|
|
74
|
+
};
|
|
75
|
+
const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUNING) => {
|
|
76
|
+
if (!info || !Number.isFinite(info.capturedAtMs)) return null;
|
|
77
|
+
const linearAcceleration = availableFreshVector(info.linearAcceleration, info.capturedAtMs, tuning.maxSampleAgeMs);
|
|
78
|
+
const gyroscope = availableFreshVector(info.gyroscope, info.capturedAtMs, tuning.maxSampleAgeMs);
|
|
79
|
+
if (linearAcceleration === null || gyroscope === null) return null;
|
|
80
|
+
const longitudinal = -linearAcceleration.z;
|
|
81
|
+
const lateral = linearAcceleration.x;
|
|
82
|
+
const yaw = gyroscope.y;
|
|
83
|
+
const motionIntensity = info.motionIntensity === void 0 ? vectorMagnitude(linearAcceleration) : availableFreshNumber(info.motionIntensity, info.capturedAtMs, tuning.maxSampleAgeMs);
|
|
84
|
+
if (motionIntensity === null) return null;
|
|
85
|
+
return {
|
|
86
|
+
capturedAtMs: info.capturedAtMs,
|
|
87
|
+
longitudinal,
|
|
88
|
+
lateral,
|
|
89
|
+
yaw,
|
|
90
|
+
motionIntensity,
|
|
91
|
+
gyroMagnitude: vectorMagnitude(gyroscope)
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const classifyDrivingMetrics = (metrics, tuning = DEFAULT_DRIVING_STATUS_TUNING) => {
|
|
95
|
+
const { longitudinal, lateral, yaw, motionIntensity, gyroMagnitude } = metrics;
|
|
96
|
+
if (longitudinal <= -tuning.rapidLongitudinalThreshold) return "SUDDEN_BRAKING";
|
|
97
|
+
if (longitudinal >= tuning.rapidLongitudinalThreshold) return "RAPID_ACCELERATION";
|
|
98
|
+
if (Math.abs(yaw) >= tuning.turnYawThreshold) return yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
|
|
99
|
+
if (Math.abs(lateral) >= tuning.turnLateralThreshold) return lateral < 0 ? "LEFT_TURN" : "RIGHT_TURN";
|
|
100
|
+
if (longitudinal <= -tuning.longitudinalThreshold) return "BRAKING";
|
|
101
|
+
if (longitudinal >= tuning.longitudinalThreshold) return "ACCELERATION";
|
|
102
|
+
if (motionIntensity < tuning.drivingMotionThreshold && gyroMagnitude <= tuning.stoppedGyroThreshold) return "STOPPED";
|
|
103
|
+
return "STEADY_DRIVING";
|
|
104
|
+
};
|
|
105
|
+
const availableFreshVector = (metric, capturedAtMs, maxSampleAgeMs) => {
|
|
106
|
+
if (!metric?.available || !isFresh(metric.sampledAtMs, capturedAtMs, maxSampleAgeMs)) return null;
|
|
107
|
+
const { x, y, z } = metric.value;
|
|
108
|
+
return [
|
|
109
|
+
x,
|
|
110
|
+
y,
|
|
111
|
+
z
|
|
112
|
+
].every(Number.isFinite) ? metric.value : null;
|
|
113
|
+
};
|
|
114
|
+
const availableFreshNumber = (metric, capturedAtMs, maxSampleAgeMs) => {
|
|
115
|
+
if (!metric?.available || !isFresh(metric.sampledAtMs, capturedAtMs, maxSampleAgeMs)) return null;
|
|
116
|
+
return Number.isFinite(metric.value) && metric.value >= 0 ? metric.value : null;
|
|
117
|
+
};
|
|
118
|
+
const isFresh = (sampledAtMs, capturedAtMs, maxSampleAgeMs) => {
|
|
119
|
+
if (sampledAtMs === null || !Number.isFinite(sampledAtMs)) return false;
|
|
120
|
+
const age = capturedAtMs - sampledAtMs;
|
|
121
|
+
return age >= 0 && age <= maxSampleAgeMs;
|
|
122
|
+
};
|
|
123
|
+
const vectorMagnitude = ({ x, y, z }) => Math.hypot(x, y, z);
|
|
124
|
+
const assertPositiveFinite = (name, value) => {
|
|
125
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be a positive finite number`);
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
1
128
|
//#region src/sensor/unified_info.ts
|
|
2
129
|
const unifiedSensorInfo = () => {
|
|
3
130
|
return window.unifiedSensorInfo();
|
|
4
131
|
};
|
|
5
132
|
//#endregion
|
|
6
|
-
|
|
133
|
+
//#region src/sensor/constants.ts
|
|
134
|
+
const BATTERY_HEALTH_LABELS = {
|
|
135
|
+
GOOD: "良好",
|
|
136
|
+
OVERHEAT: "过热",
|
|
137
|
+
DEAD: "失效",
|
|
138
|
+
OVER_VOLTAGE: "电压过高",
|
|
139
|
+
UNSPECIFIED_FAILURE: "异常",
|
|
140
|
+
COLD: "温度过低",
|
|
141
|
+
UNKNOWN: "未知"
|
|
142
|
+
};
|
|
143
|
+
const CAR_RUNNING_LABELS = {
|
|
144
|
+
STOPPED: "静止",
|
|
145
|
+
STEADY_DRIVING: "匀速行驶",
|
|
146
|
+
SUDDEN_BRAKING: "急刹车",
|
|
147
|
+
BRAKING: "刹车",
|
|
148
|
+
LEFT_TURN: "左转",
|
|
149
|
+
RIGHT_TURN: "右转",
|
|
150
|
+
ACCELERATION: "加速",
|
|
151
|
+
RAPID_ACCELERATION: "急加速"
|
|
152
|
+
};
|
|
153
|
+
const CAR_RUNNING_STATUSES = [
|
|
154
|
+
"STOPPED",
|
|
155
|
+
"STEADY_DRIVING",
|
|
156
|
+
"ACCELERATION",
|
|
157
|
+
"RAPID_ACCELERATION",
|
|
158
|
+
"BRAKING",
|
|
159
|
+
"SUDDEN_BRAKING",
|
|
160
|
+
"LEFT_TURN",
|
|
161
|
+
"RIGHT_TURN"
|
|
162
|
+
];
|
|
163
|
+
const CAR_RUNNING_STATUS_PRIORITY = {
|
|
164
|
+
STOPPED: 0,
|
|
165
|
+
STEADY_DRIVING: 0,
|
|
166
|
+
ACCELERATION: 1,
|
|
167
|
+
BRAKING: 2,
|
|
168
|
+
LEFT_TURN: 3,
|
|
169
|
+
RIGHT_TURN: 3,
|
|
170
|
+
RAPID_ACCELERATION: 4,
|
|
171
|
+
SUDDEN_BRAKING: 5
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/sensor/driving_expressions.ts
|
|
175
|
+
const OFFICIAL_DRIVING_EXPRESSION_FALLBACKS = Object.freeze({
|
|
176
|
+
STOPPED: Object.freeze({
|
|
177
|
+
kind: "emoji",
|
|
178
|
+
text: "😐"
|
|
179
|
+
}),
|
|
180
|
+
STEADY_DRIVING: Object.freeze({
|
|
181
|
+
kind: "emoji",
|
|
182
|
+
text: "😊"
|
|
183
|
+
}),
|
|
184
|
+
ACCELERATION: Object.freeze({
|
|
185
|
+
kind: "emoji",
|
|
186
|
+
text: "😄"
|
|
187
|
+
}),
|
|
188
|
+
RAPID_ACCELERATION: Object.freeze({
|
|
189
|
+
kind: "emoji",
|
|
190
|
+
text: "😲"
|
|
191
|
+
}),
|
|
192
|
+
BRAKING: Object.freeze({
|
|
193
|
+
kind: "emoji",
|
|
194
|
+
text: "😣"
|
|
195
|
+
}),
|
|
196
|
+
SUDDEN_BRAKING: Object.freeze({
|
|
197
|
+
kind: "emoji",
|
|
198
|
+
text: "😱"
|
|
199
|
+
}),
|
|
200
|
+
LEFT_TURN: Object.freeze({
|
|
201
|
+
kind: "emoji",
|
|
202
|
+
text: "🤨"
|
|
203
|
+
}),
|
|
204
|
+
RIGHT_TURN: Object.freeze({
|
|
205
|
+
kind: "emoji",
|
|
206
|
+
text: "🤨"
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
const OFFICIAL_DRIVING_EXPRESSIONS_CONFIG = Object.freeze({
|
|
210
|
+
schemaVersion: 1,
|
|
211
|
+
states: OFFICIAL_DRIVING_EXPRESSION_FALLBACKS
|
|
212
|
+
});
|
|
213
|
+
var DrivingExpressionsConfigError = class extends Error {
|
|
214
|
+
constructor(code, message, state) {
|
|
215
|
+
super(message);
|
|
216
|
+
this.name = "DrivingExpressionsConfigError";
|
|
217
|
+
this.code = code;
|
|
218
|
+
this.state = state;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
var DrivingExpressionResolutionError = class extends Error {
|
|
222
|
+
constructor(status) {
|
|
223
|
+
super(`No driving expression is configured for ${status}`);
|
|
224
|
+
this.name = "DrivingExpressionResolutionError";
|
|
225
|
+
this.status = status;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const parseDrivingExpressionsConfig = (raw, options = {}) => {
|
|
229
|
+
const parsed = parseJsonObject(raw);
|
|
230
|
+
if (parsed.schemaVersion !== 1 || !isRecord$1(parsed.states)) throw new DrivingExpressionsConfigError("DRIVING_EXPRESSIONS_INVALID_SCHEMA", "drivingExpressions must use schemaVersion 1 and contain a states object");
|
|
231
|
+
const knownStatuses = new Set(CAR_RUNNING_STATUSES);
|
|
232
|
+
const stateKeys = Object.keys(parsed.states);
|
|
233
|
+
if (stateKeys.length !== CAR_RUNNING_STATUSES.length || stateKeys.some((key) => !knownStatuses.has(key))) throw new DrivingExpressionsConfigError("DRIVING_EXPRESSIONS_INCOMPLETE", "drivingExpressions.states must contain exactly the eight supported statuses");
|
|
234
|
+
const states = {};
|
|
235
|
+
for (const status of CAR_RUNNING_STATUSES) {
|
|
236
|
+
if (!Object.prototype.hasOwnProperty.call(parsed.states, status)) throw new DrivingExpressionsConfigError("DRIVING_EXPRESSIONS_INCOMPLETE", `drivingExpressions.states is missing ${status}`, status);
|
|
237
|
+
states[status] = parseDrivingExpressionMedia(parsed.states[status], status, options);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
schemaVersion: 1,
|
|
241
|
+
states
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
const tryParseDrivingExpressionsConfig = (raw, options) => {
|
|
245
|
+
try {
|
|
246
|
+
return parseDrivingExpressionsConfig(raw, options);
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Reads Runtime's typed injection first, then the legacy face-config value.
|
|
253
|
+
* It never installs official fallbacks implicitly.
|
|
254
|
+
*/
|
|
255
|
+
const readDrivingExpressionsConfig = (raw, options) => {
|
|
256
|
+
const source = raw === void 0 ? readRuntimeDrivingExpressionsValue() : raw;
|
|
257
|
+
return source === void 0 ? null : tryParseDrivingExpressionsConfig(source, options);
|
|
258
|
+
};
|
|
259
|
+
/** Original resolver behavior: missing legacy slots fall back to STEADY_DRIVING. */
|
|
260
|
+
const resolveDrivingExpression = (config, status) => config.states[status] ?? config.states.STEADY_DRIVING;
|
|
261
|
+
/** Strict resolver used by the player; official fallbacks are opt-in. */
|
|
262
|
+
const resolveDrivingExpressionStrict = (config, status, fallbacks) => {
|
|
263
|
+
const configured = config.states[status];
|
|
264
|
+
if (configured !== void 0) return configured;
|
|
265
|
+
const fallback = fallbacks?.[status];
|
|
266
|
+
if (fallback !== void 0) return fallback;
|
|
267
|
+
throw new DrivingExpressionResolutionError(status);
|
|
268
|
+
};
|
|
269
|
+
const isPublicDrivingMediaUrl = (value) => {
|
|
270
|
+
try {
|
|
271
|
+
const url = new URL(value);
|
|
272
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "") return false;
|
|
273
|
+
if (!isPublicHostname(url.hostname)) return false;
|
|
274
|
+
for (const key of url.searchParams.keys()) if (TEMPORARY_SIGNATURE_QUERY_KEYS.has(key.toLowerCase())) return false;
|
|
275
|
+
return true;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const isPublicHostname = (rawHostname) => {
|
|
281
|
+
const hostname = rawHostname.toLowerCase().replace(/^\[|\]$/gu, "").replace(/\.+$/gu, "");
|
|
282
|
+
if (hostname === "") return false;
|
|
283
|
+
const ipv4 = parseIpv4(hostname);
|
|
284
|
+
if (ipv4 !== null) return !isRestrictedIpv4(ipv4);
|
|
285
|
+
const ipv6 = parseIpv6(hostname);
|
|
286
|
+
if (ipv6 !== null) return !isRestrictedIpv6(ipv6);
|
|
287
|
+
if (!hostname.includes(".")) return false;
|
|
288
|
+
return !INTERNAL_HOST_SUFFIXES.some((suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`));
|
|
289
|
+
};
|
|
290
|
+
const INTERNAL_HOST_SUFFIXES = [
|
|
291
|
+
"corp",
|
|
292
|
+
"home",
|
|
293
|
+
"home.arpa",
|
|
294
|
+
"internal",
|
|
295
|
+
"intranet",
|
|
296
|
+
"lan",
|
|
297
|
+
"local",
|
|
298
|
+
"localdomain",
|
|
299
|
+
"localhost",
|
|
300
|
+
"private"
|
|
301
|
+
];
|
|
302
|
+
const parseIpv4 = (hostname) => {
|
|
303
|
+
const parts = hostname.split(".");
|
|
304
|
+
if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/u.test(part))) return null;
|
|
305
|
+
const octets = parts.map(Number);
|
|
306
|
+
if (octets.some((octet) => octet > 255)) return null;
|
|
307
|
+
return octets;
|
|
308
|
+
};
|
|
309
|
+
const isRestrictedIpv4 = ([a, b, c]) => a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 0 && c === 0 || a === 192 && b === 0 && c === 2 || a === 192 && b === 31 && c === 196 || a === 192 && b === 52 && c === 193 || a === 192 && b === 88 && c === 99 || a === 192 && b === 168 || a === 192 && b === 175 && c === 48 || a === 198 && (b === 18 || b === 19) || a === 198 && b === 51 && c === 100 || a === 203 && b === 0 && c === 113 || a >= 224;
|
|
310
|
+
const parseIpv6 = (hostname) => {
|
|
311
|
+
if (!hostname.includes(":")) return null;
|
|
312
|
+
let normalized = hostname;
|
|
313
|
+
if (normalized.includes(".")) {
|
|
314
|
+
const lastColon = normalized.lastIndexOf(":");
|
|
315
|
+
const ipv4 = parseIpv4(normalized.slice(lastColon + 1));
|
|
316
|
+
if (ipv4 === null) return null;
|
|
317
|
+
normalized = `${normalized.slice(0, lastColon)}:${(ipv4[0] << 8 | ipv4[1]).toString(16)}:${(ipv4[2] << 8 | ipv4[3]).toString(16)}`;
|
|
318
|
+
}
|
|
319
|
+
const halves = normalized.split("::");
|
|
320
|
+
if (halves.length > 2) return null;
|
|
321
|
+
const left = halves[0] === "" ? [] : halves[0].split(":");
|
|
322
|
+
const right = halves.length === 1 || halves[1] === "" ? [] : halves[1].split(":");
|
|
323
|
+
if ([...left, ...right].some((part) => !/^[\da-f]{1,4}$/u.test(part))) return null;
|
|
324
|
+
const omitted = 8 - left.length - right.length;
|
|
325
|
+
if (halves.length === 1 && omitted !== 0 || halves.length === 2 && omitted < 1) return null;
|
|
326
|
+
const groups = [
|
|
327
|
+
...left,
|
|
328
|
+
...Array.from({ length: omitted }, () => "0"),
|
|
329
|
+
...right
|
|
330
|
+
].map((part) => Number.parseInt(part, 16));
|
|
331
|
+
if (groups.length !== 8) return null;
|
|
332
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
333
|
+
groups.forEach((group, index) => {
|
|
334
|
+
bytes[index * 2] = group >>> 8;
|
|
335
|
+
bytes[index * 2 + 1] = group & 255;
|
|
336
|
+
});
|
|
337
|
+
return bytes;
|
|
338
|
+
};
|
|
339
|
+
const isRestrictedIpv6 = (bytes) => {
|
|
340
|
+
const allZeroPrefix = (length) => bytes.subarray(0, length).every((value) => value === 0);
|
|
341
|
+
if (bytes.every((value) => value === 0)) return true;
|
|
342
|
+
if (allZeroPrefix(15) && bytes[15] === 1) return true;
|
|
343
|
+
if (allZeroPrefix(12)) return true;
|
|
344
|
+
if (allZeroPrefix(10) && bytes[10] === 255 && bytes[11] === 255) return true;
|
|
345
|
+
if (bytes[0] === 0 && bytes[1] === 100 && bytes[2] === 255 && bytes[3] === 155) return true;
|
|
346
|
+
if ((bytes[0] & 254) === 252) return true;
|
|
347
|
+
if (bytes[0] === 254 && (bytes[1] & 192) === 128) return true;
|
|
348
|
+
if (bytes[0] === 255) return true;
|
|
349
|
+
if (bytes[0] === 1 && bytes.subarray(1, 8).every((value) => value === 0)) return true;
|
|
350
|
+
if (bytes[0] === 32 && bytes[1] === 1 && bytes[2] === 13 && bytes[3] === 184) return true;
|
|
351
|
+
if (bytes[0] === 32 && bytes[1] === 1 && bytes[2] <= 1) return true;
|
|
352
|
+
if (bytes[0] === 32 && bytes[1] === 2) return true;
|
|
353
|
+
return bytes[0] === 63 && bytes[1] === 254;
|
|
354
|
+
};
|
|
355
|
+
const TEMPORARY_SIGNATURE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
356
|
+
"expires",
|
|
357
|
+
"googleaccessid",
|
|
358
|
+
"ossaccesskeyid",
|
|
359
|
+
"q-ak",
|
|
360
|
+
"q-key-time",
|
|
361
|
+
"q-sign-algorithm",
|
|
362
|
+
"q-sign-time",
|
|
363
|
+
"q-signature",
|
|
364
|
+
"security-token",
|
|
365
|
+
"sig",
|
|
366
|
+
"signature",
|
|
367
|
+
"token",
|
|
368
|
+
"x-amz-algorithm",
|
|
369
|
+
"x-amz-credential",
|
|
370
|
+
"x-amz-date",
|
|
371
|
+
"x-amz-expires",
|
|
372
|
+
"x-amz-security-token",
|
|
373
|
+
"x-amz-signature",
|
|
374
|
+
"x-amz-signedheaders",
|
|
375
|
+
"x-cos-security-token",
|
|
376
|
+
"x-cos-signature",
|
|
377
|
+
"x-goog-algorithm",
|
|
378
|
+
"x-goog-credential",
|
|
379
|
+
"x-goog-date",
|
|
380
|
+
"x-goog-expires",
|
|
381
|
+
"x-goog-signature",
|
|
382
|
+
"x-goog-signedheaders",
|
|
383
|
+
"x-oss-credential",
|
|
384
|
+
"x-oss-expires",
|
|
385
|
+
"x-oss-security-token",
|
|
386
|
+
"x-oss-signature"
|
|
387
|
+
]);
|
|
388
|
+
const parseDrivingExpressionMedia = (raw, status, options) => {
|
|
389
|
+
if (typeof raw === "string" && options.allowLegacyImageUrls) {
|
|
390
|
+
assertPublicUrl(raw, status, "url");
|
|
391
|
+
return {
|
|
392
|
+
kind: "image",
|
|
393
|
+
url: raw
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (!isRecord$1(raw) || typeof raw.kind !== "string") throw invalidMedia(status, "media must be a structured object with a supported kind");
|
|
397
|
+
const mediaAssetId = parseMediaAssetId(raw.mediaAssetId, status);
|
|
398
|
+
assertFieldsAbsent(raw, status, [
|
|
399
|
+
"items",
|
|
400
|
+
"urls",
|
|
401
|
+
"duration"
|
|
402
|
+
]);
|
|
403
|
+
switch (raw.kind) {
|
|
404
|
+
case "emoji":
|
|
405
|
+
assertFieldsAbsent(raw, status, [
|
|
406
|
+
"mediaAssetId",
|
|
407
|
+
"url",
|
|
408
|
+
"coverUrl",
|
|
409
|
+
"motionUrl",
|
|
410
|
+
"mimeType"
|
|
411
|
+
]);
|
|
412
|
+
if (typeof raw.text !== "string" || !isOneVisibleGrapheme(raw.text)) throw invalidMedia(status, "emoji.text must contain exactly one visible grapheme");
|
|
413
|
+
return {
|
|
414
|
+
kind: "emoji",
|
|
415
|
+
text: raw.text.trim()
|
|
416
|
+
};
|
|
417
|
+
case "image": {
|
|
418
|
+
assertFieldsAbsent(raw, status, [
|
|
419
|
+
"text",
|
|
420
|
+
"coverUrl",
|
|
421
|
+
"motionUrl"
|
|
422
|
+
]);
|
|
423
|
+
const url = parseRequiredUrl(raw.url, status, "url");
|
|
424
|
+
const mimeType = parseOptionalString(raw.mimeType, status, "mimeType");
|
|
425
|
+
return compact({
|
|
426
|
+
kind: "image",
|
|
427
|
+
mediaAssetId,
|
|
428
|
+
url,
|
|
429
|
+
mimeType
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
case "video": {
|
|
433
|
+
assertFieldsAbsent(raw, status, ["text", "motionUrl"]);
|
|
434
|
+
const url = parseRequiredUrl(raw.url, status, "url");
|
|
435
|
+
const coverUrl = parseOptionalUrl(raw.coverUrl, status, "coverUrl");
|
|
436
|
+
const mimeType = parseOptionalString(raw.mimeType, status, "mimeType");
|
|
437
|
+
return compact({
|
|
438
|
+
kind: "video",
|
|
439
|
+
mediaAssetId,
|
|
440
|
+
url,
|
|
441
|
+
coverUrl,
|
|
442
|
+
mimeType
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
case "live_photo": {
|
|
446
|
+
assertFieldsAbsent(raw, status, [
|
|
447
|
+
"text",
|
|
448
|
+
"url",
|
|
449
|
+
"mimeType"
|
|
450
|
+
]);
|
|
451
|
+
const coverUrl = parseRequiredUrl(raw.coverUrl, status, "coverUrl");
|
|
452
|
+
const motionUrl = parseRequiredUrl(raw.motionUrl, status, "motionUrl");
|
|
453
|
+
return compact({
|
|
454
|
+
kind: "live_photo",
|
|
455
|
+
mediaAssetId,
|
|
456
|
+
coverUrl,
|
|
457
|
+
motionUrl
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
case "tgs": {
|
|
461
|
+
assertFieldsAbsent(raw, status, [
|
|
462
|
+
"text",
|
|
463
|
+
"motionUrl",
|
|
464
|
+
"mimeType"
|
|
465
|
+
]);
|
|
466
|
+
const url = parseRequiredUrl(raw.url, status, "url");
|
|
467
|
+
const coverUrl = parseOptionalUrl(raw.coverUrl, status, "coverUrl");
|
|
468
|
+
return compact({
|
|
469
|
+
kind: "tgs",
|
|
470
|
+
mediaAssetId,
|
|
471
|
+
url,
|
|
472
|
+
coverUrl
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
default: throw invalidMedia(status, `unsupported media kind: ${raw.kind}`);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const parseJsonObject = (raw) => {
|
|
479
|
+
let value = raw;
|
|
480
|
+
if (typeof value === "string") try {
|
|
481
|
+
value = JSON.parse(value);
|
|
482
|
+
} catch {
|
|
483
|
+
throw new DrivingExpressionsConfigError("DRIVING_EXPRESSIONS_INVALID_SCHEMA", "drivingExpressions is not valid JSON");
|
|
484
|
+
}
|
|
485
|
+
if (!isRecord$1(value)) throw new DrivingExpressionsConfigError("DRIVING_EXPRESSIONS_INVALID_SCHEMA", "drivingExpressions must be a JSON object or JSON object string");
|
|
486
|
+
return value;
|
|
487
|
+
};
|
|
488
|
+
const readRuntimeDrivingExpressionsValue = () => {
|
|
489
|
+
const runtimeGlobal = globalThis;
|
|
490
|
+
if (runtimeGlobal.__TOOOONY_DRIVING_EXPRESSIONS__ !== void 0) return runtimeGlobal.__TOOOONY_DRIVING_EXPRESSIONS__;
|
|
491
|
+
const faceConfig = runtimeGlobal.__TOOOONY_FACE_CONFIG__;
|
|
492
|
+
if (!isRecord$1(faceConfig)) return void 0;
|
|
493
|
+
if (faceConfig.drivingExpressions !== void 0) return faceConfig.drivingExpressions;
|
|
494
|
+
return isRecord$1(faceConfig.values) ? faceConfig.values.drivingExpressions : void 0;
|
|
495
|
+
};
|
|
496
|
+
const parseMediaAssetId = (raw, status) => {
|
|
497
|
+
if (raw === void 0) return void 0;
|
|
498
|
+
if (!Number.isSafeInteger(raw) || raw <= 0) throw invalidMedia(status, "mediaAssetId must be a positive safe integer when present");
|
|
499
|
+
return raw;
|
|
500
|
+
};
|
|
501
|
+
const assertFieldsAbsent = (value, status, fields) => {
|
|
502
|
+
const present = fields.find((field) => Object.prototype.hasOwnProperty.call(value, field));
|
|
503
|
+
if (present !== void 0) throw invalidMedia(status, `${present} is not allowed for this media kind`);
|
|
504
|
+
};
|
|
505
|
+
const parseRequiredUrl = (raw, status, field) => {
|
|
506
|
+
if (typeof raw !== "string" || raw.length === 0) throw invalidMedia(status, `${field} is required`);
|
|
507
|
+
assertPublicUrl(raw, status, field);
|
|
508
|
+
return raw;
|
|
509
|
+
};
|
|
510
|
+
const parseOptionalUrl = (raw, status, field) => {
|
|
511
|
+
if (raw === void 0) return void 0;
|
|
512
|
+
return parseRequiredUrl(raw, status, field);
|
|
513
|
+
};
|
|
514
|
+
const parseOptionalString = (raw, status, field) => {
|
|
515
|
+
if (raw === void 0) return void 0;
|
|
516
|
+
if (typeof raw !== "string" || raw.trim() === "" || raw.length > 255) throw invalidMedia(status, `${field} must be a non-empty string of at most 255 characters`);
|
|
517
|
+
return raw;
|
|
518
|
+
};
|
|
519
|
+
const assertPublicUrl = (value, status, field) => {
|
|
520
|
+
if (!isPublicDrivingMediaUrl(value)) throw new DrivingExpressionsConfigError("MEDIA_URL_NOT_PUBLIC_HTTPS", `${status}.${field} must be a long-lived public HTTPS URL`, status);
|
|
521
|
+
};
|
|
522
|
+
const isOneVisibleGrapheme = (value) => {
|
|
523
|
+
const trimmed = value.trim();
|
|
524
|
+
if (trimmed === "" || trimmed.length > 32) return false;
|
|
525
|
+
const Segmenter = Intl.Segmenter;
|
|
526
|
+
return (Segmenter ? Array.from(new Segmenter(void 0, { granularity: "grapheme" }).segment(trimmed)) : Array.from(trimmed)).length === 1;
|
|
527
|
+
};
|
|
528
|
+
const invalidMedia = (status, detail) => new DrivingExpressionsConfigError("DRIVING_EXPRESSION_INVALID_MEDIA", `Invalid driving expression for ${status}: ${detail}`, status);
|
|
529
|
+
const compact = (value) => {
|
|
530
|
+
for (const key of Object.keys(value)) if (value[key] === void 0) delete value[key];
|
|
531
|
+
return value;
|
|
532
|
+
};
|
|
533
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/sensor/page_lifecycle.ts
|
|
536
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
537
|
+
let installedWindow = null;
|
|
538
|
+
let installedDocument = null;
|
|
539
|
+
let previousPauseHook;
|
|
540
|
+
let previousResumeHook;
|
|
541
|
+
let hadOwnPauseHook = false;
|
|
542
|
+
let hadOwnResumeHook = false;
|
|
543
|
+
let pauseHook = null;
|
|
544
|
+
let resumeHook = null;
|
|
545
|
+
const subscribeDrivingPageLifecycle = (pause, resume) => {
|
|
546
|
+
const subscriber = {
|
|
547
|
+
pause,
|
|
548
|
+
resume
|
|
549
|
+
};
|
|
550
|
+
subscribers.add(subscriber);
|
|
551
|
+
if (subscribers.size === 1) installLifecycleHooks();
|
|
552
|
+
if (isDrivingPageHidden()) safeInvoke(pause);
|
|
553
|
+
let subscribed = true;
|
|
554
|
+
return () => {
|
|
555
|
+
if (!subscribed) return;
|
|
556
|
+
subscribed = false;
|
|
557
|
+
subscribers.delete(subscriber);
|
|
558
|
+
if (subscribers.size === 0) uninstallLifecycleHooks();
|
|
559
|
+
};
|
|
560
|
+
};
|
|
561
|
+
const isDrivingPageHidden = () => typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
562
|
+
const installLifecycleHooks = () => {
|
|
563
|
+
installedDocument = typeof document === "undefined" ? null : document;
|
|
564
|
+
installedWindow = typeof window === "undefined" ? null : window;
|
|
565
|
+
installedDocument?.addEventListener("visibilitychange", handleVisibilityChange);
|
|
566
|
+
installedWindow?.addEventListener("pagehide", dispatchPause);
|
|
567
|
+
installedWindow?.addEventListener("freeze", dispatchPause);
|
|
568
|
+
installedWindow?.addEventListener("pageshow", dispatchResume);
|
|
569
|
+
installedWindow?.addEventListener("resume", dispatchResume);
|
|
570
|
+
installedWindow?.addEventListener("toooony-device-sensors-release", dispatchPause);
|
|
571
|
+
installedWindow?.addEventListener("toooony-device-sensors-resume", dispatchResume);
|
|
572
|
+
if (!installedWindow) return;
|
|
573
|
+
hadOwnPauseHook = Object.prototype.hasOwnProperty.call(installedWindow, "__watchFaceRuntimePause");
|
|
574
|
+
hadOwnResumeHook = Object.prototype.hasOwnProperty.call(installedWindow, "__watchFaceRuntimeResume");
|
|
575
|
+
previousPauseHook = installedWindow.__watchFaceRuntimePause;
|
|
576
|
+
previousResumeHook = installedWindow.__watchFaceRuntimeResume;
|
|
577
|
+
pauseHook = function(...args) {
|
|
578
|
+
try {
|
|
579
|
+
return typeof previousPauseHook === "function" ? previousPauseHook.apply(this, args) : void 0;
|
|
580
|
+
} finally {
|
|
581
|
+
dispatchPause();
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
resumeHook = function(...args) {
|
|
585
|
+
try {
|
|
586
|
+
return typeof previousResumeHook === "function" ? previousResumeHook.apply(this, args) : void 0;
|
|
587
|
+
} finally {
|
|
588
|
+
dispatchResume();
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
installedWindow.__watchFaceRuntimePause = pauseHook;
|
|
592
|
+
installedWindow.__watchFaceRuntimeResume = resumeHook;
|
|
593
|
+
};
|
|
594
|
+
const uninstallLifecycleHooks = () => {
|
|
595
|
+
installedDocument?.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
596
|
+
installedWindow?.removeEventListener("pagehide", dispatchPause);
|
|
597
|
+
installedWindow?.removeEventListener("freeze", dispatchPause);
|
|
598
|
+
installedWindow?.removeEventListener("pageshow", dispatchResume);
|
|
599
|
+
installedWindow?.removeEventListener("resume", dispatchResume);
|
|
600
|
+
installedWindow?.removeEventListener("toooony-device-sensors-release", dispatchPause);
|
|
601
|
+
installedWindow?.removeEventListener("toooony-device-sensors-resume", dispatchResume);
|
|
602
|
+
if (installedWindow?.__watchFaceRuntimePause === pauseHook) if (hadOwnPauseHook) installedWindow.__watchFaceRuntimePause = previousPauseHook;
|
|
603
|
+
else delete installedWindow.__watchFaceRuntimePause;
|
|
604
|
+
if (installedWindow?.__watchFaceRuntimeResume === resumeHook) if (hadOwnResumeHook) installedWindow.__watchFaceRuntimeResume = previousResumeHook;
|
|
605
|
+
else delete installedWindow.__watchFaceRuntimeResume;
|
|
606
|
+
installedWindow = null;
|
|
607
|
+
installedDocument = null;
|
|
608
|
+
previousPauseHook = void 0;
|
|
609
|
+
previousResumeHook = void 0;
|
|
610
|
+
hadOwnPauseHook = false;
|
|
611
|
+
hadOwnResumeHook = false;
|
|
612
|
+
pauseHook = null;
|
|
613
|
+
resumeHook = null;
|
|
614
|
+
};
|
|
615
|
+
const handleVisibilityChange = () => {
|
|
616
|
+
if (isDrivingPageHidden()) dispatchPause();
|
|
617
|
+
else dispatchResume();
|
|
618
|
+
};
|
|
619
|
+
const dispatchPause = () => {
|
|
620
|
+
for (const subscriber of [...subscribers]) safeInvoke(subscriber.pause);
|
|
621
|
+
};
|
|
622
|
+
const dispatchResume = () => {
|
|
623
|
+
if (isDrivingPageHidden()) return;
|
|
624
|
+
for (const subscriber of [...subscribers]) safeInvoke(subscriber.resume);
|
|
625
|
+
};
|
|
626
|
+
const safeInvoke = (callback) => {
|
|
627
|
+
try {
|
|
628
|
+
callback();
|
|
629
|
+
} catch {}
|
|
630
|
+
};
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/sensor/driving_status_controller.ts
|
|
633
|
+
const createDrivingStatusController = (options = {}) => new DefaultDrivingStatusController(options);
|
|
634
|
+
var DefaultDrivingStatusController = class {
|
|
635
|
+
constructor(options) {
|
|
636
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
637
|
+
this.metricWindow = [];
|
|
638
|
+
this.motionWindow = [];
|
|
639
|
+
this.candidate = null;
|
|
640
|
+
this.candidateConfirmationCount = 0;
|
|
641
|
+
this.lastAvailableAtMs = null;
|
|
642
|
+
this.unavailableSinceMs = null;
|
|
643
|
+
this.fallbackApplied = false;
|
|
644
|
+
this.heldCandidate = null;
|
|
645
|
+
this.requestedRunning = false;
|
|
646
|
+
this.polling = false;
|
|
647
|
+
this.lifecycleSuspended = false;
|
|
648
|
+
this.destroyed = false;
|
|
649
|
+
this.generation = 0;
|
|
650
|
+
this.pendingRead = null;
|
|
651
|
+
this.unsubscribePageLifecycle = null;
|
|
652
|
+
this.handleLifecyclePause = () => {
|
|
653
|
+
if (!this.requestedRunning || this.lifecycleSuspended) return;
|
|
654
|
+
this.lifecycleSuspended = true;
|
|
655
|
+
this.deactivatePolling();
|
|
656
|
+
this.resetTransientRecognition();
|
|
657
|
+
};
|
|
658
|
+
this.handleLifecycleResume = () => {
|
|
659
|
+
if (!this.requestedRunning || !this.lifecycleSuspended || this.destroyed || isDrivingPageHidden()) return;
|
|
660
|
+
this.lifecycleSuspended = false;
|
|
661
|
+
this.activatePolling();
|
|
662
|
+
};
|
|
663
|
+
this.sensorProvider = options.sensorProvider ?? defaultSensorProvider;
|
|
664
|
+
this.pollIntervalMs = requirePositiveFinite("pollIntervalMs", options.pollIntervalMs ?? 200);
|
|
665
|
+
this.unavailableFallbackMs = requirePositiveFinite("unavailableFallbackMs", options.unavailableFallbackMs ?? 3e3);
|
|
666
|
+
this.tuning = resolveDrivingStatusTuning(options.tuning);
|
|
667
|
+
this.clock = options.clock ?? systemClock;
|
|
668
|
+
this.onDiagnostic = options.onDiagnostic;
|
|
669
|
+
this.managePageLifecycle = options.managePageLifecycle ?? true;
|
|
670
|
+
const initialStatus = options.initialStatus ?? "STOPPED";
|
|
671
|
+
if (!CAR_RUNNING_STATUSES.includes(initialStatus)) throw new TypeError(`Unsupported initialStatus: ${initialStatus}`);
|
|
672
|
+
this.status = initialStatus;
|
|
673
|
+
this.baseStatus = isBaseStatus(initialStatus) ? initialStatus : "STOPPED";
|
|
674
|
+
this.statusCommittedAtMs = this.clock.now();
|
|
675
|
+
}
|
|
676
|
+
start() {
|
|
677
|
+
if (this.destroyed || this.requestedRunning) return;
|
|
678
|
+
this.requestedRunning = true;
|
|
679
|
+
this.attachLifecycleListeners();
|
|
680
|
+
if (!this.lifecycleSuspended) this.activatePolling();
|
|
681
|
+
}
|
|
682
|
+
stop() {
|
|
683
|
+
if (this.destroyed || !this.requestedRunning) return;
|
|
684
|
+
this.requestedRunning = false;
|
|
685
|
+
this.lifecycleSuspended = false;
|
|
686
|
+
this.detachLifecycleListeners();
|
|
687
|
+
this.deactivatePolling();
|
|
688
|
+
this.resetTransientRecognition();
|
|
689
|
+
}
|
|
690
|
+
destroy() {
|
|
691
|
+
if (this.destroyed) return;
|
|
692
|
+
if (this.requestedRunning) this.stop();
|
|
693
|
+
else {
|
|
694
|
+
this.detachLifecycleListeners();
|
|
695
|
+
this.deactivatePolling();
|
|
696
|
+
}
|
|
697
|
+
this.destroyed = true;
|
|
698
|
+
this.listeners.clear();
|
|
699
|
+
this.metricWindow.length = 0;
|
|
700
|
+
this.motionWindow.length = 0;
|
|
701
|
+
}
|
|
702
|
+
getSnapshot() {
|
|
703
|
+
return {
|
|
704
|
+
lifecycle: this.destroyed ? "destroyed" : this.polling ? "running" : "stopped",
|
|
705
|
+
status: this.status,
|
|
706
|
+
candidate: this.candidate,
|
|
707
|
+
candidateConfirmationCount: this.candidateConfirmationCount,
|
|
708
|
+
statusCommittedAtMs: this.statusCommittedAtMs,
|
|
709
|
+
lastAvailableAtMs: this.lastAvailableAtMs,
|
|
710
|
+
unavailableSinceMs: this.unavailableSinceMs,
|
|
711
|
+
tuningVersion: this.tuning.version
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
subscribe(listener) {
|
|
715
|
+
if (this.destroyed) return noop;
|
|
716
|
+
this.listeners.add(listener);
|
|
717
|
+
let subscribed = true;
|
|
718
|
+
return () => {
|
|
719
|
+
if (!subscribed) return;
|
|
720
|
+
subscribed = false;
|
|
721
|
+
this.listeners.delete(listener);
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
activatePolling() {
|
|
725
|
+
if (this.destroyed || this.polling || !this.requestedRunning || this.lifecycleSuspended) return;
|
|
726
|
+
this.polling = true;
|
|
727
|
+
this.generation += 1;
|
|
728
|
+
this.schedulePoll(0, this.generation);
|
|
729
|
+
}
|
|
730
|
+
deactivatePolling() {
|
|
731
|
+
if (!this.polling && this.timer === void 0 && this.pendingRead === null) return;
|
|
732
|
+
this.polling = false;
|
|
733
|
+
this.generation += 1;
|
|
734
|
+
if (this.timer !== void 0) {
|
|
735
|
+
this.clock.clearTimeout(this.timer);
|
|
736
|
+
this.timer = void 0;
|
|
737
|
+
}
|
|
738
|
+
this.pendingRead?.abort();
|
|
739
|
+
this.pendingRead = null;
|
|
740
|
+
}
|
|
741
|
+
schedulePoll(delayMs, generation) {
|
|
742
|
+
if (!this.polling || generation !== this.generation || this.timer !== void 0) return;
|
|
743
|
+
this.timer = this.clock.setTimeout(() => {
|
|
744
|
+
this.timer = void 0;
|
|
745
|
+
this.poll(generation);
|
|
746
|
+
}, delayMs);
|
|
747
|
+
}
|
|
748
|
+
async poll(generation) {
|
|
749
|
+
if (!this.polling || generation !== this.generation || this.pendingRead !== null) return;
|
|
750
|
+
const pendingRead = new AbortController();
|
|
751
|
+
this.pendingRead = pendingRead;
|
|
752
|
+
try {
|
|
753
|
+
const payload = await this.sensorProvider(pendingRead.signal);
|
|
754
|
+
if (!this.polling || generation !== this.generation || pendingRead.signal.aborted) return;
|
|
755
|
+
const metrics = extractDrivingSnapshotMetrics(payload, this.tuning);
|
|
756
|
+
const accelerometerSample = extractFreshAccelerometerSample(payload, this.tuning.maxSampleAgeMs);
|
|
757
|
+
if (metrics === null || accelerometerSample === null) this.processUnavailable("invalid_or_stale_snapshot");
|
|
758
|
+
else this.processAvailable(metrics, accelerometerSample);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (!this.polling || generation !== this.generation || pendingRead.signal.aborted || isAbortError$1(error)) return;
|
|
761
|
+
this.processUnavailable("provider_error");
|
|
762
|
+
} finally {
|
|
763
|
+
if (this.pendingRead === pendingRead) this.pendingRead = null;
|
|
764
|
+
if (this.polling && generation === this.generation) this.schedulePoll(this.pollIntervalMs, generation);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
processAvailable(rawMetrics, accelerometerSample) {
|
|
768
|
+
const now = this.clock.now();
|
|
769
|
+
this.lastAvailableAtMs = now;
|
|
770
|
+
this.unavailableSinceMs = null;
|
|
771
|
+
this.fallbackApplied = false;
|
|
772
|
+
this.emitDiagnostic({
|
|
773
|
+
type: "sample_available",
|
|
774
|
+
atMs: now,
|
|
775
|
+
capturedAtMs: rawMetrics.capturedAtMs,
|
|
776
|
+
tuningVersion: this.tuning.version
|
|
777
|
+
});
|
|
778
|
+
this.metricWindow.push(rawMetrics);
|
|
779
|
+
while (this.metricWindow.length > Math.floor(this.tuning.smoothingWindowSize)) this.metricWindow.shift();
|
|
780
|
+
const smoothedMetrics = averageMetrics(this.metricWindow);
|
|
781
|
+
const motionIntensity = this.recordMotionSample(accelerometerSample, rawMetrics.capturedAtMs);
|
|
782
|
+
const nextCandidate = classifyControllerCandidate(rawMetrics, smoothedMetrics, motionIntensity, this.status, this.baseStatus, this.tuning);
|
|
783
|
+
this.processCandidate(nextCandidate, now);
|
|
784
|
+
}
|
|
785
|
+
recordMotionSample(sample, capturedAtMs) {
|
|
786
|
+
if (!this.motionWindow.some((existing) => existing.sampledAtMs === sample.sampledAtMs)) {
|
|
787
|
+
this.motionWindow.push(sample);
|
|
788
|
+
this.motionWindow.sort((left, right) => left.sampledAtMs - right.sampledAtMs);
|
|
789
|
+
}
|
|
790
|
+
const cutoffMs = capturedAtMs - this.tuning.motionWindowMs;
|
|
791
|
+
while (this.motionWindow[0] && this.motionWindow[0].sampledAtMs <= cutoffMs) this.motionWindow.shift();
|
|
792
|
+
if (this.motionWindow.length < this.tuning.motionMinimumSamples) return null;
|
|
793
|
+
return combinedAxisStandardDeviation(this.motionWindow);
|
|
794
|
+
}
|
|
795
|
+
processUnavailable(category) {
|
|
796
|
+
const now = this.clock.now();
|
|
797
|
+
if (this.unavailableSinceMs === null) this.unavailableSinceMs = now;
|
|
798
|
+
const unavailableForMs = Math.max(0, now - this.unavailableSinceMs);
|
|
799
|
+
this.metricWindow.length = 0;
|
|
800
|
+
this.motionWindow.length = 0;
|
|
801
|
+
this.setCandidate(null, now);
|
|
802
|
+
this.emitDiagnostic({
|
|
803
|
+
type: "sample_unavailable",
|
|
804
|
+
atMs: now,
|
|
805
|
+
unavailableForMs,
|
|
806
|
+
category,
|
|
807
|
+
tuningVersion: this.tuning.version
|
|
808
|
+
});
|
|
809
|
+
if (unavailableForMs < this.unavailableFallbackMs || this.fallbackApplied) return;
|
|
810
|
+
this.fallbackApplied = true;
|
|
811
|
+
this.emitDiagnostic({
|
|
812
|
+
type: "fallback_to_stopped",
|
|
813
|
+
atMs: now,
|
|
814
|
+
unavailableForMs,
|
|
815
|
+
tuningVersion: this.tuning.version
|
|
816
|
+
});
|
|
817
|
+
this.commit("STOPPED", now, "unavailable_fallback", true);
|
|
818
|
+
}
|
|
819
|
+
processCandidate(nextCandidate, now) {
|
|
820
|
+
if (nextCandidate === this.status) {
|
|
821
|
+
this.setCandidate(null, now);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (nextCandidate !== this.candidate) {
|
|
825
|
+
this.setCandidate(nextCandidate, now);
|
|
826
|
+
this.candidateConfirmationCount = 1;
|
|
827
|
+
this.heldCandidate = null;
|
|
828
|
+
} else this.candidateConfirmationCount += 1;
|
|
829
|
+
if (this.candidateConfirmationCount < this.tuning.confirmationSamples[nextCandidate]) return;
|
|
830
|
+
const elapsedMs = Math.max(0, now - this.statusCommittedAtMs);
|
|
831
|
+
const remainingMs = Math.max(0, this.tuning.minimumDisplayMs[this.status] - elapsedMs);
|
|
832
|
+
const canPreempt = CAR_RUNNING_STATUS_PRIORITY[nextCandidate] > CAR_RUNNING_STATUS_PRIORITY[this.status];
|
|
833
|
+
if (remainingMs > 0 && !canPreempt) {
|
|
834
|
+
if (this.heldCandidate !== nextCandidate) {
|
|
835
|
+
this.heldCandidate = nextCandidate;
|
|
836
|
+
this.emitDiagnostic({
|
|
837
|
+
type: "status_held_by_min_duration",
|
|
838
|
+
atMs: now,
|
|
839
|
+
status: this.status,
|
|
840
|
+
candidate: nextCandidate,
|
|
841
|
+
remainingMs,
|
|
842
|
+
tuningVersion: this.tuning.version
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
this.commit(nextCandidate, now, "candidate_confirmed");
|
|
848
|
+
}
|
|
849
|
+
commit(nextStatus, now, reason, force = false) {
|
|
850
|
+
if (!force && nextStatus === this.status) return;
|
|
851
|
+
if (nextStatus === this.status) return;
|
|
852
|
+
const previousStatus = this.status;
|
|
853
|
+
this.status = nextStatus;
|
|
854
|
+
if (isBaseStatus(nextStatus)) this.baseStatus = nextStatus;
|
|
855
|
+
this.statusCommittedAtMs = now;
|
|
856
|
+
this.setCandidate(null, now);
|
|
857
|
+
this.heldCandidate = null;
|
|
858
|
+
const event = {
|
|
859
|
+
type: "status_changed",
|
|
860
|
+
status: nextStatus,
|
|
861
|
+
previousStatus,
|
|
862
|
+
reason,
|
|
863
|
+
atMs: now
|
|
864
|
+
};
|
|
865
|
+
for (const listener of [...this.listeners]) safeCall(listener, event);
|
|
866
|
+
this.emitDiagnostic({
|
|
867
|
+
type: "status_committed",
|
|
868
|
+
atMs: now,
|
|
869
|
+
status: nextStatus,
|
|
870
|
+
previousStatus,
|
|
871
|
+
reason,
|
|
872
|
+
tuningVersion: this.tuning.version
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
setCandidate(candidate, now) {
|
|
876
|
+
if (candidate === this.candidate) {
|
|
877
|
+
if (candidate === null) this.candidateConfirmationCount = 0;
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const previousCandidate = this.candidate;
|
|
881
|
+
this.candidate = candidate;
|
|
882
|
+
this.candidateConfirmationCount = 0;
|
|
883
|
+
this.heldCandidate = null;
|
|
884
|
+
this.emitDiagnostic({
|
|
885
|
+
type: "candidate_changed",
|
|
886
|
+
atMs: now,
|
|
887
|
+
candidate,
|
|
888
|
+
previousCandidate,
|
|
889
|
+
tuningVersion: this.tuning.version
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
resetTransientRecognition() {
|
|
893
|
+
this.metricWindow.length = 0;
|
|
894
|
+
this.motionWindow.length = 0;
|
|
895
|
+
this.candidate = null;
|
|
896
|
+
this.candidateConfirmationCount = 0;
|
|
897
|
+
this.unavailableSinceMs = null;
|
|
898
|
+
this.fallbackApplied = false;
|
|
899
|
+
this.heldCandidate = null;
|
|
900
|
+
}
|
|
901
|
+
emitDiagnostic(event) {
|
|
902
|
+
if (this.onDiagnostic) safeCall(this.onDiagnostic, event);
|
|
903
|
+
}
|
|
904
|
+
attachLifecycleListeners() {
|
|
905
|
+
if (!this.managePageLifecycle || this.unsubscribePageLifecycle !== null) return;
|
|
906
|
+
this.unsubscribePageLifecycle = subscribeDrivingPageLifecycle(this.handleLifecyclePause, this.handleLifecycleResume);
|
|
907
|
+
}
|
|
908
|
+
detachLifecycleListeners() {
|
|
909
|
+
this.unsubscribePageLifecycle?.();
|
|
910
|
+
this.unsubscribePageLifecycle = null;
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
const classifyControllerCandidate = (raw, smoothed, motionIntensity, current, baseStatus, tuning) => {
|
|
914
|
+
const entry = classifyEntryCandidate(raw, smoothed, motionIntensity, baseStatus, tuning);
|
|
915
|
+
if (!retainsCurrentStatus(raw, smoothed, motionIntensity, current, tuning)) return entry;
|
|
916
|
+
if (entry === current) return current;
|
|
917
|
+
if (CAR_RUNNING_STATUS_PRIORITY[entry] > CAR_RUNNING_STATUS_PRIORITY[current]) return entry;
|
|
918
|
+
if (isOppositeTurn(entry, current)) return entry;
|
|
919
|
+
if ((entry === "STOPPED" || entry === "STEADY_DRIVING") && (current === "STOPPED" || current === "STEADY_DRIVING")) return entry;
|
|
920
|
+
return current;
|
|
921
|
+
};
|
|
922
|
+
const classifyEntryCandidate = (raw, smoothed, motionIntensity, baseStatus, tuning) => {
|
|
923
|
+
if (raw.longitudinal <= -tuning.rapidLongitudinalThreshold) return "SUDDEN_BRAKING";
|
|
924
|
+
if (raw.longitudinal >= tuning.rapidLongitudinalThreshold) return "RAPID_ACCELERATION";
|
|
925
|
+
if (Math.abs(smoothed.yaw) >= tuning.turnYawThreshold) return smoothed.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
|
|
926
|
+
if (Math.abs(smoothed.lateral) >= tuning.turnLateralThreshold) return smoothed.lateral < 0 ? "LEFT_TURN" : "RIGHT_TURN";
|
|
927
|
+
if (smoothed.longitudinal <= -tuning.longitudinalThreshold) return "BRAKING";
|
|
928
|
+
if (smoothed.longitudinal >= tuning.longitudinalThreshold) return "ACCELERATION";
|
|
929
|
+
if (motionIntensity === null) return baseStatus;
|
|
930
|
+
if (motionIntensity > tuning.drivingMotionThreshold || smoothed.gyroMagnitude > tuning.stoppedGyroThreshold) return "STEADY_DRIVING";
|
|
931
|
+
if (motionIntensity < tuning.stoppedMotionThreshold) return "STOPPED";
|
|
932
|
+
return baseStatus;
|
|
933
|
+
};
|
|
934
|
+
const retainsCurrentStatus = (raw, smoothed, motionIntensity, current, tuning) => {
|
|
935
|
+
const ratio = tuning.hysteresisRatio;
|
|
936
|
+
switch (current) {
|
|
937
|
+
case "SUDDEN_BRAKING": return raw.longitudinal <= -tuning.rapidLongitudinalThreshold * ratio;
|
|
938
|
+
case "RAPID_ACCELERATION": return raw.longitudinal >= tuning.rapidLongitudinalThreshold * ratio;
|
|
939
|
+
case "LEFT_TURN": return smoothed.yaw >= tuning.turnYawThreshold * ratio || smoothed.lateral <= -tuning.turnLateralThreshold * ratio;
|
|
940
|
+
case "RIGHT_TURN": return smoothed.yaw <= -tuning.turnYawThreshold * ratio || smoothed.lateral >= tuning.turnLateralThreshold * ratio;
|
|
941
|
+
case "BRAKING": return smoothed.longitudinal <= -tuning.longitudinalThreshold * ratio;
|
|
942
|
+
case "ACCELERATION": return smoothed.longitudinal >= tuning.longitudinalThreshold * ratio;
|
|
943
|
+
case "STOPPED": return (motionIntensity === null || motionIntensity <= tuning.drivingMotionThreshold) && smoothed.gyroMagnitude <= tuning.stoppedGyroThreshold;
|
|
944
|
+
case "STEADY_DRIVING": return motionIntensity === null || motionIntensity >= tuning.stoppedMotionThreshold || smoothed.gyroMagnitude > tuning.stoppedGyroThreshold;
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
const averageMetrics = (metrics) => {
|
|
948
|
+
const result = {
|
|
949
|
+
capturedAtMs: metrics[metrics.length - 1]?.capturedAtMs ?? 0,
|
|
950
|
+
longitudinal: 0,
|
|
951
|
+
lateral: 0,
|
|
952
|
+
yaw: 0,
|
|
953
|
+
motionIntensity: 0,
|
|
954
|
+
gyroMagnitude: 0
|
|
955
|
+
};
|
|
956
|
+
for (const value of metrics) {
|
|
957
|
+
result.longitudinal += value.longitudinal;
|
|
958
|
+
result.lateral += value.lateral;
|
|
959
|
+
result.yaw += value.yaw;
|
|
960
|
+
result.motionIntensity += value.motionIntensity;
|
|
961
|
+
result.gyroMagnitude += value.gyroMagnitude;
|
|
962
|
+
}
|
|
963
|
+
const count = Math.max(1, metrics.length);
|
|
964
|
+
result.longitudinal /= count;
|
|
965
|
+
result.lateral /= count;
|
|
966
|
+
result.yaw /= count;
|
|
967
|
+
result.motionIntensity /= count;
|
|
968
|
+
result.gyroMagnitude /= count;
|
|
969
|
+
return result;
|
|
970
|
+
};
|
|
971
|
+
const extractFreshAccelerometerSample = (info, maxSampleAgeMs) => {
|
|
972
|
+
const metric = info?.accelerometer;
|
|
973
|
+
if (!metric?.available || !Number.isFinite(info.capturedAtMs) || !Number.isFinite(metric.sampledAtMs)) return null;
|
|
974
|
+
const ageMs = info.capturedAtMs - metric.sampledAtMs;
|
|
975
|
+
if (ageMs < 0 || ageMs > maxSampleAgeMs) return null;
|
|
976
|
+
const { x, y, z } = metric.value;
|
|
977
|
+
if (![
|
|
978
|
+
x,
|
|
979
|
+
y,
|
|
980
|
+
z
|
|
981
|
+
].every(Number.isFinite)) return null;
|
|
982
|
+
return {
|
|
983
|
+
sampledAtMs: metric.sampledAtMs,
|
|
984
|
+
value: metric.value
|
|
985
|
+
};
|
|
986
|
+
};
|
|
987
|
+
const combinedAxisStandardDeviation = (samples) => {
|
|
988
|
+
const count = samples.length;
|
|
989
|
+
let meanX = 0;
|
|
990
|
+
let meanY = 0;
|
|
991
|
+
let meanZ = 0;
|
|
992
|
+
for (const sample of samples) {
|
|
993
|
+
meanX += sample.value.x;
|
|
994
|
+
meanY += sample.value.y;
|
|
995
|
+
meanZ += sample.value.z;
|
|
996
|
+
}
|
|
997
|
+
meanX /= count;
|
|
998
|
+
meanY /= count;
|
|
999
|
+
meanZ /= count;
|
|
1000
|
+
let varianceX = 0;
|
|
1001
|
+
let varianceY = 0;
|
|
1002
|
+
let varianceZ = 0;
|
|
1003
|
+
for (const sample of samples) {
|
|
1004
|
+
varianceX += (sample.value.x - meanX) ** 2;
|
|
1005
|
+
varianceY += (sample.value.y - meanY) ** 2;
|
|
1006
|
+
varianceZ += (sample.value.z - meanZ) ** 2;
|
|
1007
|
+
}
|
|
1008
|
+
return Math.sqrt((varianceX + varianceY + varianceZ) / count);
|
|
1009
|
+
};
|
|
1010
|
+
const defaultSensorProvider = () => {
|
|
1011
|
+
const runtimeGlobal = globalThis;
|
|
1012
|
+
if (typeof runtimeGlobal.unifiedSensorInfo !== "function") return Promise.reject(/* @__PURE__ */ new Error("window.unifiedSensorInfo is unavailable"));
|
|
1013
|
+
return runtimeGlobal.unifiedSensorInfo();
|
|
1014
|
+
};
|
|
1015
|
+
const systemClock = {
|
|
1016
|
+
now: () => Date.now(),
|
|
1017
|
+
setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
|
|
1018
|
+
clearTimeout: (handle) => globalThis.clearTimeout(handle)
|
|
1019
|
+
};
|
|
1020
|
+
const isOppositeTurn = (candidate, current) => candidate === "LEFT_TURN" && current === "RIGHT_TURN" || candidate === "RIGHT_TURN" && current === "LEFT_TURN";
|
|
1021
|
+
const isBaseStatus = (status) => status === "STOPPED" || status === "STEADY_DRIVING";
|
|
1022
|
+
const isAbortError$1 = (error) => error instanceof Error && error.name === "AbortError";
|
|
1023
|
+
const requirePositiveFinite = (name, value) => {
|
|
1024
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be a positive finite number`);
|
|
1025
|
+
return value;
|
|
1026
|
+
};
|
|
1027
|
+
const safeCall = (callback, value) => {
|
|
1028
|
+
try {
|
|
1029
|
+
callback(value);
|
|
1030
|
+
} catch {}
|
|
1031
|
+
};
|
|
1032
|
+
const noop = () => {};
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/sensor/driving_expression_player.ts
|
|
1035
|
+
const createDrivingExpressionPlayer = (container, options) => new DefaultDrivingExpressionPlayer(container, options);
|
|
1036
|
+
var DefaultDrivingExpressionPlayer = class {
|
|
1037
|
+
constructor(container, options) {
|
|
1038
|
+
this.status = null;
|
|
1039
|
+
this.phase = "idle";
|
|
1040
|
+
this.mediaKind = null;
|
|
1041
|
+
this.errorCategory = null;
|
|
1042
|
+
this.desiredMedia = null;
|
|
1043
|
+
this.userPaused = false;
|
|
1044
|
+
this.lifecycleSuspended = false;
|
|
1045
|
+
this.paused = false;
|
|
1046
|
+
this.destroyed = false;
|
|
1047
|
+
this.operation = 0;
|
|
1048
|
+
this.failedOperation = null;
|
|
1049
|
+
this.loadAbort = null;
|
|
1050
|
+
this.preloadAbort = null;
|
|
1051
|
+
this.activeVideo = null;
|
|
1052
|
+
this.activeVideoListenersCleanup = null;
|
|
1053
|
+
this.reusableVideo = null;
|
|
1054
|
+
this.activeTgs = null;
|
|
1055
|
+
this.unsubscribePageLifecycle = null;
|
|
1056
|
+
this.handleLifecyclePause = () => {
|
|
1057
|
+
if (this.destroyed || this.lifecycleSuspended) return;
|
|
1058
|
+
this.lifecycleSuspended = true;
|
|
1059
|
+
this.updatePauseState();
|
|
1060
|
+
};
|
|
1061
|
+
this.handleLifecycleResume = () => {
|
|
1062
|
+
if (this.destroyed || !this.lifecycleSuspended) return;
|
|
1063
|
+
this.lifecycleSuspended = false;
|
|
1064
|
+
this.updatePauseState();
|
|
1065
|
+
};
|
|
1066
|
+
if (!container || typeof container.replaceChildren !== "function") throw new TypeError("container must be an HTMLElement-like DOM container");
|
|
1067
|
+
this.container = container;
|
|
1068
|
+
this.document = container.ownerDocument;
|
|
1069
|
+
this.config = options.config;
|
|
1070
|
+
this.fallbacks = options.fallbacks;
|
|
1071
|
+
this.loadTimeoutMs = positiveFinite("loadTimeoutMs", options.loadTimeoutMs ?? 5e3);
|
|
1072
|
+
this.retryBackoffMs = positiveFinite("retryBackoffMs", options.retryBackoffMs ?? 15e3);
|
|
1073
|
+
this.fit = options.fit ?? "contain";
|
|
1074
|
+
this.tgsRenderer = options.tgsRenderer ?? defaultTgsRenderer;
|
|
1075
|
+
this.fetchImplementation = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
1076
|
+
this.onDiagnostic = options.onDiagnostic;
|
|
1077
|
+
this.maxTgsCompressedBytes = positiveFinite("maxTgsCompressedBytes", options.maxTgsCompressedBytes ?? 2 * 1024 * 1024);
|
|
1078
|
+
this.maxTgsJsonBytes = positiveFinite("maxTgsJsonBytes", options.maxTgsJsonBytes ?? 8 * 1024 * 1024);
|
|
1079
|
+
this.maxTgsLayers = positiveFinite("maxTgsLayers", options.maxTgsLayers ?? 500);
|
|
1080
|
+
if (options.managePageLifecycle ?? true) this.unsubscribePageLifecycle = subscribeDrivingPageLifecycle(this.handleLifecyclePause, this.handleLifecycleResume);
|
|
1081
|
+
}
|
|
1082
|
+
async show(status) {
|
|
1083
|
+
if (this.destroyed || status === this.status && this.phase !== "idle") return;
|
|
1084
|
+
this.status = status;
|
|
1085
|
+
const operation = this.beginOperation();
|
|
1086
|
+
const configured = this.config.states[status];
|
|
1087
|
+
if (configured === void 0) {
|
|
1088
|
+
const fallback = this.fallbacks?.[status];
|
|
1089
|
+
if (fallback === void 0) {
|
|
1090
|
+
this.handleFailure(status, null, new DrivingExpressionResolutionError(status), "invalid_config", operation, false);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
this.desiredMedia = fallback;
|
|
1094
|
+
if (this.paused) {
|
|
1095
|
+
this.phase = "idle";
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
this.renderFallback(status, fallback);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const media = normalizeMedia(configured);
|
|
1102
|
+
this.desiredMedia = media;
|
|
1103
|
+
if (this.paused) {
|
|
1104
|
+
this.phase = "idle";
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
await this.load(status, media, operation);
|
|
1108
|
+
}
|
|
1109
|
+
pause() {
|
|
1110
|
+
if (this.destroyed || this.userPaused) return;
|
|
1111
|
+
this.userPaused = true;
|
|
1112
|
+
this.updatePauseState();
|
|
1113
|
+
}
|
|
1114
|
+
resume() {
|
|
1115
|
+
if (this.destroyed || !this.userPaused) return;
|
|
1116
|
+
this.userPaused = false;
|
|
1117
|
+
this.updatePauseState();
|
|
1118
|
+
}
|
|
1119
|
+
destroy() {
|
|
1120
|
+
if (this.destroyed) return;
|
|
1121
|
+
this.destroyed = true;
|
|
1122
|
+
this.unsubscribePageLifecycle?.();
|
|
1123
|
+
this.unsubscribePageLifecycle = null;
|
|
1124
|
+
this.operation += 1;
|
|
1125
|
+
this.failedOperation = null;
|
|
1126
|
+
this.cancelPendingWork();
|
|
1127
|
+
this.releaseActiveMedia();
|
|
1128
|
+
this.container.replaceChildren();
|
|
1129
|
+
this.status = null;
|
|
1130
|
+
this.desiredMedia = null;
|
|
1131
|
+
this.phase = "idle";
|
|
1132
|
+
this.mediaKind = null;
|
|
1133
|
+
this.errorCategory = null;
|
|
1134
|
+
}
|
|
1135
|
+
getSnapshot() {
|
|
1136
|
+
return {
|
|
1137
|
+
lifecycle: this.destroyed ? "destroyed" : this.paused ? "paused" : "active",
|
|
1138
|
+
status: this.status,
|
|
1139
|
+
phase: this.phase,
|
|
1140
|
+
mediaKind: this.mediaKind,
|
|
1141
|
+
errorCategory: this.errorCategory
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
beginOperation() {
|
|
1145
|
+
this.operation += 1;
|
|
1146
|
+
this.failedOperation = null;
|
|
1147
|
+
this.cancelPendingWork();
|
|
1148
|
+
this.releaseActiveMedia();
|
|
1149
|
+
this.container.replaceChildren();
|
|
1150
|
+
this.phase = "loading";
|
|
1151
|
+
this.mediaKind = null;
|
|
1152
|
+
this.errorCategory = null;
|
|
1153
|
+
return this.operation;
|
|
1154
|
+
}
|
|
1155
|
+
updatePauseState() {
|
|
1156
|
+
const shouldPause = this.userPaused || this.lifecycleSuspended;
|
|
1157
|
+
if (shouldPause === this.paused) return;
|
|
1158
|
+
this.paused = shouldPause;
|
|
1159
|
+
if (shouldPause) {
|
|
1160
|
+
this.operation += 1;
|
|
1161
|
+
this.cancelPendingWork();
|
|
1162
|
+
this.activeVideo?.pause();
|
|
1163
|
+
this.activeTgs?.pause?.();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (this.phase === "ready") {
|
|
1167
|
+
if (this.activeVideo && this.status !== null && this.desiredMedia !== null) {
|
|
1168
|
+
const video = this.activeVideo;
|
|
1169
|
+
const status = this.status;
|
|
1170
|
+
const media = this.desiredMedia;
|
|
1171
|
+
const operation = this.operation;
|
|
1172
|
+
this.attachActiveVideoFailureListeners(video, status, media, operation);
|
|
1173
|
+
video.play().catch((error) => this.handleActiveVideoFailure(video, status, media, operation, "decode", error));
|
|
1174
|
+
}
|
|
1175
|
+
this.activeTgs?.play?.();
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (this.status !== null && this.desiredMedia !== null) {
|
|
1179
|
+
const operation = this.beginOperation();
|
|
1180
|
+
this.load(this.status, this.desiredMedia, operation);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async load(status, media, operation) {
|
|
1184
|
+
const startedAt = Date.now();
|
|
1185
|
+
this.mediaKind = media.kind;
|
|
1186
|
+
if (media.kind === "emoji") {
|
|
1187
|
+
this.renderEmoji(media.text);
|
|
1188
|
+
this.markReady(status, media.kind, startedAt, operation);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (firstInvalidMediaUrl(media) !== null) {
|
|
1192
|
+
this.handleFailure(status, media, /* @__PURE__ */ new Error("One or more media URLs are not long-lived public HTTPS URLs"), "invalid_config", operation);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const loadAbort = new AbortController();
|
|
1196
|
+
this.loadAbort = loadAbort;
|
|
1197
|
+
try {
|
|
1198
|
+
switch (media.kind) {
|
|
1199
|
+
case "image":
|
|
1200
|
+
this.renderConfiguredFallback(status, false);
|
|
1201
|
+
await this.loadImage(media.url, loadAbort.signal, operation);
|
|
1202
|
+
break;
|
|
1203
|
+
case "video":
|
|
1204
|
+
this.renderLoadingPlaceholder(status, media.coverUrl, operation, loadAbort.signal);
|
|
1205
|
+
await this.loadVideo(media.url, media.mimeType, status, media, loadAbort.signal, operation);
|
|
1206
|
+
break;
|
|
1207
|
+
case "live_photo":
|
|
1208
|
+
await this.loadLivePhoto(media, status, loadAbort.signal, operation);
|
|
1209
|
+
break;
|
|
1210
|
+
case "tgs":
|
|
1211
|
+
this.renderLoadingPlaceholder(status, media.coverUrl, operation, loadAbort.signal);
|
|
1212
|
+
await this.loadTgs(media.url, loadAbort.signal, operation);
|
|
1213
|
+
break;
|
|
1214
|
+
}
|
|
1215
|
+
if (!this.isCurrent(operation) || this.failedOperation === operation) return;
|
|
1216
|
+
this.markReady(status, media.kind, startedAt, operation);
|
|
1217
|
+
this.startPreloading(status, operation);
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
if (!this.isCurrent(operation) || isAbortError(error)) return;
|
|
1220
|
+
this.handleFailure(status, media, error, categorizeMediaError(error), operation);
|
|
1221
|
+
} finally {
|
|
1222
|
+
loadAbort.abort();
|
|
1223
|
+
if (this.loadAbort === loadAbort) this.loadAbort = null;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async loadImage(url, signal, operation) {
|
|
1227
|
+
const image = this.createImage();
|
|
1228
|
+
await waitForElementEvent(image, "load", "error", signal, this.loadTimeoutMs, () => {
|
|
1229
|
+
image.src = url;
|
|
1230
|
+
});
|
|
1231
|
+
if (!this.isCurrent(operation)) return;
|
|
1232
|
+
this.container.replaceChildren(image);
|
|
1233
|
+
}
|
|
1234
|
+
async loadVideo(url, mimeType, status, media, signal, operation) {
|
|
1235
|
+
const video = this.getReusableVideo();
|
|
1236
|
+
resetVideo(video);
|
|
1237
|
+
video.preload = "auto";
|
|
1238
|
+
if (mimeType) video.setAttribute("type", mimeType);
|
|
1239
|
+
await waitForElementEvent(video, "canplay", "error", signal, this.loadTimeoutMs, () => {
|
|
1240
|
+
video.src = url;
|
|
1241
|
+
video.load();
|
|
1242
|
+
});
|
|
1243
|
+
if (!this.isCurrent(operation)) return;
|
|
1244
|
+
video.currentTime = 0;
|
|
1245
|
+
this.container.replaceChildren(video);
|
|
1246
|
+
this.activeVideo = video;
|
|
1247
|
+
this.attachActiveVideoFailureListeners(video, status, media, operation);
|
|
1248
|
+
await video.play();
|
|
1249
|
+
}
|
|
1250
|
+
async loadLivePhoto(media, status, signal, operation) {
|
|
1251
|
+
this.renderConfiguredFallback(status, false);
|
|
1252
|
+
const cover = this.createImage();
|
|
1253
|
+
await waitForElementEvent(cover, "load", "error", signal, this.loadTimeoutMs, () => {
|
|
1254
|
+
cover.src = media.coverUrl;
|
|
1255
|
+
});
|
|
1256
|
+
if (!this.isCurrent(operation)) return;
|
|
1257
|
+
this.container.replaceChildren(cover);
|
|
1258
|
+
await this.loadVideo(media.motionUrl, void 0, status, media, signal, operation);
|
|
1259
|
+
}
|
|
1260
|
+
async loadTgs(url, signal, operation) {
|
|
1261
|
+
if (!this.fetchImplementation) throw new MediaPlayerError("unsupported", "fetch is unavailable");
|
|
1262
|
+
const response = await withTimeout(this.fetchImplementation(url, {
|
|
1263
|
+
credentials: "omit",
|
|
1264
|
+
mode: "cors",
|
|
1265
|
+
signal
|
|
1266
|
+
}), this.loadTimeoutMs, signal);
|
|
1267
|
+
if (!response.ok) throw new MediaPlayerError("network", `TGS request failed with HTTP ${response.status}`);
|
|
1268
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
1269
|
+
if (!SUPPORTED_TGS_CONTENT_TYPES.has(contentType)) throw new MediaPlayerError("unsupported", `Unsupported TGS Content-Type: ${contentType || "missing"}`);
|
|
1270
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
1271
|
+
if (Number.isFinite(contentLength) && contentLength > this.maxTgsCompressedBytes) throw new MediaPlayerError("decode", "TGS compressed payload exceeds the configured limit");
|
|
1272
|
+
const compressed = new Uint8Array(await withTimeout(response.arrayBuffer(), this.loadTimeoutMs, signal));
|
|
1273
|
+
if (compressed.byteLength > this.maxTgsCompressedBytes) throw new MediaPlayerError("decode", "TGS compressed payload exceeds the configured limit");
|
|
1274
|
+
const bytes = compressed[0] === 31 && compressed[1] === 139 ? gunzipWithLimit(compressed, this.maxTgsJsonBytes) : compressed;
|
|
1275
|
+
if (bytes.byteLength > this.maxTgsJsonBytes) throw new MediaPlayerError("decode", "TGS JSON payload exceeds the configured limit");
|
|
1276
|
+
let animationData;
|
|
1277
|
+
try {
|
|
1278
|
+
animationData = JSON.parse(new TextDecoder().decode(bytes));
|
|
1279
|
+
} catch {
|
|
1280
|
+
throw new MediaPlayerError("decode", "TGS JSON is malformed");
|
|
1281
|
+
}
|
|
1282
|
+
validateTgsAnimation(animationData, this.maxTgsLayers);
|
|
1283
|
+
const host = this.document.createElement("div");
|
|
1284
|
+
styleMediaElement(host, this.fit);
|
|
1285
|
+
const handle = await withTimeout(Promise.resolve(this.tgsRenderer(host, animationData, signal)), this.loadTimeoutMs, signal);
|
|
1286
|
+
if (!this.isCurrent(operation)) {
|
|
1287
|
+
handle.destroy();
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
this.activeTgs = handle;
|
|
1291
|
+
this.container.replaceChildren(host);
|
|
1292
|
+
}
|
|
1293
|
+
renderLoadingPlaceholder(status, coverUrl, operation, signal) {
|
|
1294
|
+
this.renderConfiguredFallback(status, false);
|
|
1295
|
+
if (!coverUrl) return;
|
|
1296
|
+
const cover = this.createImage();
|
|
1297
|
+
waitForElementEvent(cover, "load", "error", signal, this.loadTimeoutMs, () => {
|
|
1298
|
+
cover.src = coverUrl;
|
|
1299
|
+
}).then(() => {
|
|
1300
|
+
if (this.isCurrent(operation) && this.phase === "loading") this.container.replaceChildren(cover);
|
|
1301
|
+
}).catch(() => {
|
|
1302
|
+
if (this.isCurrent(operation) && this.phase === "loading") this.renderConfiguredFallback(status, false);
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
renderConfiguredFallback(status, report = true) {
|
|
1306
|
+
const fallback = this.fallbacks?.[status];
|
|
1307
|
+
if (!fallback) {
|
|
1308
|
+
this.container.replaceChildren();
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
this.renderFallback(status, fallback, report);
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
renderFallback(status, fallback, report = true) {
|
|
1315
|
+
if (report) {
|
|
1316
|
+
this.phase = "fallback";
|
|
1317
|
+
this.mediaKind = fallback.kind;
|
|
1318
|
+
this.errorCategory = null;
|
|
1319
|
+
}
|
|
1320
|
+
if (fallback.kind === "emoji") this.renderEmoji(fallback.text);
|
|
1321
|
+
else if (fallback.kind === "image") {
|
|
1322
|
+
const image = this.createImage();
|
|
1323
|
+
image.src = fallback.url;
|
|
1324
|
+
this.container.replaceChildren(image);
|
|
1325
|
+
} else this.container.replaceChildren();
|
|
1326
|
+
if (report) this.emitDiagnostic({
|
|
1327
|
+
type: "media_fallback_applied",
|
|
1328
|
+
status,
|
|
1329
|
+
kind: fallback.kind
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
renderEmoji(text) {
|
|
1333
|
+
const emoji = this.document.createElement("span");
|
|
1334
|
+
emoji.textContent = text;
|
|
1335
|
+
emoji.setAttribute("role", "img");
|
|
1336
|
+
emoji.setAttribute("aria-label", "driving expression");
|
|
1337
|
+
Object.assign(emoji.style, {
|
|
1338
|
+
alignItems: "center",
|
|
1339
|
+
display: "flex",
|
|
1340
|
+
fontFamily: "system-ui, \"Apple Color Emoji\", \"Noto Color Emoji\", sans-serif",
|
|
1341
|
+
fontSize: "min(52vw, 240px)",
|
|
1342
|
+
height: "100%",
|
|
1343
|
+
justifyContent: "center",
|
|
1344
|
+
lineHeight: "1",
|
|
1345
|
+
width: "100%"
|
|
1346
|
+
});
|
|
1347
|
+
this.container.replaceChildren(emoji);
|
|
1348
|
+
}
|
|
1349
|
+
markReady(status, kind, startedAt, operation) {
|
|
1350
|
+
if (!this.isCurrent(operation) || this.failedOperation === operation) return;
|
|
1351
|
+
this.phase = "ready";
|
|
1352
|
+
this.mediaKind = kind;
|
|
1353
|
+
this.errorCategory = null;
|
|
1354
|
+
this.emitDiagnostic({
|
|
1355
|
+
type: "media_ready",
|
|
1356
|
+
status,
|
|
1357
|
+
kind,
|
|
1358
|
+
elapsedMs: Math.max(0, Date.now() - startedAt)
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
handleFailure(status, media, error, category, operation, scheduleRetry = true) {
|
|
1362
|
+
if (!this.isCurrent(operation) || this.failedOperation === operation) return;
|
|
1363
|
+
this.failedOperation = operation;
|
|
1364
|
+
this.stopActiveVideo();
|
|
1365
|
+
this.phase = "error";
|
|
1366
|
+
this.errorCategory = category;
|
|
1367
|
+
this.emitDiagnostic({
|
|
1368
|
+
type: "media_load_failed",
|
|
1369
|
+
status,
|
|
1370
|
+
kind: media?.kind ?? "unknown",
|
|
1371
|
+
category,
|
|
1372
|
+
message: safeErrorMessage(error)
|
|
1373
|
+
});
|
|
1374
|
+
if (!this.renderConfiguredFallback(status)) {
|
|
1375
|
+
this.phase = "error";
|
|
1376
|
+
this.errorCategory = category;
|
|
1377
|
+
}
|
|
1378
|
+
if (scheduleRetry && media !== null) this.scheduleRetry(status, media, operation);
|
|
1379
|
+
}
|
|
1380
|
+
scheduleRetry(status, media, failedOperation) {
|
|
1381
|
+
if (this.destroyed || this.paused || !this.isCurrent(failedOperation)) return;
|
|
1382
|
+
this.retryTimer = globalThis.setTimeout(() => {
|
|
1383
|
+
this.retryTimer = void 0;
|
|
1384
|
+
if (this.destroyed || this.paused || this.status !== status || !this.isCurrent(failedOperation)) return;
|
|
1385
|
+
const operation = this.beginOperation();
|
|
1386
|
+
this.load(status, media, operation);
|
|
1387
|
+
}, this.retryBackoffMs);
|
|
1388
|
+
}
|
|
1389
|
+
startPreloading(currentStatus, operation) {
|
|
1390
|
+
this.preloadAbort?.abort();
|
|
1391
|
+
const preloadAbort = new AbortController();
|
|
1392
|
+
this.preloadAbort = preloadAbort;
|
|
1393
|
+
const statuses = [
|
|
1394
|
+
currentStatus,
|
|
1395
|
+
"STOPPED",
|
|
1396
|
+
...CAR_RUNNING_STATUSES
|
|
1397
|
+
].filter((status, index, all) => all.indexOf(status) === index);
|
|
1398
|
+
(async () => {
|
|
1399
|
+
for (const status of statuses) {
|
|
1400
|
+
if (!this.isCurrent(operation) || preloadAbort.signal.aborted) return;
|
|
1401
|
+
const configured = this.config.states[status];
|
|
1402
|
+
if (configured === void 0 || status === currentStatus) continue;
|
|
1403
|
+
const media = normalizeMedia(configured);
|
|
1404
|
+
const hintUrl = mediaPreloadHint(media);
|
|
1405
|
+
if (!hintUrl || !isPublicDrivingMediaUrl(hintUrl)) continue;
|
|
1406
|
+
try {
|
|
1407
|
+
const image = this.createImage();
|
|
1408
|
+
await waitForElementEvent(image, "load", "error", preloadAbort.signal, this.loadTimeoutMs, () => {
|
|
1409
|
+
image.src = hintUrl;
|
|
1410
|
+
});
|
|
1411
|
+
image.removeAttribute("src");
|
|
1412
|
+
} catch {
|
|
1413
|
+
if (preloadAbort.signal.aborted) return;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
})();
|
|
1417
|
+
}
|
|
1418
|
+
createImage() {
|
|
1419
|
+
const image = this.document.createElement("img");
|
|
1420
|
+
image.alt = "";
|
|
1421
|
+
image.decoding = "async";
|
|
1422
|
+
styleMediaElement(image, this.fit);
|
|
1423
|
+
return image;
|
|
1424
|
+
}
|
|
1425
|
+
getReusableVideo() {
|
|
1426
|
+
if (!this.reusableVideo) {
|
|
1427
|
+
this.reusableVideo = this.document.createElement("video");
|
|
1428
|
+
this.reusableVideo.muted = true;
|
|
1429
|
+
this.reusableVideo.loop = true;
|
|
1430
|
+
this.reusableVideo.playsInline = true;
|
|
1431
|
+
this.reusableVideo.setAttribute("muted", "");
|
|
1432
|
+
this.reusableVideo.setAttribute("playsinline", "");
|
|
1433
|
+
styleMediaElement(this.reusableVideo, this.fit);
|
|
1434
|
+
}
|
|
1435
|
+
return this.reusableVideo;
|
|
1436
|
+
}
|
|
1437
|
+
attachActiveVideoFailureListeners(video, status, media, operation) {
|
|
1438
|
+
this.detachActiveVideoFailureListeners();
|
|
1439
|
+
let attached = true;
|
|
1440
|
+
const cleanup = () => {
|
|
1441
|
+
if (!attached) return;
|
|
1442
|
+
attached = false;
|
|
1443
|
+
video.removeEventListener("error", handleError);
|
|
1444
|
+
video.removeEventListener("stalled", handleStalled);
|
|
1445
|
+
if (this.activeVideoListenersCleanup === cleanup) this.activeVideoListenersCleanup = null;
|
|
1446
|
+
};
|
|
1447
|
+
const failed = (category, event) => {
|
|
1448
|
+
if (!attached) return;
|
|
1449
|
+
this.handleActiveVideoFailure(video, status, media, operation, category, new MediaPlayerError(category, `${event.type} after video playback started`));
|
|
1450
|
+
};
|
|
1451
|
+
const handleError = (event) => failed("decode", event);
|
|
1452
|
+
const handleStalled = (event) => failed("network", event);
|
|
1453
|
+
video.addEventListener("error", handleError);
|
|
1454
|
+
video.addEventListener("stalled", handleStalled);
|
|
1455
|
+
this.activeVideoListenersCleanup = cleanup;
|
|
1456
|
+
}
|
|
1457
|
+
handleActiveVideoFailure(video, status, media, operation, category, error) {
|
|
1458
|
+
if (this.activeVideo !== video || !this.isCurrent(operation)) return;
|
|
1459
|
+
this.stopActiveVideo();
|
|
1460
|
+
this.handleFailure(status, media, error, category, operation);
|
|
1461
|
+
}
|
|
1462
|
+
detachActiveVideoFailureListeners() {
|
|
1463
|
+
const cleanup = this.activeVideoListenersCleanup;
|
|
1464
|
+
this.activeVideoListenersCleanup = null;
|
|
1465
|
+
cleanup?.();
|
|
1466
|
+
}
|
|
1467
|
+
stopActiveVideo() {
|
|
1468
|
+
this.detachActiveVideoFailureListeners();
|
|
1469
|
+
if (!this.activeVideo) return;
|
|
1470
|
+
resetVideo(this.activeVideo);
|
|
1471
|
+
this.activeVideo = null;
|
|
1472
|
+
}
|
|
1473
|
+
releaseActiveMedia() {
|
|
1474
|
+
this.stopActiveVideo();
|
|
1475
|
+
if (this.activeTgs) {
|
|
1476
|
+
safeDestroy(this.activeTgs);
|
|
1477
|
+
this.activeTgs = null;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
cancelPendingWork() {
|
|
1481
|
+
this.loadAbort?.abort();
|
|
1482
|
+
this.loadAbort = null;
|
|
1483
|
+
this.detachActiveVideoFailureListeners();
|
|
1484
|
+
if (this.activeVideo === null && this.reusableVideo !== null) resetVideo(this.reusableVideo);
|
|
1485
|
+
this.preloadAbort?.abort();
|
|
1486
|
+
this.preloadAbort = null;
|
|
1487
|
+
if (this.retryTimer !== void 0) {
|
|
1488
|
+
globalThis.clearTimeout(this.retryTimer);
|
|
1489
|
+
this.retryTimer = void 0;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
isCurrent(operation) {
|
|
1493
|
+
return !this.destroyed && !this.paused && operation === this.operation;
|
|
1494
|
+
}
|
|
1495
|
+
emitDiagnostic(event) {
|
|
1496
|
+
if (!this.onDiagnostic) return;
|
|
1497
|
+
try {
|
|
1498
|
+
this.onDiagnostic(event);
|
|
1499
|
+
} catch {}
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
var MediaPlayerError = class extends Error {
|
|
1503
|
+
constructor(category, message) {
|
|
1504
|
+
super(message);
|
|
1505
|
+
this.name = "MediaPlayerError";
|
|
1506
|
+
this.category = category;
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
const SUPPORTED_TGS_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
1510
|
+
"application/gzip",
|
|
1511
|
+
"application/json",
|
|
1512
|
+
"application/vnd.telegram.tgsticker",
|
|
1513
|
+
"application/x-gzip",
|
|
1514
|
+
"application/x-tgsticker"
|
|
1515
|
+
]);
|
|
1516
|
+
const normalizeMedia = (media) => typeof media === "string" ? {
|
|
1517
|
+
kind: "image",
|
|
1518
|
+
url: media
|
|
1519
|
+
} : media;
|
|
1520
|
+
const firstInvalidMediaUrl = (media) => {
|
|
1521
|
+
let values;
|
|
1522
|
+
switch (media.kind) {
|
|
1523
|
+
case "emoji":
|
|
1524
|
+
values = [];
|
|
1525
|
+
break;
|
|
1526
|
+
case "image":
|
|
1527
|
+
values = [media.url];
|
|
1528
|
+
break;
|
|
1529
|
+
case "live_photo":
|
|
1530
|
+
values = [media.coverUrl, media.motionUrl];
|
|
1531
|
+
break;
|
|
1532
|
+
case "video":
|
|
1533
|
+
case "tgs":
|
|
1534
|
+
values = [media.url, media.coverUrl];
|
|
1535
|
+
break;
|
|
1536
|
+
}
|
|
1537
|
+
return values.find((value) => typeof value === "string" && !isPublicDrivingMediaUrl(value)) ?? null;
|
|
1538
|
+
};
|
|
1539
|
+
const mediaPreloadHint = (media) => {
|
|
1540
|
+
switch (media.kind) {
|
|
1541
|
+
case "image": return media.url;
|
|
1542
|
+
case "video":
|
|
1543
|
+
case "tgs": return media.coverUrl;
|
|
1544
|
+
case "live_photo": return media.coverUrl;
|
|
1545
|
+
case "emoji": return;
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
const waitForElementEvent = (element, successEvent, failureEvent, signal, timeoutMs, start) => new Promise((resolve, reject) => {
|
|
1549
|
+
let timer;
|
|
1550
|
+
const cleanup = () => {
|
|
1551
|
+
element.removeEventListener(successEvent, handleSuccess);
|
|
1552
|
+
element.removeEventListener(failureEvent, handleFailure);
|
|
1553
|
+
signal.removeEventListener("abort", handleAbort);
|
|
1554
|
+
if (timer !== void 0) globalThis.clearTimeout(timer);
|
|
1555
|
+
};
|
|
1556
|
+
const handleSuccess = () => {
|
|
1557
|
+
cleanup();
|
|
1558
|
+
resolve();
|
|
1559
|
+
};
|
|
1560
|
+
const handleFailure = () => {
|
|
1561
|
+
cleanup();
|
|
1562
|
+
reject(new MediaPlayerError("decode", `${failureEvent} while loading media`));
|
|
1563
|
+
};
|
|
1564
|
+
const handleAbort = () => {
|
|
1565
|
+
cleanup();
|
|
1566
|
+
reject(createAbortError());
|
|
1567
|
+
};
|
|
1568
|
+
element.addEventListener(successEvent, handleSuccess, { once: true });
|
|
1569
|
+
element.addEventListener(failureEvent, handleFailure, { once: true });
|
|
1570
|
+
signal.addEventListener("abort", handleAbort, { once: true });
|
|
1571
|
+
timer = globalThis.setTimeout(() => {
|
|
1572
|
+
cleanup();
|
|
1573
|
+
reject(new MediaPlayerError("timeout", `media load timed out after ${timeoutMs}ms`));
|
|
1574
|
+
}, timeoutMs);
|
|
1575
|
+
try {
|
|
1576
|
+
start();
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
cleanup();
|
|
1579
|
+
reject(error);
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
const withTimeout = (promise, timeoutMs, signal) => new Promise((resolve, reject) => {
|
|
1583
|
+
let timer;
|
|
1584
|
+
const cleanup = () => {
|
|
1585
|
+
signal.removeEventListener("abort", handleAbort);
|
|
1586
|
+
if (timer !== void 0) globalThis.clearTimeout(timer);
|
|
1587
|
+
};
|
|
1588
|
+
const handleAbort = () => {
|
|
1589
|
+
cleanup();
|
|
1590
|
+
reject(createAbortError());
|
|
1591
|
+
};
|
|
1592
|
+
signal.addEventListener("abort", handleAbort, { once: true });
|
|
1593
|
+
timer = globalThis.setTimeout(() => {
|
|
1594
|
+
cleanup();
|
|
1595
|
+
reject(new MediaPlayerError("timeout", `media load timed out after ${timeoutMs}ms`));
|
|
1596
|
+
}, timeoutMs);
|
|
1597
|
+
promise.then((value) => {
|
|
1598
|
+
cleanup();
|
|
1599
|
+
resolve(value);
|
|
1600
|
+
}, (error) => {
|
|
1601
|
+
cleanup();
|
|
1602
|
+
reject(error);
|
|
1603
|
+
});
|
|
1604
|
+
});
|
|
1605
|
+
const defaultTgsRenderer = async (container, animationData, signal) => {
|
|
1606
|
+
if (signal.aborted) throw createAbortError();
|
|
1607
|
+
const module = await import("lottie-web");
|
|
1608
|
+
if (signal.aborted) throw createAbortError();
|
|
1609
|
+
const animation = module.default.loadAnimation({
|
|
1610
|
+
animationData,
|
|
1611
|
+
autoplay: true,
|
|
1612
|
+
container,
|
|
1613
|
+
loop: true,
|
|
1614
|
+
renderer: "svg",
|
|
1615
|
+
rendererSettings: { progressiveLoad: true }
|
|
1616
|
+
});
|
|
1617
|
+
await new Promise((resolve, reject) => {
|
|
1618
|
+
const ready = () => {
|
|
1619
|
+
cleanup();
|
|
1620
|
+
resolve();
|
|
1621
|
+
};
|
|
1622
|
+
const failed = () => {
|
|
1623
|
+
cleanup();
|
|
1624
|
+
reject(new MediaPlayerError("decode", "Lottie could not initialize the TGS animation"));
|
|
1625
|
+
};
|
|
1626
|
+
const aborted = () => {
|
|
1627
|
+
cleanup();
|
|
1628
|
+
animation.destroy();
|
|
1629
|
+
reject(createAbortError());
|
|
1630
|
+
};
|
|
1631
|
+
const cleanup = () => {
|
|
1632
|
+
animation.removeEventListener("DOMLoaded", ready);
|
|
1633
|
+
animation.removeEventListener("data_failed", failed);
|
|
1634
|
+
signal.removeEventListener("abort", aborted);
|
|
1635
|
+
};
|
|
1636
|
+
animation.addEventListener("DOMLoaded", ready);
|
|
1637
|
+
animation.addEventListener("data_failed", failed);
|
|
1638
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
1639
|
+
});
|
|
1640
|
+
return {
|
|
1641
|
+
destroy: () => animation.destroy(),
|
|
1642
|
+
pause: () => animation.pause(),
|
|
1643
|
+
play: () => animation.play(),
|
|
1644
|
+
restart: () => animation.goToAndPlay(0, true)
|
|
1645
|
+
};
|
|
1646
|
+
};
|
|
1647
|
+
function validateTgsAnimation(value, maxLayers) {
|
|
1648
|
+
if (!isRecord(value) || !Array.isArray(value.layers)) throw new MediaPlayerError("decode", "TGS animation does not contain a layers array");
|
|
1649
|
+
if (value.layers.length > maxLayers) throw new MediaPlayerError("decode", "TGS animation has too many layers");
|
|
1650
|
+
if (typeof value.fr === "number" && (!Number.isFinite(value.fr) || value.fr <= 0 || value.fr > 120)) throw new MediaPlayerError("decode", "TGS animation frame rate is outside the supported range");
|
|
1651
|
+
if (typeof value.ip === "number" && typeof value.op === "number" && (value.op <= value.ip || value.op - value.ip > 3e4)) throw new MediaPlayerError("decode", "TGS animation duration is outside the supported range");
|
|
1652
|
+
}
|
|
1653
|
+
const gunzipWithLimit = (compressed, maxOutputBytes) => {
|
|
1654
|
+
if (compressed.byteLength < 18) throw new MediaPlayerError("decode", "TGS gzip payload is truncated");
|
|
1655
|
+
if (new DataView(compressed.buffer, compressed.byteOffset + compressed.byteLength - 4, 4).getUint32(0, true) > maxOutputBytes) throw new MediaPlayerError("decode", "TGS JSON payload exceeds the configured limit");
|
|
1656
|
+
const chunks = [];
|
|
1657
|
+
let outputBytes = 0;
|
|
1658
|
+
try {
|
|
1659
|
+
const gunzip = new Gunzip((chunk) => {
|
|
1660
|
+
outputBytes += chunk.byteLength;
|
|
1661
|
+
if (outputBytes > maxOutputBytes) throw new MediaPlayerError("decode", "TGS JSON payload exceeds the configured limit");
|
|
1662
|
+
if (chunk.byteLength > 0) chunks.push(chunk);
|
|
1663
|
+
});
|
|
1664
|
+
const compressedChunkBytes = 64;
|
|
1665
|
+
for (let offset = 0; offset < compressed.byteLength; offset += compressedChunkBytes) {
|
|
1666
|
+
const end = Math.min(compressed.byteLength, offset + compressedChunkBytes);
|
|
1667
|
+
gunzip.push(compressed.subarray(offset, end), end === compressed.byteLength);
|
|
1668
|
+
}
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
if (error instanceof MediaPlayerError) throw error;
|
|
1671
|
+
throw new MediaPlayerError("decode", "TGS gzip decompression failed");
|
|
1672
|
+
}
|
|
1673
|
+
const output = new Uint8Array(outputBytes);
|
|
1674
|
+
let offset = 0;
|
|
1675
|
+
for (const chunk of chunks) {
|
|
1676
|
+
output.set(chunk, offset);
|
|
1677
|
+
offset += chunk.byteLength;
|
|
1678
|
+
}
|
|
1679
|
+
return output;
|
|
1680
|
+
};
|
|
1681
|
+
const resetVideo = (video) => {
|
|
1682
|
+
video.pause();
|
|
1683
|
+
video.removeAttribute("src");
|
|
1684
|
+
video.removeAttribute("type");
|
|
1685
|
+
try {
|
|
1686
|
+
video.load();
|
|
1687
|
+
} catch {}
|
|
1688
|
+
};
|
|
1689
|
+
const styleMediaElement = (element, fit) => {
|
|
1690
|
+
Object.assign(element.style, {
|
|
1691
|
+
display: "block",
|
|
1692
|
+
height: "100%",
|
|
1693
|
+
maxHeight: "100%",
|
|
1694
|
+
maxWidth: "100%",
|
|
1695
|
+
objectFit: fit,
|
|
1696
|
+
width: "100%"
|
|
1697
|
+
});
|
|
1698
|
+
};
|
|
1699
|
+
const categorizeMediaError = (error) => {
|
|
1700
|
+
if (error instanceof MediaPlayerError) return error.category;
|
|
1701
|
+
if (error instanceof TypeError) return "network";
|
|
1702
|
+
return "decode";
|
|
1703
|
+
};
|
|
1704
|
+
const safeErrorMessage = (error) => {
|
|
1705
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1706
|
+
return redactUrlQueries(message).slice(0, 240);
|
|
1707
|
+
};
|
|
1708
|
+
const redactUrlQueries = (message) => message.replace(/https:\/\/[^\s"'<>]+/gu, (candidate) => {
|
|
1709
|
+
try {
|
|
1710
|
+
const url = new URL(candidate);
|
|
1711
|
+
return `${url.origin}${url.pathname}`;
|
|
1712
|
+
} catch {
|
|
1713
|
+
return "[url]";
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
const createAbortError = () => {
|
|
1717
|
+
if (typeof DOMException !== "undefined") return new DOMException("Aborted", "AbortError");
|
|
1718
|
+
const error = /* @__PURE__ */ new Error("Aborted");
|
|
1719
|
+
error.name = "AbortError";
|
|
1720
|
+
return error;
|
|
1721
|
+
};
|
|
1722
|
+
const isAbortError = (error) => error instanceof Error && error.name === "AbortError";
|
|
1723
|
+
const positiveFinite = (name, value) => {
|
|
1724
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be a positive finite number`);
|
|
1725
|
+
return value;
|
|
1726
|
+
};
|
|
1727
|
+
const safeDestroy = (handle) => {
|
|
1728
|
+
try {
|
|
1729
|
+
handle.destroy();
|
|
1730
|
+
} catch {}
|
|
1731
|
+
};
|
|
1732
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1733
|
+
//#endregion
|
|
1734
|
+
export { BATTERY_HEALTH_LABELS, CAR_RUNNING_LABELS, CAR_RUNNING_STATUSES, CAR_RUNNING_STATUS_PRIORITY, DEFAULT_DRIVING_STATUS_TUNING, DrivingExpressionResolutionError, DrivingExpressionsConfigError, OFFICIAL_DRIVING_EXPRESSIONS_CONFIG, OFFICIAL_DRIVING_EXPRESSION_FALLBACKS, checkRunningStatus, classifyDrivingMetrics, classifyDrivingSnapshot, createDrivingExpressionPlayer, createDrivingStatusController, extractDrivingSnapshotMetrics, isPublicDrivingMediaUrl, parseDrivingExpressionsConfig, readDrivingExpressionsConfig, resolveDrivingExpression, resolveDrivingExpressionStrict, resolveDrivingStatusTuning, tryParseDrivingExpressionsConfig, unifiedSensorInfo };
|