var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
  for (var i = decorators.length - 1, decorator; i >= 0; i--)
    if (decorator = decorators[i])
      result = (kind ? decorator(target, key, result) : decorator(result)) || result;
  if (kind && result) __defProp(target, key, result);
  return result;
};
/**
 * @license
 * Copyright 2025-2026 Open Home Foundation
 * SPDX-License-Identifier: Apache-2.0
 */
import { consume } from "@lit/context";
import { mdiAlertCircleOutline, mdiVideoOutline } from "@mdi/js";
import { LitElement, css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { clientContext } from "../client/client-context.js";
import { asObject, pickNumber } from "../util/attribute-shapes.js";
import "./ha-svg-icon.js";
const STREAM_USAGE_LIVE_VIEW = 3;
const END_REASON_USER_HANGUP = 2;
const CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID = 1361;
const WEBRTC_TRANSPORT_PROVIDER_CLUSTER_ID = 1363;
const AVSM_FEAT_WMARK = 1 << 6;
const AVSM_FEAT_OSD = 1 << 7;
const AVSM_FEATURE_MAP_ATTR_ID = 65532;
const DEFAULT_MAX_RESOLUTION = { width: 1920, height: 1080 };
const DEFAULT_MIN_RESOLUTION = { width: 640, height: 480 };
function parseStreamAllocate(value, idKey) {
  const obj = asObject(value);
  if (!obj) return null;
  return pickNumber(obj, idKey);
}
function parseSnapshotAllocateResponse(value) {
  const obj = asObject(value);
  if (!obj) return null;
  return pickNumber(obj, "snapshotStreamId");
}
const SNAPSHOT_DEFAULTS = {
  resolution: { width: 1920, height: 1080 },
  maxFrameRate: 30,
  imageCodec: 0
};
function parseSnapshotCapabilitiesFromList(list) {
  const candidates = list.map(asObject).filter((c) => c !== void 0);
  if (candidates.length === 0) return SNAPSHOT_DEFAULTS;
  const parsed = candidates.map((cap) => {
    const res = asObject(cap["resolution"] ?? cap["0"]);
    const width = res ? pickNumber(res, "width", "0") : null;
    const height = res ? pickNumber(res, "height", "1") : null;
    const maxFrameRate = pickNumber(cap, "maxFrameRate", "1");
    const imageCodec = pickNumber(cap, "imageCodec", "2");
    return {
      resolution: {
        width: width ?? SNAPSHOT_DEFAULTS.resolution.width,
        height: height ?? SNAPSHOT_DEFAULTS.resolution.height
      },
      maxFrameRate: maxFrameRate ?? SNAPSHOT_DEFAULTS.maxFrameRate,
      imageCodec: imageCodec ?? SNAPSHOT_DEFAULTS.imageCodec
    };
  });
  return parsed.reduce(
    (best, cur) => cur.resolution.width * cur.resolution.height > best.resolution.width * best.resolution.height ? cur : best
  );
}
function bytesToBase64(bytes) {
  const CHUNK = 8192;
  let binary = "";
  for (let i = 0; i < bytes.length; i += CHUNK) {
    binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
  }
  return btoa(binary);
}
function parseDataToBase64(data) {
  if (typeof data === "string") return data;
  if (data instanceof Uint8Array) return bytesToBase64(data);
  if (data instanceof ArrayBuffer) return bytesToBase64(new Uint8Array(data));
  if (Array.isArray(data)) return bytesToBase64(new Uint8Array(data));
  throw new Error(`Unexpected snapshot data shape: ${typeof data}`);
}
function parseCaptureSnapshotResponse(value) {
  const obj = asObject(value);
  if (!obj) throw new Error("CaptureSnapshot returned no response");
  const data = obj["data"];
  if (data == null) throw new Error("CaptureSnapshot response missing data field");
  const imageCodec = pickNumber(obj, "imageCodec") ?? 0;
  const res = asObject(obj["resolution"]);
  const width = res ? pickNumber(res, "width") ?? 0 : 0;
  const height = res ? pickNumber(res, "height") ?? 0 : 0;
  return { dataBase64: parseDataToBase64(data), imageCodec, resolution: { width, height } };
}
function parseProvideOfferResponse(value) {
  const obj = asObject(value);
  if (!obj) return null;
  const sessionId = pickNumber(obj, "webRtcSessionId");
  if (sessionId === null) return null;
  return {
    webRtcSessionId: sessionId,
    videoStreamId: parseStreamAllocate(value, "videoStreamId"),
    audioStreamId: parseStreamAllocate(value, "audioStreamId")
  };
}
let WebRtcStreamView = class extends LitElement {
  constructor() {
    super(...arguments);
    this.resolution = null;
    this.watermarkEnabled = false;
    this.osdEnabled = false;
    this._state = "idle";
    this._errorMessage = null;
    this.snapshotResolution = null;
    this._snapshotStreamId = null;
    this._snapshotResolution = null;
    /** True when we allocated this stream ourselves and must Deallocate it on stop. False when reusing an existing allocation. */
    this._videoStreamOwned = false;
    this._audioStreamOwned = false;
    this._snapshotStreamOwned = false;
    this._pc = null;
    this._localIceQueue = [];
    this._answerReceived = false;
    this._webRtcSessionId = null;
    this._videoStreamId = null;
    this._audioStreamId = null;
    this._unsubscribe = null;
    this._stopping = false;
    this._endReceivedFromPeer = false;
    this._preSessionQueue = [];
  }
  get state() {
    return this._state;
  }
  get videoStreamId() {
    return this._videoStreamId;
  }
  get muted() {
    return this._video?.muted ?? true;
  }
  setMuted(muted) {
    if (this._video) this._video.muted = muted;
  }
  disconnectedCallback() {
    super.disconnectedCallback();
    void this.deallocateSnapshot();
    void this.stop();
  }
  render() {
    return html`
            <video
                autoplay
                playsinline
                muted
                disablepictureinpicture
                disableremoteplayback
                ?hidden=${this._state !== "streaming"}
            ></video>
            ${this._state === "idle" ? html`<div class="placeholder">
                      <ha-svg-icon class="placeholder-icon" .path=${mdiVideoOutline}></ha-svg-icon>
                      <div class="placeholder-text">Click <b>Start</b> to begin streaming</div>
                  </div>` : null}
            ${this._state === "connecting" ? html`<div class="placeholder">
                      <div class="spinner"></div>
                      <div class="placeholder-text">Connecting…</div>
                  </div>` : null}
            ${this._state === "error" ? html`<div class="placeholder error">
                      <ha-svg-icon class="placeholder-icon" .path=${mdiAlertCircleOutline}></ha-svg-icon>
                      <div class="placeholder-text">${this._errorMessage ?? "Stream error"}</div>
                  </div>` : null}
        `;
  }
  async start() {
    if (!this.client) throw new Error("Matter client not available");
    if (this._state === "connecting" || this._state === "streaming") return;
    this._fireStateChange("connecting", null);
    this._answerReceived = false;
    this._localIceQueue = [];
    this._stopping = false;
    this._endReceivedFromPeer = false;
    this._preSessionQueue = [];
    try {
      const pc = new RTCPeerConnection({ iceServers: [] });
      this._pc = pc;
      pc.addTransceiver("video", { direction: "recvonly" });
      pc.addTransceiver("audio", { direction: "recvonly" });
      pc.onicecandidate = (ev) => {
        if (ev.candidate === null) {
          console.log("[webrtc-stream-view] local ICE gathering complete");
          return;
        }
        console.log("[webrtc-stream-view] local ICE candidate", ev.candidate.candidate, {
          queued: !this._answerReceived,
          queueDepth: this._localIceQueue.length
        });
        if (!this._answerReceived) {
          this._localIceQueue.push(ev.candidate);
          return;
        }
        void this._sendLocalIceCandidates([ev.candidate]);
      };
      pc.oniceconnectionstatechange = () => {
        console.log("[webrtc-stream-view] iceConnectionState ->", pc.iceConnectionState);
      };
      pc.onconnectionstatechange = () => {
        console.log("[webrtc-stream-view] connectionState ->", pc.connectionState);
      };
      pc.onsignalingstatechange = () => {
        console.log("[webrtc-stream-view] signalingState ->", pc.signalingState);
      };
      pc.ontrack = (ev) => {
        console.log("[webrtc-stream-view] ontrack", {
          kind: ev.track.kind,
          readyState: ev.track.readyState,
          streams: ev.streams.length
        });
        const video = this._video;
        if (!video) {
          console.warn("[webrtc-stream-view] ontrack fired but <video> query is null");
          return;
        }
        const stream = ev.streams[0];
        if (!stream) return;
        if (video.srcObject !== stream) {
          video.srcObject = stream;
        }
      };
      const minResolution = this.resolution ?? DEFAULT_MIN_RESOLUTION;
      const maxResolution = this.resolution ?? DEFAULT_MAX_RESOLUTION;
      const avsmFeatures = this._readAvsmFeatures();
      const videoAllocPayload = {
        streamUsage: STREAM_USAGE_LIVE_VIEW,
        videoCodec: 0,
        minFrameRate: 30,
        maxFrameRate: 120,
        minResolution,
        maxResolution,
        minBitRate: 1e4,
        maxBitRate: 1e4,
        keyFrameInterval: 4e3
      };
      if (avsmFeatures.wmark) videoAllocPayload.watermarkEnabled = this.watermarkEnabled;
      if (avsmFeatures.osd) videoAllocPayload.osdEnabled = this.osdEnabled;
      const reusedVideoId = this._findMatchingVideoStream({
        streamUsage: STREAM_USAGE_LIVE_VIEW,
        videoCodec: 0,
        minRes: minResolution,
        maxRes: maxResolution,
        watermarkEnabled: avsmFeatures.wmark ? this.watermarkEnabled : void 0,
        osdEnabled: avsmFeatures.osd ? this.osdEnabled : void 0
      });
      let videoAlloc = null;
      if (reusedVideoId !== null) {
        console.info("[webrtc-stream-view] reusing existing video stream", reusedVideoId);
        this._videoStreamId = reusedVideoId;
        this._videoStreamOwned = false;
      } else {
        try {
          videoAlloc = await this.client.deviceCommand(
            this.nodeId,
            this.endpointId,
            CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
            "VideoStreamAllocate",
            videoAllocPayload
          );
        } catch (err) {
          const message = err instanceof Error ? err.message : String(err);
          const isResourceExhausted = message.includes("Resource exhausted") || message.includes("(code 137)");
          if (!isResourceExhausted || this._snapshotStreamId === null) throw err;
          console.info(
            "[webrtc-stream-view] VideoStreamAllocate ResourceExhausted; freeing snapshot stream and retrying"
          );
          await this.deallocateSnapshot();
          videoAlloc = await this.client.deviceCommand(
            this.nodeId,
            this.endpointId,
            CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
            "VideoStreamAllocate",
            videoAllocPayload
          );
        }
        const videoStreamId = parseStreamAllocate(videoAlloc, "videoStreamId");
        if (videoStreamId === null) {
          throw new Error("VideoStreamAllocate did not return a videoStreamId");
        }
        this._videoStreamId = videoStreamId;
        this._videoStreamOwned = true;
      }
      const audioWant = {
        streamUsage: STREAM_USAGE_LIVE_VIEW,
        audioCodec: 0,
        channelCount: 1,
        sampleRate: 48e3
      };
      const reusedAudioId = this._findMatchingAudioStream(audioWant);
      if (reusedAudioId !== null) {
        console.info("[webrtc-stream-view] reusing existing audio stream", reusedAudioId);
        this._audioStreamId = reusedAudioId;
        this._audioStreamOwned = false;
      } else {
        try {
          const audioAlloc = await this.client.deviceCommand(
            this.nodeId,
            this.endpointId,
            CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
            "AudioStreamAllocate",
            {
              ...audioWant,
              bitRate: 2e4,
              bitDepth: 24
            }
          );
          this._audioStreamId = parseStreamAllocate(audioAlloc, "audioStreamId");
          this._audioStreamOwned = this._audioStreamId !== null;
        } catch (err) {
          console.info("AudioStreamAllocate failed; continuing video-only", err);
          this._audioStreamId = null;
        }
      }
      const offer = await pc.createOffer();
      await pc.setLocalDescription(offer);
      this._unsubscribe = this.client.addWebRtcCallbackListener((data) => void this._onWebRtcCallback(data));
      const sdp = pc.localDescription?.sdp;
      if (!sdp) {
        throw new Error("Failed to create local SDP offer");
      }
      console.log("[webrtc-stream-view] sending ProvideOffer", {
        nodeId: this.nodeId,
        endpointId: this.endpointId,
        videoStreamId: this._videoStreamId,
        audioStreamId: this._audioStreamId,
        streamUsage: STREAM_USAGE_LIVE_VIEW,
        sdpLength: sdp.length
      });
      const offerResponse = await this.client.sendWebRtcProviderCommand(
        this.nodeId,
        this.endpointId,
        "ProvideOffer",
        {
          webRtcSessionId: null,
          sdp,
          streamUsage: STREAM_USAGE_LIVE_VIEW,
          videoStreamId: this._videoStreamId,
          audioStreamId: this._audioStreamId
        }
      );
      console.log("[webrtc-stream-view] ProvideOffer response", offerResponse);
      const parsed = parseProvideOfferResponse(offerResponse);
      if (parsed === null) {
        throw new Error("ProvideOffer response missing webRtcSessionId");
      }
      this._webRtcSessionId = parsed.webRtcSessionId;
      console.log("[webrtc-stream-view] webRtcSessionId established", this._webRtcSessionId);
      const buffered = this._preSessionQueue;
      this._preSessionQueue = [];
      for (const ev of buffered) {
        if (ev.webrtc_session_id === parsed.webRtcSessionId) {
          await this._onWebRtcCallback(ev);
        }
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      this._fireStateChange("error", message);
      await this.stop();
    }
  }
  async stop() {
    if (this._stopping) return;
    this._stopping = true;
    if (this._unsubscribe) {
      this._unsubscribe();
      this._unsubscribe = null;
    }
    const sessionId = this._webRtcSessionId;
    const initiatedEnd = sessionId !== null && !this._endReceivedFromPeer;
    if (initiatedEnd) {
      try {
        await this.client?.deviceCommand(
          this.nodeId,
          this.endpointId,
          WEBRTC_TRANSPORT_PROVIDER_CLUSTER_ID,
          "EndSession",
          { webRtcSessionId: sessionId, reason: END_REASON_USER_HANGUP }
        );
      } catch (err) {
        console.warn("EndSession failed during stop", err);
      }
    }
    if (this._videoStreamId !== null && this._videoStreamOwned) {
      try {
        await this.client?.deviceCommand(
          this.nodeId,
          this.endpointId,
          CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
          "VideoStreamDeallocate",
          { videoStreamId: this._videoStreamId }
        );
      } catch (err) {
        console.warn("VideoStreamDeallocate failed during stop", err);
      }
    }
    if (this._audioStreamId !== null && this._audioStreamOwned) {
      try {
        await this.client?.deviceCommand(
          this.nodeId,
          this.endpointId,
          CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
          "AudioStreamDeallocate",
          { audioStreamId: this._audioStreamId }
        );
      } catch (err) {
        console.warn("AudioStreamDeallocate failed during stop", err);
      }
    }
    this._videoStreamOwned = false;
    this._audioStreamOwned = false;
    await this.deallocateSnapshot();
    const video = this._video;
    if (video && video.srcObject) {
      video.srcObject = null;
    }
    if (this._pc) {
      try {
        this._pc.close();
      } catch {
      }
      this._pc = null;
    }
    this._localIceQueue = [];
    this._answerReceived = false;
    this._webRtcSessionId = null;
    this._videoStreamId = null;
    this._audioStreamId = null;
    this._endReceivedFromPeer = false;
    this._preSessionQueue = [];
    if (this._state !== "idle" && this._state !== "error") {
      this._fireStateChange("idle", null);
    }
    this._stopping = false;
  }
  async takeSnapshot() {
    if (!this.client) throw new Error("Matter client not available");
    const streamId = await this._ensureSnapshotStream();
    const requestedResolution = this._snapshotResolution ?? SNAPSHOT_DEFAULTS.resolution;
    const response = await this.client.deviceCommand(
      this.nodeId,
      this.endpointId,
      CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
      "CaptureSnapshot",
      { snapshotStreamId: streamId, requestedResolution }
    );
    const parsed = parseCaptureSnapshotResponse(response);
    const mimeType = parsed.imageCodec === 0 ? "jpeg" : "png";
    return {
      dataUri: `data:image/${mimeType};base64,${parsed.dataBase64}`,
      resolution: parsed.resolution
    };
  }
  _readAvsmFeatures() {
    const node = this.client?.nodes[String(this.nodeId)];
    const raw = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/${AVSM_FEATURE_MAP_ATTR_ID}`];
    const bits = typeof raw === "number" ? raw : 0;
    return {
      wmark: (bits & AVSM_FEAT_WMARK) !== 0,
      osd: (bits & AVSM_FEAT_OSD) !== 0
    };
  }
  async deallocateSnapshot() {
    if (this._snapshotStreamId === null) return;
    const id = this._snapshotStreamId;
    const wasOwned = this._snapshotStreamOwned;
    this._snapshotStreamId = null;
    this._snapshotResolution = null;
    this._snapshotStreamOwned = false;
    if (!wasOwned) return;
    try {
      await this.client?.deviceCommand(
        this.nodeId,
        this.endpointId,
        CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
        "SnapshotStreamDeallocate",
        { snapshotStreamId: id }
      );
    } catch (e) {
      console.warn("SnapshotStreamDeallocate failed (continuing):", e);
    }
  }
  /**
   * Search AllocatedVideoStreams (attr 0x000F) for an entry whose advertised
   * capability ranges cover our requested params. Spec §11.2.1.2.1 says the
   * camera SHALL reuse a matching stream — reading first avoids creating
   * duplicates when a server's reuse logic is buggy or matching is too strict.
   */
  _findMatchingVideoStream(want) {
    const node = this.client?.nodes[String(this.nodeId)];
    const list = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/15`];
    if (!Array.isArray(list)) return null;
    for (const item of list) {
      const obj = asObject(item);
      if (!obj) continue;
      const streamUsage = pickNumber(obj, "streamUsage", "1");
      const videoCodec = pickNumber(obj, "videoCodec", "2");
      if (streamUsage !== want.streamUsage || videoCodec !== want.videoCodec) continue;
      const minRes = asObject(obj["minResolution"] ?? obj["5"]);
      const maxRes = asObject(obj["maxResolution"] ?? obj["6"]);
      if (!minRes || !maxRes) continue;
      const eMinW = pickNumber(minRes, "width", "0") ?? 0;
      const eMinH = pickNumber(minRes, "height", "1") ?? 0;
      const eMaxW = pickNumber(maxRes, "width", "0") ?? 0;
      const eMaxH = pickNumber(maxRes, "height", "1") ?? 0;
      if (eMinW > want.minRes.width || eMaxW < want.maxRes.width) continue;
      if (eMinH > want.minRes.height || eMaxH < want.maxRes.height) continue;
      if (want.watermarkEnabled !== void 0) {
        const v = obj["watermarkEnabled"] ?? obj["10"];
        if (v !== want.watermarkEnabled) continue;
      }
      if (want.osdEnabled !== void 0) {
        const v = obj["osdEnabled"] ?? obj["11"];
        if (v !== want.osdEnabled) continue;
      }
      const id = pickNumber(obj, "videoStreamId", "0");
      if (id !== null) return id;
    }
    return null;
  }
  _findMatchingAudioStream(want) {
    const node = this.client?.nodes[String(this.nodeId)];
    const list = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/16`];
    if (!Array.isArray(list)) return null;
    for (const item of list) {
      const obj = asObject(item);
      if (!obj) continue;
      const streamUsage = pickNumber(obj, "streamUsage", "1");
      const audioCodec = pickNumber(obj, "audioCodec", "2");
      const channelCount = pickNumber(obj, "channelCount", "3");
      const sampleRate = pickNumber(obj, "sampleRate", "4");
      if (streamUsage !== want.streamUsage || audioCodec !== want.audioCodec || channelCount !== want.channelCount || sampleRate !== want.sampleRate)
        continue;
      const id = pickNumber(obj, "audioStreamId", "0");
      if (id !== null) return id;
    }
    return null;
  }
  _findMatchingSnapshotStream(want) {
    const node = this.client?.nodes[String(this.nodeId)];
    const list = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/17`];
    if (!Array.isArray(list)) return null;
    for (const item of list) {
      const obj = asObject(item);
      if (!obj) continue;
      const imageCodec = pickNumber(obj, "imageCodec", "1");
      if (imageCodec !== want.imageCodec) continue;
      const minRes = asObject(obj["minResolution"] ?? obj["3"]);
      const maxRes = asObject(obj["maxResolution"] ?? obj["4"]);
      if (!minRes || !maxRes) continue;
      const eMinW = pickNumber(minRes, "width", "0") ?? 0;
      const eMinH = pickNumber(minRes, "height", "1") ?? 0;
      const eMaxW = pickNumber(maxRes, "width", "0") ?? 0;
      const eMaxH = pickNumber(maxRes, "height", "1") ?? 0;
      if (eMinW > want.resolution.width || eMaxW < want.resolution.width) continue;
      if (eMinH > want.resolution.height || eMaxH < want.resolution.height) continue;
      if (want.watermarkEnabled !== void 0) {
        const v = obj["watermarkEnabled"] ?? obj["9"];
        if (v !== want.watermarkEnabled) continue;
      }
      if (want.osdEnabled !== void 0) {
        const v = obj["osdEnabled"] ?? obj["10"];
        if (v !== want.osdEnabled) continue;
      }
      const id = pickNumber(obj, "snapshotStreamId", "0");
      const resolution = { width: eMaxW, height: eMaxH };
      if (id !== null) return { id, resolution };
    }
    return null;
  }
  async _ensureSnapshotStream() {
    if (this._snapshotStreamId !== null) return this._snapshotStreamId;
    if (!this.client) throw new Error("Matter client not available");
    const node = this.client.nodes[String(this.nodeId)];
    const capsRaw = node?.attributes[`${this.endpointId}/${CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID}/10`];
    const cap = Array.isArray(capsRaw) && capsRaw.length > 0 ? parseSnapshotCapabilitiesFromList(capsRaw) : SNAPSHOT_DEFAULTS;
    const targetResolution = this.snapshotResolution ?? cap.resolution;
    const avsmFeatures = this._readAvsmFeatures();
    const reused = this._findMatchingSnapshotStream({
      imageCodec: cap.imageCodec,
      resolution: targetResolution,
      watermarkEnabled: avsmFeatures.wmark ? this.watermarkEnabled : void 0,
      osdEnabled: avsmFeatures.osd ? this.osdEnabled : void 0
    });
    if (reused !== null) {
      console.info("[webrtc-stream-view] reusing existing snapshot stream", reused.id);
      this._snapshotStreamId = reused.id;
      this._snapshotResolution = reused.resolution;
      this._snapshotStreamOwned = false;
      return reused.id;
    }
    const snapshotAllocPayload = {
      imageCodec: cap.imageCodec,
      maxFrameRate: cap.maxFrameRate,
      minResolution: targetResolution,
      maxResolution: targetResolution,
      quality: 90
    };
    if (avsmFeatures.wmark) snapshotAllocPayload.watermarkEnabled = this.watermarkEnabled;
    if (avsmFeatures.osd) snapshotAllocPayload.osdEnabled = this.osdEnabled;
    const response = await this.client.deviceCommand(
      this.nodeId,
      this.endpointId,
      CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
      "SnapshotStreamAllocate",
      snapshotAllocPayload
    );
    const snapshotStreamId = parseSnapshotAllocateResponse(response);
    if (snapshotStreamId === null) {
      throw new Error("SnapshotStreamAllocate did not return a snapshot stream id");
    }
    this._snapshotStreamId = snapshotStreamId;
    this._snapshotResolution = targetResolution;
    this._snapshotStreamOwned = true;
    return this._snapshotStreamId;
  }
  async _onWebRtcCallback(data) {
    console.log("[webrtc-stream-view] webrtc_callback received", {
      event_type: data.event_type,
      webrtc_session_id: data.webrtc_session_id,
      node_id: data.node_id,
      endpoint_id: data.endpoint_id,
      fabric_index: data.fabric_index,
      myNodeId: this.nodeId,
      myEndpointId: this.endpointId,
      mySessionId: this._webRtcSessionId
    });
    if (this._webRtcSessionId === null) {
      if (String(data.node_id) === String(this.nodeId) && data.endpoint_id === this.endpointId) {
        this._preSessionQueue.push(data);
        console.log("[webrtc-stream-view] queued pre-session callback");
      } else {
        console.log("[webrtc-stream-view] dropped pre-session callback (different node/endpoint)");
      }
      return;
    }
    if (data.webrtc_session_id !== this._webRtcSessionId) {
      console.log("[webrtc-stream-view] dropped: session id mismatch");
      return;
    }
    if (String(data.node_id) !== String(this.nodeId)) {
      console.log("[webrtc-stream-view] dropped: node id mismatch");
      return;
    }
    if (data.endpoint_id !== this.endpointId) {
      console.log("[webrtc-stream-view] dropped: endpoint id mismatch");
      return;
    }
    switch (data.event_type) {
      case "answer":
        void this._handleAnswer(data.data);
        return;
      case "ice_candidates":
        void this._handleRemoteIceCandidates(data.data);
        return;
      case "end":
        this._handleEnd(data.data);
        return;
      case "offer":
        console.info("WebRTC re-offer received; renegotiation unsupported", data);
        return;
    }
  }
  async _handleAnswer(data) {
    if (!this._pc || !data) return;
    try {
      const sdp = this._sanitizeAnswerSdp(data.sdp);
      await this._pc.setRemoteDescription({ type: "answer", sdp });
      this._answerReceived = true;
      const queue = this._localIceQueue;
      this._localIceQueue = [];
      if (queue.length > 0) {
        await this._sendLocalIceCandidates(queue);
      }
      this._fireStateChange("streaming", null);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      this._fireStateChange("error", `Failed to apply answer: ${message}`);
      await this.stop();
    }
  }
  /**
   * Some Matter cameras (notably the matter.js camera-controller example app) answer
   * `a=sendrecv` on audio m-lines even when our offer is `a=recvonly`. Per RFC 3264 the
   * only valid mirror of recvonly is sendonly; sendrecv triggers
   * "Answer tried to set recv when offer did not set send" in setRemoteDescription.
   * We add both video and audio transceivers as recvonly, so any sendrecv answer must
   * be coerced to sendonly to apply cleanly.
   */
  _sanitizeAnswerSdp(sdp) {
    const lines = sdp.split(/\r\n|\n/);
    let inMediaSection = false;
    let mutated = false;
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      if (line.startsWith("m=")) {
        inMediaSection = true;
        continue;
      }
      if (inMediaSection && line === "a=sendrecv") {
        lines[i] = "a=sendonly";
        mutated = true;
      }
    }
    if (mutated) {
      console.warn(
        "[webrtc-stream-view] coerced a=sendrecv -> a=sendonly in answer (offer was recvonly on all m-lines)"
      );
    }
    return lines.join("\r\n");
  }
  async _handleRemoteIceCandidates(data) {
    if (!this._pc || !data) return;
    for (const c of data.ice_candidates) {
      try {
        await this._pc.addIceCandidate({
          candidate: c.candidate,
          sdpMid: c.sdpMid ?? void 0,
          sdpMLineIndex: c.sdpMLineIndex ?? void 0
        });
      } catch (err) {
        console.warn("Failed to add remote ICE candidate", err, c);
      }
    }
  }
  _handleEnd(_data) {
    this._endReceivedFromPeer = true;
    void this.stop();
  }
  async _sendLocalIceCandidates(candidates) {
    if (!this.client || this._webRtcSessionId === null || candidates.length === 0) return;
    try {
      await this.client.deviceCommand(
        this.nodeId,
        this.endpointId,
        WEBRTC_TRANSPORT_PROVIDER_CLUSTER_ID,
        "ProvideICECandidates",
        {
          webRtcSessionId: this._webRtcSessionId,
          ICECandidates: candidates.map((c) => ({
            candidate: c.candidate,
            SDPMid: c.sdpMid,
            SDPMLineIndex: c.sdpMLineIndex
          }))
        }
      );
    } catch (err) {
      console.warn("ProvideICECandidates failed", err);
    }
  }
  _fireStateChange(state2, errorMessage) {
    this._state = state2;
    this._errorMessage = errorMessage;
    this.dispatchEvent(
      new CustomEvent("streamstate", {
        detail: { state: state2, errorMessage },
        bubbles: false,
        composed: false
      })
    );
  }
};
WebRtcStreamView.styles = css`
        :host {
            display: flex;
            flex-direction: column;
            width: 100%;
            height: 100%;
            background: black;
            position: relative;
        }
        video {
            display: block;
            flex: 1 1 0;
            min-height: 0;
            width: 100%;
            object-fit: contain;
            background: black;
        }
        video[hidden] {
            display: none;
        }
        .placeholder {
            flex: 1 1 0;
            min-height: 0;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 16px;
            color: rgba(255, 255, 255, 0.6);
            text-align: center;
            padding: 24px;
        }
        .placeholder-icon {
            --icon-primary-color: rgba(255, 255, 255, 0.3);
            width: 64px;
            height: 64px;
        }
        .placeholder-text {
            font-size: 0.95rem;
        }
        .placeholder-text b {
            color: rgba(255, 255, 255, 0.85);
            font-weight: 500;
        }
        .placeholder.error {
            color: var(--danger-color, #ff6b6b);
        }
        .placeholder.error .placeholder-icon {
            --icon-primary-color: var(--danger-color, #ff6b6b);
        }
        .spinner {
            width: 32px;
            height: 32px;
            border: 3px solid rgba(255, 255, 255, 0.15);
            border-top-color: rgba(255, 255, 255, 0.7);
            border-radius: 50%;
            animation: spin 0.9s linear infinite;
        }
        @keyframes spin {
            to {
                transform: rotate(360deg);
            }
        }
        @media (prefers-reduced-motion: reduce) {
            .spinner {
                animation: none;
            }
        }
    `;
