@matter-server/dashboard 0.7.0-alpha.0-20260512-b404bea → 0.7.0

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.
Files changed (171) hide show
  1. package/dist/esm/client/client-context.d.ts +3 -0
  2. package/dist/esm/client/client-context.d.ts.map +1 -1
  3. package/dist/esm/client/client-context.js +3 -1
  4. package/dist/esm/client/client-context.js.map +1 -1
  5. package/dist/esm/client/models/descriptions.js +45 -45
  6. package/dist/esm/components/avsum-ptz-strip.d.ts +41 -0
  7. package/dist/esm/components/avsum-ptz-strip.d.ts.map +1 -0
  8. package/dist/esm/components/avsum-ptz-strip.js +379 -0
  9. package/dist/esm/components/avsum-ptz-strip.js.map +6 -0
  10. package/dist/esm/components/dialogs/binding/node-binding-dialog.js +1 -1
  11. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.d.ts +2 -2
  12. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.d.ts.map +1 -1
  13. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.js +1 -2
  14. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.js.map +1 -1
  15. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.d.ts +1 -0
  16. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.d.ts.map +1 -1
  17. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.js +12 -3
  18. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.js.map +1 -1
  19. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.d.ts +1 -2
  20. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.d.ts.map +1 -1
  21. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.js +1 -2
  22. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.js.map +1 -1
  23. package/dist/esm/components/dialogs/settings/log-level-dialog.d.ts +0 -2
  24. package/dist/esm/components/dialogs/settings/log-level-dialog.d.ts.map +1 -1
  25. package/dist/esm/components/dialogs/settings/log-level-dialog.js +2 -8
  26. package/dist/esm/components/dialogs/settings/log-level-dialog.js.map +1 -1
  27. package/dist/esm/components/dialogs/settings/log-level-section.d.ts +2 -1
  28. package/dist/esm/components/dialogs/settings/log-level-section.d.ts.map +1 -1
  29. package/dist/esm/components/dialogs/settings/log-level-section.js +10 -2
  30. package/dist/esm/components/dialogs/settings/log-level-section.js.map +1 -1
  31. package/dist/esm/components/dialogs/settings/settings-dialog.d.ts +1 -0
  32. package/dist/esm/components/dialogs/settings/settings-dialog.d.ts.map +1 -1
  33. package/dist/esm/components/dialogs/settings/settings-dialog.js +8 -2
  34. package/dist/esm/components/dialogs/settings/settings-dialog.js.map +1 -1
  35. package/dist/esm/components/dialogs/settings/show-log-level-dialog.d.ts +1 -2
  36. package/dist/esm/components/dialogs/settings/show-log-level-dialog.d.ts.map +1 -1
  37. package/dist/esm/components/dialogs/settings/show-log-level-dialog.js +1 -2
  38. package/dist/esm/components/dialogs/settings/show-log-level-dialog.js.map +1 -1
  39. package/dist/esm/components/dialogs/settings/show-settings-dialog.d.ts +1 -2
  40. package/dist/esm/components/dialogs/settings/show-settings-dialog.d.ts.map +1 -1
  41. package/dist/esm/components/dialogs/settings/show-settings-dialog.js +1 -2
  42. package/dist/esm/components/dialogs/settings/show-settings-dialog.js.map +1 -1
  43. package/dist/esm/components/webrtc-stream-view.d.ts +97 -0
  44. package/dist/esm/components/webrtc-stream-view.d.ts.map +1 -0
  45. package/dist/esm/components/webrtc-stream-view.js +878 -0
  46. package/dist/esm/components/webrtc-stream-view.js.map +6 -0
  47. package/dist/esm/pages/camera-overlay.d.ts +63 -0
  48. package/dist/esm/pages/camera-overlay.d.ts.map +1 -0
  49. package/dist/esm/pages/camera-overlay.js +546 -0
  50. package/dist/esm/pages/camera-overlay.js.map +6 -0
  51. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts +1 -0
  52. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts.map +1 -1
  53. package/dist/esm/pages/cluster-commands/base-cluster-commands.js +11 -1
  54. package/dist/esm/pages/cluster-commands/base-cluster-commands.js.map +1 -1
  55. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.d.ts +38 -0
  56. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.d.ts.map +1 -0
  57. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.js +534 -0
  58. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.js.map +6 -0
  59. package/dist/esm/pages/cluster-commands/clusters/chime-commands.d.ts +32 -0
  60. package/dist/esm/pages/cluster-commands/clusters/chime-commands.d.ts.map +1 -0
  61. package/dist/esm/pages/cluster-commands/clusters/chime-commands.js +261 -0
  62. package/dist/esm/pages/cluster-commands/clusters/chime-commands.js.map +6 -0
  63. package/dist/esm/pages/cluster-commands/index.d.ts +2 -0
  64. package/dist/esm/pages/cluster-commands/index.d.ts.map +1 -1
  65. package/dist/esm/pages/cluster-commands/index.js +2 -0
  66. package/dist/esm/pages/cluster-commands/index.js.map +1 -1
  67. package/dist/esm/pages/components/header.d.ts +1 -0
  68. package/dist/esm/pages/components/header.d.ts.map +1 -1
  69. package/dist/esm/pages/components/header.js +10 -3
  70. package/dist/esm/pages/components/header.js.map +1 -1
  71. package/dist/esm/pages/components/node-details.d.ts +3 -0
  72. package/dist/esm/pages/components/node-details.d.ts.map +1 -1
  73. package/dist/esm/pages/components/node-details.js +34 -4
  74. package/dist/esm/pages/components/node-details.js.map +1 -1
  75. package/dist/esm/pages/components/server-details.d.ts +1 -0
  76. package/dist/esm/pages/components/server-details.d.ts.map +1 -1
  77. package/dist/esm/pages/components/server-details.js +11 -2
  78. package/dist/esm/pages/components/server-details.js.map +1 -1
  79. package/dist/esm/pages/matter-cluster-view.d.ts +1 -0
  80. package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
  81. package/dist/esm/pages/matter-cluster-view.js +15 -8
  82. package/dist/esm/pages/matter-cluster-view.js.map +1 -1
  83. package/dist/esm/pages/matter-dashboard-app.d.ts +3 -1
  84. package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -1
  85. package/dist/esm/pages/matter-dashboard-app.js +12 -16
  86. package/dist/esm/pages/matter-dashboard-app.js.map +1 -1
  87. package/dist/esm/pages/matter-endpoint-view.d.ts +2 -1
  88. package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -1
  89. package/dist/esm/pages/matter-endpoint-view.js +17 -3
  90. package/dist/esm/pages/matter-endpoint-view.js.map +1 -1
  91. package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
  92. package/dist/esm/pages/matter-network-view.js +5 -1
  93. package/dist/esm/pages/matter-network-view.js.map +1 -1
  94. package/dist/esm/pages/matter-node-view.d.ts +1 -0
  95. package/dist/esm/pages/matter-node-view.d.ts.map +1 -1
  96. package/dist/esm/pages/matter-node-view.js +15 -7
  97. package/dist/esm/pages/matter-node-view.js.map +1 -1
  98. package/dist/esm/pages/matter-server-view.d.ts +1 -0
  99. package/dist/esm/pages/matter-server-view.d.ts.map +1 -1
  100. package/dist/esm/pages/matter-server-view.js +13 -2
  101. package/dist/esm/pages/matter-server-view.js.map +1 -1
  102. package/dist/esm/pages/network/network-details.d.ts.map +1 -1
  103. package/dist/esm/pages/network/network-details.js +0 -3
  104. package/dist/esm/pages/network/network-details.js.map +1 -1
  105. package/dist/esm/pages/network/update-connections-dialog.d.ts +1 -0
  106. package/dist/esm/pages/network/update-connections-dialog.d.ts.map +1 -1
  107. package/dist/esm/pages/network/update-connections-dialog.js +7 -1
  108. package/dist/esm/pages/network/update-connections-dialog.js.map +1 -1
  109. package/dist/esm/util/attribute-shapes.d.ts +11 -0
  110. package/dist/esm/util/attribute-shapes.d.ts.map +1 -0
  111. package/dist/esm/util/attribute-shapes.js +37 -0
  112. package/dist/esm/util/attribute-shapes.js.map +6 -0
  113. package/dist/esm/util/avsum.d.ts +69 -0
  114. package/dist/esm/util/avsum.d.ts.map +1 -0
  115. package/dist/esm/util/avsum.js +150 -0
  116. package/dist/esm/util/avsum.js.map +6 -0
  117. package/dist/esm/util/chime.d.ts +20 -0
  118. package/dist/esm/util/chime.d.ts.map +1 -0
  119. package/dist/esm/util/chime.js +60 -0
  120. package/dist/esm/util/chime.js.map +6 -0
  121. package/dist/web/js/{attribute-write-dialog-DzMWN_T3.js → attribute-write-dialog-DLRP2hlx.js} +1 -1
  122. package/dist/web/js/{command-invoke-dialog-BhAOXzjX.js → command-invoke-dialog-BcCfT1sn.js} +1 -1
  123. package/dist/web/js/{commission-node-dialog-DF87YIqR.js → commission-node-dialog-BfldkUxD.js} +16 -8
  124. package/dist/web/js/{commission-node-existing-DLcWvJTL.js → commission-node-existing-omW-An0J.js} +2 -2
  125. package/dist/web/js/{commission-node-thread-m2fqED-2.js → commission-node-thread-BZpJIxlI.js} +2 -2
  126. package/dist/web/js/{commission-node-wifi-PCsot-CX.js → commission-node-wifi-HA2pYTW2.js} +2 -2
  127. package/dist/web/js/{dialog-box-DiqYXM8c.js → dialog-box-BjDID9j4.js} +1 -1
  128. package/dist/web/js/{fire_event-BnpND_gK.js → fire_event-CChLgSiV.js} +1 -1
  129. package/dist/web/js/main.js +39 -1
  130. package/dist/web/js/{matter-dashboard-app-BZOhBELR.js → matter-dashboard-app-XUWBcruM.js} +8265 -2035
  131. package/dist/web/js/{node-binding-dialog-B3lVN-FU.js → node-binding-dialog-_yHtf7WP.js} +2 -2
  132. package/dist/web/js/settings-dialog-BFPfOSOI.js +607 -0
  133. package/package.json +4 -4
  134. package/src/client/client-context.ts +1 -0
  135. package/src/client/models/descriptions.ts +45 -45
  136. package/src/components/avsum-ptz-strip.ts +388 -0
  137. package/src/components/dialogs/binding/node-binding-dialog.ts +1 -1
  138. package/src/components/dialogs/binding/show-node-binding-dialog.ts +2 -3
  139. package/src/components/dialogs/commission-node-dialog/commission-node-dialog.ts +10 -4
  140. package/src/components/dialogs/commission-node-dialog/show-commission-node-dialog.ts +1 -4
  141. package/src/components/dialogs/settings/log-level-dialog.ts +2 -9
  142. package/src/components/dialogs/settings/log-level-section.ts +11 -4
  143. package/src/components/dialogs/settings/settings-dialog.ts +8 -3
  144. package/src/components/dialogs/settings/show-log-level-dialog.ts +1 -4
  145. package/src/components/dialogs/settings/show-settings-dialog.ts +1 -4
  146. package/src/components/webrtc-stream-view.ts +999 -0
  147. package/src/pages/camera-overlay.ts +562 -0
  148. package/src/pages/cluster-commands/base-cluster-commands.ts +7 -1
  149. package/src/pages/cluster-commands/clusters/avsum-commands.ts +562 -0
  150. package/src/pages/cluster-commands/clusters/chime-commands.ts +272 -0
  151. package/src/pages/cluster-commands/index.ts +2 -0
  152. package/src/pages/components/header.ts +7 -3
  153. package/src/pages/components/node-details.ts +33 -3
  154. package/src/pages/components/server-details.ts +8 -2
  155. package/src/pages/matter-cluster-view.ts +13 -9
  156. package/src/pages/matter-dashboard-app.ts +11 -16
  157. package/src/pages/matter-endpoint-view.ts +13 -5
  158. package/src/pages/matter-network-view.ts +3 -1
  159. package/src/pages/matter-node-view.ts +9 -7
  160. package/src/pages/matter-server-view.ts +7 -2
  161. package/src/pages/network/network-details.ts +0 -3
  162. package/src/pages/network/update-connections-dialog.ts +6 -1
  163. package/src/util/attribute-shapes.ts +34 -0
  164. package/src/util/avsum.ts +212 -0
  165. package/src/util/chime.ts +82 -0
  166. package/dist/esm/util/clone_class.d.ts +0 -7
  167. package/dist/esm/util/clone_class.d.ts.map +0 -1
  168. package/dist/esm/util/clone_class.js +0 -10
  169. package/dist/esm/util/clone_class.js.map +0 -6
  170. package/dist/web/js/settings-dialog-D75IAPKe.js +0 -4039
  171. package/src/util/clone_class.ts +0 -7
