@7365admin1/layer-common 4.57.1 → 4.57.2

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.57.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
3
9
  ## 4.57.1
4
10
 
5
11
  ### Patch Changes
@@ -1599,11 +1605,11 @@
1599
1605
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1600
1606
  own rule from two endpoints the console already calls, unprojected:
1601
1607
 
1602
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1603
- GET /api/roles/id/:role that membership's role document
1608
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1609
+ GET /api/roles/id/:role that membership's role document
1604
1610
 
1605
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1606
- staff = member.type === "admin" && role.type === "admin"
1611
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1612
+ staff = member.type === "admin" && role.type === "admin"
1607
1613
 
1608
1614
  `role.default` is the marker because it is the only property of a platform
1609
1615
  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
- // The browser can't show H.265: no silent retries behind the message; Retry is the user's.
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.1",
5
+ "version": "4.57.2",
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.",
@@ -36,6 +36,14 @@ 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). Seen on the PadBot S2 (EZVIZ H3): EZVIZ device video
42
+ * encryption scrambles the frames, so plain HLS can't show them. It's not the browser.
43
+ */
44
+ export const LIVE_UNDECODABLE =
45
+ "The robot's camera video couldn't be decoded. This usually means video encryption is switched on in the camera's EZVIZ app. Ask the administrator to turn it off, or contact PadBot.";
46
+ const MEDIA_ERR_DECODE = 3;
39
47
 
40
48
  /** Wait before automatic reconnect attempt n (0-based): 2 s, 4 s, 8 s, 16 s, then 30 s. */
41
49
  export const liveRetryDelayMs = (n: number) => Math.min(30_000, 2_000 * 2 ** n);
@@ -88,6 +96,8 @@ const CODEC_ERRORS = new Set(["manifestIncompatibleCodecsError", "bufferAddCodec
88
96
  * `onFail` fires once, with LIVE_NO_PICTURE when the stream plays but no
89
97
  * picture ever decodes (Chrome's native HLS takes H.265-in-TS audio but shows
90
98
  * no video, videoWidth stays 0) or the codec is refused; LIVE_DOWN for
99
+ * LIVE_UNDECODABLE when the video element reported MEDIA_ERR_DECODE (after at
100
+ * most two hls.js recoveries - no endless retrying); LIVE_DOWN for
91
101
  * everything else (network, expired URL, repeated media errors, a stall) -
92
102
  * the caller fetches a fresh URL and starts again.
93
103
  */
@@ -113,6 +123,7 @@ export function startLivePlayer(
113
123
  let pictured = false;
114
124
  let lastTime = -1;
115
125
  let still = 0;
126
+ let decodeFailed = false; // the video element reported MEDIA_ERR_DECODE at some point
116
127
 
117
128
  const fail = (message: string) => {
118
129
  if (stopped) return;
@@ -128,7 +139,8 @@ export function startLivePlayer(
128
139
  opts.onPicture?.();
129
140
  } else if (waited >= noPictureMs) {
130
141
  // Time moving with no picture = the codec; nothing moving = no stream.
131
- return fail(video.currentTime > 0 ? LIVE_NO_PICTURE : LIVE_DOWN);
142
+ noteDecode();
143
+ return fail(decodeFailed ? LIVE_UNDECODABLE : video.currentTime > 0 ? LIVE_NO_PICTURE : LIVE_DOWN);
132
144
  }
133
145
  return;
134
146
  }
@@ -138,12 +150,19 @@ export function startLivePlayer(
138
150
  still = 0;
139
151
  } else if ((still += tickMs) >= stallMs) fail(LIVE_DOWN);
140
152
  }, tickMs);
141
- const onError = () => fail(LIVE_DOWN);
153
+ const noteDecode = () => {
154
+ if (video.error?.code === MEDIA_ERR_DECODE) decodeFailed = true;
155
+ };
156
+ const onError = () => {
157
+ noteDecode();
158
+ fail(decodeFailed ? LIVE_UNDECODABLE : LIVE_DOWN);
159
+ };
142
160
 
143
161
  function stop() {
144
162
  stopped = true;
145
163
  clearInterval(tick);
146
164
  video.removeEventListener("error", onError);
165
+ video.removeEventListener("error", noteDecode);
147
166
  hls?.destroy();
148
167
  hls = undefined;
149
168
  video.removeAttribute("src");
@@ -165,9 +184,11 @@ export function startLivePlayer(
165
184
  .then((Hls) => {
166
185
  if (stopped) return;
167
186
  if (!Hls.isSupported()) return native();
187
+ video.addEventListener("error", noteDecode);
168
188
  let recoveries = 0;
169
189
  const h = (hls = new Hls({ enableWorker: true, lowLatencyMode: false }));
170
190
  h.on("hlsError", (_e, data) => {
191
+ noteDecode();
171
192
  if (!data.fatal) return;
172
193
  if (CODEC_ERRORS.has(data.details ?? "")) return fail(LIVE_NO_PICTURE);
173
194
  // The standard hls.js ladder: recover, then swap audio codec and recover, then give up.
@@ -175,7 +196,7 @@ export function startLivePlayer(
175
196
  if (recoveries++ === 1) h.swapAudioCodec();
176
197
  return h.recoverMediaError();
177
198
  }
178
- fail(LIVE_DOWN);
199
+ fail(decodeFailed ? LIVE_UNDECODABLE : LIVE_DOWN);
179
200
  });
180
201
  h.loadSource(url);
181
202
  h.attachMedia(video);