__decorateClass([
  consume({ context: clientContext, subscribe: true }),
  property({ attribute: false })
], WebRtcStreamView.prototype, "client", 2);
__decorateClass([
  property({ attribute: false })
], WebRtcStreamView.prototype, "nodeId", 2);
__decorateClass([
  property({ type: Number })
], WebRtcStreamView.prototype, "endpointId", 2);
__decorateClass([
  property({ type: Object })
], WebRtcStreamView.prototype, "resolution", 2);
__decorateClass([
  property({ type: Boolean })
], WebRtcStreamView.prototype, "watermarkEnabled", 2);
__decorateClass([
  property({ type: Boolean })
], WebRtcStreamView.prototype, "osdEnabled", 2);
__decorateClass([
  state()
], WebRtcStreamView.prototype, "_state", 2);
__decorateClass([
  state()
], WebRtcStreamView.prototype, "_errorMessage", 2);
__decorateClass([
  property({ attribute: false })
], WebRtcStreamView.prototype, "snapshotResolution", 2);
__decorateClass([
  query("video")
], WebRtcStreamView.prototype, "_video", 2);
WebRtcStreamView = __decorateClass([
  customElement("webrtc-stream-view")
], WebRtcStreamView);
export {
  AVSM_FEATURE_MAP_ATTR_ID,
  AVSM_FEAT_OSD,
  AVSM_FEAT_WMARK,
  CAMERA_AV_STREAM_MANAGEMENT_CLUSTER_ID,
  WebRtcStreamView
};
//# sourceMappingURL=webrtc-stream-view.js.map