@@ -0,0 +1,878 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ /**
12
+ * @license
13
+ * Copyright 2025-2026 Open Home Foundation
14
+ * SPDX-License-Identifier: Apache-2.0
15
+ */
16
+ import { consume } from "@lit/context";
17
+ import { mdiAlertCircleOutline, mdiVideoOutline } from "@mdi/js";
18
+ import { LitElement, css, html } from "lit";
19
+ import { customElement, property, query, state } from "lit/decorators.js";
20
+ import { clientContext } from "../client/client-context.js";
21
+ import { asObject, pickNumber } from "../util/attribute-shapes.js";
22
+ import "./ha-svg-icon.js";
23
+ const STREAM_USAGE_LIVE_VIEW = 3;
24
+ const END_REASON_USER_HANGUP = 2;
25
+ const CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID = 1361;
26
+ const WEBRTC_TRANSPORT_PROVIDER_CLUSTER_ID = 1363;
27
+ const AVSM_FEAT_WMARK = 1 << 6;
28
+ const AVSM_FEAT_OSD = 1 << 7;
29
+ const AVSM_FEATURE_MAP_ATTR_ID = 65532;
30
+ const DEFAULT_MAX_RESOLUTION = { width: 1920, height: 1080 };
31
+ const DEFAULT_MIN_RESOLUTION = { width: 640, height: 480 };
32
+ function parseStreamAllocate(value, idKey) {
33
+ const obj = asObject(value);
34
+ if (!obj) return null;
35
+ return pickNumber(obj, idKey);
36
+ }
37
+ function parseSnapshotAllocateResponse(value) {
38
+ const obj = asObject(value);
39
+ if (!obj) return null;
40
+ return pickNumber(obj, "snapshotStreamId");
41
+ }
42
+ const SNAPSHOT_DEFAULTS = {
43
+ resolution: { width: 1920, height: 1080 },
44
+ maxFrameRate: 30,
45
+ imageCodec: 0
46
+ };
47
+ function parseSnapshotCapabilitiesFromList(list) {
48
+ const candidates = list.map(asObject).filter((c) => c !== void 0);
49
+ if (candidates.length === 0) return SNAPSHOT_DEFAULTS;
50
+ const parsed = candidates.map((cap) => {
51
+ const res = asObject(cap["resolution"] ?? cap["0"]);
52
+ const width = res ? pickNumber(res, "width", "0") : null;
53
+ const height = res ? pickNumber(res, "height", "1") : null;
54
+ const maxFrameRate = pickNumber(cap, "maxFrameRate", "1");
55
+ const imageCodec = pickNumber(cap, "imageCodec", "2");
56
+ return {
57
+ resolution: {
58
+ width: width ?? SNAPSHOT_DEFAULTS.resolution.width,
59
+ height: height ?? SNAPSHOT_DEFAULTS.resolution.height
60
+ },
61
+ maxFrameRate: maxFrameRate ?? SNAPSHOT_DEFAULTS.maxFrameRate,
62
+ imageCodec: imageCodec ?? SNAPSHOT_DEFAULTS.imageCodec
63
+ };
64
+ });
65
+ return parsed.reduce(
66
+ (best, cur) => cur.resolution.width * cur.resolution.height > best.resolution.width * best.resolution.height ? cur : best
67
+ );
68
+ }
69
+ function bytesToBase64(bytes) {
70
+ const CHUNK = 8192;
71
+ let binary = "";
72
+ for (let i = 0; i < bytes.length; i += CHUNK) {
73
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
74
+ }
75
+ return btoa(binary);
76
+ }
77
+ function parseDataToBase64(data) {
78
+ if (typeof data === "string") return data;
79
+ if (data instanceof Uint8Array) return bytesToBase64(data);
80
+ if (data instanceof ArrayBuffer) return bytesToBase64(new Uint8Array(data));
81
+ if (Array.isArray(data)) return bytesToBase64(new Uint8Array(data));
82
+ throw new Error(`Unexpected snapshot data shape: ${typeof data}`);
83
+ }
84
+ function parseCaptureSnapshotResponse(value) {
85
+ const obj = asObject(value);
86
+ if (!obj) throw new Error("CaptureSnapshot returned no response");
87
+ const data = obj["data"];
88
+ if (data == null) throw new Error("CaptureSnapshot response missing data field");
89
+ const imageCodec = pickNumber(obj, "imageCodec") ?? 0;
90
+ const res = asObject(obj["resolution"]);
91
+ const width = res ? pickNumber(res, "width") ?? 0 : 0;
92
+ const height = res ? pickNumber(res, "height") ?? 0 : 0;
93
+ return { dataBase64: parseDataToBase64(data), imageCodec, resolution: { width, height } };
94
+ }
95
+ function parseProvideOfferResponse(value) {
96
+ const obj = asObject(value);
97
+ if (!obj) return null;
98
+ const sessionId = pickNumber(obj, "webRtcSessionId");
99
+ if (sessionId === null) return null;
100
+ return {
101
+ webRtcSessionId: sessionId,
102
+ videoStreamId: parseStreamAllocate(value, "videoStreamId"),
103
+ audioStreamId: parseStreamAllocate(value, "audioStreamId")
104
+ };
105
+ }
106
+ let WebRtcStreamView = class extends LitElement {
107
+ constructor() {
108
+ super(...arguments);
109
+ this.resolution = null;
110
+ this.watermarkEnabled = false;
111
+ this.osdEnabled = false;
112
+ this._state = "idle";
113
+ this._errorMessage = null;
114
+ this.snapshotResolution = null;
115
+ this._snapshotStreamId = null;
116
+ this._snapshotResolution = null;
117
+ /** True when we allocated this stream ourselves and must Deallocate it on stop. False when reusing an existing allocation. */
118
+ this._videoStreamOwned = false;
119
+ this._audioStreamOwned = false;
120
+ this._snapshotStreamOwned = false;
121
+ this._pc = null;
122
+ this._localIceQueue = [];
123
+ this._answerReceived = false;
124
+ this._webRtcSessionId = null;
125
+ this._videoStreamId = null;
126
+ this._audioStreamId = null;
127
+ this._unsubscribe = null;
128
+ this._stopping = false;
129
+ this._endReceivedFromPeer = false;
130
+ this._preSessionQueue = [];
131
+ }
132
+ get state() {
133
+ return this._state;
134
+ }
135
+ get videoStreamId() {
136
+ return this._videoStreamId;
137
+ }
138
+ get muted() {
139
+ return this._video?.muted ?? true;
140
+ }
141
+ setMuted(muted) {
142
+ if (this._video) this._video.muted = muted;
143
+ }
144
+ disconnectedCallback() {
145
+ super.disconnectedCallback();
146
+ void this.deallocateSnapshot();
147
+ void this.stop();
148
+ }
149
+ render() {
150
+ return html`
151
+ <video
152
+ autoplay
153
+ playsinline
154
+ muted
155
+ disablepictureinpicture
156
+ disableremoteplayback
157
+ ?hidden=${this._state !== "streaming"}
158
+ ></video>
159
+ ${this._state === "idle" ? html`<div class="placeholder">
160
+ <ha-svg-icon class="placeholder-icon" .path=${mdiVideoOutline}></ha-svg-icon>
161
+ <div class="placeholder-text">Click <b>Start</b> to begin streaming</div>
162
+ </div>` : null}
163
+ ${this._state === "connecting" ? html`<div class="placeholder">
164
+ <div class="spinner"></div>
165
+ <div class="placeholder-text">Connecting…</div>
166
+ </div>` : null}
167
+ ${this._state === "error" ? html`<div class="placeholder error">
168
+ <ha-svg-icon class="placeholder-icon" .path=${mdiAlertCircleOutline}></ha-svg-icon>
169
+ <div class="placeholder-text">${this._errorMessage ?? "Stream error"}</div>
170
+ </div>` : null}
171
+ `;
172
+ }
173
+ async start() {
174
+ if (!this.client) throw new Error("Matter client not available");
175
+ if (this._state === "connecting" || this._state === "streaming") return;
176
+ this._fireStateChange("connecting", null);
177
+ this._answerReceived = false;
178
+ this._localIceQueue = [];
179
+ this._stopping = false;
180
+ this._endReceivedFromPeer = false;
181
+ this._preSessionQueue = [];
182
+ try {
183
+ const pc = new RTCPeerConnection({ iceServers: [] });
184
+ this._pc = pc;
185
+ pc.addTransceiver("video", { direction: "recvonly" });
186
+ pc.addTransceiver("audio", { direction: "recvonly" });
187
+ pc.onicecandidate = (ev) => {
188
+ if (ev.candidate === null) {
189
+ console.log("[webrtc-stream-view] local ICE gathering complete");
190
+ return;
191
+ }
192
+ console.log("[webrtc-stream-view] local ICE candidate", ev.candidate.candidate, {
193
+ queued: !this._answerReceived,
194
+ queueDepth: this._localIceQueue.length
195
+ });
196
+ if (!this._answerReceived) {
197
+ this._localIceQueue.push(ev.candidate);
198
+ return;
199
+ }
200
+ void this._sendLocalIceCandidates([ev.candidate]);
201
+ };
202
+ pc.oniceconnectionstatechange = () => {
203
+ console.log("[webrtc-stream-view] iceConnectionState ->", pc.iceConnectionState);
204
+ };
205
+ pc.onconnectionstatechange = () => {
206
+ console.log("[webrtc-stream-view] connectionState ->", pc.connectionState);
207
+ };
208
+ pc.onsignalingstatechange = () => {
209
+ console.log("[webrtc-stream-view] signalingState ->", pc.signalingState);
210
+ };
211
+ pc.ontrack = (ev) => {
212
+ console.log("[webrtc-stream-view] ontrack", {
213
+ kind: ev.track.kind,
214
+ readyState: ev.track.readyState,
215
+ streams: ev.streams.length
216
+ });
217
+ const video = this._video;
218
+ if (!video) {
219
+ console.warn("[webrtc-stream-view] ontrack fired but <video> query is null");
220
+ return;
221
+ }
222
+ const stream = ev.streams[0];
223
+ if (!stream) return;
224
+ if (video.srcObject !== stream) {
225
+ video.srcObject = stream;
226
+ }
227
+ };
228
+ const minResolution = this.resolution ?? DEFAULT_MIN_RESOLUTION;
229
+ const maxResolution = this.resolution ?? DEFAULT_MAX_RESOLUTION;
230
+ const avsmFeatures = this._readAvsmFeatures();
231
+ const videoAllocPayload = {
232
+ streamUsage: STREAM_USAGE_LIVE_VIEW,
233
+ videoCodec: 0,
234
+ minFrameRate: 30,
235
+ maxFrameRate: 120,
236
+ minResolution,
237
+ maxResolution,
238
+ minBitRate: 1e4,
239
+ maxBitRate: 1e4,
240
+ keyFrameInterval: 4e3
241
+ };
242
+ if (avsmFeatures.wmark) videoAllocPayload.watermarkEnabled = this.watermarkEnabled;
243
+ if (avsmFeatures.osd) videoAllocPayload.osdEnabled = this.osdEnabled;
244
+ const reusedVideoId = this._findMatchingVideoStream({
245
+ streamUsage: STREAM_USAGE_LIVE_VIEW,
246
+ videoCodec: 0,
247
+ minRes: minResolution,
248
+ maxRes: maxResolution,
249
+ watermarkEnabled: avsmFeatures.wmark ? this.watermarkEnabled : void 0,
250
+ osdEnabled: avsmFeatures.osd ? this.osdEnabled : void 0
251
+ });
252
+ let videoAlloc = null;
253
+ if (reusedVideoId !== null) {
254
+ console.info("[webrtc-stream-view] reusing existing video stream", reusedVideoId);
255
+ this._videoStreamId = reusedVideoId;
256
+ this._videoStreamOwned = false;
257
+ } else {
258
+ try {
259
+ videoAlloc = await this.client.deviceCommand(
260
+ this.nodeId,
261
+ this.endpointId,
262
+ CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
263
+ "VideoStreamAllocate",
264
+ videoAllocPayload
265
+ );
266
+ } catch (err) {
267
+ const message = err instanceof Error ? err.message : String(err);
268
+ const isResourceExhausted = message.includes("Resource exhausted") || message.includes("(code 137)");
269
+ if (!isResourceExhausted || this._snapshotStreamId === null) throw err;
270
+ console.info(
271
+ "[webrtc-stream-view] VideoStreamAllocate ResourceExhausted; freeing snapshot stream and retrying"
272
+ );
273
+ await this.deallocateSnapshot();
274
+ videoAlloc = await this.client.deviceCommand(
275
+ this.nodeId,
276
+ this.endpointId,
277
+ CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
278
+ "VideoStreamAllocate",
279
+ videoAllocPayload
280
+ );
281
+ }
282
+ const videoStreamId = parseStreamAllocate(videoAlloc, "videoStreamId");
283
+ if (videoStreamId === null) {
284
+ throw new Error("VideoStreamAllocate did not return a videoStreamId");
285
+ }
286
+ this._videoStreamId = videoStreamId;
287
+ this._videoStreamOwned = true;
288
+ }
289
+ const audioWant = {
290
+ streamUsage: STREAM_USAGE_LIVE_VIEW,
291
+ audioCodec: 0,
292
+ channelCount: 1,
293
+ sampleRate: 48e3
294
+ };
295
+ const reusedAudioId = this._findMatchingAudioStream(audioWant);
296
+ if (reusedAudioId !== null) {
297
+ console.info("[webrtc-stream-view] reusing existing audio stream", reusedAudioId);
298
+ this._audioStreamId = reusedAudioId;
299
+ this._audioStreamOwned = false;
300
+ } else {
301
+ try {
302
+ const audioAlloc = await this.client.deviceCommand(
303
+ this.nodeId,
304
+ this.endpointId,
305
+ CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
306
+ "AudioStreamAllocate",
307
+ {
308
+ ...audioWant,
309
+ bitRate: 2e4,
310
+ bitDepth: 24
311
+ }
312
+ );
313
+ this._audioStreamId = parseStreamAllocate(audioAlloc, "audioStreamId");
314
+ this._audioStreamOwned = this._audioStreamId !== null;
315
+ } catch (err) {
316
+ console.info("AudioStreamAllocate failed; continuing video-only", err);
317
+ this._audioStreamId = null;
318
+ }
319
+ }
320
+ const offer = await pc.createOffer();
321
+ await pc.setLocalDescription(offer);
322
+ this._unsubscribe = this.client.addWebRtcCallbackListener((data) => void this._onWebRtcCallback(data));
323
+ const sdp = pc.localDescription?.sdp;
324
+ if (!sdp) {
325
+ throw new Error("Failed to create local SDP offer");
326
+ }
327
+ console.log("[webrtc-stream-view] sending ProvideOffer", {
328
+ nodeId: this.nodeId,
329
+ endpointId: this.endpointId,
330
+ videoStreamId: this._videoStreamId,
331
+ audioStreamId: this._audioStreamId,
332
+ streamUsage: STREAM_USAGE_LIVE_VIEW,
333
+ sdpLength: sdp.length
334
+ });
335
+ const offerResponse = await this.client.sendWebRtcProviderCommand(
336
+ this.nodeId,
337
+ this.endpointId,
338
+ "ProvideOffer",
339
+ {
340
+ webRtcSessionId: null,
341
+ sdp,
342
+ streamUsage: STREAM_USAGE_LIVE_VIEW,
343
+ videoStreamId: this._videoStreamId,
344
+ audioStreamId: this._audioStreamId
345
+ }
346
+ );
347
+ console.log("[webrtc-stream-view] ProvideOffer response", offerResponse);
348
+ const parsed = parseProvideOfferResponse(offerResponse);
349
+ if (parsed === null) {
350
+ throw new Error("ProvideOffer response missing webRtcSessionId");
351
+ }
352
+ this._webRtcSessionId = parsed.webRtcSessionId;
353
+ console.log("[webrtc-stream-view] webRtcSessionId established", this._webRtcSessionId);
354
+ const buffered = this._preSessionQueue;
355
+ this._preSessionQueue = [];
356
+ for (const ev of buffered) {
357
+ if (ev.webrtc_session_id === parsed.webRtcSessionId) {
358
+ await this._onWebRtcCallback(ev);
359
+ }
360
+ }
361
+ } catch (err) {
362
+ const message = err instanceof Error ? err.message : String(err);
363
+ this._fireStateChange("error", message);
364
+ await this.stop();
365
+ }
366
+ }
367
+ async stop() {
368
+ if (this._stopping) return;
369
+ this._stopping = true;
370
+ if (this._unsubscribe) {
371
+ this._unsubscribe();
372
+ this._unsubscribe = null;
373
+ }
374
+ const sessionId = this._webRtcSessionId;
375
+ const initiatedEnd = sessionId !== null && !this._endReceivedFromPeer;
376
+ if (initiatedEnd) {
377
+ try {
378
+ await this.client?.deviceCommand(
379
+ this.nodeId,
380
+ this.endpointId,
381
+ WEBRTC_TRANSPORT_PROVIDER_CLUSTER_ID,
382
+ "EndSession",
383
+ { webRtcSessionId: sessionId, reason: END_REASON_USER_HANGUP }
384
+ );
385
+ } catch (err) {
386
+ console.warn("EndSession failed during stop", err);
387
+ }
388
+ }
389
+ if (this._videoStreamId !== null && this._videoStreamOwned) {
390
+ try {
391
+ await this.client?.deviceCommand(
392
+ this.nodeId,
393
+ this.endpointId,
394
+ CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
395
+ "VideoStreamDeallocate",
396
+ { videoStreamId: this._videoStreamId }
397
+ );
398
+ } catch (err) {
399
+ console.warn("VideoStreamDeallocate failed during stop", err);
400
+ }
401
+ }
402
+ if (this._audioStreamId !== null && this._audioStreamOwned) {
403
+ try {
404
+ await this.client?.deviceCommand(
405
+ this.nodeId,
406
+ this.endpointId,
407
+ CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
408
+ "AudioStreamDeallocate",
409
+ { audioStreamId: this._audioStreamId }
410
+ );
411
+ } catch (err) {
412
+ console.warn("AudioStreamDeallocate failed during stop", err);
413
+ }
414
+ }
415
+ this._videoStreamOwned = false;
416
+ this._audioStreamOwned = false;
417
+ await this.deallocateSnapshot();
418
+ const video = this._video;
419
+ if (video && video.srcObject) {
420
+ video.srcObject = null;
421
+ }
422
+ if (this._pc) {
423
+ try {
424
+ this._pc.close();
425
+ } catch {
426
+ }
427
+ this._pc = null;
428
+ }
429
+ this._localIceQueue = [];
430
+ this._answerReceived = false;
431
+ this._webRtcSessionId = null;
432
+ this._videoStreamId = null;
433
+ this._audioStreamId = null;
434
+ this._endReceivedFromPeer = false;
435
+ this._preSessionQueue = [];
436
+ if (this._state !== "idle" && this._state !== "error") {
437
+ this._fireStateChange("idle", null);
438
+ }
439
+ this._stopping = false;
440
+ }
441
+ async takeSnapshot() {
442
+ if (!this.client) throw new Error("Matter client not available");
443
+ const streamId = await this._ensureSnapshotStream();
444
+ const requestedResolution = this._snapshotResolution ?? SNAPSHOT_DEFAULTS.resolution;
445
+ const response = await this.client.deviceCommand(
446
+ this.nodeId,
447
+ this.endpointId,
448
+ CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
449
+ "CaptureSnapshot",
450
+ { snapshotStreamId: streamId, requestedResolution }
451
+ );
452
+ const parsed = parseCaptureSnapshotResponse(response);
453
+ const mimeType = parsed.imageCodec === 0 ? "jpeg" : "png";
454
+ return {
455
+ dataUri: `data:image/${mimeType};base64,${parsed.dataBase64}`,
456
+ resolution: parsed.resolution
457
+ };
458
+ }
459
+ _readAvsmFeatures() {
460
+ const node = this.client?.nodes[String(this.nodeId)];
461
+ const raw = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/${AVSM_FEATURE_MAP_ATTR_ID}`];
462
+ const bits = typeof raw === "number" ? raw : 0;
463
+ return {
464
+ wmark: (bits & AVSM_FEAT_WMARK) !== 0,
465
+ osd: (bits & AVSM_FEAT_OSD) !== 0
466
+ };
467
+ }
468
+ async deallocateSnapshot() {
469
+ if (this._snapshotStreamId === null) return;
470
+ const id = this._snapshotStreamId;
471
+ const wasOwned = this._snapshotStreamOwned;
472
+ this._snapshotStreamId = null;
473
+ this._snapshotResolution = null;
474
+ this._snapshotStreamOwned = false;
475
+ if (!wasOwned) return;
476
+ try {
477
+ await this.client?.deviceCommand(
478
+ this.nodeId,
479
+ this.endpointId,
480
+ CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
481
+ "SnapshotStreamDeallocate",
482
+ { snapshotStreamId: id }
483
+ );
484
+ } catch (e) {
485
+ console.warn("SnapshotStreamDeallocate failed (continuing):", e);
486
+ }
487
+ }
488
+ /**
489
+ * Search AllocatedVideoStreams (attr 0x000F) for an entry whose advertised
490
+ * capability ranges cover our requested params. Spec §11.2.1.2.1 says the
491
+ * camera SHALL reuse a matching stream — reading first avoids creating
492
+ * duplicates when a server's reuse logic is buggy or matching is too strict.
493
+ */
494
+ _findMatchingVideoStream(want) {
495
+ const node = this.client?.nodes[String(this.nodeId)];
496
+ const list = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/15`];
497
+ if (!Array.isArray(list)) return null;
498
+ for (const item of list) {
499
+ const obj = asObject(item);
500
+ if (!obj) continue;
501
+ const streamUsage = pickNumber(obj, "streamUsage", "1");
502
+ const videoCodec = pickNumber(obj, "videoCodec", "2");
503
+ if (streamUsage !== want.streamUsage || videoCodec !== want.videoCodec) continue;
504
+ const minRes = asObject(obj["minResolution"] ?? obj["5"]);
505
+ const maxRes = asObject(obj["maxResolution"] ?? obj["6"]);
506
+ if (!minRes || !maxRes) continue;
507
+ const eMinW = pickNumber(minRes, "width", "0") ?? 0;
508
+ const eMinH = pickNumber(minRes, "height", "1") ?? 0;
509
+ const eMaxW = pickNumber(maxRes, "width", "0") ?? 0;
510
+ const eMaxH = pickNumber(maxRes, "height", "1") ?? 0;
511
+ if (eMinW > want.minRes.width || eMaxW < want.maxRes.width) continue;
512
+ if (eMinH > want.minRes.height || eMaxH < want.maxRes.height) continue;
513
+ if (want.watermarkEnabled !== void 0) {
514
+ const v = obj["watermarkEnabled"] ?? obj["10"];
515
+ if (v !== want.watermarkEnabled) continue;
516
+ }
517
+ if (want.osdEnabled !== void 0) {
518
+ const v = obj["osdEnabled"] ?? obj["11"];
519
+ if (v !== want.osdEnabled) continue;
520
+ }
521
+ const id = pickNumber(obj, "videoStreamId", "0");
522
+ if (id !== null) return id;
523
+ }
524
+ return null;
525
+ }
526
+ _findMatchingAudioStream(want) {
527
+ const node = this.client?.nodes[String(this.nodeId)];
528
+ const list = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/16`];
529
+ if (!Array.isArray(list)) return null;
530
+ for (const item of list) {
531
+ const obj = asObject(item);
532
+ if (!obj) continue;
533
+ const streamUsage = pickNumber(obj, "streamUsage", "1");
534
+ const audioCodec = pickNumber(obj, "audioCodec", "2");
535
+ const channelCount = pickNumber(obj, "channelCount", "3");
536
+ const sampleRate = pickNumber(obj, "sampleRate", "4");
537
+ if (streamUsage !== want.streamUsage || audioCodec !== want.audioCodec || channelCount !== want.channelCount || sampleRate !== want.sampleRate)
538
+ continue;
539
+ const id = pickNumber(obj, "audioStreamId", "0");
540
+ if (id !== null) return id;
541
+ }
542
+ return null;
543
+ }
544
+ _findMatchingSnapshotStream(want) {
545
+ const node = this.client?.nodes[String(this.nodeId)];
546
+ const list = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/17`];
547
+ if (!Array.isArray(list)) return null;
548
+ for (const item of list) {
549
+ const obj = asObject(item);
550
+ if (!obj) continue;
551
+ const imageCodec = pickNumber(obj, "imageCodec", "1");
552
+ if (imageCodec !== want.imageCodec) continue;
553
+ const minRes = asObject(obj["minResolution"] ?? obj["3"]);
554
+ const maxRes = asObject(obj["maxResolution"] ?? obj["4"]);
555
+ if (!minRes || !maxRes) continue;
556
+ const eMinW = pickNumber(minRes, "width", "0") ?? 0;
557
+ const eMinH = pickNumber(minRes, "height", "1") ?? 0;
558
+ const eMaxW = pickNumber(maxRes, "width", "0") ?? 0;
559
+ const eMaxH = pickNumber(maxRes, "height", "1") ?? 0;
560
+ if (eMinW > want.resolution.width || eMaxW < want.resolution.width) continue;
561
+ if (eMinH > want.resolution.height || eMaxH < want.resolution.height) continue;
562
+ if (want.watermarkEnabled !== void 0) {
563
+ const v = obj["watermarkEnabled"] ?? obj["9"];
564
+ if (v !== want.watermarkEnabled) continue;
565
+ }
566
+ if (want.osdEnabled !== void 0) {
567
+ const v = obj["osdEnabled"] ?? obj["10"];
568
+ if (v !== want.osdEnabled) continue;
569
+ }
570
+ const id = pickNumber(obj, "snapshotStreamId", "0");
571
+ const resolution = { width: eMaxW, height: eMaxH };
572
+ if (id !== null) return { id, resolution };
573
+ }
574
+ return null;
575
+ }
576
+ async _ensureSnapshotStream() {
577
+ if (this._snapshotStreamId !== null) return this._snapshotStreamId;
578
+ if (!this.client) throw new Error("Matter client not available");
579
+ const node = this.client.nodes[String(this.nodeId)];
580
+ const capsRaw = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/10`];
581
+ const cap = Array.isArray(capsRaw) && capsRaw.length > 0 ? parseSnapshotCapabilitiesFromList(capsRaw) : SNAPSHOT_DEFAULTS;
582
+ const targetResolution = this.snapshotResolution ?? cap.resolution;
583
+ const avsmFeatures = this._readAvsmFeatures();
584
+ const reused = this._findMatchingSnapshotStream({
585
+ imageCodec: cap.imageCodec,
586
+ resolution: targetResolution,
587
+ watermarkEnabled: avsmFeatures.wmark ? this.watermarkEnabled : void 0,
588
+ osdEnabled: avsmFeatures.osd ? this.osdEnabled : void 0
589
+ });
590
+ if (reused !== null) {
591
+ console.info("[webrtc-stream-view] reusing existing snapshot stream", reused.id);
592
+ this._snapshotStreamId = reused.id;
593
+ this._snapshotResolution = reused.resolution;
594
+ this._snapshotStreamOwned = false;
595
+ return reused.id;
596
+ }
597
+ const snapshotAllocPayload = {
598
+ imageCodec: cap.imageCodec,
599
+ maxFrameRate: cap.maxFrameRate,
600
+ minResolution: targetResolution,
601
+ maxResolution: targetResolution,
602
+ quality: 90
603
+ };
604
+ if (avsmFeatures.wmark) snapshotAllocPayload.watermarkEnabled = this.watermarkEnabled;
605
+ if (avsmFeatures.osd) snapshotAllocPayload.osdEnabled = this.osdEnabled;
606
+ const response = await this.client.deviceCommand(
607
+ this.nodeId,
608
+ this.endpointId,
609
+ CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
610
+ "SnapshotStreamAllocate",
611
+ snapshotAllocPayload
612
+ );
613
+ const snapshotStreamId = parseSnapshotAllocateResponse(response);
614
+ if (snapshotStreamId === null) {
615
+ throw new Error("SnapshotStreamAllocate did not return a snapshot stream id");
616
+ }
617
+ this._snapshotStreamId = snapshotStreamId;
618
+ this._snapshotResolution = targetResolution;
619
+ this._snapshotStreamOwned = true;
620
+ return this._snapshotStreamId;
621
+ }
622
+ async _onWebRtcCallback(data) {
623
+ console.log("[webrtc-stream-view] webrtc_callback received", {
624
+ event_type: data.event_type,
625
+ webrtc_session_id: data.webrtc_session_id,
626
+ node_id: data.node_id,
627
+ endpoint_id: data.endpoint_id,
628
+ fabric_index: data.fabric_index,
629
+ myNodeId: this.nodeId,
630
+ myEndpointId: this.endpointId,
631
+ mySessionId: this._webRtcSessionId
632
+ });
633
+ if (this._webRtcSessionId === null) {
634
+ if (String(data.node_id) === String(this.nodeId) && data.endpoint_id === this.endpointId) {
635
+ this._preSessionQueue.push(data);
636
+ console.log("[webrtc-stream-view] queued pre-session callback");
637
+ } else {
638
+ console.log("[webrtc-stream-view] dropped pre-session callback (different node/endpoint)");
639
+ }
640
+ return;
641
+ }
642
+ if (data.webrtc_session_id !== this._webRtcSessionId) {
643
+ console.log("[webrtc-stream-view] dropped: session id mismatch");
644
+ return;
645
+ }
646
+ if (String(data.node_id) !== String(this.nodeId)) {
647
+ console.log("[webrtc-stream-view] dropped: node id mismatch");
648
+ return;
649
+ }
650
+ if (data.endpoint_id !== this.endpointId) {
651
+ console.log("[webrtc-stream-view] dropped: endpoint id mismatch");
652
+ return;
653
+ }
654
+ switch (data.event_type) {
655
+ case "answer":
656
+ void this._handleAnswer(data.data);
657
+ return;
658
+ case "ice_candidates":
659
+ void this._handleRemoteIceCandidates(data.data);
660
+ return;
661
+ case "end":
662
+ this._handleEnd(data.data);
663
+ return;
664
+ case "offer":
665
+ console.info("WebRTC re-offer received; renegotiation unsupported", data);
666
+ return;
667
+ }
668
+ }
669
+ async _handleAnswer(data) {
670
+ if (!this._pc || !data) return;
671
+ try {
672
+ const sdp = this._sanitizeAnswerSdp(data.sdp);
673
+ await this._pc.setRemoteDescription({ type: "answer", sdp });
674
+ this._answerReceived = true;
675
+ const queue = this._localIceQueue;
676
+ this._localIceQueue = [];
677
+ if (queue.length > 0) {
678
+ await this._sendLocalIceCandidates(queue);
679
+ }
680
+ this._fireStateChange("streaming", null);
681
+ } catch (err) {
682
+ const message = err instanceof Error ? err.message : String(err);
683
+ this._fireStateChange("error", `Failed to apply answer: ${message}`);
684
+ await this.stop();
685
+ }
686
+ }
687
+ /**
688
+ * Some Matter cameras (notably the matter.js camera-controller example app) answer
689
+ * `a=sendrecv` on audio m-lines even when our offer is `a=recvonly`. Per RFC 3264 the
690
+ * only valid mirror of recvonly is sendonly; sendrecv triggers
691
+ * "Answer tried to set recv when offer did not set send" in setRemoteDescription.
692
+ * We add both video and audio transceivers as recvonly, so any sendrecv answer must
693
+ * be coerced to sendonly to apply cleanly.
694
+ */
695
+ _sanitizeAnswerSdp(sdp) {
696
+ const lines = sdp.split(/\r\n|\n/);
697
+ let inMediaSection = false;
698
+ let mutated = false;
699
+ for (let i = 0; i < lines.length; i++) {
700
+ const line = lines[i];
701
+ if (line.startsWith("m=")) {
702
+ inMediaSection = true;
703
+ continue;
704
+ }
705
+ if (inMediaSection && line === "a=sendrecv") {
706
+ lines[i] = "a=sendonly";
707
+ mutated = true;
708
+ }
709
+ }
710
+ if (mutated) {
711
+ console.warn(
712
+ "[webrtc-stream-view] coerced a=sendrecv -> a=sendonly in answer (offer was recvonly on all m-lines)"
713
+ );
714
+ }
715
+ return lines.join("\r\n");
716
+ }
717
+ async _handleRemoteIceCandidates(data) {
718
+ if (!this._pc || !data) return;
719
+ for (const c of data.ice_candidates) {
720
+ try {
721
+ await this._pc.addIceCandidate({
722
+ candidate: c.candidate,
723
+ sdpMid: c.sdpMid ?? void 0,
724
+ sdpMLineIndex: c.sdpMLineIndex ?? void 0
725
+ });
726
+ } catch (err) {
727
+ console.warn("Failed to add remote ICE candidate", err, c);
728
+ }
729
+ }
730
+ }
731
+ _handleEnd(_data) {
732
+ this._endReceivedFromPeer = true;
733
+ void this.stop();
734
+ }
735
+ async _sendLocalIceCandidates(candidates) {
736
+ if (!this.client || this._webRtcSessionId === null || candidates.length === 0) return;
737
+ try {
738
+ await this.client.deviceCommand(
739
+ this.nodeId,
740
+ this.endpointId,
741
+ WEBRTC_TRANSPORT_PROVIDER_CLUSTER_ID,
742
+ "ProvideICECandidates",
743
+ {
744
+ webRtcSessionId: this._webRtcSessionId,
745
+ ICECandidates: candidates.map((c) => ({
746
+ candidate: c.candidate,
747
+ SDPMid: c.sdpMid,
748
+ SDPMLineIndex: c.sdpMLineIndex
749
+ }))
750
+ }
751
+ );
752
+ } catch (err) {
753
+ console.warn("ProvideICECandidates failed", err);
754
+ }
755
+ }
756
+ _fireStateChange(state2, errorMessage) {
757
+ this._state = state2;
758
+ this._errorMessage = errorMessage;
759
+ this.dispatchEvent(
760
+ new CustomEvent("streamstate", {
761
+ detail: { state: state2, errorMessage },
762
+ bubbles: false,
763
+ composed: false
764
+ })
765
+ );
766
+ }
767
+ };
768
+ WebRtcStreamView.styles = css`
769
+ :host {
770
+ display: flex;
771
+ flex-direction: column;
772
+ width: 100%;
773
+ height: 100%;
774
+ background: black;
775
+ position: relative;
776
+ }
777
+ video {
778
+ display: block;
779
+ flex: 1 1 0;
780
+ min-height: 0;
781
+ width: 100%;
782
+ object-fit: contain;
783
+ background: black;
784
+ }
785
+ video[hidden] {
786
+ display: none;
787
+ }
788
+ .placeholder {
789
+ flex: 1 1 0;
790
+ min-height: 0;
791
+ display: flex;
792
+ flex-direction: column;
793
+ align-items: center;
794
+ justify-content: center;
795
+ gap: 16px;
796
+ color: rgba(255, 255, 255, 0.6);
797
+ text-align: center;
798
+ padding: 24px;
799
+ }
800
+ .placeholder-icon {
801
+ --icon-primary-color: rgba(255, 255, 255, 0.3);
802
+ width: 64px;
803
+ height: 64px;
804
+ }
805
+ .placeholder-text {
806
+ font-size: 0.95rem;
807
+ }
808
+ .placeholder-text b {
809
+ color: rgba(255, 255, 255, 0.85);
810
+ font-weight: 500;
811
+ }
812
+ .placeholder.error {
813
+ color: var(--danger-color, #ff6b6b);
814
+ }
815
+ .placeholder.error .placeholder-icon {
816
+ --icon-primary-color: var(--danger-color, #ff6b6b);
817
+ }
818
+ .spinner {
819
+ width: 32px;
820
+ height: 32px;
821
+ border: 3px solid rgba(255, 255, 255, 0.15);
822
+ border-top-color: rgba(255, 255, 255, 0.7);
823
+ border-radius: 50%;
824
+ animation: spin 0.9s linear infinite;
825
+ }
826
+ @keyframes spin {
827
+ to {
828
+ transform: rotate(360deg);
829
+ }
830
+ }
831
+ @media (prefers-reduced-motion: reduce) {
832
+ .spinner {
833
+ animation: none;
834
+ }
835
+ }
836
+ `;
837
+ __decorateClass([
838
+ consume({ context: clientContext, subscribe: true }),
839
+ property({ attribute: false })
840
+ ], WebRtcStreamView.prototype, "client", 2);
841
+ __decorateClass([
842
+ property({ attribute: false })
843
+ ], WebRtcStreamView.prototype, "nodeId", 2);
844
+ __decorateClass([
845
+ property({ type: Number })
846
+ ], WebRtcStreamView.prototype, "endpointId", 2);
847
+ __decorateClass([
848
+ property({ type: Object })
849
+ ], WebRtcStreamView.prototype, "resolution", 2);
850
+ __decorateClass([
851
+ property({ type: Boolean })
852
+ ], WebRtcStreamView.prototype, "watermarkEnabled", 2);
853
+ __decorateClass([
854
+ property({ type: Boolean })
855
+ ], WebRtcStreamView.prototype, "osdEnabled", 2);
856
+ __decorateClass([
857
+ state()
858
+ ], WebRtcStreamView.prototype, "_state", 2);
859
+ __decorateClass([
860
+ state()
861
+ ], WebRtcStreamView.prototype, "_errorMessage", 2);
862
+ __decorateClass([
863
+ property({ attribute: false })
864
+ ], WebRtcStreamView.prototype, "snapshotResolution", 2);
865
+ __decorateClass([
866
+ query("video")
867
+ ], WebRtcStreamView.prototype, "_video", 2);
868
+ WebRtcStreamView = __decorateClass([
869
+ customElement("webrtc-stream-view")
870
+ ], WebRtcStreamView);
871
+ export {
872
+ AVSM_FEATURE_MAP_ATTR_ID,
873
+ AVSM_FEAT_OSD,
874
+ AVSM_FEAT_WMARK,
875
+ CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
876
+ WebRtcStreamView
877
+ };
878
+ //# sourceMappingURL=webrtc-stream-view.js.map