rvms-vue 0.1.11 → 0.1.13

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/dist/lib/index.js CHANGED
@@ -1,41 +1,41 @@
1
- import { defineComponent as N, ref as k, onMounted as q, onBeforeUnmount as F, watch as J, openBlock as d, createElementBlock as f, normalizeStyle as j, createElementVNode as i, toDisplayString as B, createCommentVNode as S, createTextVNode as K, Fragment as Y, renderList as G } from "vue";
2
- const X = {
1
+ import { defineComponent as J, ref as S, onMounted as Y, onBeforeUnmount as q, watch as K, openBlock as c, createElementBlock as d, normalizeStyle as H, createElementVNode as i, toDisplayString as _, createCommentVNode as B, createTextVNode as G, Fragment as X, renderList as Z } from "vue";
2
+ const ee = {
3
3
  width: "64",
4
4
  height: "64",
5
5
  viewBox: "0 0 64 64",
6
6
  fill: "none",
7
7
  style: { opacity: "0.8" }
8
- }, Z = {
8
+ }, te = {
9
9
  key: 1,
10
10
  class: "rvms-overlay"
11
- }, ee = {
11
+ }, ne = {
12
12
  key: 2,
13
13
  class: "rvms-overlay"
14
- }, te = {
14
+ }, re = {
15
15
  key: 3,
16
16
  class: "rvms-overlay"
17
- }, ne = {
17
+ }, oe = {
18
18
  key: 0,
19
19
  style: { color: "#fca5a5", "font-size": "0.8em", "text-align": "center", padding: "0 1em", "max-width": "80%" }
20
- }, re = {
20
+ }, ae = {
21
21
  key: 4,
22
22
  class: "rvms-overlay"
23
- }, oe = { style: { position: "absolute", top: "8px", left: "8px", display: "flex", gap: "6px", "align-items": "center", "pointer-events": "none" } }, ae = {
23
+ }, le = { style: { position: "absolute", top: "8px", left: "8px", display: "flex", gap: "6px", "align-items": "center", "pointer-events": "none" } }, se = {
24
24
  key: 1,
25
25
  style: { padding: "2px 6px", "border-radius": "4px", "font-size": "10px", "font-family": "monospace", background: "rgba(51, 65, 85, 0.8)", color: "#cbd5e1" }
26
- }, le = {
26
+ }, ie = {
27
27
  key: 2,
28
28
  style: { display: "inline-flex", "align-items": "center", gap: "4px", padding: "2px 8px", "border-radius": "4px", "font-size": "10px", background: "#065f46", color: "#6ee7b7" }
29
- }, se = {
29
+ }, ue = {
30
30
  key: 5,
31
31
  style: { position: "absolute", bottom: "12px", right: "12px", display: "flex", gap: "6px" }
32
- }, ie = ["title"], ue = {
32
+ }, ce = ["title"], de = {
33
33
  key: 6,
34
34
  style: { position: "absolute", bottom: "48px", left: "8px", right: "8px", "max-height": "140px", "overflow-y": "auto", display: "flex", "flex-direction": "column", gap: "4px" }
35
- }, ce = ["title", "onClick"], de = ["src"], fe = { style: { flex: "1", "min-width": "0" } }, pe = { style: { display: "flex", gap: "6px", "align-items": "baseline" } }, ve = { style: { color: "#fbbf24", "font-weight": "600", "text-transform": "uppercase", "font-size": "10px" } }, me = {
35
+ }, fe = ["title", "onClick"], pe = ["src"], ve = { style: { flex: "1", "min-width": "0" } }, me = { style: { display: "flex", gap: "6px", "align-items": "baseline" } }, ge = { style: { color: "#fbbf24", "font-weight": "600", "text-transform": "uppercase", "font-size": "10px" } }, ye = {
36
36
  key: 0,
37
37
  style: { color: "#94a3b8" }
38
- }, ge = { style: { color: "#94a3b8", "font-size": "10px", "white-space": "nowrap", overflow: "hidden", "text-overflow": "ellipsis" } }, Re = /* @__PURE__ */ N({
38
+ }, he = { style: { color: "#94a3b8", "font-size": "10px", "white-space": "nowrap", overflow: "hidden", "text-overflow": "ellipsis" } }, Ue = /* @__PURE__ */ J({
39
39
  __name: "RvmsVideoPlayer",
40
40
  props: {
41
41
  nvrId: {},
@@ -50,27 +50,27 @@ const X = {
50
50
  token: {}
51
51
  },
52
52
  emits: ["alarm", "stream-status", "alarm-click"],
53
- setup($, { expose: P, emit: x }) {
54
- const s = $, v = x, l = k(null), a = k("idle"), g = k(null), w = k(null), h = k(!1), C = k(s.initialProfile);
55
- function I() {
56
- C.value = C.value === "main" ? "sub" : "main", h.value && m();
53
+ setup(C, { expose: U, emit: x }) {
54
+ const s = C, f = x, l = S(null), a = S("idle"), m = S(null), w = S(null), b = S(!1), I = S(s.initialProfile);
55
+ function E() {
56
+ I.value = I.value === "main" ? "sub" : "main", b.value && u();
57
57
  }
58
- const E = k(null), L = k([]);
59
- function U(e) {
58
+ const L = S(null), z = S([]);
59
+ function R(e) {
60
60
  return `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}${e}`;
61
61
  }
62
- function D() {
62
+ function P() {
63
63
  M();
64
- const e = new WebSocket(s.token ? `/ws/alarms?token=${encodeURIComponent(s.token)}` : U("/ws/alarms"));
65
- E.value = e, e.onmessage = (t) => {
64
+ const e = new WebSocket(s.token ? `/ws/alarms?token=${encodeURIComponent(s.token)}` : R("/ws/alarms"));
65
+ L.value = e, e.onmessage = (t) => {
66
66
  try {
67
67
  const r = JSON.parse(t.data);
68
68
  switch (r.type) {
69
69
  case "snapshot":
70
- r.events && (L.value = r.events, r.events.forEach((p) => v("alarm", p)));
70
+ r.events && (z.value = r.events, r.events.forEach((p) => f("alarm", p)));
71
71
  break;
72
72
  case "alarm":
73
- r.event && (L.value.unshift(r.event), L.value.length > 50 && (L.value.length = 50), v("alarm", r.event));
73
+ r.event && (z.value.unshift(r.event), z.value.length > 50 && (z.value.length = 50), f("alarm", r.event));
74
74
  break;
75
75
  default:
76
76
  break;
@@ -78,33 +78,33 @@ const X = {
78
78
  } catch {
79
79
  }
80
80
  }, e.onclose = () => {
81
- E.value = null, setTimeout(D, 3e3);
81
+ L.value = null, setTimeout(P, 3e3);
82
82
  }, e.onerror = () => {
83
83
  };
84
84
  }
85
85
  function M() {
86
- if (E.value) {
86
+ if (L.value) {
87
87
  try {
88
- E.value.onclose = null, E.value.close();
88
+ L.value.onclose = null, L.value.close();
89
89
  } catch {
90
90
  }
91
- E.value = null;
91
+ L.value = null;
92
92
  }
93
93
  }
94
- async function A() {
94
+ async function Q() {
95
95
  try {
96
96
  const e = {};
97
97
  s.token && (e.Authorization = `Bearer ${s.token}`);
98
98
  const t = await fetch("/api/alarms/recent", { headers: e });
99
99
  if (t.ok) {
100
100
  const r = await t.json();
101
- L.value = r;
101
+ z.value = r;
102
102
  }
103
103
  } catch {
104
104
  }
105
105
  }
106
- let H = null;
107
- function O() {
106
+ let A = null;
107
+ function T() {
108
108
  return {
109
109
  cancelled: !1,
110
110
  ws: null,
@@ -116,9 +116,9 @@ const X = {
116
116
  sourceOpenHandler: null
117
117
  };
118
118
  }
119
- function z() {
119
+ function $() {
120
120
  var t;
121
- const e = H;
121
+ const e = A;
122
122
  if (e) {
123
123
  e.cancelled = !0;
124
124
  try {
@@ -140,160 +140,169 @@ const X = {
140
140
  e.mediaSource.endOfStream();
141
141
  } catch {
142
142
  }
143
- if (e.sourceBuffer = null, e.mediaSource = null, e.appendQueue.length = 0, e.preBuffer.length = 0, H = null, l.value)
143
+ if (e.sourceBuffer = null, e.mediaSource = null, e.appendQueue.length = 0, e.preBuffer.length = 0, A = null, l.value)
144
144
  try {
145
145
  l.value.pause(), l.value.removeAttribute("src"), l.value.load();
146
146
  } catch {
147
147
  }
148
148
  }
149
149
  }
150
- function W(e) {
150
+ function j(e) {
151
151
  if (!l.value) return;
152
152
  if (!("MediaSource" in window)) {
153
- g.value = "MediaSource Extensions not supported", a.value = "error", v("stream-status", "error");
153
+ m.value = "MediaSource Extensions not supported", a.value = "error", f("stream-status", "error");
154
154
  return;
155
155
  }
156
- a.value = "connecting", g.value = null, w.value = null, v("stream-status", "connecting");
157
- const t = O();
158
- H = t;
156
+ a.value = "connecting", m.value = null, w.value = null, f("stream-status", "connecting");
157
+ const t = T();
158
+ A = t;
159
159
  const r = new MediaSource();
160
160
  t.mediaSource = r, l.value.src = URL.createObjectURL(r);
161
161
  const p = () => {
162
- t.cancelled || t.mediaSource !== r || (a.value = "buffering", v("stream-status", "buffering"), T(t));
162
+ t.cancelled || t.mediaSource !== r || (a.value = "buffering", f("stream-status", "buffering"), D(t));
163
163
  };
164
164
  t.sourceOpenHandler = p, r.addEventListener("sourceopen", p);
165
- const u = new WebSocket(e);
166
- u.binaryType = "arraybuffer", t.ws = u, u.onmessage = (b) => {
165
+ const h = new WebSocket(e);
166
+ h.binaryType = "arraybuffer", t.ws = h, h.onmessage = (y) => {
167
167
  if (t.cancelled) return;
168
- if (typeof b.data == "string") {
168
+ if (typeof y.data == "string") {
169
169
  try {
170
- const R = JSON.parse(b.data);
171
- R.type === "error" && R.message && (g.value = R.message, a.value = "error", v("stream-status", "error"), z());
170
+ const O = JSON.parse(y.data);
171
+ O.type === "error" && O.message && (m.value = O.message, a.value = "error", f("stream-status", "error"), $());
172
172
  } catch {
173
173
  }
174
174
  return;
175
175
  }
176
- const _ = new Uint8Array(b.data);
176
+ const k = new Uint8Array(y.data);
177
177
  if (!t.sourceBuffer) {
178
- t.preBuffer.push(_), T(t);
178
+ t.preBuffer.push(k), D(t);
179
179
  return;
180
180
  }
181
181
  t.appendQueue.push(
182
- _.buffer.slice(_.byteOffset, _.byteOffset + _.byteLength)
182
+ k.buffer.slice(k.byteOffset, k.byteOffset + k.byteLength)
183
183
  ), o(t);
184
- }, u.onerror = () => {
185
- t.cancelled || a.value !== "error" && (g.value = "WebSocket error", a.value = "error", v("stream-status", "error"));
186
- }, u.onclose = (b) => {
187
- t.cancelled || (a.value === "connecting" || a.value === "buffering" ? (g.value = b.reason || `Stream closed (code ${b.code})`, a.value = "error", v("stream-status", "error")) : a.value === "playing" && (a.value = "ended", v("stream-status", "ended")));
184
+ }, h.onerror = () => {
185
+ t.cancelled || a.value !== "error" && (m.value = "WebSocket error", a.value = "error", f("stream-status", "error"));
186
+ }, h.onclose = (y) => {
187
+ t.cancelled || (a.value === "connecting" || a.value === "buffering" ? (m.value = y.reason || `Stream closed (code ${y.code})`, a.value = "error", f("stream-status", "error")) : a.value === "playing" && (a.value = "ended", f("stream-status", "ended")));
188
188
  };
189
189
  }
190
190
  function V(e) {
191
- const t = (u) => u.toString(16).padStart(2, "0").toUpperCase();
192
- let r = null, p = null;
193
- for (let u = 0; u + 7 < e.length; u++) {
194
- const b = String.fromCharCode(e[u], e[u + 1], e[u + 2], e[u + 3]);
195
- if (b === "avcC") {
196
- const _ = e[u + 5], R = e[u + 6], Q = e[u + 7];
197
- _ !== void 0 && R !== void 0 && Q !== void 0 && (r = `avc1.${t(_)}${t(R)}${t(Q)}`);
191
+ const t = (r) => r.toString(16).padStart(2, "0").toUpperCase();
192
+ for (let r = 0; r + 7 < e.length; r++) {
193
+ const p = String.fromCharCode(e[r], e[r + 1], e[r + 2], e[r + 3]);
194
+ if (p === "avcC") {
195
+ const h = e[r + 5], y = e[r + 6], k = e[r + 7];
196
+ if (h !== void 0 && y !== void 0 && k !== void 0)
197
+ return `avc1.${t(h)}${t(y)}${t(k)}`;
198
198
  }
199
- b === "hvcC" && (r = "hev1.1.6.L93.B0"), b === "mp4a" && (p = "mp4a.40.2");
199
+ if (p === "hvcC") return "hev1.1.6.L93.B0";
200
200
  }
201
- return r ? p ? `${r}, ${p}` : r : null;
201
+ return null;
202
202
  }
203
- function T(e) {
203
+ function W(e) {
204
+ for (let t = 0; t + 3 < e.length; t++)
205
+ if (e[t] === 101 && e[t + 1] === 115 && e[t + 2] === 100 && e[t + 3] === 115)
206
+ return !0;
207
+ return !1;
208
+ }
209
+ function D(e) {
204
210
  if (e.cancelled || e.sourceBuffer || !e.mediaSource || e.mediaSource.readyState !== "open") return;
205
211
  const t = n(e.preBuffer), r = V(t);
206
212
  if (!r) return;
207
- w.value = r;
208
- const p = `video/mp4; codecs="${r}"`;
209
- if (!MediaSource.isTypeSupported(p)) {
210
- g.value = `Codec not supported: ${p}`, a.value = "error", z();
213
+ const p = W(t), h = p ? `${r}, mp4a.40.2` : r;
214
+ w.value = h;
215
+ let y = `video/mp4; codecs="${h}"`;
216
+ if (!MediaSource.isTypeSupported(y) && p && (y = `video/mp4; codecs="${r}"`, w.value = r), !MediaSource.isTypeSupported(y)) {
217
+ m.value = `Codec not supported: ${y}`, a.value = "error", $();
211
218
  return;
212
219
  }
213
- let u;
220
+ let k;
214
221
  try {
215
- u = e.mediaSource.addSourceBuffer(p);
216
- } catch (_) {
217
- g.value = `addSourceBuffer failed: ${_.message}`, a.value = "error", z();
222
+ k = e.mediaSource.addSourceBuffer(y);
223
+ } catch (N) {
224
+ m.value = `addSourceBuffer failed: ${N.message}`, a.value = "error", $();
218
225
  return;
219
226
  }
220
- u.mode = "segments";
221
- const b = () => {
227
+ k.mode = "segments";
228
+ const O = () => {
222
229
  e.cancelled || o(e);
223
230
  };
224
- e.updateEndHandler = b, u.addEventListener("updateend", b), u.addEventListener("error", () => {
225
- e.cancelled || (g.value = "SourceBuffer error", a.value = "error");
226
- }), e.sourceBuffer = u, e.appendQueue.unshift(t.buffer.slice(t.byteOffset, t.byteOffset + t.byteLength)), e.preBuffer.length = 0, o(e);
231
+ e.updateEndHandler = O, k.addEventListener("updateend", O), k.addEventListener("error", () => {
232
+ e.cancelled || (m.value = "SourceBuffer error", a.value = "error");
233
+ }), e.sourceBuffer = k, e.appendQueue.unshift(t.buffer.slice(t.byteOffset, t.byteOffset + t.byteLength)), e.preBuffer.length = 0, o(e);
227
234
  }
228
235
  function n(e) {
229
- const t = e.reduce((u, b) => u + b.byteLength, 0), r = new Uint8Array(t);
236
+ const t = e.reduce((h, y) => h + y.byteLength, 0), r = new Uint8Array(t);
230
237
  let p = 0;
231
- for (const u of e)
232
- r.set(u, p), p += u.byteLength;
238
+ for (const h of e)
239
+ r.set(h, p), p += h.byteLength;
233
240
  return r;
234
241
  }
235
242
  function o(e) {
236
- var u;
243
+ var h, y;
237
244
  if (e.cancelled) return;
238
245
  const t = e.sourceBuffer, r = e.mediaSource;
239
246
  if (!t || !r || r.readyState !== "open" || t.updating) return;
240
247
  const p = e.appendQueue.shift();
241
248
  if (p)
242
249
  try {
243
- t.appendBuffer(p), a.value !== "playing" && (a.value = "playing", v("stream-status", "playing"));
244
- } catch (b) {
245
- const _ = b;
246
- if (_.name === "QuotaExceededError") {
247
- const R = ((u = l.value) == null ? void 0 : u.currentTime) ?? 0, Q = Math.max(0, R - 5);
250
+ t.appendBuffer(p), a.value !== "playing" && (a.value = "playing", f("stream-status", "playing"));
251
+ } catch (k) {
252
+ const O = k;
253
+ if (e.cancelled || (h = O.message) != null && h.includes("removed from the parent"))
254
+ return;
255
+ if (O.name === "QuotaExceededError") {
256
+ const N = ((y = l.value) == null ? void 0 : y.currentTime) ?? 0, F = Math.max(0, N - 5);
248
257
  try {
249
- t.buffered.length > 0 && t.buffered.start(0) < Q && t.remove(t.buffered.start(0), Q);
258
+ t.buffered.length > 0 && t.buffered.start(0) < F && t.remove(t.buffered.start(0), F);
250
259
  } catch {
251
260
  }
252
261
  e.appendQueue.unshift(p);
253
262
  } else
254
- g.value = `appendBuffer failed: ${_.message}`, a.value = "error", z();
263
+ m.value = `appendBuffer failed: ${O.message}`, a.value = "error", $();
255
264
  }
256
265
  }
257
- async function m() {
258
- if (z(), h.value = !0, !s.nvrId || !s.deviceId) {
259
- g.value = "nvrId and deviceId are required", a.value = "error", h.value = !1, v("stream-status", "error");
266
+ async function u() {
267
+ if ($(), b.value = !0, !s.nvrId || !s.deviceId) {
268
+ m.value = "nvrId and deviceId are required", a.value = "error", b.value = !1, f("stream-status", "error");
260
269
  return;
261
270
  }
262
- a.value = "connecting", g.value = null, w.value = null, v("stream-status", "connecting");
271
+ a.value = "connecting", m.value = null, w.value = null, f("stream-status", "connecting");
263
272
  try {
264
273
  const e = new URLSearchParams({
265
274
  nvrId: s.nvrId,
266
275
  deviceId: s.deviceId,
267
276
  mode: s.mode,
268
- profile: C.value
277
+ profile: I.value
269
278
  });
270
279
  s.from && e.set("from", s.from), s.to && e.set("to", s.to);
271
280
  const t = location.protocol === "https:" ? "wss:" : "ws:";
272
281
  s.token && e.set("token", s.token);
273
282
  const r = `${t}//${location.host}/ws/stream?${e.toString()}`;
274
- W(r), A();
283
+ j(r), Q();
275
284
  } catch (e) {
276
- g.value = e.message, a.value = "error", h.value = !1, v("stream-status", "error"), console.error("[RvmsVideoPlayer] start failed:", e.message);
285
+ m.value = e.message, a.value = "error", b.value = !1, f("stream-status", "error"), console.error("[RvmsVideoPlayer] start failed:", e.message);
277
286
  }
278
287
  }
279
- function y() {
280
- h.value = !1, z(), a.value = "idle", g.value = null, v("stream-status", "idle");
288
+ function g() {
289
+ b.value = !1, $(), a.value = "idle", m.value = null, f("stream-status", "idle");
281
290
  }
282
- function c(e) {
291
+ function v(e) {
283
292
  return e || "";
284
293
  }
285
- return q(() => {
286
- D(), A();
287
- }), F(() => {
288
- M(), y();
289
- }), J(
294
+ return Y(() => {
295
+ P(), Q();
296
+ }), q(() => {
297
+ M(), g();
298
+ }), K(
290
299
  () => [s.nvrId, s.deviceId, s.mode].join("|"),
291
300
  () => {
292
- h.value && s.nvrId && s.deviceId && m();
301
+ b.value && s.nvrId && s.deviceId && u();
293
302
  }
294
- ), P({ start: m, stop: y }), (e, t) => (d(), f("div", {
303
+ ), U({ start: u, stop: g }), (e, t) => (c(), d("div", {
295
304
  class: "rvms-player-wrapper",
296
- style: j({ width: $.width, height: $.height, position: "relative", background: "#000", overflow: "hidden", borderRadius: "8px" })
305
+ style: H({ width: C.width, height: C.height, position: "relative", background: "#000", overflow: "hidden", borderRadius: "8px" })
297
306
  }, [
298
307
  i("video", {
299
308
  ref_key: "videoEl",
@@ -303,13 +312,13 @@ const X = {
303
312
  controls: "",
304
313
  style: { width: "100%", height: "100%", "object-fit": "contain", display: "block" }
305
314
  }, null, 512),
306
- a.value === "idle" ? (d(), f("div", {
315
+ a.value === "idle" ? (c(), d("div", {
307
316
  key: 0,
308
317
  class: "rvms-overlay",
309
318
  style: { cursor: "pointer" },
310
- onClick: m
319
+ onClick: u
311
320
  }, [
312
- (d(), f("svg", X, [...t[2] || (t[2] = [
321
+ (c(), d("svg", ee, [...t[2] || (t[2] = [
313
322
  i("circle", {
314
323
  cx: "32",
315
324
  cy: "32",
@@ -324,108 +333,108 @@ const X = {
324
333
  }, null, -1)
325
334
  ])])),
326
335
  t[3] || (t[3] = i("span", { style: { color: "#94a3b8", "font-size": "0.85rem" } }, "Click to play", -1))
327
- ])) : a.value === "connecting" ? (d(), f("div", Z, [...t[4] || (t[4] = [
336
+ ])) : a.value === "connecting" ? (c(), d("div", te, [...t[4] || (t[4] = [
328
337
  i("span", { style: { color: "#93c5fd", animation: "rvms-pulse 1.5s infinite" } }, "Connecting…", -1)
329
- ])])) : a.value === "buffering" ? (d(), f("div", ee, [...t[5] || (t[5] = [
338
+ ])])) : a.value === "buffering" ? (c(), d("div", ne, [...t[5] || (t[5] = [
330
339
  i("span", { style: { color: "#93c5fd", animation: "rvms-pulse 1.5s infinite" } }, "Buffering…", -1)
331
- ])])) : a.value === "error" ? (d(), f("div", te, [
340
+ ])])) : a.value === "error" ? (c(), d("div", re, [
332
341
  t[6] || (t[6] = i("span", { style: { color: "#f87171", "font-weight": "600" } }, "Stream error", -1)),
333
- g.value ? (d(), f("span", ne, B(g.value), 1)) : S("", !0),
342
+ m.value ? (c(), d("span", oe, _(m.value), 1)) : B("", !0),
334
343
  i("button", {
335
344
  class: "rvms-btn",
336
- onClick: m
345
+ onClick: u
337
346
  }, " Retry ")
338
- ])) : a.value === "ended" ? (d(), f("div", re, [
347
+ ])) : a.value === "ended" ? (c(), d("div", ae, [
339
348
  t[7] || (t[7] = i("span", { style: { color: "#94a3b8" } }, "Stream ended", -1)),
340
349
  i("button", {
341
350
  class: "rvms-btn",
342
- onClick: m
351
+ onClick: u
343
352
  }, " Reconnect ")
344
- ])) : S("", !0),
345
- i("div", oe, [
346
- a.value !== "idle" ? (d(), f("span", {
353
+ ])) : B("", !0),
354
+ i("div", le, [
355
+ a.value !== "idle" ? (c(), d("span", {
347
356
  key: 0,
348
- style: j({
357
+ style: H({
349
358
  padding: "2px 8px",
350
359
  borderRadius: "4px",
351
360
  fontSize: "10px",
352
361
  fontWeight: 700,
353
362
  textTransform: "uppercase",
354
363
  letterSpacing: "0.05em",
355
- background: $.mode === "live" && a.value === "playing" ? "#dc2626" : "#334155",
364
+ background: C.mode === "live" && a.value === "playing" ? "#dc2626" : "#334155",
356
365
  color: "#fff"
357
366
  })
358
- }, B($.mode === "live" ? "LIVE" : "PLAYBACK"), 5)) : S("", !0),
359
- w.value ? (d(), f("span", ae, B(w.value), 1)) : S("", !0),
360
- E.value ? (d(), f("span", le, [...t[8] || (t[8] = [
367
+ }, _(C.mode === "live" ? "LIVE" : "PLAYBACK"), 5)) : B("", !0),
368
+ w.value ? (c(), d("span", se, _(w.value), 1)) : B("", !0),
369
+ L.value ? (c(), d("span", ie, [...t[8] || (t[8] = [
361
370
  i("span", { style: { width: "6px", height: "6px", "border-radius": "50%", background: "#22c55e", display: "inline-block" } }, null, -1),
362
- K(" Alarms ", -1)
363
- ])])) : S("", !0)
371
+ G(" Alarms ", -1)
372
+ ])])) : B("", !0)
364
373
  ]),
365
- a.value !== "idle" ? (d(), f("div", se, [
374
+ a.value !== "idle" ? (c(), d("div", ue, [
366
375
  i("button", {
367
376
  class: "rvms-btn",
368
377
  title: "Stop stream",
369
- onClick: y
378
+ onClick: g
370
379
  }, " ⏹ Stop "),
371
380
  i("button", {
372
381
  class: "rvms-btn",
373
- title: `Switch to ${C.value === "main" ? "sub" : "main"} stream`,
374
- onClick: I
375
- }, B(C.value === "main" ? "HD" : "SD"), 9, ie)
376
- ])) : S("", !0),
377
- $.showAlarms && L.value.length > 0 ? (d(), f("div", ue, [
378
- (d(!0), f(Y, null, G(L.value.slice(0, 5), (r) => (d(), f("div", {
382
+ title: `Switch to ${I.value === "main" ? "sub" : "main"} stream`,
383
+ onClick: E
384
+ }, _(I.value === "main" ? "HD" : "SD"), 9, ce)
385
+ ])) : B("", !0),
386
+ C.showAlarms && z.value.length > 0 ? (c(), d("div", de, [
387
+ (c(!0), d(X, null, Z(z.value.slice(0, 5), (r) => (c(), d("div", {
379
388
  key: r.id,
380
389
  title: (r.description || r.type) + " — click to view playback",
381
390
  style: { display: "flex", "align-items": "center", gap: "8px", padding: "4px 8px", "border-radius": "4px", background: "rgba(0, 0, 0, 0.65)", "font-size": "11px", "backdrop-filter": "blur(4px)", cursor: "pointer", transition: "background 0.15s" },
382
- onClick: (p) => v("alarm-click", r),
391
+ onClick: (p) => f("alarm-click", r),
383
392
  onMouseenter: t[0] || (t[0] = (p) => p.target.style.background = "rgba(51, 65, 85, 0.8)"),
384
393
  onMouseleave: t[1] || (t[1] = (p) => p.target.style.background = "rgba(0, 0, 0, 0.65)")
385
394
  }, [
386
- r.snapshotUrl ? (d(), f("img", {
395
+ r.snapshotUrl ? (c(), d("img", {
387
396
  key: 0,
388
- src: c(r.snapshotUrl),
397
+ src: v(r.snapshotUrl),
389
398
  alt: "snapshot",
390
399
  style: { width: "32px", height: "24px", "border-radius": "2px", "object-fit": "cover", background: "#1e293b" },
391
400
  loading: "lazy"
392
- }, null, 8, de)) : S("", !0),
393
- i("div", fe, [
394
- i("div", pe, [
395
- i("span", ve, B(r.type), 1),
396
- r.channel ? (d(), f("span", me, " ch" + B(r.channel), 1)) : S("", !0)
401
+ }, null, 8, pe)) : B("", !0),
402
+ i("div", ve, [
403
+ i("div", me, [
404
+ i("span", ge, _(r.type), 1),
405
+ r.channel ? (c(), d("span", ye, " ch" + _(r.channel), 1)) : B("", !0)
397
406
  ]),
398
- i("div", ge, B(new Date(r.timestamp).toLocaleString()), 1),
407
+ i("div", he, _(new Date(r.timestamp).toLocaleString()), 1),
399
408
  t[9] || (t[9] = i("div", { style: { color: "#60a5fa", "font-size": "9px", "margin-top": "1px" } }, " Click to view playback ", -1))
400
409
  ])
401
- ], 40, ce))), 128))
402
- ])) : S("", !0)
410
+ ], 40, fe))), 128))
411
+ ])) : B("", !0)
403
412
  ], 4));
404
413
  }
405
- }), ye = {
414
+ }), be = {
406
415
  key: 1,
407
416
  style: { position: "absolute", inset: "0", display: "flex", "flex-direction": "column", "align-items": "center", "justify-content": "center", gap: "8px", background: "rgba(0, 0, 0, 0.4)", "z-index": "10" }
408
- }, he = { style: { color: "#e2e8f0", "font-size": "0.85rem" } }, be = {
417
+ }, xe = { style: { color: "#e2e8f0", "font-size": "0.85rem" } }, ke = {
409
418
  key: 2,
410
419
  style: { position: "absolute", inset: "0", display: "flex", "flex-direction": "column", "align-items": "center", "justify-content": "center", gap: "12px", background: "rgba(0, 0, 0, 0.5)", "z-index": "10" }
411
- }, xe = { style: { color: "#ef4444", "font-size": "0.85rem", "text-align": "center", padding: "0 16px" } }, ke = {
420
+ }, Se = { style: { color: "#ef4444", "font-size": "0.85rem", "text-align": "center", padding: "0 16px" } }, we = {
412
421
  key: 3,
413
422
  style: { position: "absolute", inset: "0", display: "flex", "flex-direction": "column", "align-items": "center", "justify-content": "center", gap: "12px", background: "rgba(0, 0, 0, 0.4)", "z-index": "10" }
414
- }, Se = { style: { position: "absolute", top: "8px", left: "8px", right: "8px", display: "flex", gap: "6px", "align-items": "center", "flex-wrap": "wrap", "z-index": "11", "pointer-events": "none" } }, we = ["title"], Be = ["title"], $e = {
423
+ }, Be = { style: { position: "absolute", top: "8px", left: "8px", right: "8px", display: "flex", gap: "6px", "align-items": "center", "flex-wrap": "wrap", "z-index": "11", "pointer-events": "none" } }, $e = ["title"], _e = ["title"], Ce = {
415
424
  key: 0,
416
425
  class: "rvms-chip",
417
426
  style: { background: "rgba(51, 65, 85, 0.8)", color: "#cbd5e1", "font-family": "monospace" }
418
- }, _e = ["value"], Ce = {
427
+ }, Ie = ["value"], Ee = {
419
428
  key: 2,
420
429
  class: "rvms-chip",
421
430
  style: { background: "rgba(15, 23, 42, 0.85)", color: "#94a3b8", cursor: "default", "pointer-events": "auto" }
422
- }, Ie = {
431
+ }, Le = {
423
432
  key: 4,
424
433
  style: { position: "absolute", bottom: "44px", left: "8px", display: "flex", gap: "6px", "align-items": "center", "z-index": "11", "pointer-events": "none" }
425
- }, Ee = {
434
+ }, Oe = {
426
435
  key: 5,
427
436
  style: { position: "absolute", bottom: "8px", right: "8px", display: "flex", gap: "6px", "z-index": "11" }
428
- }, Le = /* @__PURE__ */ N({
437
+ }, ze = /* @__PURE__ */ J({
429
438
  __name: "RvmsVideo",
430
439
  props: {
431
440
  nvrId: {},
@@ -436,10 +445,10 @@ const X = {
436
445
  token: {}
437
446
  },
438
447
  emits: ["status-change"],
439
- setup($, { emit: P }) {
440
- const x = $, s = P, v = k(null), l = k("idle"), a = k(null), g = k(null), w = k(!1), h = k("live"), C = k(x.initialProfile), I = k("");
441
- let E = null;
442
- function L() {
448
+ setup(C, { emit: U }) {
449
+ const x = C, s = U, f = S(null), l = S("idle"), a = S(null), m = S(null), w = S(!1), b = S("live"), I = S(x.initialProfile), E = S("");
450
+ let L = null;
451
+ function z() {
443
452
  return {
444
453
  cancelled: !1,
445
454
  ws: null,
@@ -451,9 +460,9 @@ const X = {
451
460
  sourceOpenHandler: null
452
461
  };
453
462
  }
454
- function U() {
463
+ function R() {
455
464
  var o;
456
- const n = E;
465
+ const n = L;
457
466
  if (n) {
458
467
  n.cancelled = !0;
459
468
  try {
@@ -475,35 +484,35 @@ const X = {
475
484
  n.mediaSource.endOfStream();
476
485
  } catch {
477
486
  }
478
- if (n.sourceBuffer = null, n.mediaSource = null, n.appendQueue.length = 0, n.preBuffer.length = 0, E = null, v.value)
487
+ if (n.sourceBuffer = null, n.mediaSource = null, n.appendQueue.length = 0, n.preBuffer.length = 0, L = null, f.value)
479
488
  try {
480
- v.value.pause(), v.value.removeAttribute("src"), v.value.load();
489
+ f.value.pause(), f.value.removeAttribute("src"), f.value.load();
481
490
  } catch {
482
491
  }
483
492
  }
484
493
  }
485
- function D(n) {
486
- if (!v.value) return;
494
+ function P(n) {
495
+ if (!f.value) return;
487
496
  if (!("MediaSource" in window)) {
488
497
  a.value = "MediaSource Extensions not supported", l.value = "error", s("status-change", "error");
489
498
  return;
490
499
  }
491
- l.value = "connecting", a.value = null, g.value = null, s("status-change", "connecting");
492
- const o = L();
493
- E = o;
494
- const m = new MediaSource();
495
- o.mediaSource = m, v.value.src = URL.createObjectURL(m);
496
- const y = () => {
497
- o.cancelled || o.mediaSource !== m || (l.value = "buffering", s("status-change", "buffering"), A(o));
500
+ l.value = "connecting", a.value = null, m.value = null, s("status-change", "connecting");
501
+ const o = z();
502
+ L = o;
503
+ const u = new MediaSource();
504
+ o.mediaSource = u, f.value.src = URL.createObjectURL(u);
505
+ const g = () => {
506
+ o.cancelled || o.mediaSource !== u || (l.value = "buffering", s("status-change", "buffering"), A(o));
498
507
  };
499
- o.sourceOpenHandler = y, m.addEventListener("sourceopen", y);
500
- const c = new WebSocket(n);
501
- c.binaryType = "arraybuffer", o.ws = c, c.onmessage = (e) => {
508
+ o.sourceOpenHandler = g, u.addEventListener("sourceopen", g);
509
+ const v = new WebSocket(n);
510
+ v.binaryType = "arraybuffer", o.ws = v, v.onmessage = (e) => {
502
511
  if (o.cancelled) return;
503
512
  if (typeof e.data == "string") {
504
513
  try {
505
514
  const r = JSON.parse(e.data);
506
- r.type === "error" && r.message && (a.value = r.message, l.value = "error", s("status-change", "error"), U());
515
+ r.type === "error" && r.message && (a.value = r.message, l.value = "error", s("status-change", "error"), R());
507
516
  } catch {
508
517
  }
509
518
  return;
@@ -515,43 +524,49 @@ const X = {
515
524
  }
516
525
  o.appendQueue.push(
517
526
  t.buffer.slice(t.byteOffset, t.byteOffset + t.byteLength)
518
- ), H(o);
519
- }, c.onerror = () => {
527
+ ), T(o);
528
+ }, v.onerror = () => {
520
529
  o.cancelled || l.value !== "error" && (a.value = "WebSocket error", l.value = "error", s("status-change", "error"));
521
- }, c.onclose = (e) => {
530
+ }, v.onclose = (e) => {
522
531
  o.cancelled || (l.value === "connecting" || l.value === "buffering" ? (a.value = e.reason || `Stream closed (code ${e.code})`, l.value = "error", s("status-change", "error")) : l.value === "playing" && (l.value = "ended", s("status-change", "ended")));
523
532
  };
524
533
  }
525
534
  function M(n) {
526
- const o = (c) => c.toString(16).padStart(2, "0").toUpperCase();
527
- let m = null, y = null;
528
- for (let c = 0; c + 7 < n.length; c++) {
529
- const e = String.fromCharCode(n[c], n[c + 1], n[c + 2], n[c + 3]);
530
- if (e === "avcC") {
531
- const t = n[c + 5], r = n[c + 6], p = n[c + 7];
532
- t !== void 0 && r !== void 0 && p !== void 0 && (m = `avc1.${o(t)}${o(r)}${o(p)}`);
535
+ const o = (u) => u.toString(16).padStart(2, "0").toUpperCase();
536
+ for (let u = 0; u + 7 < n.length; u++) {
537
+ const g = String.fromCharCode(n[u], n[u + 1], n[u + 2], n[u + 3]);
538
+ if (g === "avcC") {
539
+ const v = n[u + 5], e = n[u + 6], t = n[u + 7];
540
+ if (v !== void 0 && e !== void 0 && t !== void 0)
541
+ return `avc1.${o(v)}${o(e)}${o(t)}`;
533
542
  }
534
- e === "hvcC" && (m = "hev1.1.6.L93.B0"), e === "mp4a" && (y = "mp4a.40.2");
543
+ if (g === "hvcC") return "hev1.1.6.L93.B0";
535
544
  }
536
- return m ? y ? `${m}, ${y}` : m : null;
545
+ return null;
546
+ }
547
+ function Q(n) {
548
+ for (let o = 0; o + 3 < n.length; o++)
549
+ if (n[o] === 101 && n[o + 1] === 115 && n[o + 2] === 100 && n[o + 3] === 115) return !0;
550
+ return !1;
537
551
  }
538
552
  function A(n) {
539
553
  if (n.sourceBuffer || !n.mediaSource || n.mediaSource.readyState !== "open") return;
540
554
  const o = ["avc1.64001F", "avc1.4D0028", "avc1.42001E", "hev1.1.6.L93.B0"];
541
- let m = 'video/mp4; codecs="avc1.64001F"';
542
- for (const c of n.preBuffer) {
543
- const e = M(c);
555
+ let u = 'video/mp4; codecs="avc1.64001F"';
556
+ for (const v of n.preBuffer) {
557
+ const e = M(v);
544
558
  if (e) {
545
- g.value = e, m = `video/mp4; codecs="${e}"`;
559
+ const t = Q(v), r = t ? `${e}, mp4a.40.2` : e;
560
+ m.value = r, u = `video/mp4; codecs="${r}"`, !MediaSource.isTypeSupported(u) && t && (u = `video/mp4; codecs="${e}"`, m.value = e);
546
561
  break;
547
562
  }
548
563
  }
549
564
  try {
550
- n.sourceBuffer = n.mediaSource.addSourceBuffer(m);
565
+ n.sourceBuffer = n.mediaSource.addSourceBuffer(u);
551
566
  } catch {
552
- for (const c of o)
567
+ for (const v of o)
553
568
  try {
554
- n.sourceBuffer = n.mediaSource.addSourceBuffer(`video/mp4; codecs="${c}"`);
569
+ n.sourceBuffer = n.mediaSource.addSourceBuffer(`video/mp4; codecs="${v}"`);
555
570
  break;
556
571
  } catch {
557
572
  }
@@ -560,89 +575,91 @@ const X = {
560
575
  a.value = "No compatible video codec", l.value = "error";
561
576
  return;
562
577
  }
563
- for (const c of n.preBuffer)
578
+ for (const v of n.preBuffer)
564
579
  try {
565
- n.sourceBuffer.appendBuffer(c);
580
+ n.sourceBuffer.appendBuffer(v);
566
581
  } catch {
567
582
  }
568
583
  n.preBuffer.length = 0;
569
- const y = () => {
570
- H(n);
584
+ const g = () => {
585
+ T(n);
571
586
  };
572
- n.updateEndHandler = y, n.sourceBuffer.addEventListener("updateend", y);
587
+ n.updateEndHandler = g, n.sourceBuffer.addEventListener("updateend", g);
573
588
  }
574
- function H(n) {
575
- var y;
589
+ function T(n) {
590
+ var g, v;
576
591
  const o = n.sourceBuffer;
577
592
  if (!o || o.updating || n.appendQueue.length === 0) return;
578
- const m = n.appendQueue.shift();
593
+ const u = n.appendQueue.shift();
579
594
  try {
580
- o.appendBuffer(m), l.value !== "playing" && (l.value = "playing", s("status-change", "playing"));
581
- } catch (c) {
582
- const e = c;
583
- if (e.name === "QuotaExceededError") {
584
- const t = ((y = v.value) == null ? void 0 : y.currentTime) ?? 0, r = Math.max(0, t - 5);
595
+ o.appendBuffer(u), l.value !== "playing" && (l.value = "playing", s("status-change", "playing"));
596
+ } catch (e) {
597
+ const t = e;
598
+ if (n.cancelled || (g = t.message) != null && g.includes("removed from the parent"))
599
+ return;
600
+ if (t.name === "QuotaExceededError") {
601
+ const r = ((v = f.value) == null ? void 0 : v.currentTime) ?? 0, p = Math.max(0, r - 5);
585
602
  try {
586
- o.buffered.length > 0 && o.buffered.start(0) < r && o.remove(o.buffered.start(0), r);
603
+ o.buffered.length > 0 && o.buffered.start(0) < p && o.remove(o.buffered.start(0), p);
587
604
  } catch {
588
605
  }
589
- n.appendQueue.unshift(m);
606
+ n.appendQueue.unshift(u);
590
607
  } else
591
- a.value = `appendBuffer failed: ${e.message}`, l.value = "error", U();
608
+ a.value = `appendBuffer failed: ${t.message}`, l.value = "error", R();
592
609
  }
593
610
  }
594
- async function O() {
595
- if (h.value === "playback" && !I.value && (I.value = (/* @__PURE__ */ new Date()).toISOString()), U(), w.value = !0, !x.nvrId || !x.channelId) {
611
+ async function $() {
612
+ if (b.value === "playback" && !E.value && (E.value = (/* @__PURE__ */ new Date()).toISOString()), R(), w.value = !0, !x.nvrId || !x.channelId) {
596
613
  a.value = "nvrId and channelId are required", l.value = "error", w.value = !1, s("status-change", "error");
597
614
  return;
598
615
  }
599
- l.value = "connecting", a.value = null, g.value = null, s("status-change", "connecting");
616
+ l.value = "connecting", a.value = null, m.value = null, s("status-change", "connecting");
600
617
  try {
601
618
  const n = new URLSearchParams({
602
619
  nvrId: x.nvrId,
603
620
  deviceId: x.channelId,
604
- mode: h.value,
605
- profile: C.value
621
+ mode: b.value,
622
+ profile: I.value
606
623
  });
607
- if (h.value === "playback") {
608
- let y = I.value;
609
- y || (y = (/* @__PURE__ */ new Date()).toISOString(), I.value = y);
610
- const c = new Date(y).getTime();
611
- n.set("from", new Date(c - 3e4).toISOString()), n.set("to", new Date(c + 3e4).toISOString());
624
+ if (b.value === "playback") {
625
+ let g = E.value;
626
+ g || (g = (/* @__PURE__ */ new Date()).toISOString(), E.value = g);
627
+ const v = new Date(g).getTime();
628
+ n.set("from", new Date(v - 3e4).toISOString()), n.set("to", new Date(v + 3e4).toISOString());
612
629
  }
613
630
  const o = location.protocol === "https:" ? "wss:" : "ws:";
614
631
  x.token && n.set("token", x.token);
615
- const m = `${o}//${location.host}/ws/stream?${n.toString()}`;
616
- D(m);
632
+ const u = `${o}//${location.host}/ws/stream?${n.toString()}`;
633
+ P(u);
617
634
  } catch (n) {
618
635
  a.value = n.message, l.value = "error", w.value = !1, s("status-change", "error");
619
636
  }
620
637
  }
621
- function z() {
622
- w.value = !1, U(), l.value = "idle", a.value = null, s("status-change", "idle");
623
- }
624
- function W() {
625
- h.value = h.value === "live" ? "playback" : "live", w.value && O();
638
+ function j() {
639
+ w.value = !1, R(), l.value = "idle", a.value = null, s("status-change", "idle");
626
640
  }
627
641
  function V() {
628
- C.value = C.value === "main" ? "sub" : "main", w.value && O();
642
+ b.value = b.value === "live" ? "playback" : "live", w.value && $();
629
643
  }
630
- function T(n) {
644
+ function W() {
645
+ I.value = I.value === "main" ? "sub" : "main", w.value && $();
646
+ }
647
+ function D(n) {
631
648
  const o = n.target;
632
- o.value && (I.value = new Date(o.value).toISOString());
649
+ o.value && (E.value = new Date(o.value).toISOString());
633
650
  }
634
- return F(() => {
635
- z();
636
- }), J(
651
+ return q(() => {
652
+ j();
653
+ }), K(
637
654
  () => [x.nvrId, x.channelId].join("|"),
638
655
  () => {
639
- w.value && x.nvrId && x.channelId && O();
656
+ w.value && x.nvrId && x.channelId && $();
640
657
  }
641
- ), (n, o) => (d(), f("div", {
658
+ ), (n, o) => (c(), d("div", {
642
659
  class: "rvms-video",
643
- style: j({
644
- width: $.width,
645
- height: $.height,
660
+ style: H({
661
+ width: C.width,
662
+ height: C.height,
646
663
  position: "relative",
647
664
  background: "#000",
648
665
  overflow: "hidden",
@@ -651,15 +668,15 @@ const X = {
651
668
  }, [
652
669
  i("video", {
653
670
  ref_key: "videoEl",
654
- ref: v,
671
+ ref: f,
655
672
  muted: "",
656
673
  playsinline: "",
657
674
  style: { width: "100%", height: "100%", display: "block", "object-fit": "contain" }
658
675
  }, null, 512),
659
- l.value === "idle" ? (d(), f("div", {
676
+ l.value === "idle" ? (c(), d("div", {
660
677
  key: 0,
661
678
  style: { position: "absolute", inset: "0", display: "flex", "flex-direction": "column", "align-items": "center", "justify-content": "center", gap: "12px", background: "rgba(0, 0, 0, 0.5)", cursor: "pointer", "z-index": "10" },
662
- onClick: O
679
+ onClick: $
663
680
  }, [...o[0] || (o[0] = [
664
681
  i("svg", {
665
682
  width: "64",
@@ -681,53 +698,53 @@ const X = {
681
698
  })
682
699
  ], -1),
683
700
  i("span", { style: { color: "#e2e8f0", "font-size": "0.9rem", "font-weight": "600" } }, "Click to play", -1)
684
- ])])) : l.value === "connecting" || l.value === "buffering" ? (d(), f("div", ye, [
701
+ ])])) : l.value === "connecting" || l.value === "buffering" ? (c(), d("div", be, [
685
702
  o[1] || (o[1] = i("div", { class: "rvms-spinner" }, null, -1)),
686
- i("span", he, B(l.value === "connecting" ? "Connecting…" : "Buffering…"), 1)
687
- ])) : l.value === "error" ? (d(), f("div", be, [
688
- i("span", xe, B(a.value || "Stream error"), 1),
703
+ i("span", xe, _(l.value === "connecting" ? "Connecting…" : "Buffering…"), 1)
704
+ ])) : l.value === "error" ? (c(), d("div", ke, [
705
+ i("span", Se, _(a.value || "Stream error"), 1),
689
706
  i("button", {
690
707
  class: "rvms-btn",
691
- onClick: O
708
+ onClick: $
692
709
  }, " Retry ")
693
- ])) : l.value === "ended" ? (d(), f("div", ke, [
710
+ ])) : l.value === "ended" ? (c(), d("div", we, [
694
711
  o[2] || (o[2] = i("span", { style: { color: "#94a3b8" } }, "Stream ended", -1)),
695
712
  i("button", {
696
713
  class: "rvms-btn",
697
- onClick: O
714
+ onClick: $
698
715
  }, "Reconnect")
699
- ])) : S("", !0),
700
- i("div", Se, [
716
+ ])) : B("", !0),
717
+ i("div", Be, [
701
718
  i("button", {
702
719
  class: "rvms-chip",
703
- title: `Switch to ${h.value === "live" ? "playback" : "live"} mode`,
704
- onClick: W,
705
- style: j({
706
- background: h.value === "live" ? "#dc2626" : "#2563eb",
720
+ title: `Switch to ${b.value === "live" ? "playback" : "live"} mode`,
721
+ onClick: V,
722
+ style: H({
723
+ background: b.value === "live" ? "#dc2626" : "#2563eb",
707
724
  pointerEvents: "auto"
708
725
  })
709
- }, B(h.value === "live" ? "🔴 LIVE" : "🔵 PLAYBACK"), 13, we),
726
+ }, _(b.value === "live" ? "🔴 LIVE" : "🔵 PLAYBACK"), 13, $e),
710
727
  i("button", {
711
728
  class: "rvms-chip",
712
- title: `Switch to ${C.value === "main" ? "sub" : "main"} stream`,
713
- onClick: V,
729
+ title: `Switch to ${I.value === "main" ? "sub" : "main"} stream`,
730
+ onClick: W,
714
731
  style: { pointerEvents: "auto" }
715
- }, B(C.value === "main" ? "HD" : "SD"), 9, Be),
716
- g.value ? (d(), f("span", $e, B(g.value), 1)) : S("", !0),
717
- h.value === "playback" ? (d(), f("input", {
732
+ }, _(I.value === "main" ? "HD" : "SD"), 9, _e),
733
+ m.value ? (c(), d("span", Ce, _(m.value), 1)) : B("", !0),
734
+ b.value === "playback" ? (c(), d("input", {
718
735
  key: 1,
719
736
  type: "datetime-local",
720
- value: I.value ? I.value.slice(0, 16) : "",
721
- onInput: T,
737
+ value: E.value ? E.value.slice(0, 16) : "",
738
+ onInput: D,
722
739
  class: "rvms-datetime",
723
740
  title: "Playback timestamp",
724
741
  style: { "pointer-events": "auto" }
725
- }, null, 40, _e)) : S("", !0),
726
- h.value === "playback" && I.value ? (d(), f("span", Ce, B(new Date(I.value).toLocaleString()), 1)) : S("", !0)
742
+ }, null, 40, Ie)) : B("", !0),
743
+ b.value === "playback" && E.value ? (c(), d("span", Ee, _(new Date(E.value).toLocaleString()), 1)) : B("", !0)
727
744
  ]),
728
- l.value !== "idle" ? (d(), f("div", Ie, [
745
+ l.value !== "idle" ? (c(), d("div", Le, [
729
746
  i("span", {
730
- style: j({
747
+ style: H({
731
748
  padding: "2px 8px",
732
749
  borderRadius: "4px",
733
750
  fontSize: "10px",
@@ -736,30 +753,30 @@ const X = {
736
753
  background: l.value === "playing" ? "rgba(34, 197, 94, 0.8)" : "rgba(100, 116, 139, 0.6)",
737
754
  color: l.value === "playing" ? "#052e16" : "#e2e8f0"
738
755
  })
739
- }, B(l.value), 5)
740
- ])) : S("", !0),
741
- l.value !== "idle" ? (d(), f("div", Ee, [
742
- w.value ? (d(), f("button", {
756
+ }, _(l.value), 5)
757
+ ])) : B("", !0),
758
+ l.value !== "idle" ? (c(), d("div", Oe, [
759
+ w.value ? (c(), d("button", {
743
760
  key: 0,
744
761
  class: "rvms-btn",
745
762
  title: "Stop stream",
746
- onClick: z
747
- }, " ⏹ Stop ")) : (d(), f("button", {
763
+ onClick: j
764
+ }, " ⏹ Stop ")) : (c(), d("button", {
748
765
  key: 1,
749
766
  class: "rvms-btn",
750
767
  title: "Start stream",
751
- onClick: O
768
+ onClick: $
752
769
  }, " ▶ Play "))
753
- ])) : S("", !0)
770
+ ])) : B("", !0)
754
771
  ], 4));
755
772
  }
756
- }), Oe = ($, P) => {
757
- const x = $.__vccOpts || $;
758
- for (const [s, v] of P)
759
- x[s] = v;
773
+ }), Re = (C, U) => {
774
+ const x = C.__vccOpts || C;
775
+ for (const [s, f] of U)
776
+ x[s] = f;
760
777
  return x;
761
- }, Ue = /* @__PURE__ */ Oe(Le, [["__scopeId", "data-v-bfc7094e"]]);
778
+ }, He = /* @__PURE__ */ Re(ze, [["__scopeId", "data-v-ac4e493f"]]);
762
779
  export {
763
- Ue as RvmsVideo,
764
- Re as RvmsVideoPlayer
780
+ He as RvmsVideo,
781
+ Ue as RvmsVideoPlayer
765
782
  };
@@ -1 +1 @@
1
- @keyframes rvms-pulse{0%,to{opacity:1}50%{opacity:.4}}.rvms-overlay{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#00000080;font-size:14px}.rvms-btn{padding:4px 12px;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:#1e293bcc;color:#e2e8f0;font-size:11px;font-weight:600;cursor:pointer;pointer-events:auto;transition:background .15s}.rvms-btn:hover{background:#334155e6}@keyframes rvms-spin-bfc7094e{to{transform:rotate(360deg)}}.rvms-spinner[data-v-bfc7094e]{width:32px;height:32px;border:3px solid rgba(255,255,255,.2);border-top-color:#60a5fa;border-radius:50%;animation:rvms-spin-bfc7094e .8s linear infinite}.rvms-btn[data-v-bfc7094e]{padding:6px 14px;border:none;border-radius:6px;background:#0f172ad9;color:#e2e8f0;font-size:.8rem;font-weight:700;cursor:pointer;transition:background .15s;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.rvms-btn[data-v-bfc7094e]:hover{background:#1e293bf2}.rvms-chip[data-v-bfc7094e]{padding:2px 8px;border:none;border-radius:4px;font-size:10px;font-weight:700;color:#fff;cursor:pointer;transition:opacity .15s;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);text-transform:uppercase;letter-spacing:.05em}.rvms-chip[data-v-bfc7094e]:hover{opacity:.85}.rvms-datetime[data-v-bfc7094e]{padding:2px 6px;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:#0f172ad9;color:#e2e8f0;font-size:11px;cursor:pointer;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.rvms-datetime[data-v-bfc7094e]:focus{outline:none;border-color:#60a5fa}
1
+ @keyframes rvms-pulse{0%,to{opacity:1}50%{opacity:.4}}.rvms-overlay{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#00000080;font-size:14px}.rvms-btn{padding:4px 12px;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:#1e293bcc;color:#e2e8f0;font-size:11px;font-weight:600;cursor:pointer;pointer-events:auto;transition:background .15s}.rvms-btn:hover{background:#334155e6}@keyframes rvms-spin-ac4e493f{to{transform:rotate(360deg)}}.rvms-spinner[data-v-ac4e493f]{width:32px;height:32px;border:3px solid rgba(255,255,255,.2);border-top-color:#60a5fa;border-radius:50%;animation:rvms-spin-ac4e493f .8s linear infinite}.rvms-btn[data-v-ac4e493f]{padding:6px 14px;border:none;border-radius:6px;background:#0f172ad9;color:#e2e8f0;font-size:.8rem;font-weight:700;cursor:pointer;transition:background .15s;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.rvms-btn[data-v-ac4e493f]:hover{background:#1e293bf2}.rvms-chip[data-v-ac4e493f]{padding:2px 8px;border:none;border-radius:4px;font-size:10px;font-weight:700;color:#fff;cursor:pointer;transition:opacity .15s;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);text-transform:uppercase;letter-spacing:.05em}.rvms-chip[data-v-ac4e493f]:hover{opacity:.85}.rvms-datetime[data-v-ac4e493f]{padding:2px 6px;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:#0f172ad9;color:#e2e8f0;font-size:11px;cursor:pointer;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.rvms-datetime[data-v-ac4e493f]:focus{outline:none;border-color:#60a5fa}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvms-vue",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "RVMS (Video-MS) Vue 3 components — RvmsVideoPlayer, RvmsVideo",
6
6
  "main": "./dist/lib/index.js",
@@ -244,39 +244,54 @@ function connectStream(wsUrl: string): void {
244
244
 
245
245
  // ── MSE helpers ──────────────────────────────────────────────────────────────
246
246
 
247
- function detectCodec(buffer: Uint8Array): string | null {
247
+ function detectVideoCodec(buffer: Uint8Array): string | null {
248
248
  const hex2 = (n: number) => n.toString(16).padStart(2, '0').toUpperCase();
249
- let videoCodec: string | null = null;
250
- let audioCodec: string | null = null;
251
249
  for (let i = 0; i + 7 < buffer.length; i++) {
252
250
  const tag = String.fromCharCode(buffer[i]!, buffer[i + 1]!, buffer[i + 2]!, buffer[i + 3]!);
253
251
  if (tag === 'avcC') {
254
252
  const profile = buffer[i + 5], compat = buffer[i + 6], level = buffer[i + 7];
255
253
  if (profile !== undefined && compat !== undefined && level !== undefined) {
256
- videoCodec = `avc1.${hex2(profile)}${hex2(compat)}${hex2(level)}`;
254
+ return `avc1.${hex2(profile)}${hex2(compat)}${hex2(level)}`;
257
255
  }
258
256
  }
259
- if (tag === 'hvcC') videoCodec = 'hev1.1.6.L93.B0';
260
- if (tag === 'mp4a') audioCodec = 'mp4a.40.2';
257
+ if (tag === 'hvcC') return 'hev1.1.6.L93.B0';
261
258
  }
262
- if (!videoCodec) return null;
263
- return audioCodec ? `${videoCodec}, ${audioCodec}` : videoCodec;
259
+ return null;
260
+ }
261
+
262
+ /** Check if the init segment contains an audio track (esds box inside mp4a). */
263
+ function hasAudio(buffer: Uint8Array): boolean {
264
+ for (let i = 0; i + 3 < buffer.length; i++) {
265
+ if (buffer[i] === 0x65 && buffer[i+1] === 0x73 && buffer[i+2] === 0x64 && buffer[i+3] === 0x73) return true;
266
+ }
267
+ return false;
264
268
  }
265
269
 
266
270
  function ensureSourceBuffer(s: Session): void {
267
271
  if (s.sourceBuffer || !s.mediaSource || s.mediaSource.readyState !== 'open') return;
268
- const codecs = ['avc1.64001F', 'avc1.4D0028', 'avc1.42001E', 'hev1.1.6.L93.B0'];
272
+ const fallbackCodecs = ['avc1.64001F', 'avc1.4D0028', 'avc1.42001E', 'hev1.1.6.L93.B0'];
269
273
  let mime = 'video/mp4; codecs="avc1.64001F"';
270
274
 
271
275
  for (const buf of s.preBuffer) {
272
- const c = detectCodec(buf);
273
- if (c) { detectedCodec.value = c; mime = `video/mp4; codecs="${c}"`; break; }
276
+ const vc = detectVideoCodec(buf);
277
+ if (vc) {
278
+ const ha = hasAudio(buf);
279
+ const codecStr = ha ? `${vc}, mp4a.40.2` : vc;
280
+ detectedCodec.value = codecStr;
281
+ mime = `video/mp4; codecs="${codecStr}"`;
282
+ if (!MediaSource.isTypeSupported(mime) && ha) {
283
+ // Audio codec not supported — try video-only
284
+ mime = `video/mp4; codecs="${vc}"`;
285
+ detectedCodec.value = vc;
286
+ }
287
+ break;
288
+ }
274
289
  }
275
290
 
276
291
  try {
277
292
  s.sourceBuffer = s.mediaSource.addSourceBuffer(mime);
278
293
  } catch {
279
- for (const base of codecs) {
294
+ for (const base of fallbackCodecs) {
280
295
  try {
281
296
  s.sourceBuffer = s.mediaSource.addSourceBuffer(`video/mp4; codecs="${base}"`);
282
297
  break;
@@ -312,6 +327,10 @@ function flushQueue(s: Session): void {
312
327
  }
313
328
  } catch (err) {
314
329
  const e = err as DOMException;
330
+ // SourceBuffer was removed during teardown — stream already ended, ignore.
331
+ if (s.cancelled || e.message?.includes('removed from the parent')) {
332
+ return;
333
+ }
315
334
  if (e.name === 'QuotaExceededError') {
316
335
  const ct = videoEl.value?.currentTime ?? 0;
317
336
  const cutoff = Math.max(0, ct - 5);
@@ -369,24 +369,30 @@ function connectStream(wsUrl: string): void {
369
369
 
370
370
  // ── MSE helpers ─────────────────────────────────────────────────────────────
371
371
 
372
- /** Detect video + audio codec from fMP4 init segment. */
373
- function detectCodec(buffer: Uint8Array): string | null {
372
+ /** Detect video codec from fMP4 init segment. */
373
+ function detectVideoCodec(buffer: Uint8Array): string | null {
374
374
  const hex2 = (n: number) => n.toString(16).padStart(2, '0').toUpperCase();
375
- let videoCodec: string | null = null;
376
- let audioCodec: string | null = null;
377
375
  for (let i = 0; i + 7 < buffer.length; i++) {
378
376
  const tag = String.fromCharCode(buffer[i]!, buffer[i + 1]!, buffer[i + 2]!, buffer[i + 3]!);
379
377
  if (tag === 'avcC') {
380
378
  const profile = buffer[i + 5], compat = buffer[i + 6], level = buffer[i + 7];
381
379
  if (profile !== undefined && compat !== undefined && level !== undefined) {
382
- videoCodec = `avc1.${hex2(profile)}${hex2(compat)}${hex2(level)}`;
380
+ return `avc1.${hex2(profile)}${hex2(compat)}${hex2(level)}`;
383
381
  }
384
382
  }
385
- if (tag === 'hvcC') videoCodec = 'hev1.1.6.L93.B0';
386
- if (tag === 'mp4a') audioCodec = 'mp4a.40.2'; // AAC audio present
383
+ if (tag === 'hvcC') return 'hev1.1.6.L93.B0';
387
384
  }
388
- if (!videoCodec) return null;
389
- return audioCodec ? `${videoCodec}, ${audioCodec}` : videoCodec;
385
+ return null;
386
+ }
387
+
388
+ /** Check if the init segment contains an audio track (esds box inside mp4a). */
389
+ function hasAudio(buffer: Uint8Array): boolean {
390
+ for (let i = 0; i + 3 < buffer.length; i++) {
391
+ if (buffer[i] === 0x65 && buffer[i+1] === 0x73 && buffer[i+2] === 0x64 && buffer[i+3] === 0x73) {
392
+ return true; // 'esds' box found — reliable indicator of audio track
393
+ }
394
+ }
395
+ return false;
390
396
  }
391
397
 
392
398
  function ensureSourceBuffer(session: Session): void {
@@ -394,11 +400,20 @@ function ensureSourceBuffer(session: Session): void {
394
400
  if (!session.mediaSource || session.mediaSource.readyState !== 'open') return;
395
401
 
396
402
  const merged = mergeBuffers(session.preBuffer);
397
- const codec = detectCodec(merged);
398
- if (!codec) return; // not enough bytes yet
399
-
400
- detectedCodec.value = codec;
401
- const mime = `video/mp4; codecs="${codec}"`;
403
+ const videoCodec = detectVideoCodec(merged);
404
+ if (!videoCodec) return; // not enough bytes yet
405
+
406
+ // Build MIME: try with audio codec first, fall back to video-only.
407
+ const hasAudioTrack = hasAudio(merged);
408
+ const codecStr = hasAudioTrack ? `${videoCodec}, mp4a.40.2` : videoCodec;
409
+ detectedCodec.value = codecStr;
410
+ let mime = `video/mp4; codecs="${codecStr}"`;
411
+
412
+ if (!MediaSource.isTypeSupported(mime) && hasAudioTrack) {
413
+ // Browser doesn't support the detected audio codec — try video-only
414
+ mime = `video/mp4; codecs="${videoCodec}"`;
415
+ detectedCodec.value = videoCodec;
416
+ }
402
417
 
403
418
  if (!MediaSource.isTypeSupported(mime)) {
404
419
  streamError.value = `Codec not supported: ${mime}`;
@@ -456,6 +471,10 @@ function flushQueue(session: Session): void {
456
471
  }
457
472
  } catch (err) {
458
473
  const e = err as DOMException;
474
+ // SourceBuffer was removed during teardown — stream already ended, ignore.
475
+ if (session.cancelled || e.message?.includes('removed from the parent')) {
476
+ return;
477
+ }
459
478
  if (e.name === 'QuotaExceededError') {
460
479
  const ct = videoEl.value?.currentTime ?? 0;
461
480
  const cutoff = Math.max(0, ct - 5);