@7365admin1/layer-common 4.58.0 → 4.58.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 +17 -4
- package/assets/css/robot-hero.css +4 -0
- package/components/Robot/BuildingMap.vue +10 -1
- package/components/Robot/LivePlan.vue +10 -3
- package/package.json +1 -1
- package/utils/robot-control.ts +40 -8
- package/utils/robot-display.ts +21 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.58.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 40a1da2: Robot live view (PadBot S2): the camera's stray H.264 delimiters are now rewritten into real H.265 access-unit delimiters (after a 4-byte start code). The S2 sends VPS/SPS/PPS each in its own PES; without an H.265 delimiter hls.js 1.7.3 dropped the PPS from the init segment and Chrome decoded 0 frames (black picture, clock running).
|
|
8
|
+
|
|
9
|
+
## 4.58.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- e186caf: Robot live view: no more silent reload every ~6 s. PadBot's video URL expiry comes without a time zone ("2026-09-26 16:26:02"); read as local time it was already past, so the panel fetched a new URL every 5 s forever. An expiry that has no zone, or is under a minute away, no longer schedules a re-fetch (the stream's own failure still reconnects, with capped backoff, then Retry). The no-picture and stall watchdogs no longer count while the page is hidden.
|
|
14
|
+
- c4adb97: Robots: a robot's name tag stays inside the plan. Near the right edge it sits to the robot's left, and near the top it goes below: on robot detail (name + status) and on the building map, as the mini-map already did. Before, the S2 docked at the top-right showed only "HERO-OT".
|
|
15
|
+
|
|
3
16
|
## 4.58.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
|
@@ -1617,11 +1630,11 @@
|
|
|
1617
1630
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1618
1631
|
own rule from two endpoints the console already calls, unprojected:
|
|
1619
1632
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1633
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1634
|
+
GET /api/roles/id/:role that membership's role document
|
|
1622
1635
|
|
|
1623
|
-
|
|
1624
|
-
|
|
1636
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1637
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1625
1638
|
|
|
1626
1639
|
`role.default` is the marker because it is the only property of a platform
|
|
1627
1640
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -829,6 +829,8 @@
|
|
|
829
829
|
.hero-me { position: absolute; width: 0; height: 0; }
|
|
830
830
|
.hero-me__tag { position: absolute; left: 14px; top: -26px; white-space: nowrap; font-size: 11px; font-weight: 800; padding: 2px 6px; background: var(--hero-text); color: var(--hero-bg); line-height: 1.45; }
|
|
831
831
|
.hero-me__tag > span { font-weight: 400; opacity: 0.75; }
|
|
832
|
+
.hero-me__tag--left { left: auto; right: 14px; }
|
|
833
|
+
.hero-me__tag--below { top: 14px; }
|
|
832
834
|
.hero-me__radar {
|
|
833
835
|
position: absolute;
|
|
834
836
|
left: -36px;
|
|
@@ -1009,6 +1011,8 @@
|
|
|
1009
1011
|
box-shadow: var(--hero-shadow-sm);
|
|
1010
1012
|
cursor: pointer;
|
|
1011
1013
|
}
|
|
1014
|
+
/* Near the plan's right edge the name sits to the pin's left (nameTagSide) - never clipped. */
|
|
1015
|
+
.hero .hero-bmap__name--left { left: auto; right: 6px; transform-origin: 100% 0; border-left: 0; border-right: 3px solid var(--hero-bot); }
|
|
1012
1016
|
.hero-bmap__floorlabel { position: absolute; left: -14px; top: 50%; transform-origin: 0 0; font-size: 12px; font-weight: 800; padding: 1px 6px; background: var(--hero-text); color: var(--hero-bg); white-space: nowrap; }
|
|
1013
1017
|
.hero-bmap__shaft { position: absolute; width: 0; height: 0; transform-style: preserve-3d; transition: opacity 0.6s ease; }
|
|
1014
1018
|
.hero-bmap__shaftwall { position: absolute; left: 0; top: 0; box-sizing: border-box; transform-origin: top; transform: rotateX(90deg); background: color-mix(in srgb, var(--hero-accent) 28%, transparent); border: 1px solid var(--hero-accent); }
|
|
@@ -172,7 +172,14 @@
|
|
|
172
172
|
<span class="hero-bmap__body" aria-hidden="true" />
|
|
173
173
|
<span class="hero-bmap__pin" :style="{ height: `${look.pinH}px` }" aria-hidden="true" />
|
|
174
174
|
<span class="hero-bmap__pin hero-bmap__pin--y" :style="{ height: `${look.pinH}px` }" aria-hidden="true" />
|
|
175
|
-
<button
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
class="hero-bmap__name"
|
|
178
|
+
:class="{ 'hero-bmap__name--left': nameTagSide(r.left, r.top, r.name, plane.w, plane.h, BUILDING_TAG).flipX }"
|
|
179
|
+
:style="{ transform: `translateZ(${look.pinZ}px) ${unskew}` }"
|
|
180
|
+
@click.stop="emit('open', r.id)"
|
|
181
|
+
@mousedown.stop
|
|
182
|
+
>
|
|
176
183
|
{{ r.name }}
|
|
177
184
|
</button>
|
|
178
185
|
</div>
|
|
@@ -236,6 +243,8 @@ import useRobotLive, { siteStatesFeed } from "../../composables/useRobotLive";
|
|
|
236
243
|
import { openDoorNames } from "../../utils/robot-fleet-core";
|
|
237
244
|
import { robotMapViews, robotMapsByFloor, type TRobotMapView } from "../../utils/robot-maps";
|
|
238
245
|
import {
|
|
246
|
+
BUILDING_TAG,
|
|
247
|
+
nameTagSide,
|
|
239
248
|
brandName,
|
|
240
249
|
floorName,
|
|
241
250
|
mapFloors,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<span class="hero-cross hero-cross--v" :style="{ left: point.left, transition: `left ${tickMs}ms linear` }" aria-hidden="true" />
|
|
21
21
|
<div v-if="readout" class="hero-readout">{{ readout }}</div>
|
|
22
22
|
<div class="hero-me" :class="{ 'hero-me--stale': stale }" :style="{ left: point.left, top: point.top, transition: `left ${tickMs}ms linear, top ${tickMs}ms linear` }">
|
|
23
|
-
<div class="hero-me__tag">{{ name }} <span>{{ statusLabel }}</span></div>
|
|
23
|
+
<div class="hero-me__tag" :class="tagClass">{{ name }} <span>{{ statusLabel }}</span></div>
|
|
24
24
|
<div class="hero-me__radar" aria-hidden="true" />
|
|
25
25
|
<div class="hero-me__ping" aria-hidden="true" />
|
|
26
26
|
<div class="hero-me__body" :style="{ transform: `rotate(${heading}deg)` }" aria-hidden="true"><div class="hero-me__tick" /></div>
|
|
@@ -35,9 +35,10 @@
|
|
|
35
35
|
import RobotFloorPlan from "./FloorPlan.vue";
|
|
36
36
|
import RobotPatrolRoutes from "./PatrolRoutes.vue";
|
|
37
37
|
import RobotReconnecting from "./Reconnecting.vue";
|
|
38
|
-
import {
|
|
38
|
+
import { computed } from "vue";
|
|
39
|
+
import { DETAIL_TAG, nameTagSide, trailDots, type TPatrolRouteView } from "../../utils/robot-display";
|
|
39
40
|
|
|
40
|
-
withDefaults(
|
|
41
|
+
const props = withDefaults(
|
|
41
42
|
defineProps<{
|
|
42
43
|
name?: string;
|
|
43
44
|
statusLabel?: string;
|
|
@@ -69,4 +70,10 @@ withDefaults(
|
|
|
69
70
|
{ stale: false, name: "", statusLabel: "", point: null, heading: 0, trail: () => [], readout: "", lost: false, route: null, routes: null, image: null, aspect: null, points: () => [], routeMoving: false, spot: () => [], pick: false, tickMs: 1200 },
|
|
70
71
|
);
|
|
71
72
|
const emit = defineEmits<{ pick: [point: { x: number; y: number }] }>();
|
|
73
|
+
// The tag stays inside the plan (flipped left near the right edge, below near the top) - never clipped.
|
|
74
|
+
const tagClass = computed(() => {
|
|
75
|
+
if (!props.point) return {};
|
|
76
|
+
const side = nameTagSide(props.point.left, props.point.top, `${props.name} ${props.statusLabel}`, 320, 240, DETAIL_TAG);
|
|
77
|
+
return { "hero-me__tag--left": side.flipX, "hero-me__tag--below": side.below };
|
|
78
|
+
});
|
|
72
79
|
</script>
|
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.58.
|
|
5
|
+
"version": "4.58.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.",
|
package/utils/robot-control.ts
CHANGED
|
@@ -27,11 +27,18 @@ export function liveVideoKind(url: unknown): TVideoKind {
|
|
|
27
27
|
return "page";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* When to ask for a fresh URL: 30 s before it expires. Null (no scheduled
|
|
32
|
+
* re-mint - the stream's own failure re-mints, with capped backoff) when the
|
|
33
|
+
* expiry is missing, has no time zone, or is under a minute away. PadBot sent
|
|
34
|
+
* "2026-09-26 16:26:02" (no zone): read as local time it was already past, so
|
|
35
|
+
* the old 5 s floor re-minted every ~6 s forever, silently (prod 2026-09-26).
|
|
36
|
+
*/
|
|
31
37
|
export function remintDelayMs(expiresAt: unknown, now: number = Date.now()): number | null {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
if (typeof expiresAt !== "string" || !/(Z|[+-]\d\d:?\d\d)$/i.test(expiresAt.trim())) return null;
|
|
39
|
+
const t = Date.parse(expiresAt);
|
|
40
|
+
if (!Number.isFinite(t) || t - now < 60_000) return null;
|
|
41
|
+
return Math.max(30_000, t - now - 30_000);
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
export const LIVE_NO_PICTURE =
|
|
@@ -71,9 +78,20 @@ export type THlsCtor = { isSupported(): boolean; new (config?: Record<string, un
|
|
|
71
78
|
* with these units removed Chrome's own decoder decodes 40/40 frames.
|
|
72
79
|
*
|
|
73
80
|
* Rewrites each such header in place, inside the TS packets of video PIDs
|
|
74
|
-
* only,
|
|
75
|
-
* ignore. Same length, so the TS stays intact. A valid single-layer H.265
|
|
81
|
+
* only. Same length, so the TS stays intact. A valid single-layer H.265
|
|
76
82
|
* header never matches (its temporal id is never 0). Returns the count.
|
|
83
|
+
*
|
|
84
|
+
* - After a 4-byte start code (the real S2 stream) it becomes a real H.265
|
|
85
|
+
* access-unit delimiter (type 35). This matters: the S2 sends every
|
|
86
|
+
* parameter set (VPS, SPS, PPS) in its own PES packet. Without an H.265
|
|
87
|
+
* delimiter hls.js 1.7.3 treats each PES as a new picture, and its HEVC
|
|
88
|
+
* parser forgets the VPS on every picture push (initVPS = null), so the PPS
|
|
89
|
+
* is never stored and the hvcC it writes has no PPS: Chrome then decodes
|
|
90
|
+
* 0 frames with no error - time runs, picture black (prod, 2026-09-26 late;
|
|
91
|
+
* the page's own hls.js decoded the real segments once the delimiter was a
|
|
92
|
+
* real one: 136 frames, PPS present).
|
|
93
|
+
* - After a 3-byte start code there is no spare byte: an "unspecified" unit
|
|
94
|
+
* (type 48) that hls.js and decoders ignore.
|
|
77
95
|
*/
|
|
78
96
|
export function neutraliseStrayAud(data: ArrayBuffer | Uint8Array): number {
|
|
79
97
|
const b = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
@@ -100,8 +118,19 @@ export function neutraliseStrayAud(data: ArrayBuffer | Uint8Array): number {
|
|
|
100
118
|
const len = t + end - p;
|
|
101
119
|
for (let k = 0; k + 4 < len; k++) {
|
|
102
120
|
if (b[at(k)] === 0 && b[at(k + 1)] === 0 && b[at(k + 2)] === 1 && b[at(k + 3)] === 0x09 && (b[at(k + 4)]! & 7) === 0) {
|
|
103
|
-
b[at(k
|
|
104
|
-
|
|
121
|
+
if (k > 0 && b[at(k - 1)] === 0) {
|
|
122
|
+
// 4-byte start code (what the S2 sends): borrow its spare zero and
|
|
123
|
+
// write a REAL H.265 access-unit delimiter, 00 00 01 46 01 50.
|
|
124
|
+
b[at(k - 1)] = 0;
|
|
125
|
+
b[at(k)] = 0;
|
|
126
|
+
b[at(k + 1)] = 1;
|
|
127
|
+
b[at(k + 2)] = 0x46;
|
|
128
|
+
b[at(k + 3)] = 0x01;
|
|
129
|
+
b[at(k + 4)] = 0x50;
|
|
130
|
+
} else {
|
|
131
|
+
b[at(k + 3)] = 0x60;
|
|
132
|
+
b[at(k + 4)] = 0x01;
|
|
133
|
+
}
|
|
105
134
|
n++;
|
|
106
135
|
k += 4;
|
|
107
136
|
}
|
|
@@ -199,6 +228,9 @@ export function startLivePlayer(
|
|
|
199
228
|
};
|
|
200
229
|
// ponytail: one 1 s poll instead of juggling media events - videoWidth has no reliable event everywhere.
|
|
201
230
|
const tick = setInterval(() => {
|
|
231
|
+
// A hidden page gets no frames painted (and Chrome pauses muted background
|
|
232
|
+
// video): not the camera's fault, so the watchdogs don't count it.
|
|
233
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
202
234
|
waited += tickMs;
|
|
203
235
|
if (!pictured) {
|
|
204
236
|
if (video.videoWidth > 0) {
|
package/utils/robot-display.ts
CHANGED
|
@@ -1609,16 +1609,29 @@ export function credentialPatch(
|
|
|
1609
1609
|
}
|
|
1610
1610
|
|
|
1611
1611
|
/**
|
|
1612
|
-
* Keep a robot's name tag inside
|
|
1613
|
-
* left near the right edge, and below the
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1612
|
+
* Keep a robot's name tag inside its plan (mini-map, building map, robot
|
|
1613
|
+
* detail): flip it to the robot's left near the right edge, and below the
|
|
1614
|
+
* robot near the top edge.
|
|
1615
|
+
* ponytail: width estimated from the text (default 10px Archivo 600: 6.2 px a
|
|
1616
|
+
* character + 8 px padding; `tag` for bigger tags) against the smallest plan
|
|
1617
|
+
* (320px wide, 240 tall); measure if a tag must be exact.
|
|
1616
1618
|
*/
|
|
1617
|
-
export function nameTagSide(
|
|
1619
|
+
export function nameTagSide(
|
|
1620
|
+
left: unknown,
|
|
1621
|
+
top: unknown,
|
|
1622
|
+
name: unknown,
|
|
1623
|
+
planW = 320,
|
|
1624
|
+
planH = 240,
|
|
1625
|
+
tag: { charW?: number; pad?: number; gap?: number; above?: number } = {},
|
|
1626
|
+
) {
|
|
1618
1627
|
const pct = (v: unknown) => (typeof v === "string" ? parseFloat(v) : typeof v === "number" ? v : 0) || 0;
|
|
1619
|
-
const width = String(name ?? "").length * 6.2 + 8;
|
|
1628
|
+
const width = String(name ?? "").length * (tag.charW ?? 6.2) + (tag.pad ?? 8);
|
|
1620
1629
|
return {
|
|
1621
|
-
flipX: (pct(left) / 100) * planW + 10 + width > planW,
|
|
1622
|
-
below: (pct(top) / 100) * planH < 20,
|
|
1630
|
+
flipX: (pct(left) / 100) * planW + (tag.gap ?? 10) + width > planW,
|
|
1631
|
+
below: (pct(top) / 100) * planH < (tag.above ?? 20),
|
|
1623
1632
|
};
|
|
1624
1633
|
}
|
|
1634
|
+
|
|
1635
|
+
/** The bigger tags: robot detail's "name + status" (11px 800, 14px from the dot, 26px above) and the building map's name (11px 800). */
|
|
1636
|
+
export const DETAIL_TAG = { charW: 7.4, pad: 12, gap: 14, above: 30 };
|
|
1637
|
+
export const BUILDING_TAG = { charW: 7.4, pad: 13, gap: 6, above: 0 };
|