@7365admin1/layer-common 4.35.1-staging.408 → 4.35.1-staging.410

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "4.35.1-staging.408",
5
+ "version": "4.35.1-staging.410",
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.",
@@ -27,11 +27,18 @@ export function liveVideoKind(url: unknown): TVideoKind {
27
27
  return "page";
28
28
  }
29
29
 
30
- /** When to ask for a fresh URL: 30 s before it expires (never sooner than 5 s); null when it does not expire. */
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
- const t = typeof expiresAt === "string" ? Date.parse(expiresAt) : NaN;
33
- if (!Number.isFinite(t)) return null;
34
- return Math.max(5_000, t - now - 30_000);
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, to an "unspecified" H.265 unit (type 48) that hls.js and decoders
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 + 3)] = 0x60;
104
- b[at(k + 4)] = 0x01;
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) {