@7365admin1/layer-common 4.57.1 → 4.57.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/CHANGELOG.md +16 -4
- package/components/Robot/LiveView.vue +4 -2
- package/package.json +1 -1
- package/utils/robot-control.ts +83 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.57.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c23d427: Robot live view: the PadBot S2's EZVIZ camera puts an H.264 access-unit delimiter in front of every H.265 frame, which made Chrome refuse every frame (decode error, black picture). The player now neutralises those units in each segment before playback, so the picture shows. The "couldn't be decoded" message no longer blames EZVIZ encryption (disproved).
|
|
8
|
+
|
|
9
|
+
## 4.57.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- e901907: Robots: live view says honestly when the camera video cannot be decoded (MEDIA_ERR_DECODE, e.g. EZVIZ video encryption switched on) instead of retrying: at most two hls.js recoveries, then a plain message with Retry. The message no longer blames the browser when the decoder reports support.
|
|
14
|
+
|
|
3
15
|
## 4.57.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1599,11 +1611,11 @@
|
|
|
1599
1611
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1600
1612
|
own rule from two endpoints the console already calls, unprojected:
|
|
1601
1613
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1614
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1615
|
+
GET /api/roles/id/:role that membership's role document
|
|
1604
1616
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1617
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1618
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1607
1619
|
|
|
1608
1620
|
`role.default` is the marker because it is the only property of a platform
|
|
1609
1621
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
LIVE_DOWN,
|
|
70
70
|
LIVE_NO_PICTURE,
|
|
71
71
|
LIVE_RETRY_LIMIT,
|
|
72
|
+
LIVE_UNDECODABLE,
|
|
72
73
|
liveRetryDelayMs,
|
|
73
74
|
liveVideoKind,
|
|
74
75
|
liveViewActive,
|
|
@@ -168,8 +169,9 @@ watch([videoEl, url], ([el, src]) => {
|
|
|
168
169
|
});
|
|
169
170
|
function onPlayerFail(message: string) {
|
|
170
171
|
stopPlayer = undefined;
|
|
171
|
-
if (message === LIVE_NO_PICTURE) {
|
|
172
|
-
//
|
|
172
|
+
if (message === LIVE_NO_PICTURE || message === LIVE_UNDECODABLE) {
|
|
173
|
+
// Can't be shown (browser codec / encrypted camera video): a fresh URL won't help,
|
|
174
|
+
// so no silent retries behind the message; Retry is the user's.
|
|
173
175
|
gen++;
|
|
174
176
|
clearTimeout(timer);
|
|
175
177
|
error.value = message;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.57.
|
|
5
|
+
"version": "4.57.3",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
package/utils/robot-control.ts
CHANGED
|
@@ -36,6 +36,15 @@ export function remintDelayMs(expiresAt: unknown, now: number = Date.now()): num
|
|
|
36
36
|
export const LIVE_NO_PICTURE =
|
|
37
37
|
"This robot's camera sends H.265 video, which this browser can't show. Try Microsoft Edge or Safari, or the PadBot app.";
|
|
38
38
|
export const LIVE_DOWN = "The robot's camera isn't available right now.";
|
|
39
|
+
/**
|
|
40
|
+
* The decoder says it supports the codec but the frames still won't decode
|
|
41
|
+
* (MEDIA_ERR_DECODE). On the PadBot S2 (EZVIZ H3) that was the stray H.264
|
|
42
|
+
* delimiters `neutraliseStrayAud` now removes (NOT encryption - disproved
|
|
43
|
+
* 2026-09-26 by a software decode of the real stream); kept for anything else.
|
|
44
|
+
*/
|
|
45
|
+
export const LIVE_UNDECODABLE =
|
|
46
|
+
"The robot's camera video couldn't be decoded. Press Retry. If it keeps happening, contact Seven365 support.";
|
|
47
|
+
const MEDIA_ERR_DECODE = 3;
|
|
39
48
|
|
|
40
49
|
/** Wait before automatic reconnect attempt n (0-based): 2 s, 4 s, 8 s, 16 s, then 30 s. */
|
|
41
50
|
export const liveRetryDelayMs = (n: number) => Math.min(30_000, 2_000 * 2 ** n);
|
|
@@ -50,7 +59,62 @@ type THls = {
|
|
|
50
59
|
swapAudioCodec(): void;
|
|
51
60
|
destroy(): void;
|
|
52
61
|
};
|
|
53
|
-
export type THlsCtor = { isSupported(): boolean; new (config?: Record<string, unknown>): THls };
|
|
62
|
+
export type THlsCtor = { isSupported(): boolean; new (config?: Record<string, unknown>): THls; DefaultConfig?: { loader?: any } };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The EZVIZ camera on the PadBot S2 puts an H.264 access-unit delimiter
|
|
66
|
+
* (00 00 01 09 xx) in front of every H.265 frame. Read as H.265 that is a
|
|
67
|
+
* "TSA_N" slice with temporal id 0 - invalid. ffmpeg skips it; hls.js packs it
|
|
68
|
+
* into the frame and Chrome's decoder then refuses every frame
|
|
69
|
+
* (PIPELINE_ERROR_DECODE, 0 frames). Proven on the real stream 2026-09-26:
|
|
70
|
+
* with these units removed Chrome's own decoder decodes 40/40 frames.
|
|
71
|
+
*
|
|
72
|
+
* Rewrites each such header in place, inside the TS packets of video PIDs
|
|
73
|
+
* only, to an "unspecified" H.265 unit (type 48) that hls.js and decoders
|
|
74
|
+
* ignore. Same length, so the TS stays intact. A valid single-layer H.265
|
|
75
|
+
* header never matches (its temporal id is never 0). Returns the count.
|
|
76
|
+
*/
|
|
77
|
+
export function neutraliseStrayAud(data: ArrayBuffer | Uint8Array): number {
|
|
78
|
+
const b = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
79
|
+
const video = new Set<number>();
|
|
80
|
+
let n = 0;
|
|
81
|
+
for (let i = 0; i + 188 <= b.length; i += 188) {
|
|
82
|
+
if (b[i] !== 0x47) continue;
|
|
83
|
+
const afc = (b[i + 3]! >> 4) & 3;
|
|
84
|
+
if (!(afc & 1)) continue;
|
|
85
|
+
const pid = ((b[i + 1]! & 0x1f) << 8) | b[i + 2]!;
|
|
86
|
+
let p = i + 4;
|
|
87
|
+
if (afc & 2) p += 1 + b[p]!;
|
|
88
|
+
// A PES start names its stream: 0xE0-0xEF = video.
|
|
89
|
+
if (b[i + 1]! & 0x40 && b[p] === 0 && b[p + 1] === 0 && b[p + 2] === 1 && (b[p + 3]! & 0xf0) === 0xe0) video.add(pid);
|
|
90
|
+
if (!video.has(pid)) continue;
|
|
91
|
+
for (let k = p; k + 4 < i + 188; k++) {
|
|
92
|
+
if (b[k] === 0 && b[k + 1] === 0 && b[k + 2] === 1 && b[k + 3] === 0x09 && (b[k + 4]! & 7) === 0) {
|
|
93
|
+
b[k + 3] = 0x60;
|
|
94
|
+
b[k + 4] = 0x01;
|
|
95
|
+
n++;
|
|
96
|
+
k += 4;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return n;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** hls.js fragment loader that runs `neutraliseStrayAud` on every loaded segment. */
|
|
104
|
+
function strayAudLoader(Base: any) {
|
|
105
|
+
return class extends Base {
|
|
106
|
+
load(context: any, config: any, callbacks: any) {
|
|
107
|
+
const ok = callbacks.onSuccess;
|
|
108
|
+
super.load(context, config, {
|
|
109
|
+
...callbacks,
|
|
110
|
+
onSuccess: (response: any, ...rest: any[]) => {
|
|
111
|
+
if (response?.data instanceof ArrayBuffer) neutraliseStrayAud(response.data);
|
|
112
|
+
ok(response, ...rest);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
54
118
|
|
|
55
119
|
/** The inline panel streams only while it is switched on, the robot is online, the tab is visible and the panel is near the screen. */
|
|
56
120
|
export const liveViewActive = (s: { on: boolean; offline?: boolean; visible: boolean; inView: boolean }) =>
|
|
@@ -88,6 +152,8 @@ const CODEC_ERRORS = new Set(["manifestIncompatibleCodecsError", "bufferAddCodec
|
|
|
88
152
|
* `onFail` fires once, with LIVE_NO_PICTURE when the stream plays but no
|
|
89
153
|
* picture ever decodes (Chrome's native HLS takes H.265-in-TS audio but shows
|
|
90
154
|
* no video, videoWidth stays 0) or the codec is refused; LIVE_DOWN for
|
|
155
|
+
* LIVE_UNDECODABLE when the video element reported MEDIA_ERR_DECODE (after at
|
|
156
|
+
* most two hls.js recoveries - no endless retrying); LIVE_DOWN for
|
|
91
157
|
* everything else (network, expired URL, repeated media errors, a stall) -
|
|
92
158
|
* the caller fetches a fresh URL and starts again.
|
|
93
159
|
*/
|
|
@@ -113,6 +179,7 @@ export function startLivePlayer(
|
|
|
113
179
|
let pictured = false;
|
|
114
180
|
let lastTime = -1;
|
|
115
181
|
let still = 0;
|
|
182
|
+
let decodeFailed = false; // the video element reported MEDIA_ERR_DECODE at some point
|
|
116
183
|
|
|
117
184
|
const fail = (message: string) => {
|
|
118
185
|
if (stopped) return;
|
|
@@ -128,7 +195,8 @@ export function startLivePlayer(
|
|
|
128
195
|
opts.onPicture?.();
|
|
129
196
|
} else if (waited >= noPictureMs) {
|
|
130
197
|
// Time moving with no picture = the codec; nothing moving = no stream.
|
|
131
|
-
|
|
198
|
+
noteDecode();
|
|
199
|
+
return fail(decodeFailed ? LIVE_UNDECODABLE : video.currentTime > 0 ? LIVE_NO_PICTURE : LIVE_DOWN);
|
|
132
200
|
}
|
|
133
201
|
return;
|
|
134
202
|
}
|
|
@@ -138,12 +206,19 @@ export function startLivePlayer(
|
|
|
138
206
|
still = 0;
|
|
139
207
|
} else if ((still += tickMs) >= stallMs) fail(LIVE_DOWN);
|
|
140
208
|
}, tickMs);
|
|
141
|
-
const
|
|
209
|
+
const noteDecode = () => {
|
|
210
|
+
if (video.error?.code === MEDIA_ERR_DECODE) decodeFailed = true;
|
|
211
|
+
};
|
|
212
|
+
const onError = () => {
|
|
213
|
+
noteDecode();
|
|
214
|
+
fail(decodeFailed ? LIVE_UNDECODABLE : LIVE_DOWN);
|
|
215
|
+
};
|
|
142
216
|
|
|
143
217
|
function stop() {
|
|
144
218
|
stopped = true;
|
|
145
219
|
clearInterval(tick);
|
|
146
220
|
video.removeEventListener("error", onError);
|
|
221
|
+
video.removeEventListener("error", noteDecode);
|
|
147
222
|
hls?.destroy();
|
|
148
223
|
hls = undefined;
|
|
149
224
|
video.removeAttribute("src");
|
|
@@ -165,9 +240,12 @@ export function startLivePlayer(
|
|
|
165
240
|
.then((Hls) => {
|
|
166
241
|
if (stopped) return;
|
|
167
242
|
if (!Hls.isSupported()) return native();
|
|
243
|
+
video.addEventListener("error", noteDecode);
|
|
168
244
|
let recoveries = 0;
|
|
169
|
-
const
|
|
245
|
+
const Base = Hls.DefaultConfig?.loader;
|
|
246
|
+
const h = (hls = new Hls({ enableWorker: true, lowLatencyMode: false, ...(Base ? { fLoader: strayAudLoader(Base) } : {}) }));
|
|
170
247
|
h.on("hlsError", (_e, data) => {
|
|
248
|
+
noteDecode();
|
|
171
249
|
if (!data.fatal) return;
|
|
172
250
|
if (CODEC_ERRORS.has(data.details ?? "")) return fail(LIVE_NO_PICTURE);
|
|
173
251
|
// The standard hls.js ladder: recover, then swap audio codec and recover, then give up.
|
|
@@ -175,7 +253,7 @@ export function startLivePlayer(
|
|
|
175
253
|
if (recoveries++ === 1) h.swapAudioCodec();
|
|
176
254
|
return h.recoverMediaError();
|
|
177
255
|
}
|
|
178
|
-
fail(LIVE_DOWN);
|
|
256
|
+
fail(decodeFailed ? LIVE_UNDECODABLE : LIVE_DOWN);
|
|
179
257
|
});
|
|
180
258
|
h.loadSource(url);
|
|
181
259
|
h.attachMedia(video);
|