@7365admin1/layer-common 4.57.2 → 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 CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 4.57.2
4
10
 
5
11
  ### Patch Changes
@@ -1605,11 +1611,11 @@
1605
1611
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
1606
1612
  own rule from two endpoints the console already calls, unprojected:
1607
1613
 
1608
- GET /api/members/user/:user/app/admin the Seven365 staff membership
1609
- GET /api/roles/id/:role that membership's role document
1614
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
1615
+ GET /api/roles/id/:role that membership's role document
1610
1616
 
1611
- owner = member.type === "admin" && role.type === "admin" && role.default === true
1612
- staff = member.type === "admin" && role.type === "admin"
1617
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
1618
+ staff = member.type === "admin" && role.type === "admin"
1613
1619
 
1614
1620
  `role.default` is the marker because it is the only property of a platform
1615
1621
  staff role no API caller can set - `role.controller.ts` validates create and
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.2",
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.",
@@ -38,11 +38,12 @@ export const LIVE_NO_PICTURE =
38
38
  export const LIVE_DOWN = "The robot's camera isn't available right now.";
39
39
  /**
40
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.
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.
43
44
  */
44
45
  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
+ "The robot's camera video couldn't be decoded. Press Retry. If it keeps happening, contact Seven365 support.";
46
47
  const MEDIA_ERR_DECODE = 3;
47
48
 
48
49
  /** Wait before automatic reconnect attempt n (0-based): 2 s, 4 s, 8 s, 16 s, then 30 s. */
@@ -58,7 +59,62 @@ type THls = {
58
59
  swapAudioCodec(): void;
59
60
  destroy(): void;
60
61
  };
61
- 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
+ }
62
118
 
63
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. */
64
120
  export const liveViewActive = (s: { on: boolean; offline?: boolean; visible: boolean; inView: boolean }) =>
@@ -186,7 +242,8 @@ export function startLivePlayer(
186
242
  if (!Hls.isSupported()) return native();
187
243
  video.addEventListener("error", noteDecode);
188
244
  let recoveries = 0;
189
- const h = (hls = new Hls({ enableWorker: true, lowLatencyMode: false }));
245
+ const Base = Hls.DefaultConfig?.loader;
246
+ const h = (hls = new Hls({ enableWorker: true, lowLatencyMode: false, ...(Base ? { fLoader: strayAudLoader(Base) } : {}) }));
190
247
  h.on("hlsError", (_e, data) => {
191
248
  noteDecode();
192
249
  if (!data.fatal) return;