@interlucent/pixel-stream 0.0.78 → 0.0.79
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/pixel-stream.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ declare interface AdmissionConfig {
|
|
|
9
9
|
appVersion?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
declare const HTMLElementBase: {
|
|
13
|
+
new (): HTMLElement;
|
|
14
|
+
prototype: HTMLElement;
|
|
15
|
+
};
|
|
16
|
+
|
|
12
17
|
declare interface PerformanceMetrics {
|
|
13
18
|
sessionStarted: number | null;
|
|
14
19
|
tokenExchangeStart: number | null;
|
|
@@ -27,7 +32,7 @@ declare interface PerformanceMetrics {
|
|
|
27
32
|
connectionComplete: number | null;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export declare class PixelStream extends
|
|
35
|
+
export declare class PixelStream extends HTMLElementBase {
|
|
31
36
|
/** Package version, injected at build time. */
|
|
32
37
|
static readonly VERSION: string;
|
|
33
38
|
/** Instance accessor for the package version. */
|
package/dist/pixel-stream.esm.js
CHANGED
|
@@ -5102,7 +5102,13 @@ const RESOLUTION_CLAMP_MAP = {
|
|
|
5102
5102
|
uhd: { maxWidth: 3840, maxHeight: 2160 },
|
|
5103
5103
|
};
|
|
5104
5104
|
// ===== MAIN WEB COMPONENT =====
|
|
5105
|
-
|
|
5105
|
+
// SSR guard: allow the module to be imported in Node.js (e.g. Next.js SSR)
|
|
5106
|
+
// without crashing on missing browser globals.
|
|
5107
|
+
const HTMLElementBase = typeof HTMLElement !== 'undefined'
|
|
5108
|
+
? HTMLElement
|
|
5109
|
+
: class {
|
|
5110
|
+
};
|
|
5111
|
+
class PixelStream extends HTMLElementBase {
|
|
5106
5112
|
/** Instance accessor for the package version. */
|
|
5107
5113
|
get version() {
|
|
5108
5114
|
return PixelStream.VERSION;
|
|
@@ -7709,7 +7715,7 @@ class PixelStream extends HTMLElement {
|
|
|
7709
7715
|
}
|
|
7710
7716
|
}
|
|
7711
7717
|
/** Package version, injected at build time. */
|
|
7712
|
-
PixelStream.VERSION = "0.0.
|
|
7718
|
+
PixelStream.VERSION = "0.0.79";
|
|
7713
7719
|
// Reconnection defaults
|
|
7714
7720
|
PixelStream.DEFAULT_RECONNECT_INTERVAL = 30000; // 30 seconds
|
|
7715
7721
|
// ===== DNS PREFETCH SETUP =====
|
|
@@ -7786,9 +7792,11 @@ if (typeof document !== 'undefined') {
|
|
|
7786
7792
|
}
|
|
7787
7793
|
// Expose DevTools namespace for standalone users: interlucent.setLogLevel('debug'), interlucent.versions, etc.
|
|
7788
7794
|
exposeDevTools();
|
|
7789
|
-
registerVersion('pixel-stream', "0.0.
|
|
7790
|
-
// Register the custom element
|
|
7791
|
-
customElements
|
|
7795
|
+
registerVersion('pixel-stream', "0.0.79");
|
|
7796
|
+
// Register the custom element (skip in non-browser environments like SSR)
|
|
7797
|
+
if (typeof customElements !== 'undefined') {
|
|
7798
|
+
customElements.define('pixel-stream', PixelStream);
|
|
7799
|
+
}
|
|
7792
7800
|
function PixelStreamResizeObserverForVideo(opts) {
|
|
7793
7801
|
const { el, onUpdate, settleMs = 200, dprCap = Infinity, even = true, cssJitterPx = 0, } = opts;
|
|
7794
7802
|
let rafId = null;
|
|
@@ -5105,7 +5105,13 @@ var PixelStream = (function (exports) {
|
|
|
5105
5105
|
uhd: { maxWidth: 3840, maxHeight: 2160 },
|
|
5106
5106
|
};
|
|
5107
5107
|
// ===== MAIN WEB COMPONENT =====
|
|
5108
|
-
|
|
5108
|
+
// SSR guard: allow the module to be imported in Node.js (e.g. Next.js SSR)
|
|
5109
|
+
// without crashing on missing browser globals.
|
|
5110
|
+
const HTMLElementBase = typeof HTMLElement !== 'undefined'
|
|
5111
|
+
? HTMLElement
|
|
5112
|
+
: class {
|
|
5113
|
+
};
|
|
5114
|
+
class PixelStream extends HTMLElementBase {
|
|
5109
5115
|
/** Instance accessor for the package version. */
|
|
5110
5116
|
get version() {
|
|
5111
5117
|
return PixelStream.VERSION;
|
|
@@ -7712,7 +7718,7 @@ var PixelStream = (function (exports) {
|
|
|
7712
7718
|
}
|
|
7713
7719
|
}
|
|
7714
7720
|
/** Package version, injected at build time. */
|
|
7715
|
-
PixelStream.VERSION = "0.0.
|
|
7721
|
+
PixelStream.VERSION = "0.0.79";
|
|
7716
7722
|
// Reconnection defaults
|
|
7717
7723
|
PixelStream.DEFAULT_RECONNECT_INTERVAL = 30000; // 30 seconds
|
|
7718
7724
|
// ===== DNS PREFETCH SETUP =====
|
|
@@ -7789,9 +7795,11 @@ var PixelStream = (function (exports) {
|
|
|
7789
7795
|
}
|
|
7790
7796
|
// Expose DevTools namespace for standalone users: interlucent.setLogLevel('debug'), interlucent.versions, etc.
|
|
7791
7797
|
exposeDevTools();
|
|
7792
|
-
registerVersion('pixel-stream', "0.0.
|
|
7793
|
-
// Register the custom element
|
|
7794
|
-
customElements
|
|
7798
|
+
registerVersion('pixel-stream', "0.0.79");
|
|
7799
|
+
// Register the custom element (skip in non-browser environments like SSR)
|
|
7800
|
+
if (typeof customElements !== 'undefined') {
|
|
7801
|
+
customElements.define('pixel-stream', PixelStream);
|
|
7802
|
+
}
|
|
7795
7803
|
function PixelStreamResizeObserverForVideo(opts) {
|
|
7796
7804
|
const { el, onUpdate, settleMs = 200, dprCap = Infinity, even = true, cssJitterPx = 0, } = opts;
|
|
7797
7805
|
let rafId = null;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! @interlucent/pixel-stream v0.0.
|
|
2
|
-
var t=function(t){"use strict";class e{constructor(){this.mimetype="",this.extension="",this.receiving=0,this.chunks=0,this.data=[],this.valid=0,this.timestampStart=0}}var s,i;!function(t){t[t.IFrameRequest=0]="IFrameRequest",t[t.RequestQualityControl=1]="RequestQualityControl",t[t.FpsRequest=2]="FpsRequest",t[t.AverageBitrateRequest=3]="AverageBitrateRequest",t[t.StartStreaming=4]="StartStreaming",t[t.StopStreaming=5]="StopStreaming",t[t.LatencyTest=6]="LatencyTest",t[t.RequestInitialSettings=7]="RequestInitialSettings",t[t.TestEcho=8]="TestEcho",t[t.DataChannelLatencyTest=9]="DataChannelLatencyTest",t[t.UIInteraction=50]="UIInteraction",t[t.Command=51]="Command",t[t.TextboxEntry=52]="TextboxEntry",t[t.KeyDown=60]="KeyDown",t[t.KeyUp=61]="KeyUp",t[t.KeyPress=62]="KeyPress",t[t.MouseEnter=70]="MouseEnter",t[t.MouseLeave=71]="MouseLeave",t[t.MouseDown=72]="MouseDown",t[t.MouseUp=73]="MouseUp",t[t.MouseMove=74]="MouseMove",t[t.MouseWheel=75]="MouseWheel",t[t.MouseDouble=76]="MouseDouble",t[t.TouchStart=80]="TouchStart",t[t.TouchEnd=81]="TouchEnd",t[t.TouchMove=82]="TouchMove",t[t.GamepadButtonPressed=90]="GamepadButtonPressed",t[t.GamepadButtonReleased=91]="GamepadButtonReleased",t[t.GamepadAnalog=92]="GamepadAnalog",t[t.GamepadConnected=93]="GamepadConnected",t[t.GamepadDisconnected=94]="GamepadDisconnected",t[t.XRHMDTransform=100]="XRHMDTransform",t[t.XREyeViews=101]="XREyeViews",t[t.XRControllerTransform=102]="XRControllerTransform",t[t.XRSystem=103]="XRSystem",t[t.XRButtonPressed=104]="XRButtonPressed",t[t.XRButtonReleased=105]="XRButtonReleased",t[t.XRButtonTouched=106]="XRButtonTouched",t[t.XRButtonTouchReleased=107]="XRButtonTouchReleased",t[t.XRAnalog=108]="XRAnalog"}(s||(s={})),function(t){t[t.QualityControlOwnership=0]="QualityControlOwnership",t[t.Response=1]="Response",t[t.Command=2]="Command",t[t.FreezeFrame=3]="FreezeFrame",t[t.UnfreezeFrame=4]="UnfreezeFrame",t[t.VideoEncoderAvgQP=5]="VideoEncoderAvgQP",t[t.LatencyTest=6]="LatencyTest",t[t.InitialSettings=7]="InitialSettings",t[t.FileExtension=8]="FileExtension",t[t.FileMimeType=9]="FileMimeType",t[t.FileContents=10]="FileContents",t[t.TestEcho=11]="TestEcho",t[t.InputControlOwnership=12]="InputControlOwnership",t[t.GamepadResponse=13]="GamepadResponse",t[t.DataChannelLatencyTest=14]="DataChannelLatencyTest",t[t.Protocol=255]="Protocol"}(i||(i={}));const n=new Map([["IFrameRequest",{id:0,structure:[]}],["RequestQualityControl",{id:1,structure:[]}],["FpsRequest",{id:2,structure:[]}],["AverageBitrateRequest",{id:3,structure:[]}],["StartStreaming",{id:4,structure:[]}],["StopStreaming",{id:5,structure:[]}],["LatencyTest",{id:6,structure:["string"]}],["RequestInitialSettings",{id:7,structure:[]}],["TestEcho",{id:8,structure:[]}],["DataChannelLatencyTest",{id:9,structure:[]}],["UIInteraction",{id:50,structure:["string"]}],["Command",{id:51,structure:["string"]}],["TextboxEntry",{id:52,structure:["string"]}],["KeyDown",{id:60,structure:["uint8","uint8"]}],["KeyUp",{id:61,structure:["uint8"]}],["KeyPress",{id:62,structure:["uint16"]}],["MouseEnter",{id:70,structure:[]}],["MouseLeave",{id:71,structure:[]}],["MouseDown",{id:72,structure:["uint8","uint16","uint16"]}],["MouseUp",{id:73,structure:["uint8","uint16","uint16"]}],["MouseMove",{id:74,structure:["uint16","uint16","int16","int16"]}],["MouseWheel",{id:75,structure:["int16","uint16","uint16"]}],["MouseDouble",{id:76,structure:["uint8","uint16","uint16"]}],["TouchStart",{id:80,structure:["uint8","uint16","uint16","uint8","uint8","uint8"]}],["TouchEnd",{id:81,structure:["uint8","uint16","uint16","uint8","uint8","uint8"]}],["TouchMove",{id:82,structure:["uint8","uint16","uint16","uint8","uint8","uint8"]}],["GamepadButtonPressed",{id:90,structure:["uint8","uint8","uint8"]}],["GamepadButtonReleased",{id:91,structure:["uint8","uint8","uint8"]}],["GamepadAnalog",{id:92,structure:["uint8","uint8","double"]}],["GamepadConnected",{id:93,structure:[]}],["GamepadDisconnected",{id:94,structure:["uint8"]}],["XRHMDTransform",{id:100,structure:Array(16).fill("float")}],["XREyeViews",{id:101,structure:Array(80).fill("float")}],["XRControllerTransform",{id:102,structure:[...Array(16).fill("float"),"uint8"]}],["XRSystem",{id:103,structure:["uint8"]}],["XRButtonPressed",{id:104,structure:["uint8","uint8","uint8","float"]}],["XRButtonReleased",{id:105,structure:["uint8","uint8","uint8"]}],["XRButtonTouched",{id:106,structure:["uint8","uint8","uint8"]}],["XRButtonTouchReleased",{id:107,structure:["uint8","uint8","uint8"]}],["XRAnalog",{id:108,structure:["uint8","uint8","float"]}]]),r=new Map([[0,"QualityControlOwnership"],[1,"Response"],[2,"Command"],[3,"FreezeFrame"],[4,"UnfreezeFrame"],[5,"VideoEncoderAvgQP"],[6,"LatencyTest"],[7,"InitialSettings"],[8,"FileExtension"],[9,"FileMimeType"],[10,"FileContents"],[11,"TestEcho"],[12,"InputControlOwnership"],[13,"GamepadResponse"],[14,"DataChannelLatencyTest"],[255,"Protocol"]]);class a{static encode(t,e=[]){const s=n.get(t);if(!s)return new ArrayBuffer(0);let i=1;e.forEach((t,e)=>{switch(s.structure[e]){case"uint8":i+=1;break;case"uint16":case"int16":i+=2;break;case"float":i+=4;break;case"double":i+=8;break;case"string":i+=2,i+=2*t.length}});const r=new ArrayBuffer(i),a=new DataView(r);let o=0;return a.setUint8(o++,s.id),e.forEach((t,e)=>{switch(s.structure[e]){case"uint8":a.setUint8(o,t),o+=1;break;case"uint16":a.setUint16(o,t,1),o+=2;break;case"int16":a.setInt16(o,t,1),o+=2;break;case"float":a.setFloat32(o,t,1),o+=4;break;case"double":a.setFloat64(o,t,1),o+=8;break;case"string":const e=t;a.setUint16(o,e.length,1),o+=2;for(let t=0;t<e.length;t++)a.setUint16(o,e.charCodeAt(t),1),o+=2}}),r}static decode(t){const e=new Uint8Array(t);if(0===e.length)return{type:"Unknown",payload:null};const s=e[0],i=r.get(s);if(!i)return{type:"Unknown",payload:null};const n=new DataView(t);switch(i){case"Response":case"Command":case"LatencyTest":case"TestEcho":case"FileExtension":case"FileMimeType":case"InitialSettings":case"Protocol":case"GamepadResponse":case"DataChannelLatencyTest":return{type:i,payload:this.decodeStringPayload(e)};case"QualityControlOwnership":case"InputControlOwnership":return e.length<2?{type:i,payload:0}:{type:i,payload:!!e[1]};case"VideoEncoderAvgQP":return e.length>=5?{type:i,payload:n.getFloat32(1,1)}:e.length>=2?{type:i,payload:e[1]}:{type:i,payload:null};case"FileContents":if(e.length<5)return{type:i,payload:new Uint8Array(0)};const t=n.getInt32(1,1),s=Math.min(5+t,e.length);return{type:i,payload:e.slice(5,s)};case"FreezeFrame":return e.length>1?{type:i,payload:e.slice(1)}:{type:i,payload:null};case"UnfreezeFrame":return{type:i,payload:null};default:return{type:i,payload:e.slice(1)}}}static decodeStringPayload(t){if(t.length<2)return null;const e=new TextDecoder("utf-16").decode(t.slice(1));try{return JSON.parse(e)}catch{return e}}}class o{constructor(t){this.video=t}computeAspectRatio(){const t=this.video.clientWidth,e=this.video.clientHeight,s=this.video.videoWidth,i=e/t,n=this.video.videoHeight/s,r=i>n;return{pw:t,ph:e,playerIsLarger:r,ratio:r?i/n:n/i}}translateUnsigned(t,e){const{pw:s,ph:i,playerIsLarger:n,ratio:r}=this.computeAspectRatio(),a=n?t/s:r*(t/s-.5)+.5,o=n?r*(e/i-.5)+.5:e/i;return a<0||a>1||o<0||o>1?{inRange:0,x:65535,y:65535}:{inRange:1,x:Math.floor(65536*a),y:Math.floor(65536*o)}}translateSigned(t,e){const{pw:s,ph:i,playerIsLarger:n,ratio:r}=this.computeAspectRatio(),a=n?r*e/(.5*i):e/(.5*i);return{x:Math.floor(32767*(n?t/(.5*s):r*t/(.5*s))),y:Math.floor(32767*a)}}}const h={error:0,warn:1,info:2,debug:3};function c(t,e,s,i){const n=(n,r,a,o)=>{if(h[n]>h[i()])return;const c={level:n,source:e,module:s,message:r,format:o?.format??"text",data:a,timestamp:performance.now()};t.dispatchEvent(new CustomEvent("interlucent:log",{detail:c}))};return{error:(t,e,s)=>n("error",t,e,s),warn:(t,e,s)=>n("warn",t,e,s),info:(t,e,s)=>n("info",t,e,s),debug:(t,e,s)=>n("debug",t,e,s)}}const l="interlucent:logLevel";let u=null;function d(t="info"){return()=>u??("function"==typeof t?t():t)}var p;u=function(){try{const t=localStorage.getItem(l);return t&&t in h?t:null}catch{return null}}();const m=(p=globalThis).interlucent??(p.interlucent={});function g(t,e){m[t]=e}const f={8:8,46:127};class v{constructor(t,e){this.ctx=t,this.log=e,this.keyDownHandler=null,this.keyUpHandler=null,this.keyPressHandler=null,this.activeKeys=new Set}setup(){this.keyDownHandler=t=>{const e=t.keyCode||t.which;this.activeKeys.add(e),this.ctx.sendToStreamer("KeyDown",[e,t.repeat?1:0]);const s=f[e];void 0!==s&&this.ctx.sendToStreamer("KeyPress",[s]),this.ctx.config.suppressBrowserKeys&&this.isBrowserKey(e)&&t.preventDefault()},this.keyUpHandler=t=>{const e=t.keyCode||t.which;this.activeKeys.delete(e),this.ctx.sendToStreamer("KeyUp",[e]),this.ctx.config.suppressBrowserKeys&&this.isBrowserKey(e)&&t.preventDefault()},this.keyPressHandler=t=>{const e=t.keyCode||t.which;this.ctx.sendToStreamer("KeyPress",[e])},document.addEventListener("keydown",this.keyDownHandler),document.addEventListener("keyup",this.keyUpHandler),document.addEventListener("keypress",this.keyPressHandler)}teardown(){this.keyDownHandler&&document.removeEventListener("keydown",this.keyDownHandler),this.keyUpHandler&&document.removeEventListener("keyup",this.keyUpHandler),this.keyPressHandler&&document.removeEventListener("keypress",this.keyPressHandler)}releaseAllKeys(){this.activeKeys.forEach(t=>{this.ctx.sendToStreamer("KeyUp",[t])}),this.activeKeys.clear()}isBrowserKey(t){return t>=112&&t<=123||9===t||8===t||46===t}}class w{constructor(t,e,s){this.ctx=t,this.log=e,this.mouseMoveHandler=null,this.virtualMouseX=0,this.virtualMouseY=0,this.normalizedCoord={inRange:1,x:0,y:0},this.mouseButtonDown=0,this.onPointerUnlocked=s?.onPointerUnlocked}getNormalizedCoord(){return this.normalizedCoord}setup(){const t=this.ctx.getVideoElement().getBoundingClientRect();this.virtualMouseX=t.width/2,this.virtualMouseY=t.height/2,this.setupMouseHandlers(),this.ctx.config.pointerLocked&&this.setupPointerLock()}teardown(){this.mouseMoveHandler&&document.removeEventListener("mousemove",this.mouseMoveHandler),this.exitPointerLock()}setupPointerLock(){if(!this.ctx.config.pointerLocked)return;const t=this.ctx.getVideoElement();t.requestPointerLock=t.requestPointerLock||t.mozRequestPointerLock,document.exitPointerLock=document.exitPointerLock||document.mozExitPointerLock,this.ctx.config.pointerLockRelease?(this.ctx.getVideoElement().addEventListener("mousedown",()=>{this.mouseButtonDown=1,t.requestPointerLock&&t.requestPointerLock()}),document.addEventListener("mouseup",()=>{this.mouseButtonDown=0,this.isPointerLocked()&&document.exitPointerLock()})):this.ctx.getVideoElement().addEventListener("click",()=>{t.requestPointerLock&&t.requestPointerLock()});const e=()=>{const t=document.pointerLockElement||document.mozPointerLockElement;t===this.ctx.getVideoElement()||t===this.ctx.host?(this.log.debug("Pointer locked"),this.ctx.host.dispatchEvent(new CustomEvent("pointerlocked")),this.ctx.config.pointerLockRelease&&!this.mouseButtonDown&&document.exitPointerLock()):(this.log.debug("Pointer unlocked"),this.onPointerUnlocked?.(),this.ctx.host.dispatchEvent(new CustomEvent("pointerunlocked")))};document.addEventListener("pointerlockchange",e),document.addEventListener("mozpointerlockchange",e)}exitPointerLock(){document.exitPointerLock&&this.isPointerLocked()&&document.exitPointerLock()}isPointerLocked(){const t=document.pointerLockElement||document.mozPointerLockElement;return t===this.ctx.getVideoElement()||t===this.ctx.host}setupMouseHandlers(){const t=this.ctx.getVideoElement(),e=this.ctx.getCoordTranslator();this.mouseMoveHandler=s=>{if(this.ctx.config.pointerLocked&&this.isPointerLocked()){const i=t.getBoundingClientRect();for(this.virtualMouseX+=s.movementX,this.virtualMouseY+=s.movementY;this.virtualMouseX>i.width;)this.virtualMouseX-=i.width;for(;this.virtualMouseY>i.height;)this.virtualMouseY-=i.height;for(;this.virtualMouseX<0;)this.virtualMouseX+=i.width;for(;this.virtualMouseY<0;)this.virtualMouseY+=i.height;this.normalizedCoord=e.translateUnsigned(this.virtualMouseX,this.virtualMouseY);const n=e.translateSigned(s.movementX,s.movementY);this.ctx.sendToStreamer("MouseMove",[this.normalizedCoord.x,this.normalizedCoord.y,n.x,n.y])}else{const t=e.translateUnsigned(s.offsetX,s.offsetY),i=e.translateSigned(s.movementX,s.movementY);this.ctx.sendToStreamer("MouseMove",[t.x,t.y,i.x,i.y])}s.preventDefault()},this.ctx.config.pointerLocked?document.addEventListener("mousemove",this.mouseMoveHandler):t.addEventListener("mousemove",this.mouseMoveHandler),t.addEventListener("mousedown",t=>{const s=this.ctx.config.pointerLocked&&this.isPointerLocked()?this.normalizedCoord:e.translateUnsigned(t.offsetX,t.offsetY);this.ctx.sendToStreamer("MouseDown",[t.button,s.x,s.y]),t.preventDefault()}),t.addEventListener("mouseup",t=>{const s=this.ctx.config.pointerLocked&&this.isPointerLocked()?this.normalizedCoord:e.translateUnsigned(t.offsetX,t.offsetY);this.ctx.sendToStreamer("MouseUp",[t.button,s.x,s.y]),t.preventDefault()}),t.addEventListener("dblclick",t=>{const s=this.ctx.config.pointerLocked&&this.isPointerLocked()?this.normalizedCoord:e.translateUnsigned(t.offsetX,t.offsetY);this.ctx.sendToStreamer("MouseDouble",[t.button,s.x,s.y])}),t.addEventListener("wheel",t=>{const s=this.ctx.config.pointerLocked&&this.isPointerLocked()?this.normalizedCoord:e.translateUnsigned(t.offsetX,t.offsetY),i=void 0!==t.wheelDelta?t.wheelDelta:-t.deltaY;this.ctx.sendToStreamer("MouseWheel",[i,s.x,s.y]),t.preventDefault()}),t.addEventListener("contextmenu",t=>{t.preventDefault()}),t.addEventListener("mouseenter",()=>{this.ctx.sendToStreamer("MouseEnter",[])}),t.addEventListener("mouseleave",()=>{this.ctx.sendToStreamer("MouseLeave",[])})}}class y{constructor(t,e){this.ctx=t,this.log=e,this.fingerIds=new Map,this.availableFingers=[9,8,7,6,5,4,3,2,1,0],this.fakeTouchFinger=null}setup(){const t=this.ctx.getVideoElement();t.addEventListener("touchstart",t=>{this.ctx.config.nativeTouch?this.handleRealTouchStart(t):this.handleFakeTouchStart(t)}),t.addEventListener("touchmove",t=>{this.ctx.config.nativeTouch?this.handleRealTouchMove(t):this.handleFakeTouchMove(t)}),t.addEventListener("touchend",t=>{this.ctx.config.nativeTouch?this.handleRealTouchEnd(t):this.handleFakeTouchEnd(t)})}teardown(){}handleRealTouchStart(t){for(let e=0;e<t.changedTouches.length;e++){const s=t.changedTouches[e],i=this.availableFingers.pop();if(void 0===i){this.log.warn("Exhausted touch identifiers");continue}this.fingerIds.set(s.identifier,i);const n=this.getTouchCoord(s);this.ctx.sendToStreamer("TouchStart",[1,n.x,n.y,i,Math.floor(255*(s.force>0?s.force:1)),n.inRange?1:0])}t.preventDefault()}handleRealTouchMove(t){for(let e=0;e<t.touches.length;e++){const s=t.touches[e],i=this.fingerIds.get(s.identifier);if(void 0===i)continue;const n=this.getTouchCoord(s);this.ctx.sendToStreamer("TouchMove",[1,n.x,n.y,i,Math.floor(255*(s.force>0?s.force:1)),n.inRange?1:0])}t.preventDefault()}handleRealTouchEnd(t){for(let e=0;e<t.changedTouches.length;e++){const s=t.changedTouches[e],i=this.fingerIds.get(s.identifier);if(void 0===i)continue;const n=this.getTouchCoord(s);this.ctx.sendToStreamer("TouchEnd",[1,n.x,n.y,i,Math.floor(255*s.force),n.inRange?1:0]),this.fingerIds.delete(s.identifier),this.availableFingers.push(i),this.availableFingers.sort((t,e)=>e-t)}t.preventDefault()}handleFakeTouchStart(t){if(null===this.fakeTouchFinger){const e=t.changedTouches[0],s=this.ctx.getVideoElement().getBoundingClientRect(),i=e.clientX-s.left,n=e.clientY-s.top,r=this.ctx.getCoordTranslator().translateUnsigned(i,n);this.fakeTouchFinger={id:e.identifier,x:i,y:n},this.ctx.sendToStreamer("MouseEnter",[]),this.ctx.sendToStreamer("MouseDown",[0,r.x,r.y])}t.preventDefault()}handleFakeTouchMove(t){if(null!==this.fakeTouchFinger){for(let e=0;e<t.touches.length;e++){const s=t.touches[e];if(s.identifier===this.fakeTouchFinger.id){const t=this.ctx.getVideoElement().getBoundingClientRect(),e=s.clientX-t.left,i=s.clientY-t.top,n=e-this.fakeTouchFinger.x,r=i-this.fakeTouchFinger.y,a=this.ctx.getCoordTranslator(),o=a.translateUnsigned(e,i),h=a.translateSigned(n,r);this.ctx.sendToStreamer("MouseMove",[o.x,o.y,h.x,h.y]),this.fakeTouchFinger.x=e,this.fakeTouchFinger.y=i;break}}t.preventDefault()}}handleFakeTouchEnd(t){if(null!==this.fakeTouchFinger){for(let e=0;e<t.changedTouches.length;e++){const s=t.changedTouches[e];if(s.identifier===this.fakeTouchFinger.id){const t=this.getTouchCoord(s);this.ctx.sendToStreamer("MouseUp",[0,t.x,t.y]),this.ctx.sendToStreamer("MouseLeave",[]),this.fakeTouchFinger=null;break}}t.preventDefault()}}getTouchCoord(t){const e=this.ctx.getVideoElement().getBoundingClientRect(),s=t.clientX-e.left,i=t.clientY-e.top;return this.ctx.getCoordTranslator().translateUnsigned(s,i)}}class b{constructor(t,e){this.ctx=t,this.log=e,this.controllers=[],this.animationFrame=null,this.handleConnected=t=>{const e=t.gamepad;this.controllers[e.index]={currentState:this.deepCopyGamepad(e),prevState:this.deepCopyGamepad(e),id:e.index},this.ctx.sendToStreamer("GamepadConnected",[e.index]),null===this.animationFrame&&this.pollStatus(),this.ctx.host.dispatchEvent(new CustomEvent("gamepadconnected",{detail:e}))},this.handleDisconnected=t=>{const e=t.gamepad,s=this.controllers[e.index];s&&(this.ctx.sendToStreamer("GamepadDisconnected",[s.id]),delete this.controllers[e.index]),this.ctx.host.dispatchEvent(new CustomEvent("gamepaddisconnected",{detail:e}))},this.pollStatus=()=>{const t=navigator.getGamepads?.();if(t){for(let e=0;e<t.length;e++)t[e]&&this.controllers[e]&&(this.controllers[e].currentState=t[e]);for(const t of this.controllers){if(!t)continue;const e=t.id,s=t.currentState,i=t.prevState;for(let t=0;t<s.buttons.length;t++){const n=s.buttons[t],r=i.buttons[t];if(n.pressed)if(6===t)this.ctx.sendToStreamer("GamepadAnalog",[e,5,n.value]);else if(7===t)this.ctx.sendToStreamer("GamepadAnalog",[e,6,n.value]);else{const s=r.pressed?1:0;this.ctx.sendToStreamer("GamepadButtonPressed",[e,t,s])}else!n.pressed&&r.pressed&&(6===t?this.ctx.sendToStreamer("GamepadAnalog",[e,5,0]):7===t?this.ctx.sendToStreamer("GamepadAnalog",[e,6,0]):this.ctx.sendToStreamer("GamepadButtonReleased",[e,t,0]))}for(let t=0;t<s.axes.length;t+=2){const i=parseFloat(s.axes[t].toFixed(4)),n=-parseFloat(s.axes[t+1].toFixed(4));this.ctx.sendToStreamer("GamepadAnalog",[e,t+1,i]),this.ctx.sendToStreamer("GamepadAnalog",[e,t+2,n])}t.prevState=this.deepCopyGamepad(s)}this.controllers.filter(t=>void 0!==t).length>0&&(this.animationFrame=requestAnimationFrame(this.pollStatus))}}}setup(){window.addEventListener("gamepadconnected",this.handleConnected),window.addEventListener("gamepaddisconnected",this.handleDisconnected);const t=navigator.getGamepads?.();if(t)for(const e of t)e&&this.handleConnected(new GamepadEvent("gamepadconnected",{gamepad:e}))}teardown(){window.removeEventListener("gamepadconnected",this.handleConnected),window.removeEventListener("gamepaddisconnected",this.handleDisconnected),null!==this.animationFrame&&(cancelAnimationFrame(this.animationFrame),this.animationFrame=null);for(const t of this.controllers)void 0!==t?.id&&this.ctx.sendToStreamer("GamepadDisconnected",[t.id]);this.controllers=[]}deepCopyGamepad(t){return k(t)}}function k(t){return JSON.parse(JSON.stringify({buttons:t.buttons.map(t=>({pressed:t.pressed,touched:t.touched,value:t.value})),axes:[...t.axes],index:t.index,id:t.id,connected:t.connected,timestamp:t.timestamp,mapping:t.mapping}))}class C{constructor(t,e){this.ctx=t,this.log=e,this.session=null,this.refSpace=null,this.controllers=[],this.transientPointers=new Map,this.gl=null,this.videoTexture=null,this.leftView=null,this.rightView=null,this.onFrame=(t,e)=>{if(!this.session||!this.refSpace)return;const s=e.getViewerPose(this.refSpace);if(s){for(const t of s.views)"left"===t.eye?this.leftView=t:"right"===t.eye&&(this.rightView=t);this.sendTransform(s),this.session.inputSources.forEach(t=>{"transient-pointer"===t.targetRayMode?this.updateTransientPointer(t,e):"tracked-pointer"===t.targetRayMode&&t.gamepad&&this.updateController(t,e)})}this.session.requestAnimationFrame((t,e)=>this.onFrame(t,e))}}setup(){this.log.info("WebXR ready - call startXRSession() to begin")}teardown(){this.endSession()}isSessionActive(){return null!==this.session}async startSession(){if(!navigator.xr)return this.log.error("WebXR not supported"),void this.ctx.host.dispatchEvent(new CustomEvent("xr-not-supported"));try{const t=await navigator.xr.requestSession("immersive-vr",{optionalFeatures:[]});await this.onSessionStarted(t)}catch(t){this.log.error("Failed to start XR session",t),this.ctx.host.dispatchEvent(new CustomEvent("xr-session-failed",{detail:t}))}}endSession(){this.session&&this.session.end()}static async isSupported(){return navigator.xr?navigator.xr.isSessionSupported("immersive-vr"):0}async onSessionStarted(t){this.session=t,this.session.addEventListener("end",()=>this.onSessionEnded()),this.session.addEventListener("selectstart",t=>this.onSelectStart(t)),this.session.addEventListener("selectend",t=>this.onSelectEnd(t)),this.session.addEventListener("select",t=>this.onSelect(t));const e=document.createElement("canvas");this.gl=e.getContext("webgl2",{xrCompatible:1}),this.refSpace=await t.requestReferenceSpace("local"),t.updateRenderState({baseLayer:new XRWebGLLayer(t,this.gl)}),t.requestAnimationFrame((t,e)=>this.onFrame(t,e)),this.ctx.host.dispatchEvent(new CustomEvent("xr-session-started"))}sendTransform(t){const e=t.transform.matrix;if(this.ctx.sendToStreamer("XRHMDTransform",[e[0],e[4],e[8],e[12],e[1],e[5],e[9],e[13],e[2],e[6],e[10],e[14],e[3],e[7],e[11],e[15]]),this.leftView&&this.rightView){const t=this.leftView.transform.matrix,s=this.leftView.projectionMatrix,i=this.rightView.transform.matrix,n=this.rightView.projectionMatrix;this.ctx.sendToStreamer("XREyeViews",[t[0],t[4],t[8],t[12],t[1],t[5],t[9],t[13],t[2],t[6],t[10],t[14],t[3],t[7],t[11],t[15],s[0],s[4],s[8],s[12],s[1],s[5],s[9],s[13],s[2],s[6],s[10],s[14],s[3],s[7],s[11],s[15],i[0],i[4],i[8],i[12],i[1],i[5],i[9],i[13],i[2],i[6],i[10],i[14],i[3],i[7],i[11],i[15],n[0],n[4],n[8],n[12],n[1],n[5],n[9],n[13],n[2],n[6],n[10],n[14],n[3],n[7],n[11],n[15],e[0],e[4],e[8],e[12],e[1],e[5],e[9],e[13],e[2],e[6],e[10],e[14],e[3],e[7],e[11],e[15]])}}updateController(t,e){if(!t.gamepad||!this.refSpace||!t.gripSpace)return;const s=e.getPose(t.gripSpace,this.refSpace);if(!s)return;let i=2;"left"===t.handedness&&(i=0),"right"===t.handedness&&(i=1);const n=s.transform.matrix;this.ctx.sendToStreamer("XRControllerTransform",[n[0],n[4],n[8],n[12],n[1],n[5],n[9],n[13],n[2],n[6],n[10],n[14],n[3],n[7],n[11],n[15],i]);let r=0;t.profiles.includes("htc-vive")?r=1:t.profiles.includes("oculus-touch")&&(r=2),this.ctx.sendToStreamer("XRSystem",[r]),this.controllers[i]||(this.controllers[i]={prevState:k(t.gamepad),currentState:t.gamepad});const a=this.controllers[i],o=t.gamepad,h=a.prevState;for(let t=0;t<o.buttons.length;t++){const e=o.buttons[t],s=h.buttons[t];e.pressed&&!s.pressed?this.ctx.sendToStreamer("XRButtonPressed",[i,t,0,e.value]):!e.pressed&&s.pressed&&this.ctx.sendToStreamer("XRButtonReleased",[i,t,0]),e.touched&&!s.touched?this.ctx.sendToStreamer("XRButtonTouched",[i,t,0]):!e.touched&&s.touched&&this.ctx.sendToStreamer("XRButtonTouchReleased",[i,t,0])}for(let t=0;t<o.axes.length;t++)o.axes[t]!==h.axes[t]&&this.ctx.sendToStreamer("XRAnalog",[i,t,o.axes[t]]);a.prevState=k(o),a.currentState=o}updateTransientPointer(t,e){if(!t.targetRaySpace||!this.refSpace)return;const s=e.getPose(t.targetRaySpace,this.refSpace);if(!s)return;const i=Array.from(this.transientPointers.keys()).find(e=>this.transientPointers.get(e)?.source===t)??this.transientPointers.size;let n=this.transientPointers.get(i);n||(n={source:t,isSelecting:0},this.transientPointers.set(i,n));let r=2;"left"===t.handedness&&(r=0),"right"===t.handedness&&(r=1);const a=s.transform.matrix;this.ctx.sendToStreamer("XRControllerTransform",[a[0],a[4],a[8],a[12],a[1],a[5],a[9],a[13],a[2],a[6],a[10],a[14],a[3],a[7],a[11],a[15],r]),n.source=t}onSelectStart(t){const e=t.inputSource;if("transient-pointer"!==e.targetRayMode)return;const s=Array.from(this.transientPointers.keys()).find(t=>this.transientPointers.get(t)?.source===e);if(void 0===s)return;const i=this.transientPointers.get(s);if(!i)return;i.isSelecting=1;let n=2;"left"===e.handedness&&(n=0),"right"===e.handedness&&(n=1),this.ctx.sendToStreamer("XRButtonPressed",[n,0,0,1]),this.log.debug("Transient pointer select start",{handedness:n,pointerId:s})}onSelectEnd(t){const e=t.inputSource;if("transient-pointer"!==e.targetRayMode)return;const s=Array.from(this.transientPointers.keys()).find(t=>this.transientPointers.get(t)?.source===e);if(void 0===s)return;const i=this.transientPointers.get(s);if(!i)return;i.isSelecting=0;let n=2;"left"===e.handedness&&(n=0),"right"===e.handedness&&(n=1),this.ctx.sendToStreamer("XRButtonReleased",[n,0,0]),this.log.debug("Transient pointer select end",{handedness:n,pointerId:s})}onSelect(t){const e=t.inputSource;"transient-pointer"===e.targetRayMode&&this.log.debug("Transient pointer select (click)",{handedness:e.handedness,targetRayMode:e.targetRayMode})}onSessionEnded(){this.session=null,this.refSpace=null,this.controllers=[],this.transientPointers.clear(),this.leftView=null,this.rightView=null,this.ctx.host.dispatchEvent(new CustomEvent("xr-session-ended"))}}class S{constructor(t,s){this.ctx=t,this.log=s,this.file=new e,this.resolvers=[]}handleExtension(t){const e=new Uint8Array(t);this.file.receiving||(this.file.mimetype="",this.file.extension="",this.file.receiving=1,this.file.valid=0,this.file.chunks=0,this.file.data=[],this.file.timestampStart=(new Date).getTime(),this.log.debug("Received first chunk of file"));const s=new TextDecoder("utf-16").decode(e.slice(1));this.log.debug("File extension",s),this.file.extension=s}handleMimeType(t){const e=new Uint8Array(t);this.file.receiving||(this.file.mimetype="",this.file.extension="",this.file.receiving=1,this.file.valid=0,this.file.chunks=0,this.file.data=[],this.file.timestampStart=(new Date).getTime(),this.log.debug("Received first chunk of file"));const s=new TextDecoder("utf-16").decode(e.slice(1));this.log.debug("File mime type",s),this.file.mimetype=s}handleContents(t){const e=new Uint8Array(t);if(!this.file.receiving)return;const s=16379;this.file.chunks=Math.ceil(new DataView(e.slice(1,5).buffer).getInt32(0,1)/s);const i=e.slice(5);this.file.data.push(i),this.log.debug(`Received file chunk: ${this.file.data.length}/${this.file.chunks}`);const n=this.file.data.length*s,r=this.file.chunks*s,a=Math.round(this.file.data.length/this.file.chunks*100);if(this.ctx.host.dispatchEvent(new CustomEvent("file-progress",{detail:{receivedChunks:this.file.data.length,totalChunks:this.file.chunks,percentage:a,bytesReceived:n,totalBytes:r,extension:this.file.extension,mimeType:this.file.mimetype}})),this.file.data.length===this.file.chunks){this.file.receiving=0,this.file.valid=1,this.log.info("Received complete file");const t=(new Date).getTime()-this.file.timestampStart,e=Math.round(16384*this.file.chunks/t);this.log.info(`Average transfer bitrate: ${e}kb/s over ${t/1e3} seconds`);const s={blob:new Blob(this.file.data,{type:this.file.mimetype}),extension:this.file.extension,mimeType:this.file.mimetype,chunks:this.file.chunks};this.ctx.host.dispatchEvent(new CustomEvent("file-received",{detail:s})),this.resolvers.forEach(t=>t(s)),this.resolvers=[]}else this.file.data.length>this.file.chunks&&(this.file.receiving=0,this.log.error(`Received bigger file than advertised: ${this.file.data.length}/${this.file.chunks}`))}getLastReceivedFile(){return this.file.valid?{blob:new Blob(this.file.data,{type:this.file.mimetype}),extension:this.file.extension,mimeType:this.file.mimetype,chunks:this.file.chunks}:null}waitForFile(){return new Promise(t=>{this.resolvers.push(t)})}}class E{constructor(t,e){this.ctx=t,this.log=e,this.sessionStartTime=null,this.latencyBreakdown={current:null,browserReceiptTimeMs:null,history:[],deltas:null},this.t=0,this.i=0,this.o=0}resetPerformanceMetrics(){const t=this.ctx.performanceMetrics;t.sessionStarted=null,t.tokenExchangeStart=null,t.tokenExchangeComplete=null,t.peerConnectionCreated=null,t.sessionWsConnectStart=null,t.sessionWsConnectComplete=null,t.sessionReady=null,t.jobRequested=null,t.subscribeStart=performance.now(),t.offerReceived=null,t.answerSent=null,t.iceConnected=null,t.firstTrack=null,t.firstFrame=null,t.connectionComplete=null}getPerformanceMetrics(){return{...this.ctx.performanceMetrics}}logPerformanceSummary(){const t=this.ctx.performanceMetrics,e=t.sessionStarted||t.subscribeStart;if(!e)return;const s=t=>null!=t?(t-e).toFixed(2)+"ms":"",i=(t,e)=>null!=t&&null!=e?(e-t).toFixed(2)+"ms":"",n={};t.tokenExchangeStart&&(n["Token exchange start"]={Offset:s(t.tokenExchangeStart),Duration:""}),t.tokenExchangeComplete&&(n["Token exchange complete"]={Offset:s(t.tokenExchangeComplete),Duration:i(t.tokenExchangeStart,t.tokenExchangeComplete)}),t.peerConnectionCreated&&(n["Peer connection created"]={Offset:s(t.peerConnectionCreated),Duration:""}),t.sessionWsConnectStart&&(n["WebSocket connect start"]={Offset:s(t.sessionWsConnectStart),Duration:""}),t.sessionWsConnectComplete&&(n["WebSocket connected"]={Offset:s(t.sessionWsConnectComplete),Duration:i(t.sessionWsConnectStart,t.sessionWsConnectComplete)}),t.sessionReady&&(n["Session ready"]={Offset:s(t.sessionReady),Duration:""}),t.jobRequested&&(n["Job requested"]={Offset:s(t.jobRequested),Duration:""}),t.offerReceived&&(n["Offer received"]={Offset:s(t.offerReceived),Duration:""}),t.answerSent&&(n["Answer sent"]={Offset:s(t.answerSent),Duration:i(t.offerReceived,t.answerSent)}),t.iceConnected&&(n["ICE connected"]={Offset:s(t.iceConnected),Duration:""}),t.firstTrack&&(n["First media track"]={Offset:s(t.firstTrack),Duration:""}),t.firstFrame&&(n["First video frame"]={Offset:s(t.firstFrame),Duration:""}),t.connectionComplete&&(n["Connection complete"]={Offset:s(t.connectionComplete),Duration:""});const r=t.firstFrame??t.connectionComplete;r&&(n.TOTAL={Offset:s(r),Duration:i(e,r)}),this.log.info("Performance Summary",n,{format:"table"}),this.ctx.host.dispatchEvent(new CustomEvent("performance-metrics",{detail:{...t}}))}handleLatencyTestResult(t){const e={TestStartTimeMs:t.TestStartTimeMs??0,ReceiptTimeMs:t.ReceiptTimeMs??0,PreCaptureTimeMs:t.PreCaptureTimeMs??0,PostCaptureTimeMs:t.PostCaptureTimeMs??0,PreEncodeTimeMs:t.PreEncodeTimeMs??0,PostEncodeTimeMs:t.PostEncodeTimeMs??0,TransmissionTimeMs:t.TransmissionTimeMs??0},s=Date.now();this.latencyBreakdown.current=e,this.latencyBreakdown.browserReceiptTimeMs=s,this.latencyBreakdown.history.push(e),this.latencyBreakdown.history.length>E.LATENCY_HISTORY_SIZE&&this.latencyBreakdown.history.shift(),this.latencyBreakdown.deltas={captureMs:e.PostCaptureTimeMs-e.PreCaptureTimeMs,encodeMs:e.PostEncodeTimeMs-e.PreEncodeTimeMs,transmitMs:e.TransmissionTimeMs-e.PostEncodeTimeMs,networkMs:s-e.TransmissionTimeMs,totalMs:s-e.TestStartTimeMs},this.ctx.host.dispatchEvent(new CustomEvent("latency-test-result",{detail:{...this.latencyBreakdown}})),this.log.debug("Latency breakdown",this.latencyBreakdown.deltas)}getLatencyBreakdown(){return{...this.latencyBreakdown}}async collectAndDisplaySessionStats(){const t=this.ctx.getPeerConnection();if(!t)return null;try{const e=await t.getStats(),s=this.parseWebRTCStats(e);return this.displaySessionStats(s),s}catch(t){return this.log.error("Failed to collect session stats",t),null}}parseWebRTCStats(t){const e={duration:this.sessionStartTime?(Date.now()-this.sessionStartTime)/1e3:0,video:{codec:null,mimeType:null,bytesReceived:0,packetsReceived:0,packetsLost:0,framesReceived:0,framesDecoded:0,framesDropped:0,frameRate:0,bitrate:0,resolution:{width:0,height:0},jitter:0,pliCount:0,nackCount:0},audio:{codec:null,mimeType:null,bytesReceived:0,packetsReceived:0,packetsLost:0,bitrate:0,jitter:0},connection:{iceLocalCandidateType:null,iceRemoteCandidateType:null,turnUsed:0,rtt:0,availableOutgoingBitrate:0,protocol:null,localAddress:null,remoteAddress:null}};return t.forEach(s=>{if("inbound-rtp"===s.type){const t="video"===s.mediaType||"video"===s.kind,i="audio"===s.mediaType||"audio"===s.kind;if(t){e.video.bytesReceived=s.bytesReceived||0,e.video.packetsReceived=s.packetsReceived||0,e.video.packetsLost=s.packetsLost||0,e.video.framesReceived=s.framesReceived||0,e.video.framesDecoded=s.framesDecoded||0,e.video.framesDropped=s.framesDropped||0,e.video.jitter=s.jitter||0,e.video.pliCount=s.pliCount||0,e.video.nackCount=s.nackCount||0,s.framesPerSecond?e.video.frameRate=s.framesPerSecond:s.framerateMean&&(e.video.frameRate=s.framerateMean),s.frameWidth&&s.frameHeight&&(e.video.resolution.width=s.frameWidth,e.video.resolution.height=s.frameHeight);const t=Date.now(),i=(t-this.o)/1e3;if(s.bitrateMean)e.video.bitrate=s.bitrateMean;else if(i>0&&this.t>0){const t=e.video.bytesReceived-this.t;e.video.bitrate=8*t/i}this.t=e.video.bytesReceived,this.o=t}else i&&(e.audio.bytesReceived=s.bytesReceived||0,e.audio.packetsReceived=s.packetsReceived||0,e.audio.packetsLost=s.packetsLost||0,e.audio.jitter=s.jitter||0,s.bitrateMean?e.audio.bitrate=s.bitrateMean:e.duration>0&&(e.audio.bitrate=8*e.audio.bytesReceived/e.duration))}if("codec"===s.type){const t=s.mimeType||"";t.includes("video")?(e.video.codec=s.mimeType,e.video.mimeType=s.mimeType):t.includes("audio")&&(e.audio.codec=s.mimeType,e.audio.mimeType=s.mimeType)}if("track"===s.type&&"video"===s.kind&&(!e.video.resolution.width&&s.frameWidth&&(e.video.resolution.width=s.frameWidth||0,e.video.resolution.height=s.frameHeight||0),!e.video.frameRate&&s.framesDecoded&&s.remoteSource)){const t=(Date.now()-this.o)/1e3;t>0&&this.i>0&&(e.video.frameRate=(s.framesDecoded-this.i)/t),this.i=s.framesDecoded}if("candidate-pair"===s.type&&"succeeded"===s.state){e.connection.rtt=s.currentRoundTripTime?1e3*s.currentRoundTripTime:0,e.connection.availableOutgoingBitrate=s.availableOutgoingBitrate||0;const i=s.localCandidateId,n=s.remoteCandidateId;t.forEach(t=>{t.id===i&&"local-candidate"===t.type&&(e.connection.iceLocalCandidateType=t.candidateType,e.connection.protocol=t.protocol,e.connection.localAddress=`${t.address||t.ip}:${t.port}`),t.id===n&&"remote-candidate"===t.type&&(e.connection.iceRemoteCandidateType=t.candidateType,e.connection.remoteAddress=`${t.address||t.ip}:${t.port}`,"relay"===t.candidateType&&(e.connection.turnUsed=1))})}}),e}displaySessionStats(t){const e=(t,e)=>e>0?(t/(e+t)*100).toFixed(2)+"%":"0%",s={Duration:{Value:t.duration.toFixed(2)+"s"},"Video Codec":{Value:t.video.codec||"N/A"},"Video Resolution":{Value:`${t.video.resolution.width}x${t.video.resolution.height}`},"Video Bytes":{Value:this.formatBytes(t.video.bytesReceived)},"Video Packets":{Value:`${t.video.packetsReceived.toLocaleString()} recv, ${t.video.packetsLost.toLocaleString()} lost (${e(t.video.packetsLost,t.video.packetsReceived)})`},"Video Frames":{Value:`${t.video.framesReceived.toLocaleString()} recv, ${t.video.framesDecoded.toLocaleString()} decoded, ${t.video.framesDropped.toLocaleString()} dropped`},"Video Rate":{Value:`${t.video.frameRate.toFixed(2)} fps, ${(t.video.bitrate/1e3).toFixed(2)} kbps, jitter ${(1e3*t.video.jitter).toFixed(2)}ms`},"Video Recovery":{Value:`${t.video.pliCount.toLocaleString()} PLI, ${t.video.nackCount.toLocaleString()} NACK`},"Audio Codec":{Value:t.audio.codec||"N/A"},"Audio Bytes":{Value:this.formatBytes(t.audio.bytesReceived)},"Audio Packets":{Value:`${t.audio.packetsReceived.toLocaleString()} recv, ${t.audio.packetsLost.toLocaleString()} lost (${e(t.audio.packetsLost,t.audio.packetsReceived)})`},"Audio Rate":{Value:`${(t.audio.bitrate/1e3).toFixed(2)} kbps, jitter ${(1e3*t.audio.jitter).toFixed(2)}ms`},"ICE Candidates":{Value:`local: ${t.connection.iceLocalCandidateType||"N/A"}, remote: ${t.connection.iceRemoteCandidateType||"N/A"}`},TURN:{Value:t.connection.turnUsed?"YES (relay)":"NO (direct)"},Protocol:{Value:t.connection.protocol||"N/A"},RTT:{Value:t.connection.rtt.toFixed(2)+"ms"},Bandwidth:{Value:(t.connection.availableOutgoingBitrate/1e3).toFixed(2)+" kbps"}};t.connection.localAddress&&(s["Local Address"]={Value:t.connection.localAddress}),t.connection.remoteAddress&&(s["Remote Address"]={Value:t.connection.remoteAddress}),this.log.info("Session Statistics",s,{format:"table"}),this.ctx.host.dispatchEvent(new CustomEvent("session-stats",{detail:t}))}formatBytes(t){if(0===t)return"0 B";const e=Math.floor(Math.log(t)/Math.log(1024));return`${(t/Math.pow(1024,e)).toFixed(2)} ${["B","KB","MB","GB"][e]}`}reportTiming(t){const e=this.ctx.performanceMetrics,s=e.sessionStarted;if(!s)return;let i;if("session-ready"===t&&e.sessionReady)i=e.sessionReady-s;else{if("first-frame"!==t||!e.firstFrame)return;i=e.firstFrame-s}const n={};e.tokenExchangeStart&&e.tokenExchangeComplete&&(n.token_exchange_ms=e.tokenExchangeComplete-e.tokenExchangeStart),e.sessionWsConnectStart&&e.sessionWsConnectComplete&&(n.ws_connect_ms=e.sessionWsConnectComplete-e.sessionWsConnectStart),e.sessionReady&&(n.session_ready_ms=e.sessionReady-s),e.jobRequested&&(n.job_requested_ms=e.jobRequested-s),e.offerReceived&&(n.offer_received_ms=e.offerReceived-s),e.answerSent&&(n.answer_sent_ms=e.answerSent-s),e.iceConnected&&(n.ice_connected_ms=e.iceConnected-s),e.firstTrack&&(n.first_track_ms=e.firstTrack-s),e.firstFrame&&(n.first_frame_ms=e.firstFrame-s),e.jobRequested&&e.firstFrame&&(n.job_to_first_frame_ms=e.firstFrame-e.jobRequested),this.ctx.sendSessionMessage({type:"report-timing",milestone:t,duration_ms:Math.round(i),breakdown:n})}}E.LATENCY_HISTORY_SIZE=60;const R={"low-latency":{minBitrate:5e3,startBitrate:1e4,maxBitrate:1e5,conferenceFlag:1,audioPtime:10,jitterBufferMs:0},balanced:{minBitrate:3e3,startBitrate:8e3,maxBitrate:1e5,conferenceFlag:1,audioPtime:null,jitterBufferMs:30},quality:{minBitrate:1e3,startBitrate:5e3,maxBitrate:15e4,conferenceFlag:0,audioPtime:null,jitterBufferMs:50}};class T{constructor(t,e){this.ctx=t,this.log=e,this.peerConnection=null,this.dataChannel=null,this.microphoneStream=null,this.h=0,this.l=0,this.videoTracks=new Map,this.activeVideoTrackId=null,this.u=null,this.p=0,this.iceCandidateBatch=[],this.iceBatchTimer=null,this.turnErrorCount=0,this.stunErrorCount=0,this.m=0,this.v=null,this.k=null,this.C=0,this.S=0,this.R=[],this.T=0,this.$=0,this.M=null,this.I=0,this.P=null,this.A=0,this.D=null,this._=0,this.V=null,this.audioElement=document.createElement("audio"),this.audioElement.autoplay=1,this.audioElement.controls=0,this.audioElement.muted=this.ctx.config.mute,this.audioElement.style.display="none"}getPeerConnection(){return this.peerConnection}getDataChannel(){return this.dataChannel}getAudioElement(){return this.audioElement}getVideoTracks(){return this.videoTracks}getActiveVideoTrackId(){return this.activeVideoTrackId}getPcGeneration(){return this.m}resetResilienceState(){this.F(),this.B(),this.C=0,this.R=[],this.S=0,this.m=0,this.M=null,this.T=0,this.$=0,this.I=0,this.D=null,this._=0}onWorkerLeft(){this.log.info("Worker left — immediately closing PeerConnection"),this.F(),this.B(),this.A=0,this.I=0,this.C=0,this.m++,this.peerConnection&&(this.peerConnection.onconnectionstatechange=null,this.peerConnection.oniceconnectionstatechange=null,this.peerConnection.onicecandidate=null,this.peerConnection.ontrack=null,this.peerConnection.onicecandidateerror=null,this.peerConnection.ondatachannel=null,this.peerConnection.onicegatheringstatechange=null),this.closePeerConnection(),this.videoTracks.forEach(({track:t})=>t.stop()),this.videoTracks.clear(),this.activeVideoTrackId=null;const t=this.ctx.getVideoElement();t.srcObject&&(t.srcObject.getTracks().forEach(t=>t.stop()),t.srcObject=null),this.u&&(this.u.getTracks().forEach(t=>t.stop()),this.u=null)}async W(){if(!this.peerConnection)return 0;try{const t=await this.peerConnection.getStats();let e=0;return t.forEach(s=>{if("inbound-rtp"===s.type&&(e+=s.bytesReceived||0),"candidate-pair"===s.type&&"succeeded"===s.state&&s.nominated){const e=s.remoteCandidateId;t.forEach(t=>{t.id===e&&"remote-candidate"===t.type&&(this.$="relay"===t.candidateType)})}}),e}catch{return 0}}async U(){return await this.W()>this.T}async q(t){const e=this.peerConnection;if(e)try{const s=await e.getStats();if(this.m!==t)return;let i=null,n=null,r=null;if(s.forEach(t=>{"candidate-pair"===t.type&&"succeeded"===t.state&&t.nominated&&(r=t)}),!r)return;s.forEach(t=>{t.id===r.localCandidateId&&"local-candidate"===t.type&&(i=t),t.id===r.remoteCandidateId&&"remote-candidate"===t.type&&(n=t)});const a="relay"===n?.candidateType||"relay"===i?.candidateType;this.$=a;const o={turnUsed:a,protocol:i?.protocol||null,localCandidateType:i?.candidateType||null,remoteCandidateType:n?.candidateType||null,localAddress:i?`${i.address||i.ip}:${i.port}`:null,remoteAddress:n?`${n.address||n.ip}:${n.port}`:null,rtt:r.currentRoundTripTime?1e3*r.currentRoundTripTime:null,generation:t};this.log.info(`Transport selected: ${a?"TURN (relay)":"direct"}, local=${o.localCandidateType}/${o.protocol}, remote=${o.remoteCandidateType}`),this.ctx.host.dispatchEvent(new CustomEvent("transport-selected",{detail:o}))}catch(t){this.log.debug("Failed to detect selected transport",t)}}L(){const t=this.ctx.getSessionWs();if(!t||t.readyState!==WebSocket.OPEN)return{allowed:0,reason:"session-ws-not-open"};const e=this.ctx.config.reconnectMode;if("recover"!==e&&"always"!==e)return{allowed:0,reason:"reconnect-mode-"+e};if(this.C)return{allowed:0,reason:"recovery-already-in-flight"};const s=Date.now(),i=s-this.S;if(this.S>0&&i<T.RECOVERY_MIN_INTERVAL_MS)return{allowed:0,reason:`min-interval-not-elapsed (${i}ms < ${T.RECOVERY_MIN_INTERVAL_MS}ms)`};if(this.R=this.R.filter(t=>s-t<T.RECOVERY_WINDOW_MS),this.R.length>=T.MAX_RECOVERY_IN_WINDOW)return{allowed:0,reason:`max-recoveries-in-window (${this.R.length}/${T.MAX_RECOVERY_IN_WINDOW} in ${T.RECOVERY_WINDOW_MS}ms)`};const n=this.ctx.getIceServers();return n&&0!==n.length?{allowed:1}:{allowed:0,reason:"no-ice-servers"}}F(){this.v&&(clearTimeout(this.v),this.v=null),this.k&&(clearInterval(this.k),this.k=null)}B(){this.P&&(clearTimeout(this.P),this.P=null),this.A=0}async N(t){const e=this.peerConnection;if(!e||!e.remoteDescription)return this.log.warn("Cannot ICE restart: no peer connection or remote description"),0;if(this.A)return this.log.debug("ICE restart already in progress"),0;if(this.I>=T.ICE_RESTART_MAX_ATTEMPTS)return this.log.info(`ICE restart exhausted (${this.I} attempts), falling back to full recovery`),0;const s=this.ctx.getSessionWs();if(!s||s.readyState!==WebSocket.OPEN)return this.log.warn("Cannot ICE restart: session WebSocket not open"),0;this.A=1,this.I++;const i=this.m;this.log.info(`Attempting ICE restart ${this.I}/${T.ICE_RESTART_MAX_ATTEMPTS}, reason=${t}, gen=${i}`),this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-started",{detail:{attempt:this.I,reason:t,generation:i}}));try{const s=await e.createOffer({iceRestart:1});if(this.m!==i)return this.A=0,0;const n=this.mungeSDP(s.sdp),r={...s,sdp:n};await e.setLocalDescription(r);const a=this.ctx.getPeerAgentId();return this.ctx.sendSessionMessage({type:"webrtc-signaling",signal_type:"offer",payload:{sdp:r.sdp,type:r.type},ice_restart:1,...a&&{target_agent_id:a}}),this.P=setTimeout(()=>{this.P=null,this.m===i&&(this.log.warn(`ICE restart timeout expired (attempt ${this.I})`),this.A=0,this.I<T.ICE_RESTART_MAX_ATTEMPTS?this.N("timeout-retry-"+this.I):(this.log.info("ICE restart failed after max attempts, falling back to full media recovery"),this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-failed",{detail:{attempts:this.I,reason:t,generation:i}})),this.O("ice-restart-exhausted:"+t)))},T.ICE_RESTART_TIMEOUT_MS),1}catch(t){return this.log.error("ICE restart failed",t),this.A=0,this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-error",{detail:{error:t instanceof Error?t.message:t+"",attempt:this.I,generation:i}})),0}}async j(){const t=this.ctx.getIceServers().filter(t=>(Array.isArray(t.urls)?t.urls:[t.urls]).some(t=>t.startsWith("turn:")||t.startsWith("turns:")));if(0===t.length)return this.log.debug("No TURN servers configured, skipping health check"),1;const e=Date.now();if(null!==this.D&&e-this._<T.TURN_HEALTH_CHECK_CACHE_MS)return this.log.debug("Using cached TURN health: "+this.D),this.D;this.log.info("Probing TURN server health...");const s=performance.now();return new Promise(e=>{let i=0,n=0;const r=new RTCPeerConnection({iceServers:t,iceTransportPolicy:"relay"}),a=()=>{i||(i=1,r.close())},o=setTimeout(()=>{i||(this.log.warn(`TURN health check timed out after ${T.TURN_HEALTH_CHECK_TIMEOUT_MS}ms`),this.D=0,this._=Date.now(),a(),e(0))},T.TURN_HEALTH_CHECK_TIMEOUT_MS);r.onicecandidate=t=>{if(t.candidate){if(("relay"===t.candidate.type||t.candidate.candidate.includes("typ relay"))&&!n){n=1;const t=(performance.now()-s).toFixed(2);this.log.info(`TURN health check passed in ${t}ms`),this.D=1,this._=Date.now(),clearTimeout(o),a(),e(1)}}else n||i||(this.log.warn("TURN health check failed: no relay candidates gathered"),this.D=0,this._=Date.now(),clearTimeout(o),a(),e(0))},r.onicecandidateerror=t=>{this.log.debug("TURN probe ICE error",{errorCode:t.errorCode,errorText:t.errorText,url:t.url})},r.createDataChannel("turn-probe"),r.createOffer().then(t=>r.setLocalDescription(t)).catch(t=>{this.log.debug("TURN probe offer failed",t),i||(this.D=0,this._=Date.now(),clearTimeout(o),a(),e(0))})})}getTurnHealthy(){return this.D}async H(t){this.F(),this.T=await this.W();const e=this.ctx.config.disconnectGraceMs,s=this.m;this.log.info(`Starting disconnect grace (${e}ms). reason=${t}, gen=${s}, bytesSnapshot=${this.T}, turnUsed=${this.$}`),this.ctx.setStreamState("interrupted",{reason:t,generation:s,graceMs:e,bytesSnapshot:this.T,turnUsed:this.$}),this.k=setInterval(async()=>{if(this.m!==s)return void this.F();const t=await this.U();this.log.debug(`Grace poll: flowing=${t}, gen=${s}`),t&&(this.log.info("RTP still flowing during grace - cancelling recovery"),this.F(),this.ctx.setStreamState("streaming",{reason:"grace-resolved",generation:s}))},T.DISCONNECT_POLL_INTERVAL_MS),this.v=setTimeout(async()=>{if(this.k&&(clearInterval(this.k),this.k=null),this.v=null,this.m===s){if(await this.U())return this.log.info("RTP flowing at grace expiry - skipping recovery"),void this.ctx.setStreamState("streaming",{reason:"grace-resolved",generation:s});this.log.info("Grace expired, RTP not flowing - attempting ICE restart first"),await this.N("grace-expired:"+t)||this.O("grace-expired:"+t)}},e)}async O(t){this.F();const e=this.L();if(!e.allowed){if(this.log.info(`Recovery blocked: ${e.reason} (reason=${t})`),e.reason?.startsWith("max-recoveries-in-window")){this.ctx.setStreamState("failed",{reason:"recovery-exhausted",blockReason:e.reason,recoveryTimestamps:[...this.R],turnUsed:this.$});const s=new CustomEvent("media-recovery-exhausted",{cancelable:1,detail:{reason:t,blockReason:e.reason,recoveryTimestamps:[...this.R],turnUsed:this.$}});this.ctx.host.dispatchEvent(s),s.defaultPrevented||(this.log.warn("Media recovery exhausted - leaving session"),this.ctx.leaveSession("media-recovery-exhausted"))}return}this.$&&(await this.j()||(this.log.warn("TURN health check failed, recovery may be slow or unsuccessful"),this.ctx.host.dispatchEvent(new CustomEvent("turn-unhealthy",{detail:{reason:t,lastKnownTurnUsed:this.$}})))),this.B(),this.I=0,this.C=1,this.S=Date.now(),this.R.push(this.S);const s=++this.m;this.log.info(`Starting recovery gen=${s}, reason=${t}, turnUsed=${this.$}, recoveriesInWindow=${this.R.length}`),this.ctx.setStreamState("recovering",{reason:t,generation:s,turnUsed:this.$,recoveriesInWindow:this.R.length});try{this.ctx.getMetricsSessionStartTime()&&this.peerConnection&&this.ctx.collectAndDisplaySessionStats(),this.peerConnection&&(this.peerConnection.onconnectionstatechange=null,this.peerConnection.oniceconnectionstatechange=null,this.peerConnection.onicecandidate=null,this.peerConnection.ontrack=null,this.peerConnection.onicecandidateerror=null,this.peerConnection.ondatachannel=null,this.peerConnection.onicegatheringstatechange=null),this.log.info(`Closing old PeerConnection (gen=${s-1})`),this.closePeerConnection(),this.videoTracks.forEach(({track:t})=>t.stop()),this.videoTracks.clear(),this.activeVideoTrackId=null;const t=this.ctx.getVideoElement();t.srcObject&&(t.srcObject.getTracks().forEach(t=>t.stop()),t.srcObject=null),this.u&&(this.u.getTracks().forEach(t=>t.stop()),this.u=null);const e=this.ctx.getIceServers();this.log.info(`Creating new PeerConnection (gen=${s})`),this.createPeerConnection({iceServers:e});const i=this.ctx.getSubscribedStreamerId();i?(this.log.info("Re-asserting streamer preference: "+i),this.ctx.sendSessionMessage({type:"update-presence",preferred_streamer_id:i})):this.log.warn("No subscribed streamer ID for recovery"),this.log.info(`Recovery initiated gen=${s}, waiting for server offer`)}catch(e){this.log.error("Recovery failed gen="+s,e),this.ctx.host.dispatchEvent(new CustomEvent("media-recovery-error",{detail:{error:e instanceof Error?e.message:e+"",generation:s,reason:t}}))}finally{this.C=0}}sendRawBinary(t){if(this.dataChannel&&"open"===this.dataChannel.readyState){const e=new ArrayBuffer(t.length);new Uint8Array(e).set(t),this.dataChannel.send(e)}}sendKeyDownSimple(t,e=0){const s=new Uint8Array([2,t,e?1:0]);this.sendRawBinary(s)}sendKeyUpSimple(t){const e=new Uint8Array([3,t,0]);this.sendRawBinary(e)}sendMouseDownSimple(t,e,s=0){const i=new Uint8Array(9);i[0]=4,i[1]=255&t,i[2]=t>>8&255,i[3]=255&e,i[4]=e>>8&255,i[5]=s,this.sendRawBinary(i)}sendMouseUpSimple(t,e,s=0){const i=new Uint8Array(9);i[0]=5,i[1]=255&t,i[2]=t>>8&255,i[3]=255&e,i[4]=e>>8&255,i[5]=s,this.sendRawBinary(i)}sendMouseMoveSimple(t,e){const s=new Uint8Array(9);s[0]=6,s[1]=255&t,s[2]=t>>8&255,s[3]=255&e,s[4]=e>>8&255,s[5]=0,this.sendRawBinary(s)}sendMouseWheelSimple(t){const e=new Uint8Array(4);e[0]=7,e[1]=255&t,e[2]=t>>8&255,this.sendRawBinary(e)}sendToStreamer(t,e=[]){const s=a.encode(t,e);this.dataChannel&&"open"===this.dataChannel.readyState?this.dataChannel.send(s):this.log.warn(`Cannot send ${t}: data channel not open`),this.ctx.host.dispatchEvent(new CustomEvent("streamer-message",{detail:{type:t,data:e}}))}createPeerConnection(t){this.C||this.ctx.setStreamState("starting",{generation:this.m}),this.warmVideoElement(),this.isAndroid()&&!this.p&&this.verifyCodecAvailability().then(t=>{t?this.p=1:this.ctx.host.dispatchEvent(new CustomEvent("codec-verification-failed"))});const e={iceServers:t?.iceServers||[{urls:"stun:stun.cloudflare.com:3478"}],iceCandidatePoolSize:10,iceTransportPolicy:"all"};(t?.forceRelay||this.ctx.config.forceRelay)&&(e.iceTransportPolicy="relay"),this.peerConnection=new RTCPeerConnection(e),this.turnErrorCount=0,this.stunErrorCount=0,this.ctx.performanceMetrics.peerConnectionCreated||(this.ctx.performanceMetrics.peerConnectionCreated=performance.now()),this.peerConnection.onicecandidate=t=>{if(t.candidate)this.iceCandidateBatch.push({candidate:t.candidate.candidate,sdpMid:t.candidate.sdpMid,sdpMLineIndex:t.candidate.sdpMLineIndex,usernameFragment:t.candidate.usernameFragment}),this.iceBatchTimer&&clearTimeout(this.iceBatchTimer),this.iceBatchTimer=setTimeout(()=>{if(this.iceCandidateBatch.length>0){this.log.debug(`Sending ${this.iceCandidateBatch.length} ICE candidates in batch`);const t=this.ctx.getPeerAgentId();this.iceCandidateBatch.forEach(e=>{const s=this.ctx.getSessionWs();s&&s.readyState===WebSocket.OPEN?this.ctx.sendSessionMessage({type:"webrtc-signaling",signal_type:"iceCandidate",payload:e,...t&&{target_agent_id:t}}):this.ctx.host.dispatchEvent(new CustomEvent("ice-candidate",{detail:e}))}),this.iceCandidateBatch=[]}},this.ctx.config.iceBatchDelay);else if(this.iceBatchTimer&&clearTimeout(this.iceBatchTimer),this.iceCandidateBatch.length>0){this.log.debug(`Sending final ${this.iceCandidateBatch.length} ICE candidates`);const t=this.ctx.getPeerAgentId();this.iceCandidateBatch.forEach(e=>{const s=this.ctx.getSessionWs();s&&s.readyState===WebSocket.OPEN?this.ctx.sendSessionMessage({type:"webrtc-signaling",signal_type:"iceCandidate",payload:e,...t&&{target_agent_id:t}}):this.ctx.host.dispatchEvent(new CustomEvent("ice-candidate",{detail:e}))}),this.iceCandidateBatch=[]}},this.peerConnection.ontrack=t=>{this.handleTrack(t)},this.peerConnection.onconnectionstatechange=()=>{const t=this.peerConnection;if(!t)return;const e=t.connectionState,s=this.m;if(this.log.info(`Connection state: ${e} (gen=${s})`),this.ctx.host.dispatchEvent(new CustomEvent("connection-state-change",{detail:e})),"connected"===e){if(this.F(),this.B(),this.ctx.setStreamState("streaming",{reason:"connected",generation:s}),this.I>0&&this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-success",{detail:{attempts:this.I,generation:s}})),this.I=0,!this.ctx.performanceMetrics.connectionComplete){if(this.ctx.performanceMetrics.connectionComplete=performance.now(),this.ctx.performanceMetrics.subscribeStart){const t=this.ctx.performanceMetrics.connectionComplete-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to connection complete: ${t.toFixed(2)}ms`)}this.ctx.setMetricsSessionStartTime(Date.now()),this.ctx.sendSessionMessage({type:"report-event",event_type:"webrtc.connected",data:{generation:this.m}})}}else"failed"===e?(this.log.error(`Connection failed (gen=${s})`),this.ctx.getMetricsSessionStartTime()&&this.ctx.collectAndDisplaySessionStats(),this.N("connection-failed").then(t=>{t||this.O("connection-failed")})):"disconnected"===e?(this.log.warn(`Connection disconnected (gen=${s})`),this.H("connection-disconnected")):"closed"===e&&(this.log.info(`Connection closed (gen=${s})`),this.F(),this.ctx.getMetricsSessionStartTime()&&this.ctx.collectAndDisplaySessionStats())},this.peerConnection.oniceconnectionstatechange=()=>{const t=this.peerConnection;if(!t)return;const e=t.iceConnectionState,s=this.M;this.M=e;const i=this.m;if(this.log.info(`ICE connection state: ${s||"null"} -> ${e} (gen=${i})`),"connected"===e||"completed"===e){if(this.F(),this.B(),this.I>0&&(this.log.info(`ICE restart succeeded after ${this.I} attempt(s)`),this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-success",{detail:{attempts:this.I,generation:i}}))),this.I=0,!this.ctx.performanceMetrics.iceConnected&&(this.ctx.performanceMetrics.iceConnected=performance.now(),this.ctx.performanceMetrics.subscribeStart)){const t=this.ctx.performanceMetrics.iceConnected-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to ICE connected: ${t.toFixed(2)}ms`)}this.q(i)}else"failed"===e?(this.log.error(`ICE connection failed (gen=${i})`),this.N("ice-failed").then(t=>{t||this.O("ice-failed")})):"disconnected"===e&&(this.log.warn(`ICE connection disconnected (gen=${i})`),this.v||this.H("ice-disconnected"));this.ctx.host.dispatchEvent(new CustomEvent("ice-connection-state-change",{detail:{prevState:s,newState:e,generation:i}}))},this.peerConnection.onicecandidateerror=t=>{const e={errorCode:t.errorCode,errorText:t.errorText,url:t.url,address:t.address,port:t.port},s=t.errorText?.toLowerCase()||"";s.includes("turn")||s.includes("relay")?this.turnErrorCount++:(s.includes("stun")||s.includes("binding"))&&this.stunErrorCount++;const i=this.turnErrorCount+this.stunErrorCount;i<=3?this.log.error("ICE candidate error",e):4===i&&(this.log.warn("Multiple ICE errors detected, suppressing further error logs"),this.log.warn(`STUN errors: ${this.stunErrorCount}, TURN errors: ${this.turnErrorCount}`)),this.ctx.host.dispatchEvent(new CustomEvent("ice-candidate-error",{detail:e}))},this.peerConnection.onicegatheringstatechange=()=>{this.ctx.host.dispatchEvent(new CustomEvent("ice-gathering-state-change",{detail:this.peerConnection.iceGatheringState}))},this.peerConnection.ondatachannel=t=>{this.handleDataChannel(t)};const s=this.peerConnection.addTransceiver("video",{direction:"recvonly"});this.peerConnection.addTransceiver("audio",{direction:"recvonly"}),this.setVideoCodecPreferences(s),t?.useMic&&(this.h=1),t?.forceMonoAudio&&(this.l=1)}closePeerConnection(){this.C||this.ctx.setStreamState("idle"),this.iceBatchTimer&&(clearTimeout(this.iceBatchTimer),this.iceBatchTimer=null),this.iceCandidateBatch=[],this.V&&(this.V.terminate(),this.V=null),this.dataChannel&&(this.dataChannel.onopen=null,this.dataChannel.onclose=null,this.dataChannel.onmessage=null,this.dataChannel.onerror=null,this.dataChannel.close(),this.dataChannel=null),this.peerConnection&&(this.peerConnection.onconnectionstatechange=null,this.peerConnection.oniceconnectionstatechange=null,this.peerConnection.onicecandidate=null,this.peerConnection.ontrack=null,this.peerConnection.onicecandidateerror=null,this.peerConnection.ondatachannel=null,this.peerConnection.onicegatheringstatechange=null,this.peerConnection.onnegotiationneeded=null,this.peerConnection.close(),this.peerConnection=null)}preCreatePeerConnection(t){this.peerConnection?this.log.debug("Peer connection already exists, skipping pre-creation"):(this.log.debug("Pre-creating peer connection for faster setup..."),this.createPeerConnection(t))}isPeerConnectionReady(){return null!==this.peerConnection}warmVideoElement(){const t=this.ctx.getVideoElement();if(!t.srcObject){try{const e=document.createElement("canvas");e.width=2,e.height=2,e.getContext("2d"),this.u=e.captureStream(0);const s=this.u.getVideoTracks()[0];s&&"requestFrame"in s&&s.requestFrame(),t.srcObject=this.u,t.play().catch(()=>{}),this.log.debug("Video pipeline warmed with placeholder stream")}catch(t){this.log.debug("Failed to warm video pipeline",t)}this.warmVideoDecoder()}}warmVideoDecoder(){const t=globalThis.VideoDecoder;if(!t)return;const e=["av01.0.08M.08","vp09.00.10.08","avc1.640028"];for(const s of e)try{const e=new t({output:()=>{},error:()=>{}});return e.configure({codec:s}),e.close(),void this.log.debug("Warmed VideoDecoder for "+s)}catch{}}async verifyCodecAvailability(t=10){const e=["H264","VP9","AV1"];for(let s=0;s<t;s++){const i=new RTCPeerConnection({});try{i.addTransceiver("video");const t=(await i.createOffer()).sdp||"";for(const i of e)if(RegExp("rtpmap:\\d+ "+i,"i").test(t))return this.log.debug(`Codec ${i} verified in SDP (attempt ${s+1})`),i}finally{i.close()}s<t-1&&await new Promise(t=>setTimeout(t,250))}return this.log.warn(`No preferred codecs found in SDP after ${t} attempts`),null}isAndroid(){return/android/i.test(navigator.userAgent)}handleTrack(t){const e=t.track,s=t.streams.length>0?t.streams[0]:new MediaStream([e]);if(!this.ctx.performanceMetrics.firstTrack&&(this.ctx.performanceMetrics.firstTrack=performance.now(),this.ctx.performanceMetrics.subscribeStart)){const t=this.ctx.performanceMetrics.firstTrack-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to first track: ${t.toFixed(2)}ms`)}if("video"===e.kind){const i=(R[this.ctx.config.streamQuality]??R["low-latency"]).jitterBufferMs,n=t.receiver;"jitterBufferTarget"in n&&(n.jitterBufferTarget=i,this.log.debug(`Video jitterBufferTarget set to ${i}ms`)),"playoutDelayHint"in n&&(n.playoutDelayHint=i/1e3,this.log.debug(`Video playoutDelayHint set to ${i/1e3}s`)),this.attachKeyframeTransform(n);const r=e.id;if(this.videoTracks.set(r,{track:e,stream:s}),1===this.videoTracks.size){this.activeVideoTrackId=r;const t=this.ctx.getVideoElement();if(t.srcObject=s,this.u&&(this.u.getTracks().forEach(t=>t.stop()),this.u=null),"function"==typeof t.requestVideoFrameCallback){let e=0;const s=(i,n)=>{if(this.ctx.performanceMetrics.firstFrame||(this.ctx.performanceMetrics.firstFrame=i,this.ctx.performanceMetrics.sessionStarted&&this.log.info(`SESSION START -> FIRST FRAME: ${(i-this.ctx.performanceMetrics.sessionStarted).toFixed(2)}ms`),this.ctx.performanceMetrics.subscribeStart&&this.log.info(`SUBSCRIBE -> FIRST FRAME: ${(i-this.ctx.performanceMetrics.subscribeStart).toFixed(2)}ms`),this.ctx.performanceMetrics.jobRequested&&this.log.info(`JOB REQUEST -> FIRST FRAME: ${(i-this.ctx.performanceMetrics.jobRequested).toFixed(2)}ms`),n.captureTime&&this.log.info(`End-to-end latency: ${(i-n.captureTime).toFixed(1)}ms`),n.receiveTime&&n.presentationTime&&this.log.info(`Local decode latency: ${(n.presentationTime-n.receiveTime).toFixed(1)}ms`),this.ctx.logPerformanceSummary(),this.ctx.reportTiming("first-frame")),e>0){const t=n.presentedFrames-e-1;t>=3&&(this.log.info(`Dropped ${t} frame(s)`),this.ctx.host.dispatchEvent(new CustomEvent("frames-dropped",{detail:{count:t,timestamp:i}})))}e=n.presentedFrames,t.requestVideoFrameCallback(s)};t.requestVideoFrameCallback(s)}else{const e=()=>{this.ctx.performanceMetrics.firstFrame||(this.ctx.performanceMetrics.firstFrame=performance.now(),this.ctx.performanceMetrics.sessionStarted&&this.log.info(`SESSION START -> FIRST FRAME: ${(performance.now()-this.ctx.performanceMetrics.sessionStarted).toFixed(2)}ms`),this.ctx.performanceMetrics.subscribeStart&&this.log.info(`SUBSCRIBE -> FIRST FRAME: ${(performance.now()-this.ctx.performanceMetrics.subscribeStart).toFixed(2)}ms`),this.ctx.performanceMetrics.jobRequested&&this.log.info(`JOB REQUEST -> FIRST FRAME: ${(performance.now()-this.ctx.performanceMetrics.jobRequested).toFixed(2)}ms`),this.ctx.logPerformanceSummary(),this.ctx.reportTiming("first-frame")),t.removeEventListener("loadeddata",e)};t.addEventListener("loadeddata",e)}t.play().catch(t=>{this.log.warn("Autoplay failed",t),this.ctx.setAutoplayBlocked(1),this.ctx.setJobState("ready")})}this.ctx.host.dispatchEvent(new CustomEvent("video-track-received",{detail:{track:e,stream:s,trackId:r,label:e.label,isActive:r===this.activeVideoTrackId,totalTracks:this.videoTracks.size}})),this.ctx.detectXRStream(),e.onended=()=>{if(this.videoTracks.delete(r),this.ctx.host.dispatchEvent(new CustomEvent("video-track-removed",{detail:{trackId:r,label:e.label}})),r===this.activeVideoTrackId&&this.videoTracks.size>0){const t=this.videoTracks.keys().next().value;this.switchVideoTrack(t)}}}else if("audio"===e.kind){if(this.ctx.getVideoElement().srcObject===s)return;this.audioElement.srcObject=s,this.audioElement.play().catch(t=>{this.log.warn("Audio autoplay failed",t)}),this.ctx.host.dispatchEvent(new CustomEvent("audio-track-received",{detail:{track:e,stream:s}}))}}switchVideoTrack(t){const e=this.videoTracks.get(t);if(!e)return void this.log.warn(`Video track ${t} not found`);this.activeVideoTrackId=t;const s=this.ctx.getVideoElement();s.srcObject=e.stream,s.play().catch(t=>{this.log.warn("Failed to play switched video track",t)}),this.ctx.host.dispatchEvent(new CustomEvent("video-track-switched",{detail:{trackId:t,label:e.track.label}}))}setVideoCodecPreferences(t){if("function"!=typeof t.setCodecPreferences)return void this.log.debug("setCodecPreferences not supported, relying on SDP codec ordering");const e=RTCRtpReceiver.getCapabilities?.("video");if(!e)return void this.log.debug("RTCRtpReceiver.getCapabilities not available");const s=["video/H265","video/H264","video/AV1","video/VP9"],i=[...e.codecs].sort((t,e)=>{const i=s.indexOf(t.mimeType),n=s.indexOf(e.mimeType),r=i>=0?i:s.length,a=n>=0?n:s.length;return r!==a?r-a:"video/H264"===t.mimeType&&"video/H264"===e.mimeType?(/profile-level-id=64/.test(t.sdpFmtpLine||"")?0:1)-(/profile-level-id=64/.test(e.sdpFmtpLine||"")?0:1):0});try{t.setCodecPreferences(i);const e=i.filter(t=>s.includes(t.mimeType)).map(t=>t.mimeType.replace("video/",""));this.log.info("Video codec preferences set",{order:e})}catch(t){this.log.warn("Failed to set codec preferences",t)}}mungeSDP(t){const e=this.ctx.config,s=R[e.streamQuality]??R["low-latency"],i=e.videoBitrateMin??s.minBitrate,n=e.videoBitrateStart??s.startBitrate,r=e.videoBitrateMax??s.maxBitrate;let a=t.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm,`$1;x-google-min-bitrate=${i};x-google-start-bitrate=${n};x-google-max-bitrate=${r}\r\n`);s.conferenceFlag&&(a=a.replace(/(m=video[^\r\n]*\r\n)/g,"$1a=x-google-flag:conference\r\n")),a=a.replace(/b=(AS|TIAS):[^\r\n]*\r\n/g,""),null!==s.audioPtime&&(a=a.replace(/(m=audio[^\r\n]*\r\n)/g,`$1a=ptime:${s.audioPtime}\r\na=maxptime:${s.audioPtime}\r\n`));const o=a.match(/a=rtpmap:(\d+) opus\/48000/);if(o){const t=o[1],e=RegExp(`(a=fmtp:${t} .+?)(\\r\\n)`);let s="maxaveragebitrate=510000";this.h&&(s+=";sprop-maxcapturerate=48000"),s+=this.l?";stereo=0":";stereo=1",s+=";useinbandfec=1",a=e.test(a)?a.replace(e,`$1;${s}$2`):a.replace(RegExp(`(a=rtpmap:${t} opus/48000/2\\r\\n)`),`$1a=fmtp:${t} ${s}\r\n`)}return a}async createOffer(){if(!this.peerConnection)throw Error("Peer connection not created");const t=await this.peerConnection.createOffer();return t.sdp=this.mungeSDP(t.sdp),await this.peerConnection.setLocalDescription(t),t}async handleOffer(t){if(!this.ctx.performanceMetrics.offerReceived&&(this.ctx.performanceMetrics.offerReceived=performance.now(),this.ctx.performanceMetrics.subscribeStart)){const t=this.ctx.performanceMetrics.offerReceived-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to receive offer: ${t.toFixed(2)}ms`)}this.peerConnection?this.log.debug("Using pre-created peer connection"):(this.log.debug("Peer connection not pre-created, creating now..."),this.createPeerConnection()),await this.peerConnection.setRemoteDescription(t);for(const t of this.peerConnection.getTransceivers())"video"===t.receiver?.track?.kind&&this.setVideoCodecPreferences(t);const e=await this.peerConnection.createAnswer();if(e.sdp=this.mungeSDP(e.sdp),await this.peerConnection.setLocalDescription(e),this.ctx.performanceMetrics.answerSent=performance.now(),this.ctx.performanceMetrics.subscribeStart){const t=this.ctx.performanceMetrics.answerSent-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to create answer: ${t.toFixed(2)}ms`)}return this.ctx.host.dispatchEvent(new CustomEvent("answer-created",{detail:e})),e}async handleAnswer(t){if(!this.peerConnection)throw Error("Peer connection not created");await this.peerConnection.setRemoteDescription(t)}async addIceCandidate(t){if(!this.peerConnection)throw Error("Peer connection not created");await this.peerConnection.addIceCandidate(t)}createDataChannel(t="cirrus"){if(!this.peerConnection)throw Error("Peer connection not created");this.dataChannel=this.peerConnection.createDataChannel(t,{ordered:1}),this.setupDataChannelHandlers()}handleDataChannel(t){this.dataChannel=t.channel,this.setupDataChannelHandlers()}setupDataChannelHandlers(){this.dataChannel&&(this.dataChannel.binaryType="arraybuffer",this.dataChannel.onopen=()=>{this.log.info("Data channel opened"),this.ctx.host.dispatchEvent(new CustomEvent("data-channel-open")),this.sendToStreamer("RequestInitialSettings",[])},this.dataChannel.onclose=()=>{this.log.info("Data channel closed"),this.ctx.host.dispatchEvent(new CustomEvent("data-channel-close"))},this.dataChannel.onerror=t=>{this.log.error("Data channel error",t),this.ctx.host.dispatchEvent(new CustomEvent("data-channel-error",{detail:t}))},this.dataChannel.onmessage=t=>{this.handleDataChannelMessage(t)})}handleDataChannelMessage(t){const e=a.decode(t.data);switch(this.log.debug("Received from UE",{type:e.type,payload:e.payload}),this.ctx.host.dispatchEvent(new CustomEvent("message-from-streamer",{detail:e})),e.type){case"QualityControlOwnership":this.ctx.host.dispatchEvent(new CustomEvent("quality-control-ownership",{detail:e.payload}));break;case"InputControlOwnership":this.ctx.host.dispatchEvent(new CustomEvent("input-control-ownership",{detail:e.payload}));break;case"Response":case"Command":this.ctx.host.dispatchEvent(new CustomEvent("ue-command-response",{detail:e.payload}));break;case"VideoEncoderAvgQP":this.ctx.host.dispatchEvent(new CustomEvent("video-encoder-qp",{detail:e.payload}));break;case"LatencyTest":case"DataChannelLatencyTest":e.payload&&"object"==typeof e.payload&&this.ctx.handleLatencyTestResult(e.payload),this.sendToStreamer("LatencyTest",[JSON.stringify(e.payload)]);break;case"InitialSettings":this.ctx.host.dispatchEvent(new CustomEvent("initial-settings",{detail:e.payload}));break;case"GamepadResponse":this.ctx.host.dispatchEvent(new CustomEvent("gamepad-response",{detail:e.payload}));break;case"FileExtension":this.ctx.handleFileExtension(t.data);break;case"FileMimeType":this.ctx.handleFileMimeType(t.data);break;case"FileContents":this.ctx.handleFileContents(t.data);break;case"FreezeFrame":this.ctx.host.dispatchEvent(new CustomEvent("freeze-frame",{detail:e.payload}));break;case"UnfreezeFrame":this.ctx.host.dispatchEvent(new CustomEvent("unfreeze-frame",{detail:e.payload}));break;case"TestEcho":this.ctx.host.dispatchEvent(new CustomEvent("test-echo",{detail:e.payload}));break;case"Protocol":this.ctx.host.dispatchEvent(new CustomEvent("protocol",{detail:e.payload}))}}requestIFrame(){this.sendToStreamer("IFrameRequest",[]),this.requestKeyFrame()}requestKeyFrame(){this.V&&this.V.postMessage({type:"keyframe"})}attachKeyframeTransform(t){if("function"==typeof globalThis.RTCRtpScriptTransform)try{if(!this.V){const t=new Blob([T.KEYFRAME_WORKER_SRC],{type:"application/javascript"});this.V=new Worker(URL.createObjectURL(t))}t.transform=new globalThis.RTCRtpScriptTransform(this.V),this.log.debug("Keyframe encoded transform attached to video receiver"),this.V.postMessage({type:"keyframe"})}catch(t){this.log.warn("Failed to attach keyframe transform",t)}else this.log.debug("RTCRtpScriptTransform not available, skipping keyframe transform")}disconnectMedia(){this.F(),this.ctx.setStreamState("idle");const t=this.ctx.getVideoElement();t.srcObject&&(t.srcObject.getTracks().forEach(t=>t.stop()),t.srcObject=null),this.videoTracks.forEach(({track:t})=>t.stop()),this.videoTracks.clear(),this.activeVideoTrackId=null,this.audioElement.srcObject&&(this.audioElement.srcObject.getTracks().forEach(t=>t.stop()),this.audioElement.srcObject=null),this.microphoneStream&&(this.microphoneStream.getTracks().forEach(t=>t.stop()),this.microphoneStream=null),this.closePeerConnection()}async connect(t){this.createPeerConnection(t);const e=await this.createOffer();return this.ctx.host.dispatchEvent(new CustomEvent("offer-created",{detail:e})),e}async switchStreamer(t){return this.disconnectMedia(),await new Promise(t=>setTimeout(t,100)),await this.connect(t)}async requestMicrophone(){if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia)return this.log.warn("Microphone unavailable: mediaDevices API not supported"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-unavailable",{detail:{reason:"api-not-supported"}})),0;const t=await this.ctx.checkPermission("microphone");if("denied"===t)return this.log.warn("Microphone permission previously denied"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-permission-denied",{detail:{reason:"previously-denied",state:t}})),0;const e={autoGainControl:0,channelCount:1,echoCancellation:0,latency:0,noiseSuppression:0,sampleRate:48e3,sampleSize:16};try{if(this.microphoneStream=await navigator.mediaDevices.getUserMedia({video:0,audio:e}),this.peerConnection){const t=this.microphoneStream.getAudioTracks()[0],e=this.peerConnection.getSenders().find(t=>"audio"===t.track?.kind);e?await e.replaceTrack(t):this.peerConnection.addTrack(t,this.microphoneStream);const s=this.peerConnection.getTransceivers().find(t=>"audio"===t.receiver.track.kind);s&&(s.direction="sendrecv")}return this.h=1,this.ctx.host.dispatchEvent(new CustomEvent("microphone-enabled")),this.ctx.watchPermission("microphone"),1}catch(t){const e=t?.name||"UnknownError",s=t?.message||"Unknown error";return"NotAllowedError"===e||"PermissionDeniedError"===e?(this.log.warn("Microphone permission denied by user"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-permission-denied",{detail:{reason:"user-denied",error:e,message:s}}))):"NotFoundError"===e||"DevicesNotFoundError"===e?(this.log.warn("No microphone device found"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-unavailable",{detail:{reason:"no-device",error:e,message:s}}))):"NotReadableError"===e||"TrackStartError"===e?(this.log.warn("Microphone not readable (may be in use by another application)"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-unavailable",{detail:{reason:"hardware-error",error:e,message:s}}))):"OverconstrainedError"===e?(this.log.warn("Microphone constraints cannot be satisfied"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-error",{detail:{reason:"overconstrained",error:e,message:s}}))):(this.log.error("Microphone error",t),this.ctx.host.dispatchEvent(new CustomEvent("microphone-error",{detail:{reason:"unknown",error:e,message:s}}))),0}}async enableMicrophone(){if(!await this.requestMicrophone())throw Error("Failed to enable microphone")}disableMicrophone(){if(this.microphoneStream&&(this.microphoneStream.getTracks().forEach(t=>t.stop()),this.microphoneStream=null),this.peerConnection){const t=this.peerConnection.getSenders().find(t=>"audio"===t.track?.kind);t&&t.replaceTrack(null);const e=this.peerConnection.getTransceivers().find(t=>"audio"===t.receiver.track.kind);e&&(e.direction="recvonly")}this.h=0,this.ctx.host.dispatchEvent(new CustomEvent("microphone-disabled"))}setMicrophoneMuted(t){this.microphoneStream&&(this.microphoneStream.getAudioTracks().forEach(e=>{e.enabled=!t}),this.ctx.host.dispatchEvent(new CustomEvent("microphone-muted-change",{detail:t})))}get isMicrophoneEnabled(){return this.h&&null!==this.microphoneStream}get isMicrophoneMuted(){if(!this.microphoneStream)return 1;const t=this.microphoneStream.getAudioTracks();return 0===t.length||!t[0].enabled}sendCommand(t){this.sendToStreamer("Command",[JSON.stringify(t)])}sendUIInteraction(t){this.sendToStreamer("UIInteraction",[JSON.stringify(t)])}}T.DISCONNECT_POLL_INTERVAL_MS=3e3,T.RECOVERY_MIN_INTERVAL_MS=1e4,T.RECOVERY_WINDOW_MS=6e4,T.MAX_RECOVERY_IN_WINDOW=3,T.ICE_RESTART_TIMEOUT_MS=5e3,T.ICE_RESTART_MAX_ATTEMPTS=2,T.TURN_HEALTH_CHECK_TIMEOUT_MS=3e3,T.TURN_HEALTH_CHECK_CACHE_MS=3e4,T.KEYFRAME_WORKER_SRC="\n let transformer = null;\n self.onrtctransform = (event) => {\n transformer = event.transformer;\n // Passthrough: pipe readable straight to writable (zero-copy)\n transformer.readable.pipeTo(transformer.writable);\n };\n self.onmessage = (event) => {\n if (event.data.type === 'keyframe' && transformer) {\n transformer.sendKeyFrameRequest().catch(() => {});\n }\n };\n ";class $ extends Error{constructor(t,e,s){super(t),this.code=e,this.statusCode=s,this.name="SessionError"}}class x{constructor(t,e){this.ctx=t,this.log=e,this.G=null,this.X="idle",this.K=null,this.J=0,this.Y=null,this.Z=null,this.tt=0,this.et=null,this.st=null,this.it=null,this.nt=null,this.rt=null,this.ot=[],this.ht=0,this.ct=null,this.lt=new Map}get ws(){return this.G}get state(){return this.X}get accessToken(){return this.st}get sessionId(){return this.nt}get agentId(){return this.rt}get iceServers(){return this.ot}get subscribedStreamerId(){return this.ct}get isCollaborator(){return this.ht}get peerAgentId(){return this.ut}get availableStreamers(){return Array.from(this.lt.keys())}async startSession(){const t=this.ctx.config;if(!t.admissionToken){const t=Error("admission-token is required to start a session");throw this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t.message,phase:"validation"}})),t}if("idle"!==this.X&&"error"!==this.X)return void this.log.info(`Already in state '${this.X}', ignoring startSession call`);this.ctx.performanceMetrics.sessionStarted=performance.now(),this.ctx.setSessionState("connecting"),this.et?.abort(),this.et=new AbortController;const e=this.et.signal;try{this.log.info("Exchanging admission token for access token..."),this.ctx.performanceMetrics.tokenExchangeStart=performance.now();const s=await this.exchangeToken(e);this.st=s.accessToken,this.it=s.expiresAt,this.nt=s.sessionId,this.rt=s.agentId,this.ot=s.iceServers||[],this.ctx.performanceMetrics.tokenExchangeComplete=performance.now();const i=this.ctx.performanceMetrics.tokenExchangeComplete-this.ctx.performanceMetrics.tokenExchangeStart;this.log.info(`Token exchange successful in ${i.toFixed(2)}ms. Session: ${this.nt}, Agent: ${this.rt}`),this.log.debug(`Received ${this.ot.length} ICE server(s)`,this.ot),this.ctx.host.dispatchEvent(new CustomEvent("session-token-exchanged",{detail:{sessionId:this.nt,agentId:this.rt,iceServers:this.ot}})),t.swiftJobRequest&&!this.ht&&(this.ctx.performanceMetrics.jobRequested=this.ctx.performanceMetrics.tokenExchangeStart,this.ctx.setJobState("pending")),this.ht&&this.ctx.setJobState("pending"),this.ot.length>0&&this.ctx.preCreatePeerConnection({iceServers:this.ot}),this.ctx.performanceMetrics.sessionWsConnectStart=performance.now();const n=3;for(let t=0;t<n;t++){if(e.aborted)throw Error("Session start aborted");try{await this.connectWs();break}catch(s){if(e.aborted)throw Error("Session start aborted");if(!(t<n-1))throw s;{const e=500*(t+1);this.log.warn(`WebSocket connection failed (attempt ${t+1}/${n}), retrying in ${e}ms...`),await new Promise(t=>setTimeout(t,e))}}}}catch(t){this.log.error("Failed to start session:",t);const e=t instanceof $?t.code:"connection_failed";throw this.ctx.setSessionState("error"),this.ctx.setStatusFailed(e),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t instanceof Error?t.message:t+"",code:t instanceof $?t.code:void 0,phase:"connection"}})),t}}leaveSession(t){this.et&&(this.et.abort(),this.et=null),"idle"!==this.X&&this.G?(this.log.info("Client-initiated disconnect"+(t?": "+t:"")),this.sendMessage({type:"report-event",event_type:"client.disconnect",data:t?{reason:t}:void 0}),this.sendMessage({type:"leave-session"}),this.terminateSession(t)):this.terminateSession()}terminateSession(t){this.log.info("Ending session"),this.X="idle",this.ctx.setSessionState("idle"),this.ctx.updateDebugIndicator(),this.stopKeepalive(),this.stopHeartbeat(),this.Z&&(clearTimeout(this.Z),this.Z=null),this.ct=null,this.lt.clear(),this.G&&(this.G.onclose=null,this.G.close(1e3,t||"client-terminated"),this.G=null),this.et&&(this.et.abort(),this.et=null),this.st=null,this.it=null,this.nt=null,this.rt=null,this.tt=0,this.ctx.setJobState("unsubmitted"),this.ctx.setAutoplayBlocked(0),this.ctx.host.dispatchEvent(new CustomEvent("session-ended"))}requestInvite(){this.G&&this.G.readyState===WebSocket.OPEN?this.ht?this.log.warn("session-collaborator role cannot request invite tokens"):(this.log.info("Requesting session invite token..."),this.sendMessage({type:"request-invite"})):this.log.warn("Cannot request invite — WebSocket not connected")}sendMessage(t){this.G&&this.G.readyState===WebSocket.OPEN?this.G.send(JSON.stringify(t)):this.log.warn("Cannot send message - WebSocket not connected")}async subscribeToStreamer(t){this.log.info("Subscribing to streamer: "+t),this.ct=t,this.ctx.resetPerformanceMetrics();const e=this.lt.get(t),s=e?.metadata?.offer_mode;if(e?.contributorAgentId&&(this.ut=e.contributorAgentId),this.log.info(`Streamer ${t} offer_mode: ${s||"not specified (defaulting to streamer)"} peer=${this.ut??"unknown"}`),this.sendMessage({type:"update-presence",preferred_streamer_id:t}),"browser"===s){this.log.info("Browser-initiated offer mode: creating WebRTC offer");try{this.ctx.getPeerConnection()||this.ctx.preCreatePeerConnection({iceServers:this.ot}),this.ctx.createDataChannel("cirrus");const t=await this.ctx.createOffer();this.ctx.setNegotiatingStarted(),this.sendMessage({type:"webrtc-signaling",signal_type:"offer",payload:{sdp:t.sdp},...this.ut&&{target_agent_id:this.ut}}),this.log.info("Browser-initiated offer sent to "+(this.ut??"broadcast"))}catch(t){this.log.error("Failed to create browser-initiated offer:",t),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t instanceof Error?t.message:t+"",phase:"webrtc-offer-creation"}}))}}}evaluateSubscription(){if(!this.G||this.G.readyState!==WebSocket.OPEN)return;if(0===this.lt.size)return;const t=this.ctx.config.streamerId;let e=null;t&&"default"!==t?this.lt.has(t)&&(e=t):e=this.lt.keys().next().value??null,e&&e!==this.ct&&this.subscribeToStreamer(e)}async exchangeToken(t){const e=this.ctx.config,s=`https://${e.apiEndpoint}/agent/token`,i=new AbortController,n=setTimeout(()=>i.abort(),15e3),r=()=>i.abort();t?.addEventListener("abort",r);const a=function(t){try{const e=JSON.parse(atob(t.split(".")[1])),s=e.typ??e.token_type;return"inv"===s||"invite"===s}catch{return 0}}(e.admissionToken);this.ht=a;const o=a?{grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer",invite_token:e.admissionToken,include_ice_servers:1}:{grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer",assertion:e.admissionToken,agent_role:"browser-agent",include_ice_servers:1,...e.swiftJobRequest&&{swift_job_request:1},preferred_streamer_id:e.streamerId||"default",...null!=e.lat&&{lat:e.lat},...null!=e.lng&&{lng:e.lng},...(null!=e.queueWaitTolerance||null!=e.webrtcNegotiationTolerance)&&{tolerances:{...null!=e.queueWaitTolerance&&{queue_wait_tolerance_seconds:e.queueWaitTolerance},...null!=e.webrtcNegotiationTolerance&&{webrtc_negotiation_tolerance_seconds:e.webrtcNegotiationTolerance}}},...e.appId&&{application_request:{app_id:e.appId,...e.appVersion&&{version_id:e.appVersion}}}};let h;try{h=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json"},signal:i.signal,body:JSON.stringify(o)})}finally{clearTimeout(n),t?.removeEventListener("abort",r)}if(!h.ok){const t=await h.text().catch(()=>"Unknown error"),e=401===(c=h.status)?/expired/i.test(t)?"token_expired":"unauthorized":c>=500?"server_error":"request_failed";throw new $(`Token exchange failed (${h.status}): ${t}`,e,h.status)}var c;const l=await h.json();return l.ice_servers&&0!==l.ice_servers.length||this.log.warn("No ICE servers received from gateway"),{accessToken:l.access_token,expiresAt:Date.now()+1e3*l.expires_in,sessionId:l.session_id,agentId:l.agent_id,iceServers:l.ice_servers||[]}}async connectWs(){if(!this.st)throw Error("No access token available for WebSocket connection");this.ctx.setSessionState("authenticating");const t=`wss://${this.ctx.config.apiEndpoint}/ws/session`;return this.log.info(`Connecting WebSocket to ${this.ctx.config.apiEndpoint}...`),new Promise((e,s)=>{try{this.G=new WebSocket(t,["Bearer."+this.st]);let i=0;const n=setTimeout(()=>{this.G&&this.G.readyState===WebSocket.CONNECTING&&(this.G.close(),i||(i=1,s(Error("WebSocket connection timeout"))))},1e4);this.G.onopen=()=>{if(clearTimeout(n),i=1,this.ctx.performanceMetrics.sessionWsConnectComplete=performance.now(),this.ctx.performanceMetrics.sessionWsConnectStart){const t=this.ctx.performanceMetrics.sessionWsConnectComplete-this.ctx.performanceMetrics.sessionWsConnectStart;this.log.info(`WebSocket connected in ${t.toFixed(2)}ms`)}else this.log.info("WebSocket connected");this.tt=0,this.X="connected",this.ctx.setSessionState("connected"),this.startKeepalive(),this.startHeartbeat(),this.ctx.host.dispatchEvent(new CustomEvent("session-connected",{detail:{sessionId:this.nt,agentId:this.rt}})),e()},this.G.onmessage=t=>{this.handleMessage(t.data)},this.G.onclose=t=>{if(clearTimeout(n),!i)return i=1,void s(Error(`WebSocket upgrade failed (code=${t.code})`));this.handleWsClose(t)},this.G.onerror=t=>{this.log.error("WebSocket error:",t)}}catch(t){s(t)}})}handleMessage(t){if("pong"===t)return;let e;try{e=JSON.parse(t)}catch{return void this.log.error("Failed to parse WebSocket message:",t)}switch(this.log.debug("Received message: "+e.type),e.type){case"offer":this.handleSignallingOffer(e);break;case"iceCandidate":this.handleSignallingIceCandidate(e);break;case"streamer-list":this.ctx.host.dispatchEvent(new CustomEvent("streamer-list",{detail:{streamers:e.streamers}}));break;case"streamer-joined":this.ctx.cancelRendezvousTimer(),this.ctx.host.dispatchEvent(new CustomEvent("streamer-joined",{detail:{streamerId:e.streamerId}}));break;case"streamer-left":this.ctx.host.dispatchEvent(new CustomEvent("streamer-left",{detail:{streamerId:e.streamerId}}));break;case"pong":case"heartbeat":case"presence-changed":break;case"error":this.log.error("Server error:",e),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:e.message||"Unknown server error",code:e.code,phase:"signalling"}}));break;case"webrtc-signaling":{const t=e;switch(this.log.info(`WebRTC signaling: ${t.signal_type} from ${t.from_agent_id}`),t.signal_type){case"offer":{const e=this.ctx.getPeerConnection();if(e&&"connected"===e.connectionState){this.log.info(`Ignoring offer from ${t.from_agent_id} — already connected (offer is for another participant)`);break}this.ut=t.from_agent_id,this.ctx.setNegotiatingStarted(),this.handleSignallingOffer({type:"offer",sdp:t.payload.sdp});break}case"iceCandidate":this.handleSignallingIceCandidate({type:"iceCandidate",candidate:t.payload.candidate,sdpMid:t.payload.sdpMid,sdpMLineIndex:t.payload.sdpMLineIndex});break;case"answer":this.ut=t.from_agent_id,this.handleSignallingAnswer({type:"answer",sdp:t.payload.sdp})}break}case"session-ready":{const t=e;this.ctx.performanceMetrics.sessionReady=performance.now();const s=t.participants?.map(t=>`${t.agent_id}(${t.agent_role})`).join(", ")||"none";if(this.ctx.performanceMetrics.sessionStarted){const e=this.ctx.performanceMetrics.sessionReady-this.ctx.performanceMetrics.sessionStarted;this.log.info(`Session ready in ${e.toFixed(2)}ms: ${t.session_id}, participants: ${s}`)}else this.log.info(`Session ready: ${t.session_id}, participants: ${s}`);this.nt=t.session_id,this.rt=t.agent_id,this.ctx.host.dispatchEvent(new CustomEvent("session-ready",{detail:{sessionId:t.session_id,agentId:t.agent_id,projectId:t.project_id,appId:t.app_id,versionId:t.version_id,participants:t.participants}})),t.participants?.some(t=>"worker-agent"===t.agent_role)&&this.ctx.setJobState("fulfilled"),this.ctx.reportTiming("session-ready"),this.ctx.config.streamerId&&!this.ct&&(this.ct="default"===this.ctx.config.streamerId?null:this.ctx.config.streamerId);break}case"participant-joined":{const t=e;this.log.info(`Participant joined: ${t.agent_id} (${t.agent_role})`),this.ctx.host.dispatchEvent(new CustomEvent("participant-joined",{detail:{agentId:t.agent_id,agentRole:t.agent_role,metadata:t.metadata}})),"worker-agent"===t.agent_role&&this.ctx.setJobState("fulfilled");break}case"participant-left":{const t=e;this.log.info(`Participant left: ${t.agent_id} (${t.agent_role}), reason: ${t.reason||"none"}`),"worker-agent"===t.agent_role&&(this.ut===t.agent_id&&(this.ut=void 0),this.ctx.setJobState("pending")),this.ctx.host.dispatchEvent(new CustomEvent("participant-left",{detail:{agentId:t.agent_id,agentRole:t.agent_role,reason:t.reason}}));break}case"session-ended":{const t=e;this.log.info("Session ended by server: "+(t.reason||"unknown reason"));const s=["queue_wait_timeout","rendezvous_timeout","webrtc_negotiation_timeout"];t.reason&&s.includes(t.reason)?this.ctx.setStatusFailed(t.reason):this.ctx.setStatusEnded(),this.ctx.host.dispatchEvent(new CustomEvent("session-ended",{detail:{reason:t.reason}})),this.terminateSession();break}case"heartbeat-ack":this.J=Date.now();break;case"job-requested":{const t=e;this.log.info("Job requested: "+t.job_id),this.ctx.setJobState("pending"),this.ctx.host.dispatchEvent(new CustomEvent("job-requested",{detail:{jobId:t.job_id}}));break}case"job-cancelled":{const t=e;this.log.info(`Job cancelled: success=${t.success}, reason=${t.reason||"none"}`),t.success&&this.ctx.setJobState("unsubmitted"),this.ctx.host.dispatchEvent(new CustomEvent("job-cancelled",{detail:{jobId:t.job_id,success:t.success,reason:t.reason}}));break}case"invite-token":{const t=e;this.log.info("Received invite token from session"),this.ctx.host.dispatchEvent(new CustomEvent("invite-token",{detail:{token:t.token}}));break}case"contribution-available":{const t=e;"pixel-streaming"===t.contribution_type&&(this.ctx.setRendezvoused(),this.lt.set(t.contribution_id,{contributorAgentId:t.contributor_agent_id,metadata:t.metadata}),this.ctx.host.dispatchEvent(new CustomEvent("streamer-available",{detail:{streamerId:t.contribution_id,contributorAgentId:t.contributor_agent_id,metadata:t.metadata}})),this.ctx.host.dispatchEvent(new CustomEvent("streamer-list-changed",{detail:{streamers:this.availableStreamers}})),this.evaluateSubscription());break}case"contribution-unavailable":{const t=e;this.lt.has(t.contribution_id)&&(this.lt.delete(t.contribution_id),this.ctx.host.dispatchEvent(new CustomEvent("streamer-unavailable",{detail:{streamerId:t.contribution_id}})),this.ctx.host.dispatchEvent(new CustomEvent("streamer-list-changed",{detail:{streamers:this.availableStreamers}})),this.ct===t.contribution_id&&(this.ct=null,this.ctx.onWorkerLeft(),this.evaluateSubscription()));break}default:this.log.debug("Unknown message type: "+e.type),this.ctx.host.dispatchEvent(new CustomEvent("session-message",{detail:e}))}}async handleSignallingOffer(t){const e=this.ctx.getPcGeneration();try{this.log.info(`Processing SDP offer from streamer (gen=${e})...`);const s=await this.ctx.handleOffer({type:"offer",sdp:t.sdp});if(this.ctx.getPcGeneration()!==e)return void this.log.warn(`Discarding stale answer (gen=${e}, current=${this.ctx.getPcGeneration()})`);this.sendMessage({type:"webrtc-signaling",signal_type:"answer",payload:{sdp:s.sdp},...this.ut&&{target_agent_id:this.ut}}),this.log.info(`Sent SDP answer (gen=${e}) target=${this.ut??"*"}`)}catch(t){if(this.ctx.getPcGeneration()!==e)return void this.log.debug("Ignoring offer error from stale gen="+e);this.log.error("Failed to handle offer:",t),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t instanceof Error?t.message:t+"",phase:"webrtc-offer"}}))}}async handleSignallingAnswer(t){const e=this.ctx.getPcGeneration();try{if(this.log.info(`Processing SDP answer from streamer (gen=${e})...`),await this.ctx.handleAnswer({type:"answer",sdp:t.sdp}),this.ctx.getPcGeneration()!==e)return void this.log.warn(`Answer processed but gen changed (gen=${e}, current=${this.ctx.getPcGeneration()})`);this.log.info(`SDP answer processed successfully (gen=${e})`)}catch(t){if(this.ctx.getPcGeneration()!==e)return void this.log.debug("Ignoring answer error from stale gen="+e);this.log.error("Failed to handle answer:",t),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t instanceof Error?t.message:t+"",phase:"webrtc-answer"}}))}}async handleSignallingIceCandidate(t){const e=this.ctx.getPcGeneration();try{await this.ctx.addIceCandidate({candidate:t.candidate,sdpMid:t.sdpMid,sdpMLineIndex:t.sdpMLineIndex})}catch(t){if(this.ctx.getPcGeneration()!==e)return void this.log.debug("Ignoring ICE candidate error from stale gen="+e);this.log.error("Failed to add ICE candidate:",t)}}handleWsClose(t){const e=this.getCloseCodeDescription(t.code);if(this.log.info(`WebSocket closed: code=${t.code} (${e}), reason="${t.reason||"none"}"`),this.stopKeepalive(),this.stopHeartbeat(),"idle"===this.X)return void this.log.info("Not reconnecting - session was intentionally ended");const s=this.ctx.config;if("none"===s.reconnectMode)return this.log.info('Reconnect mode is "none" - not attempting reconnection'),this.X="error",this.ctx.setSessionState("error"),this.ctx.setStatusFailed("disconnected"),void this.ctx.host.dispatchEvent(new CustomEvent("session-disconnected",{detail:{code:t.code,reason:t.reason||e,wasClean:t.wasClean,willReconnect:0}}));const i="connected"===this.X||"reconnecting"===this.X,n=s.reconnectAttempts===1/0||-1===s.reconnectAttempts||this.tt<s.reconnectAttempts,r="recover"===s.reconnectMode&&this.st,a="always"===s.reconnectMode;if(i&&n&&(r||a))this.log.info(`Will attempt reconnection (mode=${s.reconnectMode}, attempts=${this.tt+1}/${s.reconnectAttempts===1/0?"∞":s.reconnectAttempts})`),this.X="reconnecting",this.ctx.setSessionState("reconnecting"),this.scheduleReconnect();else{const i=n?r||"recover"!==s.reconnectMode?"session was not in connected state":"no access token for recovery":"max reconnection attempts reached";this.log.info("Not reconnecting: "+i),this.X="error",this.ctx.setSessionState("error");const a=n?r||"recover"!==s.reconnectMode?"not_connected":"no_access_token":"reconnect_exhausted";this.ctx.setStatusFailed(a),this.ctx.host.dispatchEvent(new CustomEvent("session-disconnected",{detail:{code:t.code,reason:t.reason||e,wasClean:t.wasClean,willReconnect:0,failureReason:i}}))}}getCloseCodeDescription(t){return{1e3:"Normal closure",1001:"Going away",1002:"Protocol error",1003:"Unsupported data",1005:"No status received",1006:"Abnormal closure",1007:"Invalid frame payload data",1008:"Policy violation",1009:"Message too big",1010:"Mandatory extension",1011:"Internal server error",1012:"Service restart",1013:"Try again later",1014:"Bad gateway",1015:"TLS handshake failure"}[t]||`Unknown (${t})`}scheduleReconnect(){this.Z&&clearTimeout(this.Z);const t=this.ctx.config;let e;e="periodic"===t.reconnectStrategy?t.reconnectInterval:Math.min(x.DEFAULT_MIN_RECONNECT_DELAY*Math.pow(x.DEFAULT_RECONNECT_BACKOFF,this.tt),x.DEFAULT_MAX_RECONNECT_DELAY);const s=t.reconnectAttempts===1/0||-1===t.reconnectAttempts?"∞":t.reconnectAttempts;this.log.info(`Scheduling reconnect attempt ${this.tt+1}/${s} in ${e}ms (strategy=${t.reconnectStrategy})`),this.ctx.host.dispatchEvent(new CustomEvent("session-reconnecting",{detail:{attempt:this.tt+1,maxAttempts:t.reconnectAttempts,delay:e,strategy:t.reconnectStrategy}})),this.Z=setTimeout(async()=>{this.tt++;try{if(this.it&&Date.now()>=this.it){this.log.info("Access token expired, re-exchanging before reconnect...");const t=await this.exchangeToken();this.st=t.accessToken,this.it=t.expiresAt,this.log.info("Token re-exchange successful")}this.log.info(`Attempting reconnection ${this.tt}...`),await this.connectWs()}catch(e){this.log.error("Reconnection failed:",e),t.reconnectAttempts===1/0||-1===t.reconnectAttempts||this.tt<t.reconnectAttempts?(this.X="reconnecting",this.ctx.setSessionState("reconnecting"),this.scheduleReconnect()):(this.log.info("Max reconnection attempts reached"),this.X="error",this.ctx.setSessionState("error"),this.ctx.setStatusFailed("reconnect_exhausted"),this.ctx.host.dispatchEvent(new CustomEvent("session-disconnected",{detail:{code:1006,reason:"Max reconnection attempts reached",wasClean:0,willReconnect:0,failureReason:"max reconnection attempts reached"}})))}},e)}startKeepalive(){this.stopKeepalive(),this.Y=setInterval(()=>{this.G&&this.G.readyState===WebSocket.OPEN&&this.G.send("ping")},x.SESSION_KEEPALIVE_INTERVAL)}stopKeepalive(){this.Y&&(clearInterval(this.Y),this.Y=null)}startHeartbeat(){this.stopHeartbeat(),this.J=Date.now(),this.K=setInterval(()=>{if(this.G&&this.G.readyState===WebSocket.OPEN){const t=Date.now()-this.J;if(t>x.SESSION_HEARTBEAT_TIMEOUT)return this.log.warn(`Heartbeat timeout — no ack for ${Math.round(t/1e3)}s, closing stale WebSocket`),void this.G.close(4e3,"heartbeat timeout");this.sendMessage({type:"heartbeat"})}},x.SESSION_HEARTBEAT_INTERVAL)}stopHeartbeat(){this.K&&(clearInterval(this.K),this.K=null)}}x.DEFAULT_MIN_RECONNECT_DELAY=1e3,x.DEFAULT_MAX_RECONNECT_DELAY=3e4,x.DEFAULT_RECONNECT_BACKOFF=2,x.SESSION_HEARTBEAT_INTERVAL=3e4,x.SESSION_HEARTBEAT_TIMEOUT=9e4,x.SESSION_KEEPALIVE_INTERVAL=25e3;const M={sxga:{maxWidth:1280,maxHeight:1024},hd:{maxWidth:1366,maxHeight:768},hdplus:{maxWidth:1600,maxHeight:900},fhd:{maxWidth:1920,maxHeight:1080},wuxga:{maxWidth:1920,maxHeight:1200},qhd:{maxWidth:2560,maxHeight:1440},wqhd:{maxWidth:3440,maxHeight:1440},uhd:{maxWidth:3840,maxHeight:2160}};class I extends HTMLElement{get version(){return I.VERSION}get config(){return{nativeTouch:this.dt,pointerLocked:this.gt,pointerLockRelease:this.ft,suppressBrowserKeys:this.vt,apiEndpoint:this.wt,admissionToken:this.yt,appId:this.bt,appVersion:this.kt,noAutoConnect:this.Ct,mute:this.St,volume:this.Et,rendezvousPreference:this.Rt,lingerPreference:this.Tt,leftGracePeriod:this.$t,reconnectMode:this.xt,reconnectAttempts:this.tt,reconnectStrategy:this.Mt,reconnectInterval:this.It,disconnectGraceMs:this.Pt,resizeMode:this.At,dprCap:this.Dt,resolutionClamp:this._t,queueWaitTolerance:this.Vt,webrtcNegotiationTolerance:this.Ft,debug:this.Bt,controls:this.Wt,swiftJobRequest:this.Ut,streamerId:this.qt,lat:this.Lt,lng:this.zt,forceRelay:this.Nt,useMic:this.webrtc?.isMicrophoneEnabled??0,forceMonoAudio:0,iceBatchDelay:this.Ot,streamQuality:this.jt,videoBitrateMin:this.Ht,videoBitrateStart:this.Gt,videoBitrateMax:this.Xt,logLevel:this.Kt}}static get observedAttributes(){return["native-touch","pointer-lock","pointer-lock-release","enable-gamepad","enable-xr","suppress-browser-keys","admission-token","app-id","app-version","no-auto-connect","mute","volume","rendezvous-preference","linger-preference","left-grace-period","api-endpoint","reconnect-mode","reconnect-attempts","reconnect-strategy","reconnect-interval","disconnect-grace-ms","resize-mode","dpr-cap","resolution-clamp","debug","controls","swift-job-request","force-relay","queue-wait-tolerance","webrtc-negotiation-tolerance","streamer-id","lat","lng","stream-quality","video-bitrate-min","video-bitrate-start","video-bitrate-max","log-level"]}get nativeTouch(){return this.dt}set nativeTouch(t){this.dt=t,this.toggleAttribute("native-touch",t)}get pointerLock(){return this.gt}set pointerLock(t){this.gt=t,this.toggleAttribute("pointer-lock",t)}get pointerLockRelease(){return this.ft}set pointerLockRelease(t){this.ft=t,this.toggleAttribute("pointer-lock-release",t)}get suppressBrowserKeys(){return this.vt}set suppressBrowserKeys(t){this.vt=t,this.toggleAttribute("suppress-browser-keys",t)}get debug(){return this.Bt}set debug(t){this.Bt=t,this.toggleAttribute("debug",t),this.Jt()}get logLevel(){return this.Kt}set logLevel(t){this.Kt=t,this.setAttribute("log-level",t)}get iceBatchDelay(){return this.Ot}set iceBatchDelay(t){this.Ot=Math.max(0,t)}get admissionToken(){return this.yt}set admissionToken(t){const e=this.yt;this.yt=t,null!==t?this.setAttribute("admission-token",t):this.removeAttribute("admission-token"),e!==t&&this.dispatchEvent(new CustomEvent("admission-token-change",{detail:{oldValue:e,newValue:t}}))}get appId(){return this.bt}set appId(t){const e=this.bt;this.bt=t,null!==t?this.setAttribute("app-id",t):this.removeAttribute("app-id"),e!==t&&this.dispatchEvent(new CustomEvent("app-id-change",{detail:{oldValue:e,newValue:t}}))}get appVersion(){return this.kt}set appVersion(t){const e=this.kt;this.kt=t,null!==t?this.setAttribute("app-version",t):this.removeAttribute("app-version"),e!==t&&this.dispatchEvent(new CustomEvent("app-version-change",{detail:{oldValue:e,newValue:t}}))}get noAutoConnect(){return this.Ct}set noAutoConnect(t){const e=this.Ct;this.Ct=t,this.toggleAttribute("no-auto-connect",t),e!==t&&this.dispatchEvent(new CustomEvent("no-auto-connect-change",{detail:{oldValue:e,newValue:t}}))}get mute(){return this.St}set mute(t){const e=this.St;this.St=t,this.toggleAttribute("mute",t),this.video&&(this.video.muted=t),this.webrtc.getAudioElement().muted=t,e!==t&&this.dispatchEvent(new CustomEvent("mute-change",{detail:{oldValue:e,newValue:t,muted:t}}))}get volume(){return this.Et}set volume(t){const e=Math.max(0,Math.min(1,+t||0)),s=this.Et;this.Et=e,this.setAttribute("volume",e+""),this.video&&(this.video.volume=e),this.webrtc.getAudioElement().volume=e,s!==e&&this.dispatchEvent(new CustomEvent("volume-change",{detail:{oldValue:s,newValue:e}}))}get rendezvousPreference(){return this.Rt}set rendezvousPreference(t){const e=this.Rt;this.Rt=t,null!==t?this.setAttribute("rendezvous-preference",t.toString()):this.removeAttribute("rendezvous-preference"),e!==t&&this.dispatchEvent(new CustomEvent("rendezvous-preference-change",{detail:{oldValue:e,newValue:t}}))}get lingerPreference(){return this.Tt}set lingerPreference(t){const e=this.Tt;this.Tt=t,null!==t?this.setAttribute("linger-preference",t.toString()):this.removeAttribute("linger-preference"),e!==t&&this.dispatchEvent(new CustomEvent("linger-preference-change",{detail:{oldValue:e,newValue:t}}))}get leftGracePeriod(){return this.$t}set leftGracePeriod(t){const e=this.$t;this.$t=t,null!==t?this.setAttribute("left-grace-period",t.toString()):this.removeAttribute("left-grace-period"),e!==t&&this.dispatchEvent(new CustomEvent("left-grace-period-change",{detail:{oldValue:e,newValue:t}}))}get apiEndpoint(){return this.wt}set apiEndpoint(t){const e=this.wt;this.wt=t,t?this.setAttribute("api-endpoint",t):this.removeAttribute("api-endpoint"),e!==t&&this.dispatchEvent(new CustomEvent("api-endpoint-change",{detail:{oldValue:e,newValue:t}}))}get reconnectMode(){return this.xt}set reconnectMode(t){this.xt=t,this.setAttribute("reconnect-mode",t)}get reconnectAttempts(){return this.tt}set reconnectAttempts(t){this.tt=t,t===1/0||-1===t?this.removeAttribute("reconnect-attempts"):this.setAttribute("reconnect-attempts",t+"")}get reconnectStrategy(){return this.Mt}set reconnectStrategy(t){this.Mt=t,this.setAttribute("reconnect-strategy",t)}get reconnectInterval(){return this.It}set reconnectInterval(t){this.It=t,this.setAttribute("reconnect-interval",t+"")}get disconnectGraceMs(){return this.Pt}set disconnectGraceMs(t){this.Pt=Math.max(0,t),this.setAttribute("disconnect-grace-ms",this.Pt+"")}get resizeMode(){return this.At}set resizeMode(t){this.At=t,this.Qt=null,this.Yt=0,this.Zt=0,this.setAttribute("resize-mode",t)}get dprCap(){return this.Dt}set dprCap(t){this.Dt=Math.max(.1,t),this.setAttribute("dpr-cap",this.Dt+"")}get resolutionClamp(){return this._t}set resolutionClamp(t){null===t?(this._t=null,this.removeAttribute("resolution-clamp")):"string"==typeof t?this.setAttribute("resolution-clamp",t):this._t=t}get isXRStream(){return this.te}get sessionState(){return this.ee}get streamState(){return this.se}get status(){return this.ie}get failureReason(){return this.ne}get lastUserInteraction(){return this.re}get streamStartedAt(){return this.ae}get accessToken(){return this.session.accessToken}get sessionId(){return this.session.sessionId}get agentId(){return this.session.agentId}get iceServers(){return this.session.iceServers}getAdmissionConfig(){return this.yt?{admissionToken:this.yt,appId:this.bt||void 0,appVersion:this.kt||void 0}:null}getSessionPreferences(){return{rendezvousPreference:this.Rt||void 0,lingerPreference:this.Tt||void 0,leftGracePeriod:this.$t||void 0}}get isAdmitted(){return this.oe}he(){const t=this;return{get host(){return t},get config(){return t.config},get performanceMetrics(){return t.performanceMetrics},getVideoElement:()=>t.video,getAudioElement:()=>t.webrtc.getAudioElement(),getCoordTranslator:()=>t.coordTranslator,getDataChannel:()=>t.webrtc.getDataChannel(),sendToStreamer:(e,s)=>t.sendToStreamer(e,s??[]),sendRawBinary:e=>t.webrtc.sendRawBinary(e),getPeerConnection:()=>t.webrtc.getPeerConnection(),getSessionWs:()=>t.session.ws,sendSessionMessage:e=>t.session.sendMessage(e),getSessionState:()=>t.ee,getAccessToken:()=>t.session.accessToken,getSessionId:()=>t.session.sessionId,getAgentId:()=>t.session.agentId,getIceServers:()=>t.session.iceServers,getPeerAgentId:()=>t.session.peerAgentId,logPerformanceSummary:()=>t.metrics.logPerformanceSummary(),reportTiming:e=>t.metrics.reportTiming(e),collectAndDisplaySessionStats:()=>t.metrics.collectAndDisplaySessionStats(),getMetricsSessionStartTime:()=>t.metrics.sessionStartTime,setMetricsSessionStartTime:e=>{t.metrics.sessionStartTime=e},handleFileExtension:e=>t.fileTransfer.handleExtension(e),handleFileMimeType:e=>t.fileTransfer.handleMimeType(e),handleFileContents:e=>t.fileTransfer.handleContents(e),leaveSession:e=>t.stop(e),getSubscribedStreamerId:()=>t.session.subscribedStreamerId,setAutoplayBlocked:e=>{t.ce=e},setJobState:e=>t.le(e),detectXRStream:()=>t.ue(),onWorkerLeft:()=>t.webrtc.onWorkerLeft(),checkPermission:e=>t.checkPermission(e),watchPermission:e=>t.watchPermission(e),handleOffer:e=>t.webrtc.handleOffer(e),handleAnswer:e=>t.webrtc.handleAnswer(e),createOffer:()=>t.webrtc.createOffer(),createDataChannel:e=>t.webrtc.createDataChannel(e),addIceCandidate:e=>t.webrtc.addIceCandidate(e),preCreatePeerConnection:e=>t.webrtc.preCreatePeerConnection(e),getPcGeneration:()=>t.webrtc.getPcGeneration(),handleLatencyTestResult:e=>t.metrics.handleLatencyTestResult(e),getLatencyBreakdown:()=>t.metrics.getLatencyBreakdown(),setStreamState:(e,s)=>t.de(e,s),setSessionState:e=>t.pe(e),updateDebugIndicator:()=>t.Jt(),cancelRendezvousTimer:()=>t.me(),resetPerformanceMetrics:()=>t.metrics.resetPerformanceMetrics(),setRendezvoused:()=>{t.ge=1,t.fe()},setNegotiatingStarted:()=>{t.ve=1,t.fe()},setStatusFailed:e=>{t.we("failed",e),t.webrtc.closePeerConnection(),t.webrtc.resetResilienceState()},setStatusEnded:()=>{t.le("unsubmitted"),t.we("ended"),t.webrtc.closePeerConnection(),t.webrtc.resetResilienceState()}}}constructor(){super(),this.ye=null,this.be=0,this.dt=0,this.vt=0,this.gt=0,this.ft=0,this.resizeObserver=null,this.Ot=0,this.performanceMetrics={sessionStarted:null,tokenExchangeStart:null,tokenExchangeComplete:null,peerConnectionCreated:null,sessionWsConnectStart:null,sessionWsConnectComplete:null,sessionReady:null,jobRequested:null,subscribeStart:null,offerReceived:null,answerSent:null,iceConnected:null,firstTrack:null,firstFrame:null,connectionComplete:null},this.yt=null,this.bt=null,this.kt=null,this.Ct=0,this.ke=0,this.St=0,this.Et=1,this.Rt=null,this.Tt=null,this.$t=null,this.Vt=null,this.Ft=null,this.oe=0,this.Ce=null,this.Se=null,this.wt="api.interlucent.ai",this.ee="idle",this.se="idle",this.xt="none",this.tt=1/0,this.Mt="exponential-backoff",this.It=I.DEFAULT_RECONNECT_INTERVAL,this.Pt=9e3,this.qt=null,this.Lt=null,this.zt=null,this.Ee=null,this.Re=null,this.Te=null,this.At="5.4+",this.Dt=1,this._t=null,this.Qt=null,this.Yt=0,this.$e=null,this.xe=null,this.Zt=0,this.te=0,this.Bt=0,this.jt="balanced",this.Ht=null,this.Gt=null,this.Xt=null,this.Kt="warn",this.Me=c(this,"pixel-stream","host",d(()=>this.Kt)),this.Nt=0,this.Ie="unsubmitted",this.Wt=1,this.Ut=0,this.ce=0,this.re=0,this.ae=0,this.ie="idle",this.ne=null,this.ge=0,this.ve=0,this.Pe=t=>{this.Ae(t.detail)};const t=this.attachShadow({mode:"open"});this.De=this.he(),this.keyboardInput=new v(this.De,this.Me),this.mouseInput=new w(this.De,this.Me,{onPointerUnlocked:()=>this.keyboardInput.releaseAllKeys()}),this.touchInput=new y(this.De,this.Me),this.gamepadInput=new b(this.De,this.Me),this.xrInput=new C(this.De,this.Me),this.fileTransfer=new S(this.De,this.Me),this.metrics=new E(this.De,c(this,"pixel-stream","metrics",d(()=>this.Kt))),this.webrtc=new T(this.De,c(this,"pixel-stream","webrtc",d(()=>this.Kt))),this.session=new x(this.De,c(this,"pixel-stream","session",d(()=>this.Kt))),t.innerHTML='\n <style>\n :host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n transition: box-shadow 0.3s ease;\n\n /* ═══ Overlay theme tokens (dark default) ═══ */\n --ps-overlay-bg: rgba(10, 10, 10, 0.88);\n --ps-overlay-text: #e8e4df;\n --ps-overlay-text-dim: rgba(255, 255, 255, 0.45);\n --ps-overlay-btn-bg: rgba(255, 255, 255, 0.1);\n --ps-overlay-btn-border: rgba(255, 255, 255, 0.15);\n --ps-overlay-btn-hover: rgba(255, 255, 255, 0.18);\n --ps-overlay-spinner-track: rgba(255, 255, 255, 0.1);\n --ps-overlay-spinner-fill: rgba(255, 255, 255, 0.7);\n --ps-overlay-pill-border: rgba(255, 255, 255, 0.15);\n --ps-overlay-pill-text: rgba(255, 255, 255, 0.6);\n --ps-overlay-pill-hover: rgba(255, 255, 255, 0.06);\n }\n\n @media (prefers-color-scheme: light) {\n :host {\n --ps-overlay-bg: rgba(245, 245, 245, 0.88);\n --ps-overlay-text: #1a1a1a;\n --ps-overlay-text-dim: rgba(0, 0, 0, 0.45);\n --ps-overlay-btn-bg: rgba(0, 0, 0, 0.08);\n --ps-overlay-btn-border: rgba(0, 0, 0, 0.15);\n --ps-overlay-btn-hover: rgba(0, 0, 0, 0.14);\n --ps-overlay-spinner-track: rgba(0, 0, 0, 0.1);\n --ps-overlay-spinner-fill: rgba(0, 0, 0, 0.6);\n --ps-overlay-pill-border: rgba(0, 0, 0, 0.15);\n --ps-overlay-pill-text: rgba(0, 0, 0, 0.5);\n --ps-overlay-pill-hover: rgba(0, 0, 0, 0.05);\n }\n }\n\n /* Debug mode - session connectivity indicators */\n :host([debug]) {\n box-shadow:\n inset 0 0 0 4px rgba(128, 128, 128, 0.8),\n 0 0 0 4px rgba(128, 128, 128, 0.8);\n }\n\n :host([debug].session-connecting) {\n box-shadow:\n inset 0 0 0 4px rgba(255, 180, 0, 1),\n 0 0 0 4px rgba(255, 180, 0, 1),\n 0 0 30px 10px rgba(255, 180, 0, 0.6);\n }\n\n :host([debug].session-connected) {\n box-shadow:\n inset 0 0 0 4px rgba(0, 255, 80, 1),\n 0 0 0 4px rgba(0, 255, 80, 1),\n 0 0 30px 10px rgba(0, 255, 80, 0.6);\n }\n\n :host([debug].session-error) {\n box-shadow:\n inset 0 0 0 4px rgba(255, 50, 50, 1),\n 0 0 0 4px rgba(255, 50, 50, 1),\n 0 0 30px 10px rgba(255, 50, 50, 0.6);\n }\n\n video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n pointer-events: all;\n /* Pre-allocate a GPU compositing layer so the first\n real frame can be texture-uploaded without waiting\n for layer promotion. */\n will-change: transform;\n }\n\n /* ═══ OVERLAY ═══ */\n #controls-overlay {\n position: absolute;\n inset: 0;\n display: none;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: var(--ps-overlay-bg);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n font-family: system-ui, -apple-system, sans-serif;\n color: var(--ps-overlay-text);\n transition: opacity 280ms cubic-bezier(0.16, 1, 0.3, 1);\n text-align: center;\n gap: 0.75rem;\n z-index: 1;\n }\n\n #controls-overlay.visible {\n display: flex;\n }\n\n :host([status="streaming"]) #controls-overlay {\n opacity: 0;\n pointer-events: none;\n }\n\n #controls-overlay.slotted {\n background: transparent;\n backdrop-filter: none;\n -webkit-backdrop-filter: none;\n pointer-events: none;\n }\n\n #controls-overlay.slotted ::slotted(*) {\n pointer-events: auto;\n }\n\n ::slotted([slot="overlay"]) {\n width: 100%;\n height: 100%;\n }\n\n /* ═══ STATE VISIBILITY ═══ */\n .state { display: none; flex-direction: column; align-items: center; gap: 0.75rem; }\n\n :host([status="idle"]) .state-idle,\n :host([status="connected"]) .state-idle,\n :host([status="connecting"]) .state-pending,\n :host([status="authenticating"]) .state-pending,\n :host([status="queued"]) .state-pending,\n :host([status="rendezvoused"]) .state-pending,\n :host([status="negotiating"]) .state-pending,\n :host([status="ready"]) .state-ready,\n :host([status="failed"]) .state-error,\n :host([status="ended"]) .state-ended {\n display: flex;\n }\n\n /* ═══ PLAY BUTTON ═══ */\n .play-btn {\n width: 52px;\n height: 52px;\n border: 1px solid var(--ps-overlay-btn-border);\n border-radius: 50%;\n background: var(--ps-overlay-btn-bg);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: transform 0.2s, background 0.2s;\n }\n .play-btn:hover { background: var(--ps-overlay-btn-hover); transform: scale(1.06); }\n .play-btn:active { transform: scale(0.95); }\n .play-btn svg { width: 22px; height: 22px; fill: var(--ps-overlay-text); margin-left: 2px; }\n\n /* ═══ SPINNER ═══ */\n .spinner {\n width: 32px;\n height: 32px;\n border: 2px solid var(--ps-overlay-spinner-track);\n border-top-color: var(--ps-overlay-spinner-fill);\n border-radius: 50%;\n animation: controls-spin 0.8s linear infinite;\n }\n @keyframes controls-spin { to { transform: rotate(360deg); } }\n\n /* ═══ STATUS MESSAGE ═══ */\n .status-msg {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--ps-overlay-text-dim);\n }\n\n /* ═══ PILL BUTTON ═══ */\n .pill-btn {\n background: none;\n border: 1px solid var(--ps-overlay-pill-border);\n color: var(--ps-overlay-pill-text);\n padding: 0.3rem 0.9rem;\n border-radius: 16px;\n cursor: pointer;\n font-size: 0.7rem;\n font-family: inherit;\n transition: background 0.2s;\n }\n .pill-btn:hover { background: var(--ps-overlay-pill-hover); }\n\n /* ═══ ERROR MESSAGE ═══ */\n .error-message {\n font-size: 0.7rem;\n color: var(--ps-overlay-text-dim);\n max-width: 280px;\n text-align: center;\n word-break: break-word;\n }\n .error-message:empty { display: none; }\n </style>\n\n <video playsinline disablepictureinpicture></video>\n <div id="controls-overlay">\n <slot name="overlay">\n <div class="state state-idle">\n <button class="play-btn" data-action="play" aria-label="Start session">\n <svg viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21"/></svg>\n </button>\n <span class="status-msg">Start</span>\n </div>\n <div class="state state-pending">\n <div class="spinner"></div>\n <span class="status-msg" data-pending-msg>Connecting</span>\n <button class="pill-btn" data-action="cancel">Cancel</button>\n </div>\n <div class="state state-ready">\n <button class="play-btn" data-action="play" aria-label="Play stream">\n <svg viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21"/></svg>\n </button>\n <span class="status-msg">Tap to play</span>\n </div>\n <div class="state state-error">\n <span class="status-msg">Failed</span>\n <span class="error-message"></span>\n <button class="pill-btn" data-action="retry">Retry</button>\n </div>\n <div class="state state-ended">\n <span class="status-msg">Ended</span>\n <button class="pill-btn" data-action="restart">Restart</button>\n </div>\n </slot>\n </div>\n ',this.video=t.querySelector("video"),this.coordTranslator=new o(this.video),this.controlsOverlay=t.querySelector("#controls-overlay"),this.ye=t.querySelector('slot[name="overlay"]'),this.ye?.addEventListener("slotchange",()=>{this.be=(this.ye?.assignedNodes({flatten:0}).length??0)>0,this.controlsOverlay.classList.toggle("slotted",this.be),this._e()}),this.controlsOverlay.addEventListener("click",t=>{const e=t.target.closest("[data-action]");if(e)switch(e.dataset.action){case"play":case"retry":case"restart":this.play();break;case"cancel":this.cancel()}})}attributeChangedCallback(t,e,s){if(e!==s)switch(t){case"native-touch":this.dt=null!==s;break;case"pointer-lock":this.gt=null!==s;break;case"pointer-lock-release":this.ft=null!==s;break;case"suppress-browser-keys":this.vt=null!==s;break;case"enable-gamepad":null!==s&&null===e?this.gamepadInput.setup():null===s&&null!==e&&this.gamepadInput.teardown();break;case"enable-xr":null!==s&&null===e?this.xrInput.setup():null===s&&null!==e&&this.xrInput.teardown();break;case"admission-token":this.yt=s,this.dispatchEvent(new CustomEvent("admission-token-change",{detail:{oldValue:e,newValue:s}})),s&&this.isConnected&&this.Ve();break;case"app-id":this.bt=s,this.dispatchEvent(new CustomEvent("app-id-change",{detail:{oldValue:e,newValue:s}}));break;case"app-version":this.kt=s,this.dispatchEvent(new CustomEvent("app-version-change",{detail:{oldValue:e,newValue:s}}));break;case"no-auto-connect":this.Ct=null!==s,this.dispatchEvent(new CustomEvent("no-auto-connect-change",{detail:{oldValue:null!==e,newValue:null!==s}}));break;case"mute":this.St=null!==s,this.video&&(this.video.muted=this.St),this.webrtc.getAudioElement().muted=this.St,this.dispatchEvent(new CustomEvent("mute-change",{detail:{oldValue:null!==e,newValue:null!==s,muted:this.St}}));break;case"volume":{const t=Math.max(0,Math.min(1,+s||0));this.Et=t,this.video&&(this.video.volume=t),this.webrtc.getAudioElement().volume=t,this.dispatchEvent(new CustomEvent("volume-change",{detail:{oldValue:e?+e:1,newValue:t}}));break}case"rendezvous-preference":this.Rt=null!==s?parseInt(s,10):null,this.dispatchEvent(new CustomEvent("rendezvous-preference-change",{detail:{oldValue:e?parseInt(e,10):null,newValue:this.Rt}}));break;case"linger-preference":this.Tt=null!==s?parseInt(s,10):null,this.dispatchEvent(new CustomEvent("linger-preference-change",{detail:{oldValue:e?parseInt(e,10):null,newValue:this.Tt}}));break;case"left-grace-period":this.$t=null!==s?parseInt(s,10):null,this.dispatchEvent(new CustomEvent("left-grace-period-change",{detail:{oldValue:e?parseInt(e,10):null,newValue:this.$t}}));break;case"force-relay":this.Nt=null!==s;break;case"queue-wait-tolerance":this.Vt=null!==s?parseInt(s,10):null;break;case"webrtc-negotiation-tolerance":this.Ft=null!==s?parseInt(s,10):null;break;case"api-endpoint":this.wt=s||"api.interlucent.ai",this.dispatchEvent(new CustomEvent("api-endpoint-change",{detail:{oldValue:e,newValue:this.wt}}));break;case"reconnect-mode":"none"===s||"recover"===s||"always"===s?this.xt=s:(null===s||this.Me.warn(`Invalid reconnect-mode: ${s}, using "none"`),this.xt="none"),this.Me.debug("Reconnect mode set to: "+this.xt);break;case"reconnect-attempts":if(null===s)this.tt=1/0;else{const t=parseInt(s,10);isNaN(t)||t<-1?(this.Me.warn(`Invalid reconnect-attempts: ${s}, using Infinity`),this.tt=1/0):this.tt=-1===t||0===t?1/0:t}this.Me.debug("Reconnect attempts set to: "+(this.tt===1/0?"Infinity":this.tt));break;case"reconnect-strategy":"periodic"===s||"exponential-backoff"===s?this.Mt=s:(null===s||this.Me.warn(`Invalid reconnect-strategy: ${s}, using "exponential-backoff"`),this.Mt="exponential-backoff"),this.Me.debug("Reconnect strategy set to: "+this.Mt);break;case"reconnect-interval":if(null===s)this.It=I.DEFAULT_RECONNECT_INTERVAL;else{const t=parseInt(s,10);isNaN(t)||t<100?(this.Me.warn(`Invalid reconnect-interval: ${s}, using ${I.DEFAULT_RECONNECT_INTERVAL}ms`),this.It=I.DEFAULT_RECONNECT_INTERVAL):this.It=t}this.Me.debug(`Reconnect interval set to: ${this.It}ms`);break;case"disconnect-grace-ms":if(null===s)this.Pt=9e3;else{const t=parseInt(s,10);isNaN(t)||t<0?(this.Me.warn(`Invalid disconnect-grace-ms: ${s}, using 9000ms`),this.Pt=9e3):this.Pt=t}this.Me.debug(`Disconnect grace period set to: ${this.Pt}ms`);break;case"resize-mode":"none"===s||"auto"===s||"pureweb"===s||"pre-5.4"===s||"5.4+"===s?this.At=s:(null===s||this.Me.warn(`Invalid resize-mode: ${s}, using "5.4+"`),this.At="5.4+"),this.Qt=null,this.Yt=0,this.Zt=0,this.Me.debug("Resize mode set to: "+this.At);break;case"dpr-cap":if(null===s)this.Dt=1;else{const t=parseFloat(s);isNaN(t)||t<=0?(this.Me.warn(`Invalid dpr-cap: ${s}, using 1`),this.Dt=1):this.Dt=t}this.Me.debug("DPR cap set to: "+this.Dt);break;case"resolution-clamp":if(null===s)this._t=null;else{const t=s.toLowerCase().trim(),e=M[t];e?this._t=e:(this.Me.warn(`Invalid resolution-clamp: "${s}". Valid values: ${Object.keys(M).join(", ")}`),this._t=null)}this.Me.debug("Resolution clamp set to: "+(this._t?`${this._t.maxWidth}x${this._t.maxHeight}`:"none"));break;case"debug":this.Bt=null!==s,this.Jt();break;case"controls":this.Wt=null!==s&&"false"!==s,this._e();break;case"swift-job-request":this.Ut=null!==s;break;case"streamer-id":this.qt=s,this.Fe();break;case"lat":this.Lt=null!==s?parseFloat(s):null,null!==this.Lt&&isNaN(this.Lt)&&(this.Lt=null);break;case"lng":this.zt=null!==s?parseFloat(s):null,null!==this.zt&&isNaN(this.zt)&&(this.zt=null);break;case"stream-quality":"low-latency"===s||"balanced"===s||"quality"===s?this.jt=s:(null===s||this.Me.warn(`Invalid stream-quality: ${s}, using "balanced"`),this.jt="balanced");break;case"video-bitrate-min":this.Ht=null!==s&&parseInt(s,10)||null;break;case"video-bitrate-start":this.Gt=null!==s&&parseInt(s,10)||null;break;case"video-bitrate-max":this.Xt=null!==s&&parseInt(s,10)||null;break;case"log-level":"error"===s||"warn"===s||"info"===s||"debug"===s?this.Kt=s:(null===s||this.Me.warn(`Invalid log-level: ${s}, using "warn"`),this.Kt="warn")}}connectedCallback(){if(this.Me.info(`pixel-stream v${I.VERSION} mounted`),this.hasAttribute("tabindex")||this.setAttribute("tabindex","0"),this.setAttribute("job-state",this.Ie),this.setAttribute("session-state",this.ee),this.setAttribute("stream-state",this.se),this.setAttribute("status",this.ie),this.mouseInput.setup(),this.touchInput.setup(),this.keyboardInput.setup(),this.hasAttribute("enable-gamepad")&&this.gamepadInput.setup(),this.hasAttribute("enable-xr")&&this.xrInput.setup(),this.hasAttribute("admission-token")&&(this.yt=this.getAttribute("admission-token")),this.hasAttribute("app-id")&&(this.bt=this.getAttribute("app-id")),this.hasAttribute("app-version")&&(this.kt=this.getAttribute("app-version")),this.hasAttribute("no-auto-connect")&&(this.Ct=1),this.hasAttribute("mute")&&(this.St=1,this.video.muted=1,this.webrtc.getAudioElement().muted=1),this.hasAttribute("volume")){const t=Math.max(0,Math.min(1,+this.getAttribute("volume")||0));this.Et=t,this.video.volume=t,this.webrtc.getAudioElement().volume=t}this.hasAttribute("rendezvous-preference")&&(this.Rt=parseInt(this.getAttribute("rendezvous-preference"),10)),this.hasAttribute("linger-preference")&&(this.Tt=parseInt(this.getAttribute("linger-preference"),10)),this.hasAttribute("left-grace-period")&&(this.$t=parseInt(this.getAttribute("left-grace-period"),10)),this.hasAttribute("queue-wait-tolerance")&&(this.Vt=parseInt(this.getAttribute("queue-wait-tolerance"),10)),this.hasAttribute("webrtc-negotiation-tolerance")&&(this.Ft=parseInt(this.getAttribute("webrtc-negotiation-tolerance"),10)),this.hasAttribute("api-endpoint")&&(this.wt=this.getAttribute("api-endpoint")||"api.interlucent.ai"),this.hasAttribute("controls")&&(this.Wt="false"!==this.getAttribute("controls")),this._e(),this.hasAttribute("swift-job-request")&&(this.Ut=1),this.hasAttribute("force-relay")&&(this.Nt=1),this.hasAttribute("streamer-id")&&(this.qt=this.getAttribute("streamer-id")),this.video.addEventListener("loadedmetadata",()=>{this.updateCoordTranslator({final:1,cssW:this.video.clientWidth,cssH:this.video.clientHeight,...this.Be(this.video.clientWidth,this.video.clientHeight)})}),this.video.addEventListener("resize",()=>{this.dispatchEvent(new CustomEvent("stream-resolution-change",{detail:{width:this.video.videoWidth,height:this.video.videoHeight,...this._t?{clamp:{maxWidth:this._t.maxWidth,maxHeight:this._t.maxHeight}}:{}}}))}),this.resizeObserver=P({el:this.video,dprCap:()=>this.Dt,onUpdate:t=>this.updateCoordTranslator(t)}),this.Re=()=>{this.updateCoordTranslator({final:1,cssW:this.video.clientWidth,cssH:this.video.clientHeight,...this.Be(this.video.clientWidth,this.video.clientHeight)})},document.addEventListener("fullscreenchange",this.Re),this.Te=()=>{this.updateCoordTranslator({final:1,cssW:this.video.clientWidth,cssH:this.video.clientHeight,...this.Be(this.video.clientWidth,this.video.clientHeight)})},document.addEventListener("webkitfullscreenchange",this.Te),this.We(),this.addEventListener("initial-settings",this.Pe),this.dispatchEvent(new CustomEvent("ready",{detail:{admissionConfig:this.getAdmissionConfig(),sessionPreferences:this.getSessionPreferences(),noAutoConnect:this.Ct,mute:this.St}})),this.yt&&this.Ve()}disconnectedCallback(){this.Ue(),this.Re&&(document.removeEventListener("fullscreenchange",this.Re),this.Re=null),this.Te&&(document.removeEventListener("webkitfullscreenchange",this.Te),this.Te=null),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.qe(),this.removeEventListener("initial-settings",this.Pe),this.mouseInput.teardown(),this.gamepadInput.teardown(),this.xrInput.teardown(),this.cleanupEventListeners(),this.webrtc.closePeerConnection(),this.webrtc.disableMicrophone(),this.session.terminateSession("component-unmounted")}We(){this.Ee||(this.Ee=()=>{this.session.terminateSession("page-hide")},window.addEventListener("pagehide",this.Ee))}Ue(){this.Ee&&(window.removeEventListener("pagehide",this.Ee),this.Ee=null)}cleanupEventListeners(){this.keyboardInput.teardown(),this.mouseInput.teardown(),this.touchInput.teardown()}async startXRSession(){return this.xrInput.startSession()}endXRSession(){this.xrInput.endSession()}static async isXRSupported(){return C.isSupported()}resetPerformanceMetrics(){this.metrics.resetPerformanceMetrics()}getPerformanceMetrics(){return this.metrics.getPerformanceMetrics()}async collectAndDisplaySessionStats(){return this.metrics.collectAndDisplaySessionStats()}Be(t,e){const s=Math.min(window.devicePixelRatio||1,this.Dt);let i=Math.round(t*s),n=Math.round(e*s);return i&=-2,n&=-2,i=Math.max(2,i),n=Math.max(2,n),{dpr:s,devW:i,devH:n}}Le(t,e){const s=this._t;if(!s)return{devW:t,devH:e};const i=t>=e,n=i?s.maxWidth:s.maxHeight,r=i?s.maxHeight:s.maxWidth;if(t<=n&&e<=r)return{devW:t,devH:e};const a=t/e;let o,h;return a>n/r?(o=Math.min(t,n),h=Math.round(o/a)):(h=Math.min(e,r),o=Math.round(h*a)),o&=-2,h&=-2,o=Math.max(2,o),h=Math.max(2,h),{devW:o,devH:h}}updateCoordTranslator({final:t,devW:e,devH:s}){if(!t)return;const i=this.Le(e,s),n=i.devW,r=i.devH,a=n!==e||r!==s;this.dispatchEvent(new CustomEvent("stream-resolution-change",{detail:{width:n,height:r,viewport:{width:e,height:s},...this._t?{clamp:{maxWidth:this._t.maxWidth,maxHeight:this._t.maxHeight,applied:a}}:{}}})),a&&this.Me.debug(`Resolution clamped: ${e}x${s} → ${n}x${r} (max ${this._t.maxWidth}x${this._t.maxHeight})`),"none"!==this.At&&"open"===this.webrtc.getDataChannel()?.readyState&&("auto"===this.At?this.Zt?this.Qt?this.ze(this.Qt,n,r):this.ze("5.4+",n,r):(this.Me.info(`Sending initial resize shotgun: ${n}x${r} (all formats)`),this.ze("5.4+",n,r),this.ze("pre-5.4",n,r),this.ze("pureweb",n,r),this.Zt=1,this.Qt||this.Yt||this.Ne(n,r)):this.ze(this.At,n,r))}ze(t,e,s){"pureweb"===t?(this.sendUIInteraction({type:50,width:e,height:s,action:"1"}),this.Me.debug(`Sent Pureweb resize: ${e}x${s}`)):"pre-5.4"===t?(this.sendUIInteraction({Console:`r.setres ${e}x${s}w`}),this.Me.debug(`Sent legacy resize command: r.setres ${e}x${s}w`)):"5.4+"===t&&(this.sendCommand({"Resolution.Width":e,"Resolution.Height":s}),this.Me.debug(`Sent modern resize command: ${e}x${s}`))}Ne(t,e){if(this.Yt)return;this.Yt=1,this.$e={width:t,height:e};const s=["5.4+","pre-5.4","pureweb"];let i=0;const n=this.video.videoWidth,r=this.video.videoHeight;this.Me.info(`Starting resize auto-detection. Target: ${t}x${e}, Initial video: ${n}x${r}`);const a=()=>{if(i>=s.length)return this.Me.warn("Resize auto-detection: no mode verified, defaulting to 5.4+"),this.Qt="5.4+",this.Yt=0,void this.Oe("5.4+",0);const o=s[i];this.Me.debug(`Resize auto-detection: trying ${o} (attempt ${i+1}/${s.length})`),this.ze(o,t,e),this.xe=setTimeout(()=>{const s=this.video.videoWidth,h=this.video.videoHeight;(s!==n||h!==r)&&(Math.abs(s-t)<.1*t||s!==n)&&(Math.abs(h-e)<.1*e||h!==r)?(this.Me.info(`Resize auto-detection: ${o} verified (video: ${s}x${h})`),this.Qt=o,this.Yt=0,this.Oe(o,1)):(this.Me.debug(`Resize auto-detection: ${o} not verified (video: ${s}x${h})`),i++,a())},500)};a()}Oe(t,e){this.dispatchEvent(new CustomEvent("resize-mode-detected",{detail:{mode:t,verified:e}}))}qe(){this.xe&&(clearTimeout(this.xe),this.xe=null),this.Yt=0,this.Zt=0,this.Qt=null}Ae(t){if("auto"!==this.At||this.Qt)return;const e=t?.PixelStreamingSettings?.EngineVersion||t?.EngineVersion||t?.UEVersion;if(e){this.Me.debug("InitialSettings contains version: "+e);const t=this.je(e);t&&(this.Me.info(`Inferred resize mode from UE version ${e}: ${t}`),this.Qt=t,this.Oe(t,0))}const s=t?.PixelStreamingSettings?.WebRTCDisableResolutionChange;0==s&&(this.Me.debug("InitialSettings indicates WebRTC resize is enabled"),this.Qt||(this.Qt="5.4+",this.Oe("5.4+",0)))}je(t){const e=t.match(/(\d+)\.(\d+)/);if(!e)return null;const s=parseInt(e[1],10),i=parseInt(e[2],10);return s>=5&&i>=4?"5.4+":s>=4?"pre-5.4":null}ue(){const t=Array.from(this.webrtc.getVideoTracks().entries()),e=this.te,s=t.some(([t,{track:e}])=>/left|right|stereo|xr|hmd|vr/i.test(e.label)),i=2===t.length,n=()=>{if(0===this.video.videoWidth||0===this.video.videoHeight)return void this.video.addEventListener("loadedmetadata",n,{once:1});const r=this.video.videoWidth/this.video.videoHeight,a=r>3.5,o=s||i&&a;o&&!e?(this.te=1,this.Me.debug("Stream detected as XR/stereo",{tracks:t.length,aspectRatio:r.toFixed(2),resolution:`${this.video.videoWidth}x${this.video.videoHeight}`,hasXRLabels:s,hasStereoTracks:i,hasStereoAspect:a}),this.dispatchEvent(new CustomEvent("xr-stream-detected",{detail:{trackCount:this.webrtc.getVideoTracks().size,resolution:`${this.video.videoWidth}x${this.video.videoHeight}`,aspectRatio:r,hasXRLabels:s,hasStereoTracks:i,hasStereoAspect:a}}))):!o&&e&&(this.te=0,this.Me.debug("Stream no longer detected as XR"))};n()}sendToStreamer(t,e=[]){const s=Date.now();s-this.re>1e3&&this.dispatchEvent(new CustomEvent("interlucent:user-interaction")),this.re=s,this.webrtc.sendToStreamer(t,e)}sendRawBinary(t){this.webrtc.sendRawBinary(t)}sendCommand(t){this.webrtc.sendCommand(t)}sendUIInteraction(t){this.webrtc.sendUIInteraction(t)}requestIFrame(){this.webrtc.requestIFrame()}requestKeyFrame(){this.webrtc.requestKeyFrame()}requestQualityControl(){this.sendToStreamer("RequestQualityControl",[])}requestFps(t){this.sendToStreamer("FpsRequest",[t])}requestBitrate(){this.sendToStreamer("AverageBitrateRequest",[])}startStreaming(){this.sendToStreamer("StartStreaming",[])}stopStreaming(){this.sendToStreamer("StopStreaming",[])}sendTestEcho(t){this.sendToStreamer("TestEcho",[t])}async checkPermission(t){if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia)return"unavailable";if(navigator.permissions&&navigator.permissions.query)try{return(await navigator.permissions.query({name:t})).state}catch{}return"prompt"}async checkPermissions(t){const e={};for(const s of t)e[s]=await this.checkPermission(s);return e}async watchPermission(t){if(navigator.permissions&&navigator.permissions.query)try{const e=await navigator.permissions.query({name:t});e.addEventListener("change",()=>{this.dispatchEvent(new CustomEvent("permission-change",{detail:{permission:t,state:e.state}}))})}catch{}}async requestMicrophone(){return this.webrtc.requestMicrophone()}async enableMicrophone(){return this.webrtc.enableMicrophone()}disableMicrophone(){this.webrtc.disableMicrophone()}setMicrophoneMuted(t){this.webrtc.setMicrophoneMuted(t)}get isMicrophoneEnabled(){return this.webrtc.isMicrophoneEnabled}get isMicrophoneMuted(){return this.webrtc.isMicrophoneMuted}getVideoTracks(){const t=this.webrtc.getActiveVideoTrackId();return Array.from(this.webrtc.getVideoTracks().entries()).map(([e,{track:s}])=>({trackId:e,label:s.label||"Camera "+e,isActive:e===t}))}switchVideoTrack(t){this.webrtc.switchVideoTrack(t)}getActiveVideoTrack(){const t=this.webrtc.getActiveVideoTrackId();if(!t)return null;const e=this.webrtc.getVideoTracks().get(t);return e?{trackId:t,label:e.track.label||"Camera "+t}:null}He(t){const e=this.oe;this.oe=t,t&&!e?(this.dispatchEvent(new CustomEvent("admitted",{detail:{admissionConfig:this.getAdmissionConfig(),sessionPreferences:this.getSessionPreferences()}})),this.Rt&&this.Rt>0&&this.Ge()):!t&&e&&(this.dispatchEvent(new CustomEvent("admission-revoked",{detail:{reason:"revoked"}})),this.me(),this.Xe())}Ge(){this.me(),!this.Rt||this.Rt<=0||(this.Me.info(`Starting rendezvous timer: ${this.Rt}s`),this.dispatchEvent(new CustomEvent("rendezvous-started",{detail:{timeoutSeconds:this.Rt}})),this.Ce=setTimeout(()=>{this.Me.info("Rendezvous timeout - no peer connected"),this.dispatchEvent(new CustomEvent("rendezvous-timeout",{detail:{timeoutSeconds:this.Rt,admissionConfig:this.getAdmissionConfig()}}))},1e3*this.Rt))}me(){this.Ce&&(clearTimeout(this.Ce),this.Ce=null,this.dispatchEvent(new CustomEvent("rendezvous-cancelled")))}Ke(){this.Xe(),!this.Tt||this.Tt<=0?this.dispatchEvent(new CustomEvent("linger-timeout",{detail:{timeout:0,reason:"no-linger-preference"}})):(this.Me.info(`Starting linger timer: ${this.Tt}s`),this.dispatchEvent(new CustomEvent("linger-started",{detail:{timeoutSeconds:this.Tt}})),this.Se=setTimeout(()=>{this.Me.info("Linger timeout - disconnecting"),this.dispatchEvent(new CustomEvent("linger-timeout",{detail:{timeoutSeconds:this.Tt,reason:"timeout"}}))},1e3*this.Tt))}Xe(){this.Se&&(clearTimeout(this.Se),this.Se=null,this.dispatchEvent(new CustomEvent("linger-cancelled")))}Je(t){this.me(),this.Xe(),this.dispatchEvent(new CustomEvent("peer-connected",{detail:{peerId:t}}))}Qe(t,e=1){this.dispatchEvent(new CustomEvent("peer-disconnected",{detail:{peerId:t,isLastPeer:e}})),e&&this.Ke()}Ye(){if(!this.yt){const t=Error("admission-token is required but not set");throw this.dispatchEvent(new CustomEvent("admission-error",{detail:{error:t.message}})),t}return this.getAdmissionConfig()}getLastReceivedFile(){return this.fileTransfer.getLastReceivedFile()}async waitForFile(){return this.fileTransfer.waitForFile()}setVideoSource(t){this.video.srcObject=t}async startSession(){await this.session.startSession()}Ve(){this.Ct||"idle"!==this.ee&&"error"!==this.ee||this.startSession().catch(t=>this.Me.error("Auto session start failed",t))}sendSessionMessage(t){this.session.sendMessage(t)}le(t){if(this.Ie===t)return;const e=this.Ie;this.Ie=t,this.Me.debug(`Job state: ${e} -> ${t}`),this.setAttribute("job-state",t),this.dispatchEvent(new CustomEvent("job-state-change",{detail:{oldState:e,newState:t}})),this._e(),this.fe()}_e(){if(this.be)return void this.controlsOverlay.classList.add("visible");const t="failed"===this.ie||"ended"===this.ie||"ready"===this.Ie;this.Wt||t?(this.controlsOverlay.classList.add("visible"),this.Ze(),this.ts()):this.controlsOverlay.classList.remove("visible")}es(t){switch(t){case"token_expired":return"Admission token has expired";case"unauthorized":return"Invalid admission token";case"server_error":return"Server error — try again later";case"request_failed":return"Could not reach server";case"connection_failed":return"Connection failed";case"disconnected":return"Connection lost";case"reconnect_exhausted":return"Reconnection attempts exhausted";case"no_access_token":return"No access token for recovery";case"not_connected":return"Session was not connected";case"media_pipeline_failure":return"Media pipeline error";default:return t||"An unexpected error occurred"}}Ze(){const t=this.controlsOverlay.querySelector("[data-pending-msg]");if(t)switch(this.ie){case"connecting":default:t.textContent="Connecting";break;case"authenticating":t.textContent="Authenticating";break;case"queued":t.textContent="Queued";break;case"rendezvoused":t.textContent="Agent matched";break;case"negotiating":t.textContent="Starting stream"}}ts(){const t=this.controlsOverlay.querySelector(".error-message");t&&(t.textContent=this.ne?this.es(this.ne):"")}async requestJob(){if(this.session.isCollaborator)return void this.Me.warn("session-collaborator cannot request jobs");if("pending"===this.Ie)return void this.Me.debug("Job request already in flight — ignoring");const t="failed"===this.ie;if(t&&(this.le("unsubmitted"),this.pe("idle")),this.session.ws?.readyState===WebSocket.OPEN)return this.Me.info("Requesting job..."),this.le("pending"),this.performanceMetrics.jobRequested=performance.now(),void this.sendSessionMessage({type:"request-job"});if("connecting"!==this.ee&&"authenticating"!==this.ee){if(this.config.admissionToken){if(t){this.Me.info("Error recovery — restarting session with swift job request...");const t=this.Ut;this.Ut=1;try{return void await this.session.startSession()}catch(t){return void this.Me.error("Failed to restart session",t)}finally{this.Ut=t}}return this.Me.info("Starting session — job will be requested on connect"),void this.startSession().catch(t=>this.Me.error("Failed to start session",t))}this.Me.warn("Cannot request job — no active session and no admission token"),this.dispatchEvent(new CustomEvent("session-error",{detail:{error:"No active session",phase:"job-request"}}))}else this.Me.info("Session connecting — job will be requested on connect")}async play(){if(!this.ce)return this.ke=1,this.requestJob();try{await this.video.play(),this.ce=0,this.le("fulfilled")}catch(t){throw this.Me.warn("Play still blocked",t),t}}cancel(){if(!this.session.ws||this.session.ws.readyState!==WebSocket.OPEN)return this.Me.warn("Cannot cancel job - WebSocket not connected"),void this.dispatchEvent(new CustomEvent("session-error",{detail:{error:"WebSocket not connected",phase:"job-cancel"}}));this.Me.info("Cancelling job..."),this.sendSessionMessage({type:"cancel-job"})}requestInvite(){this.session.requestInvite()}stop(t){this.le("unsubmitted"),this.we("ended"),this.webrtc.closePeerConnection(),this.session.leaveSession(t),this.webrtc.resetResilienceState()}async resumePlayback(){return this.play()}cancelJob(){this.cancel()}leaveSession(t){this.stop(t)}subscribeToStreamer(t){this.session.subscribeToStreamer(t)}get availableStreamers(){return this.session.availableStreamers}Fe(){this.session.evaluateSubscription()}Jt(){if(this.classList.remove("session-connecting","session-connected","session-error"),this.Bt)switch(this.ee){case"connecting":case"authenticating":case"reconnecting":this.classList.add("session-connecting");break;case"connected":this.classList.add("session-connected");break;case"error":this.classList.add("session-error")}}de(t,e){if(this.se===t)return;const s=this.se;this.se=t,"streaming"===t&&"streaming"!==s&&(this.re=Date.now(),this.ae=Date.now(),this.ke=0),this.Me.debug(`Stream state: ${s} -> ${t}`),this.setAttribute("stream-state",t),this.dispatchEvent(new CustomEvent("stream-state-change",{detail:{oldState:s,newState:t,...e}})),this.Jt(),this.fe()}pe(t){const e=this.ee;this.ee=t,e!==t&&("connecting"===t&&(this.ge=0,this.ve=0,this.ie="idle",this.ne=null,this.ae=0),"connected"!==t||this.oe?"error"!==t&&"idle"!==t||!this.oe||this.He(0):this.He(1),("error"===t||"idle"===t&&"idle"!==e)&&(this.ke=0),this.Me.debug(`Session state: ${e} -> ${t}`),this.setAttribute("session-state",t),this.dispatchEvent(new CustomEvent("session-state-change",{detail:{oldState:e,newState:t}})),this.Jt(),this._e(),this.fe(),"connected"===t&&this.ke&&"unsubmitted"===this.Ie&&this.requestJob())}we(t,e){if(this.ie===t)return;const s=this.ie;this.ie=t,this.ne=e??null,this.Me.debug(`Status: ${s} -> ${t}${e?` (${e})`:""}`),this.setAttribute("status",t),this.dispatchEvent(new CustomEvent("status-change",{detail:{oldStatus:s,newStatus:t,failureReason:this.ne}})),this._e()}fe(){"failed"!==this.ie&&"ended"!==this.ie&&("failed"===this.se?this.we("failed","media_pipeline_failure"):"recovering"===this.se?this.we("recovering"):"interrupted"===this.se?this.we("interrupted"):"ready"===this.Ie?this.we("ready"):"streaming"===this.se?this.we("streaming"):this.ve?this.we("negotiating"):this.ge?this.we("rendezvoused"):"pending"===this.Ie&&"connected"===this.ee?this.we("queued"):"connected"===this.ee&&"unsubmitted"===this.Ie?this.we("connected"):"reconnecting"===this.ee?this.we("connecting"):"authenticating"===this.ee?this.we("authenticating"):"connecting"===this.ee?this.we("connecting"):"idle"===this.ee&&this.we("idle"))}terminateSession(){this.session.terminateSession(),this.webrtc.resetResilienceState()}}if(I.VERSION="0.0.78",I.DEFAULT_RECONNECT_INTERVAL=3e4,"undefined"!=typeof document){const t=[],e=document.querySelector('meta[name="ps-signaling-server"]');if(e){const s=e.getAttribute("content");s&&t.push(s)}const s=document.querySelector('meta[name="ps-ice-servers"]');if(s){const e=s.getAttribute("content");e&&t.push(...e.split(",").map(t=>t.trim()).filter(t=>t))}0===t.length&&t.push("stun:stun.cloudflare.com:3478"),t.forEach(t=>{try{let e=t;const s=t.startsWith("stun:")||t.startsWith("turn:")||t.startsWith("turns:");t.startsWith("stun:")?e=t.replace("stun:","https://"):t.startsWith("turn:")?e=t.replace("turn:","https://"):t.startsWith("turns:")&&(e=t.replace("turns:","https://"));const i=new URL(e).origin;if(document.querySelector(`link[rel="dns-prefetch"][href="${i}"]`))return;const n=document.createElement("link");if(n.rel="dns-prefetch",n.href=i,document.head.appendChild(n),!s){const t=document.createElement("link");t.rel="preconnect",t.href=i,document.head.appendChild(t)}}catch(t){}})}function P(t){const{el:e,onUpdate:s,settleMs:i=200,dprCap:n=1/0,even:r=1,cssJitterPx:a=0}=t;let o=null,h=null,c=-1,l=-1,u=-1,d=-1,p=-1,m=-1;const g=(t,e,s)=>Math.abs(t-e)<=s;function f(){return{cssW:e.clientWidth,cssH:e.clientHeight}}function v(t){const{cssW:e,cssH:i}=f();if(e<=0||i<=0)return;const{dpr:a,devW:o,devH:h}=function(t,e){const s="function"==typeof n?n():n,i=Math.min(window.devicePixelRatio||1,s);let a=Math.round(t*i),o=Math.round(e*i);return r&&(a&=-2,o&=-2),a=Math.max(r?2:1,a),o=Math.max(r?2:1,o),{dpr:i,devW:a,devH:o}}(e,i);s({final:t,cssW:e,cssH:i,dpr:a,devW:o,devH:h})}const w=new ResizeObserver(()=>{const{cssW:t,cssH:e}=f();(a>0?g(t,c,a)&&g(e,l,a):t===c&&e===l)||(c=t,l=e,null===o&&(o=requestAnimationFrame(()=>{o=null;const{cssW:t,cssH:e}=f();(a>0?g(t,u,a)&&g(e,d,a):t===u&&e===d)||(u=t,d=e,v(0))})),null!==h&&clearTimeout(h),h=window.setTimeout(()=>{h=null;const{cssW:t,cssH:e}=f();(a>0?g(t,p,a)&&g(e,m,a):t===p&&e===m)||(p=t,m=e,v(1))},i))});return w.observe(e),w.dispose=()=>{w.disconnect(),null!==o&&cancelAnimationFrame(o),o=null,null!==h&&clearTimeout(h),h=null},w}return g("setLogLevel",function(t){u=t;try{localStorage.setItem(l,t)}catch{}}),g("clearLogLevel",function(){u=null;try{localStorage.removeItem(l)}catch{}}),function(t){const e=(m.versions||Object.defineProperty(m,"versions",{value:{},writable:0,enumerable:1,configurable:0}),m.versions);Object.getOwnPropertyDescriptor(e,t)||Object.defineProperty(e,t,{value:"0.0.78",writable:0,enumerable:1,configurable:0})}("pixel-stream"),customElements.define("pixel-stream",I),t.PixelStream=I,t.PixelStreamResizeObserverForVideo=P,t}({});
|
|
1
|
+
/*! @interlucent/pixel-stream v0.0.79 | (c) Interlucent */
|
|
2
|
+
var t=function(t){"use strict";class e{constructor(){this.mimetype="",this.extension="",this.receiving=0,this.chunks=0,this.data=[],this.valid=0,this.timestampStart=0}}var s,i;!function(t){t[t.IFrameRequest=0]="IFrameRequest",t[t.RequestQualityControl=1]="RequestQualityControl",t[t.FpsRequest=2]="FpsRequest",t[t.AverageBitrateRequest=3]="AverageBitrateRequest",t[t.StartStreaming=4]="StartStreaming",t[t.StopStreaming=5]="StopStreaming",t[t.LatencyTest=6]="LatencyTest",t[t.RequestInitialSettings=7]="RequestInitialSettings",t[t.TestEcho=8]="TestEcho",t[t.DataChannelLatencyTest=9]="DataChannelLatencyTest",t[t.UIInteraction=50]="UIInteraction",t[t.Command=51]="Command",t[t.TextboxEntry=52]="TextboxEntry",t[t.KeyDown=60]="KeyDown",t[t.KeyUp=61]="KeyUp",t[t.KeyPress=62]="KeyPress",t[t.MouseEnter=70]="MouseEnter",t[t.MouseLeave=71]="MouseLeave",t[t.MouseDown=72]="MouseDown",t[t.MouseUp=73]="MouseUp",t[t.MouseMove=74]="MouseMove",t[t.MouseWheel=75]="MouseWheel",t[t.MouseDouble=76]="MouseDouble",t[t.TouchStart=80]="TouchStart",t[t.TouchEnd=81]="TouchEnd",t[t.TouchMove=82]="TouchMove",t[t.GamepadButtonPressed=90]="GamepadButtonPressed",t[t.GamepadButtonReleased=91]="GamepadButtonReleased",t[t.GamepadAnalog=92]="GamepadAnalog",t[t.GamepadConnected=93]="GamepadConnected",t[t.GamepadDisconnected=94]="GamepadDisconnected",t[t.XRHMDTransform=100]="XRHMDTransform",t[t.XREyeViews=101]="XREyeViews",t[t.XRControllerTransform=102]="XRControllerTransform",t[t.XRSystem=103]="XRSystem",t[t.XRButtonPressed=104]="XRButtonPressed",t[t.XRButtonReleased=105]="XRButtonReleased",t[t.XRButtonTouched=106]="XRButtonTouched",t[t.XRButtonTouchReleased=107]="XRButtonTouchReleased",t[t.XRAnalog=108]="XRAnalog"}(s||(s={})),function(t){t[t.QualityControlOwnership=0]="QualityControlOwnership",t[t.Response=1]="Response",t[t.Command=2]="Command",t[t.FreezeFrame=3]="FreezeFrame",t[t.UnfreezeFrame=4]="UnfreezeFrame",t[t.VideoEncoderAvgQP=5]="VideoEncoderAvgQP",t[t.LatencyTest=6]="LatencyTest",t[t.InitialSettings=7]="InitialSettings",t[t.FileExtension=8]="FileExtension",t[t.FileMimeType=9]="FileMimeType",t[t.FileContents=10]="FileContents",t[t.TestEcho=11]="TestEcho",t[t.InputControlOwnership=12]="InputControlOwnership",t[t.GamepadResponse=13]="GamepadResponse",t[t.DataChannelLatencyTest=14]="DataChannelLatencyTest",t[t.Protocol=255]="Protocol"}(i||(i={}));const n=new Map([["IFrameRequest",{id:0,structure:[]}],["RequestQualityControl",{id:1,structure:[]}],["FpsRequest",{id:2,structure:[]}],["AverageBitrateRequest",{id:3,structure:[]}],["StartStreaming",{id:4,structure:[]}],["StopStreaming",{id:5,structure:[]}],["LatencyTest",{id:6,structure:["string"]}],["RequestInitialSettings",{id:7,structure:[]}],["TestEcho",{id:8,structure:[]}],["DataChannelLatencyTest",{id:9,structure:[]}],["UIInteraction",{id:50,structure:["string"]}],["Command",{id:51,structure:["string"]}],["TextboxEntry",{id:52,structure:["string"]}],["KeyDown",{id:60,structure:["uint8","uint8"]}],["KeyUp",{id:61,structure:["uint8"]}],["KeyPress",{id:62,structure:["uint16"]}],["MouseEnter",{id:70,structure:[]}],["MouseLeave",{id:71,structure:[]}],["MouseDown",{id:72,structure:["uint8","uint16","uint16"]}],["MouseUp",{id:73,structure:["uint8","uint16","uint16"]}],["MouseMove",{id:74,structure:["uint16","uint16","int16","int16"]}],["MouseWheel",{id:75,structure:["int16","uint16","uint16"]}],["MouseDouble",{id:76,structure:["uint8","uint16","uint16"]}],["TouchStart",{id:80,structure:["uint8","uint16","uint16","uint8","uint8","uint8"]}],["TouchEnd",{id:81,structure:["uint8","uint16","uint16","uint8","uint8","uint8"]}],["TouchMove",{id:82,structure:["uint8","uint16","uint16","uint8","uint8","uint8"]}],["GamepadButtonPressed",{id:90,structure:["uint8","uint8","uint8"]}],["GamepadButtonReleased",{id:91,structure:["uint8","uint8","uint8"]}],["GamepadAnalog",{id:92,structure:["uint8","uint8","double"]}],["GamepadConnected",{id:93,structure:[]}],["GamepadDisconnected",{id:94,structure:["uint8"]}],["XRHMDTransform",{id:100,structure:Array(16).fill("float")}],["XREyeViews",{id:101,structure:Array(80).fill("float")}],["XRControllerTransform",{id:102,structure:[...Array(16).fill("float"),"uint8"]}],["XRSystem",{id:103,structure:["uint8"]}],["XRButtonPressed",{id:104,structure:["uint8","uint8","uint8","float"]}],["XRButtonReleased",{id:105,structure:["uint8","uint8","uint8"]}],["XRButtonTouched",{id:106,structure:["uint8","uint8","uint8"]}],["XRButtonTouchReleased",{id:107,structure:["uint8","uint8","uint8"]}],["XRAnalog",{id:108,structure:["uint8","uint8","float"]}]]),r=new Map([[0,"QualityControlOwnership"],[1,"Response"],[2,"Command"],[3,"FreezeFrame"],[4,"UnfreezeFrame"],[5,"VideoEncoderAvgQP"],[6,"LatencyTest"],[7,"InitialSettings"],[8,"FileExtension"],[9,"FileMimeType"],[10,"FileContents"],[11,"TestEcho"],[12,"InputControlOwnership"],[13,"GamepadResponse"],[14,"DataChannelLatencyTest"],[255,"Protocol"]]);class a{static encode(t,e=[]){const s=n.get(t);if(!s)return new ArrayBuffer(0);let i=1;e.forEach((t,e)=>{switch(s.structure[e]){case"uint8":i+=1;break;case"uint16":case"int16":i+=2;break;case"float":i+=4;break;case"double":i+=8;break;case"string":i+=2,i+=2*t.length}});const r=new ArrayBuffer(i),a=new DataView(r);let o=0;return a.setUint8(o++,s.id),e.forEach((t,e)=>{switch(s.structure[e]){case"uint8":a.setUint8(o,t),o+=1;break;case"uint16":a.setUint16(o,t,1),o+=2;break;case"int16":a.setInt16(o,t,1),o+=2;break;case"float":a.setFloat32(o,t,1),o+=4;break;case"double":a.setFloat64(o,t,1),o+=8;break;case"string":const e=t;a.setUint16(o,e.length,1),o+=2;for(let t=0;t<e.length;t++)a.setUint16(o,e.charCodeAt(t),1),o+=2}}),r}static decode(t){const e=new Uint8Array(t);if(0===e.length)return{type:"Unknown",payload:null};const s=e[0],i=r.get(s);if(!i)return{type:"Unknown",payload:null};const n=new DataView(t);switch(i){case"Response":case"Command":case"LatencyTest":case"TestEcho":case"FileExtension":case"FileMimeType":case"InitialSettings":case"Protocol":case"GamepadResponse":case"DataChannelLatencyTest":return{type:i,payload:this.decodeStringPayload(e)};case"QualityControlOwnership":case"InputControlOwnership":return e.length<2?{type:i,payload:0}:{type:i,payload:!!e[1]};case"VideoEncoderAvgQP":return e.length>=5?{type:i,payload:n.getFloat32(1,1)}:e.length>=2?{type:i,payload:e[1]}:{type:i,payload:null};case"FileContents":if(e.length<5)return{type:i,payload:new Uint8Array(0)};const t=n.getInt32(1,1),s=Math.min(5+t,e.length);return{type:i,payload:e.slice(5,s)};case"FreezeFrame":return e.length>1?{type:i,payload:e.slice(1)}:{type:i,payload:null};case"UnfreezeFrame":return{type:i,payload:null};default:return{type:i,payload:e.slice(1)}}}static decodeStringPayload(t){if(t.length<2)return null;const e=new TextDecoder("utf-16").decode(t.slice(1));try{return JSON.parse(e)}catch{return e}}}class o{constructor(t){this.video=t}computeAspectRatio(){const t=this.video.clientWidth,e=this.video.clientHeight,s=this.video.videoWidth,i=e/t,n=this.video.videoHeight/s,r=i>n;return{pw:t,ph:e,playerIsLarger:r,ratio:r?i/n:n/i}}translateUnsigned(t,e){const{pw:s,ph:i,playerIsLarger:n,ratio:r}=this.computeAspectRatio(),a=n?t/s:r*(t/s-.5)+.5,o=n?r*(e/i-.5)+.5:e/i;return a<0||a>1||o<0||o>1?{inRange:0,x:65535,y:65535}:{inRange:1,x:Math.floor(65536*a),y:Math.floor(65536*o)}}translateSigned(t,e){const{pw:s,ph:i,playerIsLarger:n,ratio:r}=this.computeAspectRatio(),a=n?r*e/(.5*i):e/(.5*i);return{x:Math.floor(32767*(n?t/(.5*s):r*t/(.5*s))),y:Math.floor(32767*a)}}}const h={error:0,warn:1,info:2,debug:3};function c(t,e,s,i){const n=(n,r,a,o)=>{if(h[n]>h[i()])return;const c={level:n,source:e,module:s,message:r,format:o?.format??"text",data:a,timestamp:performance.now()};t.dispatchEvent(new CustomEvent("interlucent:log",{detail:c}))};return{error:(t,e,s)=>n("error",t,e,s),warn:(t,e,s)=>n("warn",t,e,s),info:(t,e,s)=>n("info",t,e,s),debug:(t,e,s)=>n("debug",t,e,s)}}const l="interlucent:logLevel";let u=null;function d(t="info"){return()=>u??("function"==typeof t?t():t)}var p;u=function(){try{const t=localStorage.getItem(l);return t&&t in h?t:null}catch{return null}}();const m=(p=globalThis).interlucent??(p.interlucent={});function g(t,e){m[t]=e}const f={8:8,46:127};class v{constructor(t,e){this.ctx=t,this.log=e,this.keyDownHandler=null,this.keyUpHandler=null,this.keyPressHandler=null,this.activeKeys=new Set}setup(){this.keyDownHandler=t=>{const e=t.keyCode||t.which;this.activeKeys.add(e),this.ctx.sendToStreamer("KeyDown",[e,t.repeat?1:0]);const s=f[e];void 0!==s&&this.ctx.sendToStreamer("KeyPress",[s]),this.ctx.config.suppressBrowserKeys&&this.isBrowserKey(e)&&t.preventDefault()},this.keyUpHandler=t=>{const e=t.keyCode||t.which;this.activeKeys.delete(e),this.ctx.sendToStreamer("KeyUp",[e]),this.ctx.config.suppressBrowserKeys&&this.isBrowserKey(e)&&t.preventDefault()},this.keyPressHandler=t=>{const e=t.keyCode||t.which;this.ctx.sendToStreamer("KeyPress",[e])},document.addEventListener("keydown",this.keyDownHandler),document.addEventListener("keyup",this.keyUpHandler),document.addEventListener("keypress",this.keyPressHandler)}teardown(){this.keyDownHandler&&document.removeEventListener("keydown",this.keyDownHandler),this.keyUpHandler&&document.removeEventListener("keyup",this.keyUpHandler),this.keyPressHandler&&document.removeEventListener("keypress",this.keyPressHandler)}releaseAllKeys(){this.activeKeys.forEach(t=>{this.ctx.sendToStreamer("KeyUp",[t])}),this.activeKeys.clear()}isBrowserKey(t){return t>=112&&t<=123||9===t||8===t||46===t}}class w{constructor(t,e,s){this.ctx=t,this.log=e,this.mouseMoveHandler=null,this.virtualMouseX=0,this.virtualMouseY=0,this.normalizedCoord={inRange:1,x:0,y:0},this.mouseButtonDown=0,this.onPointerUnlocked=s?.onPointerUnlocked}getNormalizedCoord(){return this.normalizedCoord}setup(){const t=this.ctx.getVideoElement().getBoundingClientRect();this.virtualMouseX=t.width/2,this.virtualMouseY=t.height/2,this.setupMouseHandlers(),this.ctx.config.pointerLocked&&this.setupPointerLock()}teardown(){this.mouseMoveHandler&&document.removeEventListener("mousemove",this.mouseMoveHandler),this.exitPointerLock()}setupPointerLock(){if(!this.ctx.config.pointerLocked)return;const t=this.ctx.getVideoElement();t.requestPointerLock=t.requestPointerLock||t.mozRequestPointerLock,document.exitPointerLock=document.exitPointerLock||document.mozExitPointerLock,this.ctx.config.pointerLockRelease?(this.ctx.getVideoElement().addEventListener("mousedown",()=>{this.mouseButtonDown=1,t.requestPointerLock&&t.requestPointerLock()}),document.addEventListener("mouseup",()=>{this.mouseButtonDown=0,this.isPointerLocked()&&document.exitPointerLock()})):this.ctx.getVideoElement().addEventListener("click",()=>{t.requestPointerLock&&t.requestPointerLock()});const e=()=>{const t=document.pointerLockElement||document.mozPointerLockElement;t===this.ctx.getVideoElement()||t===this.ctx.host?(this.log.debug("Pointer locked"),this.ctx.host.dispatchEvent(new CustomEvent("pointerlocked")),this.ctx.config.pointerLockRelease&&!this.mouseButtonDown&&document.exitPointerLock()):(this.log.debug("Pointer unlocked"),this.onPointerUnlocked?.(),this.ctx.host.dispatchEvent(new CustomEvent("pointerunlocked")))};document.addEventListener("pointerlockchange",e),document.addEventListener("mozpointerlockchange",e)}exitPointerLock(){document.exitPointerLock&&this.isPointerLocked()&&document.exitPointerLock()}isPointerLocked(){const t=document.pointerLockElement||document.mozPointerLockElement;return t===this.ctx.getVideoElement()||t===this.ctx.host}setupMouseHandlers(){const t=this.ctx.getVideoElement(),e=this.ctx.getCoordTranslator();this.mouseMoveHandler=s=>{if(this.ctx.config.pointerLocked&&this.isPointerLocked()){const i=t.getBoundingClientRect();for(this.virtualMouseX+=s.movementX,this.virtualMouseY+=s.movementY;this.virtualMouseX>i.width;)this.virtualMouseX-=i.width;for(;this.virtualMouseY>i.height;)this.virtualMouseY-=i.height;for(;this.virtualMouseX<0;)this.virtualMouseX+=i.width;for(;this.virtualMouseY<0;)this.virtualMouseY+=i.height;this.normalizedCoord=e.translateUnsigned(this.virtualMouseX,this.virtualMouseY);const n=e.translateSigned(s.movementX,s.movementY);this.ctx.sendToStreamer("MouseMove",[this.normalizedCoord.x,this.normalizedCoord.y,n.x,n.y])}else{const t=e.translateUnsigned(s.offsetX,s.offsetY),i=e.translateSigned(s.movementX,s.movementY);this.ctx.sendToStreamer("MouseMove",[t.x,t.y,i.x,i.y])}s.preventDefault()},this.ctx.config.pointerLocked?document.addEventListener("mousemove",this.mouseMoveHandler):t.addEventListener("mousemove",this.mouseMoveHandler),t.addEventListener("mousedown",t=>{const s=this.ctx.config.pointerLocked&&this.isPointerLocked()?this.normalizedCoord:e.translateUnsigned(t.offsetX,t.offsetY);this.ctx.sendToStreamer("MouseDown",[t.button,s.x,s.y]),t.preventDefault()}),t.addEventListener("mouseup",t=>{const s=this.ctx.config.pointerLocked&&this.isPointerLocked()?this.normalizedCoord:e.translateUnsigned(t.offsetX,t.offsetY);this.ctx.sendToStreamer("MouseUp",[t.button,s.x,s.y]),t.preventDefault()}),t.addEventListener("dblclick",t=>{const s=this.ctx.config.pointerLocked&&this.isPointerLocked()?this.normalizedCoord:e.translateUnsigned(t.offsetX,t.offsetY);this.ctx.sendToStreamer("MouseDouble",[t.button,s.x,s.y])}),t.addEventListener("wheel",t=>{const s=this.ctx.config.pointerLocked&&this.isPointerLocked()?this.normalizedCoord:e.translateUnsigned(t.offsetX,t.offsetY),i=void 0!==t.wheelDelta?t.wheelDelta:-t.deltaY;this.ctx.sendToStreamer("MouseWheel",[i,s.x,s.y]),t.preventDefault()}),t.addEventListener("contextmenu",t=>{t.preventDefault()}),t.addEventListener("mouseenter",()=>{this.ctx.sendToStreamer("MouseEnter",[])}),t.addEventListener("mouseleave",()=>{this.ctx.sendToStreamer("MouseLeave",[])})}}class y{constructor(t,e){this.ctx=t,this.log=e,this.fingerIds=new Map,this.availableFingers=[9,8,7,6,5,4,3,2,1,0],this.fakeTouchFinger=null}setup(){const t=this.ctx.getVideoElement();t.addEventListener("touchstart",t=>{this.ctx.config.nativeTouch?this.handleRealTouchStart(t):this.handleFakeTouchStart(t)}),t.addEventListener("touchmove",t=>{this.ctx.config.nativeTouch?this.handleRealTouchMove(t):this.handleFakeTouchMove(t)}),t.addEventListener("touchend",t=>{this.ctx.config.nativeTouch?this.handleRealTouchEnd(t):this.handleFakeTouchEnd(t)})}teardown(){}handleRealTouchStart(t){for(let e=0;e<t.changedTouches.length;e++){const s=t.changedTouches[e],i=this.availableFingers.pop();if(void 0===i){this.log.warn("Exhausted touch identifiers");continue}this.fingerIds.set(s.identifier,i);const n=this.getTouchCoord(s);this.ctx.sendToStreamer("TouchStart",[1,n.x,n.y,i,Math.floor(255*(s.force>0?s.force:1)),n.inRange?1:0])}t.preventDefault()}handleRealTouchMove(t){for(let e=0;e<t.touches.length;e++){const s=t.touches[e],i=this.fingerIds.get(s.identifier);if(void 0===i)continue;const n=this.getTouchCoord(s);this.ctx.sendToStreamer("TouchMove",[1,n.x,n.y,i,Math.floor(255*(s.force>0?s.force:1)),n.inRange?1:0])}t.preventDefault()}handleRealTouchEnd(t){for(let e=0;e<t.changedTouches.length;e++){const s=t.changedTouches[e],i=this.fingerIds.get(s.identifier);if(void 0===i)continue;const n=this.getTouchCoord(s);this.ctx.sendToStreamer("TouchEnd",[1,n.x,n.y,i,Math.floor(255*s.force),n.inRange?1:0]),this.fingerIds.delete(s.identifier),this.availableFingers.push(i),this.availableFingers.sort((t,e)=>e-t)}t.preventDefault()}handleFakeTouchStart(t){if(null===this.fakeTouchFinger){const e=t.changedTouches[0],s=this.ctx.getVideoElement().getBoundingClientRect(),i=e.clientX-s.left,n=e.clientY-s.top,r=this.ctx.getCoordTranslator().translateUnsigned(i,n);this.fakeTouchFinger={id:e.identifier,x:i,y:n},this.ctx.sendToStreamer("MouseEnter",[]),this.ctx.sendToStreamer("MouseDown",[0,r.x,r.y])}t.preventDefault()}handleFakeTouchMove(t){if(null!==this.fakeTouchFinger){for(let e=0;e<t.touches.length;e++){const s=t.touches[e];if(s.identifier===this.fakeTouchFinger.id){const t=this.ctx.getVideoElement().getBoundingClientRect(),e=s.clientX-t.left,i=s.clientY-t.top,n=e-this.fakeTouchFinger.x,r=i-this.fakeTouchFinger.y,a=this.ctx.getCoordTranslator(),o=a.translateUnsigned(e,i),h=a.translateSigned(n,r);this.ctx.sendToStreamer("MouseMove",[o.x,o.y,h.x,h.y]),this.fakeTouchFinger.x=e,this.fakeTouchFinger.y=i;break}}t.preventDefault()}}handleFakeTouchEnd(t){if(null!==this.fakeTouchFinger){for(let e=0;e<t.changedTouches.length;e++){const s=t.changedTouches[e];if(s.identifier===this.fakeTouchFinger.id){const t=this.getTouchCoord(s);this.ctx.sendToStreamer("MouseUp",[0,t.x,t.y]),this.ctx.sendToStreamer("MouseLeave",[]),this.fakeTouchFinger=null;break}}t.preventDefault()}}getTouchCoord(t){const e=this.ctx.getVideoElement().getBoundingClientRect(),s=t.clientX-e.left,i=t.clientY-e.top;return this.ctx.getCoordTranslator().translateUnsigned(s,i)}}class b{constructor(t,e){this.ctx=t,this.log=e,this.controllers=[],this.animationFrame=null,this.handleConnected=t=>{const e=t.gamepad;this.controllers[e.index]={currentState:this.deepCopyGamepad(e),prevState:this.deepCopyGamepad(e),id:e.index},this.ctx.sendToStreamer("GamepadConnected",[e.index]),null===this.animationFrame&&this.pollStatus(),this.ctx.host.dispatchEvent(new CustomEvent("gamepadconnected",{detail:e}))},this.handleDisconnected=t=>{const e=t.gamepad,s=this.controllers[e.index];s&&(this.ctx.sendToStreamer("GamepadDisconnected",[s.id]),delete this.controllers[e.index]),this.ctx.host.dispatchEvent(new CustomEvent("gamepaddisconnected",{detail:e}))},this.pollStatus=()=>{const t=navigator.getGamepads?.();if(t){for(let e=0;e<t.length;e++)t[e]&&this.controllers[e]&&(this.controllers[e].currentState=t[e]);for(const t of this.controllers){if(!t)continue;const e=t.id,s=t.currentState,i=t.prevState;for(let t=0;t<s.buttons.length;t++){const n=s.buttons[t],r=i.buttons[t];if(n.pressed)if(6===t)this.ctx.sendToStreamer("GamepadAnalog",[e,5,n.value]);else if(7===t)this.ctx.sendToStreamer("GamepadAnalog",[e,6,n.value]);else{const s=r.pressed?1:0;this.ctx.sendToStreamer("GamepadButtonPressed",[e,t,s])}else!n.pressed&&r.pressed&&(6===t?this.ctx.sendToStreamer("GamepadAnalog",[e,5,0]):7===t?this.ctx.sendToStreamer("GamepadAnalog",[e,6,0]):this.ctx.sendToStreamer("GamepadButtonReleased",[e,t,0]))}for(let t=0;t<s.axes.length;t+=2){const i=parseFloat(s.axes[t].toFixed(4)),n=-parseFloat(s.axes[t+1].toFixed(4));this.ctx.sendToStreamer("GamepadAnalog",[e,t+1,i]),this.ctx.sendToStreamer("GamepadAnalog",[e,t+2,n])}t.prevState=this.deepCopyGamepad(s)}this.controllers.filter(t=>void 0!==t).length>0&&(this.animationFrame=requestAnimationFrame(this.pollStatus))}}}setup(){window.addEventListener("gamepadconnected",this.handleConnected),window.addEventListener("gamepaddisconnected",this.handleDisconnected);const t=navigator.getGamepads?.();if(t)for(const e of t)e&&this.handleConnected(new GamepadEvent("gamepadconnected",{gamepad:e}))}teardown(){window.removeEventListener("gamepadconnected",this.handleConnected),window.removeEventListener("gamepaddisconnected",this.handleDisconnected),null!==this.animationFrame&&(cancelAnimationFrame(this.animationFrame),this.animationFrame=null);for(const t of this.controllers)void 0!==t?.id&&this.ctx.sendToStreamer("GamepadDisconnected",[t.id]);this.controllers=[]}deepCopyGamepad(t){return k(t)}}function k(t){return JSON.parse(JSON.stringify({buttons:t.buttons.map(t=>({pressed:t.pressed,touched:t.touched,value:t.value})),axes:[...t.axes],index:t.index,id:t.id,connected:t.connected,timestamp:t.timestamp,mapping:t.mapping}))}class C{constructor(t,e){this.ctx=t,this.log=e,this.session=null,this.refSpace=null,this.controllers=[],this.transientPointers=new Map,this.gl=null,this.videoTexture=null,this.leftView=null,this.rightView=null,this.onFrame=(t,e)=>{if(!this.session||!this.refSpace)return;const s=e.getViewerPose(this.refSpace);if(s){for(const t of s.views)"left"===t.eye?this.leftView=t:"right"===t.eye&&(this.rightView=t);this.sendTransform(s),this.session.inputSources.forEach(t=>{"transient-pointer"===t.targetRayMode?this.updateTransientPointer(t,e):"tracked-pointer"===t.targetRayMode&&t.gamepad&&this.updateController(t,e)})}this.session.requestAnimationFrame((t,e)=>this.onFrame(t,e))}}setup(){this.log.info("WebXR ready - call startXRSession() to begin")}teardown(){this.endSession()}isSessionActive(){return null!==this.session}async startSession(){if(!navigator.xr)return this.log.error("WebXR not supported"),void this.ctx.host.dispatchEvent(new CustomEvent("xr-not-supported"));try{const t=await navigator.xr.requestSession("immersive-vr",{optionalFeatures:[]});await this.onSessionStarted(t)}catch(t){this.log.error("Failed to start XR session",t),this.ctx.host.dispatchEvent(new CustomEvent("xr-session-failed",{detail:t}))}}endSession(){this.session&&this.session.end()}static async isSupported(){return navigator.xr?navigator.xr.isSessionSupported("immersive-vr"):0}async onSessionStarted(t){this.session=t,this.session.addEventListener("end",()=>this.onSessionEnded()),this.session.addEventListener("selectstart",t=>this.onSelectStart(t)),this.session.addEventListener("selectend",t=>this.onSelectEnd(t)),this.session.addEventListener("select",t=>this.onSelect(t));const e=document.createElement("canvas");this.gl=e.getContext("webgl2",{xrCompatible:1}),this.refSpace=await t.requestReferenceSpace("local"),t.updateRenderState({baseLayer:new XRWebGLLayer(t,this.gl)}),t.requestAnimationFrame((t,e)=>this.onFrame(t,e)),this.ctx.host.dispatchEvent(new CustomEvent("xr-session-started"))}sendTransform(t){const e=t.transform.matrix;if(this.ctx.sendToStreamer("XRHMDTransform",[e[0],e[4],e[8],e[12],e[1],e[5],e[9],e[13],e[2],e[6],e[10],e[14],e[3],e[7],e[11],e[15]]),this.leftView&&this.rightView){const t=this.leftView.transform.matrix,s=this.leftView.projectionMatrix,i=this.rightView.transform.matrix,n=this.rightView.projectionMatrix;this.ctx.sendToStreamer("XREyeViews",[t[0],t[4],t[8],t[12],t[1],t[5],t[9],t[13],t[2],t[6],t[10],t[14],t[3],t[7],t[11],t[15],s[0],s[4],s[8],s[12],s[1],s[5],s[9],s[13],s[2],s[6],s[10],s[14],s[3],s[7],s[11],s[15],i[0],i[4],i[8],i[12],i[1],i[5],i[9],i[13],i[2],i[6],i[10],i[14],i[3],i[7],i[11],i[15],n[0],n[4],n[8],n[12],n[1],n[5],n[9],n[13],n[2],n[6],n[10],n[14],n[3],n[7],n[11],n[15],e[0],e[4],e[8],e[12],e[1],e[5],e[9],e[13],e[2],e[6],e[10],e[14],e[3],e[7],e[11],e[15]])}}updateController(t,e){if(!t.gamepad||!this.refSpace||!t.gripSpace)return;const s=e.getPose(t.gripSpace,this.refSpace);if(!s)return;let i=2;"left"===t.handedness&&(i=0),"right"===t.handedness&&(i=1);const n=s.transform.matrix;this.ctx.sendToStreamer("XRControllerTransform",[n[0],n[4],n[8],n[12],n[1],n[5],n[9],n[13],n[2],n[6],n[10],n[14],n[3],n[7],n[11],n[15],i]);let r=0;t.profiles.includes("htc-vive")?r=1:t.profiles.includes("oculus-touch")&&(r=2),this.ctx.sendToStreamer("XRSystem",[r]),this.controllers[i]||(this.controllers[i]={prevState:k(t.gamepad),currentState:t.gamepad});const a=this.controllers[i],o=t.gamepad,h=a.prevState;for(let t=0;t<o.buttons.length;t++){const e=o.buttons[t],s=h.buttons[t];e.pressed&&!s.pressed?this.ctx.sendToStreamer("XRButtonPressed",[i,t,0,e.value]):!e.pressed&&s.pressed&&this.ctx.sendToStreamer("XRButtonReleased",[i,t,0]),e.touched&&!s.touched?this.ctx.sendToStreamer("XRButtonTouched",[i,t,0]):!e.touched&&s.touched&&this.ctx.sendToStreamer("XRButtonTouchReleased",[i,t,0])}for(let t=0;t<o.axes.length;t++)o.axes[t]!==h.axes[t]&&this.ctx.sendToStreamer("XRAnalog",[i,t,o.axes[t]]);a.prevState=k(o),a.currentState=o}updateTransientPointer(t,e){if(!t.targetRaySpace||!this.refSpace)return;const s=e.getPose(t.targetRaySpace,this.refSpace);if(!s)return;const i=Array.from(this.transientPointers.keys()).find(e=>this.transientPointers.get(e)?.source===t)??this.transientPointers.size;let n=this.transientPointers.get(i);n||(n={source:t,isSelecting:0},this.transientPointers.set(i,n));let r=2;"left"===t.handedness&&(r=0),"right"===t.handedness&&(r=1);const a=s.transform.matrix;this.ctx.sendToStreamer("XRControllerTransform",[a[0],a[4],a[8],a[12],a[1],a[5],a[9],a[13],a[2],a[6],a[10],a[14],a[3],a[7],a[11],a[15],r]),n.source=t}onSelectStart(t){const e=t.inputSource;if("transient-pointer"!==e.targetRayMode)return;const s=Array.from(this.transientPointers.keys()).find(t=>this.transientPointers.get(t)?.source===e);if(void 0===s)return;const i=this.transientPointers.get(s);if(!i)return;i.isSelecting=1;let n=2;"left"===e.handedness&&(n=0),"right"===e.handedness&&(n=1),this.ctx.sendToStreamer("XRButtonPressed",[n,0,0,1]),this.log.debug("Transient pointer select start",{handedness:n,pointerId:s})}onSelectEnd(t){const e=t.inputSource;if("transient-pointer"!==e.targetRayMode)return;const s=Array.from(this.transientPointers.keys()).find(t=>this.transientPointers.get(t)?.source===e);if(void 0===s)return;const i=this.transientPointers.get(s);if(!i)return;i.isSelecting=0;let n=2;"left"===e.handedness&&(n=0),"right"===e.handedness&&(n=1),this.ctx.sendToStreamer("XRButtonReleased",[n,0,0]),this.log.debug("Transient pointer select end",{handedness:n,pointerId:s})}onSelect(t){const e=t.inputSource;"transient-pointer"===e.targetRayMode&&this.log.debug("Transient pointer select (click)",{handedness:e.handedness,targetRayMode:e.targetRayMode})}onSessionEnded(){this.session=null,this.refSpace=null,this.controllers=[],this.transientPointers.clear(),this.leftView=null,this.rightView=null,this.ctx.host.dispatchEvent(new CustomEvent("xr-session-ended"))}}class S{constructor(t,s){this.ctx=t,this.log=s,this.file=new e,this.resolvers=[]}handleExtension(t){const e=new Uint8Array(t);this.file.receiving||(this.file.mimetype="",this.file.extension="",this.file.receiving=1,this.file.valid=0,this.file.chunks=0,this.file.data=[],this.file.timestampStart=(new Date).getTime(),this.log.debug("Received first chunk of file"));const s=new TextDecoder("utf-16").decode(e.slice(1));this.log.debug("File extension",s),this.file.extension=s}handleMimeType(t){const e=new Uint8Array(t);this.file.receiving||(this.file.mimetype="",this.file.extension="",this.file.receiving=1,this.file.valid=0,this.file.chunks=0,this.file.data=[],this.file.timestampStart=(new Date).getTime(),this.log.debug("Received first chunk of file"));const s=new TextDecoder("utf-16").decode(e.slice(1));this.log.debug("File mime type",s),this.file.mimetype=s}handleContents(t){const e=new Uint8Array(t);if(!this.file.receiving)return;const s=16379;this.file.chunks=Math.ceil(new DataView(e.slice(1,5).buffer).getInt32(0,1)/s);const i=e.slice(5);this.file.data.push(i),this.log.debug(`Received file chunk: ${this.file.data.length}/${this.file.chunks}`);const n=this.file.data.length*s,r=this.file.chunks*s,a=Math.round(this.file.data.length/this.file.chunks*100);if(this.ctx.host.dispatchEvent(new CustomEvent("file-progress",{detail:{receivedChunks:this.file.data.length,totalChunks:this.file.chunks,percentage:a,bytesReceived:n,totalBytes:r,extension:this.file.extension,mimeType:this.file.mimetype}})),this.file.data.length===this.file.chunks){this.file.receiving=0,this.file.valid=1,this.log.info("Received complete file");const t=(new Date).getTime()-this.file.timestampStart,e=Math.round(16384*this.file.chunks/t);this.log.info(`Average transfer bitrate: ${e}kb/s over ${t/1e3} seconds`);const s={blob:new Blob(this.file.data,{type:this.file.mimetype}),extension:this.file.extension,mimeType:this.file.mimetype,chunks:this.file.chunks};this.ctx.host.dispatchEvent(new CustomEvent("file-received",{detail:s})),this.resolvers.forEach(t=>t(s)),this.resolvers=[]}else this.file.data.length>this.file.chunks&&(this.file.receiving=0,this.log.error(`Received bigger file than advertised: ${this.file.data.length}/${this.file.chunks}`))}getLastReceivedFile(){return this.file.valid?{blob:new Blob(this.file.data,{type:this.file.mimetype}),extension:this.file.extension,mimeType:this.file.mimetype,chunks:this.file.chunks}:null}waitForFile(){return new Promise(t=>{this.resolvers.push(t)})}}class E{constructor(t,e){this.ctx=t,this.log=e,this.sessionStartTime=null,this.latencyBreakdown={current:null,browserReceiptTimeMs:null,history:[],deltas:null},this.t=0,this.i=0,this.o=0}resetPerformanceMetrics(){const t=this.ctx.performanceMetrics;t.sessionStarted=null,t.tokenExchangeStart=null,t.tokenExchangeComplete=null,t.peerConnectionCreated=null,t.sessionWsConnectStart=null,t.sessionWsConnectComplete=null,t.sessionReady=null,t.jobRequested=null,t.subscribeStart=performance.now(),t.offerReceived=null,t.answerSent=null,t.iceConnected=null,t.firstTrack=null,t.firstFrame=null,t.connectionComplete=null}getPerformanceMetrics(){return{...this.ctx.performanceMetrics}}logPerformanceSummary(){const t=this.ctx.performanceMetrics,e=t.sessionStarted||t.subscribeStart;if(!e)return;const s=t=>null!=t?(t-e).toFixed(2)+"ms":"",i=(t,e)=>null!=t&&null!=e?(e-t).toFixed(2)+"ms":"",n={};t.tokenExchangeStart&&(n["Token exchange start"]={Offset:s(t.tokenExchangeStart),Duration:""}),t.tokenExchangeComplete&&(n["Token exchange complete"]={Offset:s(t.tokenExchangeComplete),Duration:i(t.tokenExchangeStart,t.tokenExchangeComplete)}),t.peerConnectionCreated&&(n["Peer connection created"]={Offset:s(t.peerConnectionCreated),Duration:""}),t.sessionWsConnectStart&&(n["WebSocket connect start"]={Offset:s(t.sessionWsConnectStart),Duration:""}),t.sessionWsConnectComplete&&(n["WebSocket connected"]={Offset:s(t.sessionWsConnectComplete),Duration:i(t.sessionWsConnectStart,t.sessionWsConnectComplete)}),t.sessionReady&&(n["Session ready"]={Offset:s(t.sessionReady),Duration:""}),t.jobRequested&&(n["Job requested"]={Offset:s(t.jobRequested),Duration:""}),t.offerReceived&&(n["Offer received"]={Offset:s(t.offerReceived),Duration:""}),t.answerSent&&(n["Answer sent"]={Offset:s(t.answerSent),Duration:i(t.offerReceived,t.answerSent)}),t.iceConnected&&(n["ICE connected"]={Offset:s(t.iceConnected),Duration:""}),t.firstTrack&&(n["First media track"]={Offset:s(t.firstTrack),Duration:""}),t.firstFrame&&(n["First video frame"]={Offset:s(t.firstFrame),Duration:""}),t.connectionComplete&&(n["Connection complete"]={Offset:s(t.connectionComplete),Duration:""});const r=t.firstFrame??t.connectionComplete;r&&(n.TOTAL={Offset:s(r),Duration:i(e,r)}),this.log.info("Performance Summary",n,{format:"table"}),this.ctx.host.dispatchEvent(new CustomEvent("performance-metrics",{detail:{...t}}))}handleLatencyTestResult(t){const e={TestStartTimeMs:t.TestStartTimeMs??0,ReceiptTimeMs:t.ReceiptTimeMs??0,PreCaptureTimeMs:t.PreCaptureTimeMs??0,PostCaptureTimeMs:t.PostCaptureTimeMs??0,PreEncodeTimeMs:t.PreEncodeTimeMs??0,PostEncodeTimeMs:t.PostEncodeTimeMs??0,TransmissionTimeMs:t.TransmissionTimeMs??0},s=Date.now();this.latencyBreakdown.current=e,this.latencyBreakdown.browserReceiptTimeMs=s,this.latencyBreakdown.history.push(e),this.latencyBreakdown.history.length>E.LATENCY_HISTORY_SIZE&&this.latencyBreakdown.history.shift(),this.latencyBreakdown.deltas={captureMs:e.PostCaptureTimeMs-e.PreCaptureTimeMs,encodeMs:e.PostEncodeTimeMs-e.PreEncodeTimeMs,transmitMs:e.TransmissionTimeMs-e.PostEncodeTimeMs,networkMs:s-e.TransmissionTimeMs,totalMs:s-e.TestStartTimeMs},this.ctx.host.dispatchEvent(new CustomEvent("latency-test-result",{detail:{...this.latencyBreakdown}})),this.log.debug("Latency breakdown",this.latencyBreakdown.deltas)}getLatencyBreakdown(){return{...this.latencyBreakdown}}async collectAndDisplaySessionStats(){const t=this.ctx.getPeerConnection();if(!t)return null;try{const e=await t.getStats(),s=this.parseWebRTCStats(e);return this.displaySessionStats(s),s}catch(t){return this.log.error("Failed to collect session stats",t),null}}parseWebRTCStats(t){const e={duration:this.sessionStartTime?(Date.now()-this.sessionStartTime)/1e3:0,video:{codec:null,mimeType:null,bytesReceived:0,packetsReceived:0,packetsLost:0,framesReceived:0,framesDecoded:0,framesDropped:0,frameRate:0,bitrate:0,resolution:{width:0,height:0},jitter:0,pliCount:0,nackCount:0},audio:{codec:null,mimeType:null,bytesReceived:0,packetsReceived:0,packetsLost:0,bitrate:0,jitter:0},connection:{iceLocalCandidateType:null,iceRemoteCandidateType:null,turnUsed:0,rtt:0,availableOutgoingBitrate:0,protocol:null,localAddress:null,remoteAddress:null}};return t.forEach(s=>{if("inbound-rtp"===s.type){const t="video"===s.mediaType||"video"===s.kind,i="audio"===s.mediaType||"audio"===s.kind;if(t){e.video.bytesReceived=s.bytesReceived||0,e.video.packetsReceived=s.packetsReceived||0,e.video.packetsLost=s.packetsLost||0,e.video.framesReceived=s.framesReceived||0,e.video.framesDecoded=s.framesDecoded||0,e.video.framesDropped=s.framesDropped||0,e.video.jitter=s.jitter||0,e.video.pliCount=s.pliCount||0,e.video.nackCount=s.nackCount||0,s.framesPerSecond?e.video.frameRate=s.framesPerSecond:s.framerateMean&&(e.video.frameRate=s.framerateMean),s.frameWidth&&s.frameHeight&&(e.video.resolution.width=s.frameWidth,e.video.resolution.height=s.frameHeight);const t=Date.now(),i=(t-this.o)/1e3;if(s.bitrateMean)e.video.bitrate=s.bitrateMean;else if(i>0&&this.t>0){const t=e.video.bytesReceived-this.t;e.video.bitrate=8*t/i}this.t=e.video.bytesReceived,this.o=t}else i&&(e.audio.bytesReceived=s.bytesReceived||0,e.audio.packetsReceived=s.packetsReceived||0,e.audio.packetsLost=s.packetsLost||0,e.audio.jitter=s.jitter||0,s.bitrateMean?e.audio.bitrate=s.bitrateMean:e.duration>0&&(e.audio.bitrate=8*e.audio.bytesReceived/e.duration))}if("codec"===s.type){const t=s.mimeType||"";t.includes("video")?(e.video.codec=s.mimeType,e.video.mimeType=s.mimeType):t.includes("audio")&&(e.audio.codec=s.mimeType,e.audio.mimeType=s.mimeType)}if("track"===s.type&&"video"===s.kind&&(!e.video.resolution.width&&s.frameWidth&&(e.video.resolution.width=s.frameWidth||0,e.video.resolution.height=s.frameHeight||0),!e.video.frameRate&&s.framesDecoded&&s.remoteSource)){const t=(Date.now()-this.o)/1e3;t>0&&this.i>0&&(e.video.frameRate=(s.framesDecoded-this.i)/t),this.i=s.framesDecoded}if("candidate-pair"===s.type&&"succeeded"===s.state){e.connection.rtt=s.currentRoundTripTime?1e3*s.currentRoundTripTime:0,e.connection.availableOutgoingBitrate=s.availableOutgoingBitrate||0;const i=s.localCandidateId,n=s.remoteCandidateId;t.forEach(t=>{t.id===i&&"local-candidate"===t.type&&(e.connection.iceLocalCandidateType=t.candidateType,e.connection.protocol=t.protocol,e.connection.localAddress=`${t.address||t.ip}:${t.port}`),t.id===n&&"remote-candidate"===t.type&&(e.connection.iceRemoteCandidateType=t.candidateType,e.connection.remoteAddress=`${t.address||t.ip}:${t.port}`,"relay"===t.candidateType&&(e.connection.turnUsed=1))})}}),e}displaySessionStats(t){const e=(t,e)=>e>0?(t/(e+t)*100).toFixed(2)+"%":"0%",s={Duration:{Value:t.duration.toFixed(2)+"s"},"Video Codec":{Value:t.video.codec||"N/A"},"Video Resolution":{Value:`${t.video.resolution.width}x${t.video.resolution.height}`},"Video Bytes":{Value:this.formatBytes(t.video.bytesReceived)},"Video Packets":{Value:`${t.video.packetsReceived.toLocaleString()} recv, ${t.video.packetsLost.toLocaleString()} lost (${e(t.video.packetsLost,t.video.packetsReceived)})`},"Video Frames":{Value:`${t.video.framesReceived.toLocaleString()} recv, ${t.video.framesDecoded.toLocaleString()} decoded, ${t.video.framesDropped.toLocaleString()} dropped`},"Video Rate":{Value:`${t.video.frameRate.toFixed(2)} fps, ${(t.video.bitrate/1e3).toFixed(2)} kbps, jitter ${(1e3*t.video.jitter).toFixed(2)}ms`},"Video Recovery":{Value:`${t.video.pliCount.toLocaleString()} PLI, ${t.video.nackCount.toLocaleString()} NACK`},"Audio Codec":{Value:t.audio.codec||"N/A"},"Audio Bytes":{Value:this.formatBytes(t.audio.bytesReceived)},"Audio Packets":{Value:`${t.audio.packetsReceived.toLocaleString()} recv, ${t.audio.packetsLost.toLocaleString()} lost (${e(t.audio.packetsLost,t.audio.packetsReceived)})`},"Audio Rate":{Value:`${(t.audio.bitrate/1e3).toFixed(2)} kbps, jitter ${(1e3*t.audio.jitter).toFixed(2)}ms`},"ICE Candidates":{Value:`local: ${t.connection.iceLocalCandidateType||"N/A"}, remote: ${t.connection.iceRemoteCandidateType||"N/A"}`},TURN:{Value:t.connection.turnUsed?"YES (relay)":"NO (direct)"},Protocol:{Value:t.connection.protocol||"N/A"},RTT:{Value:t.connection.rtt.toFixed(2)+"ms"},Bandwidth:{Value:(t.connection.availableOutgoingBitrate/1e3).toFixed(2)+" kbps"}};t.connection.localAddress&&(s["Local Address"]={Value:t.connection.localAddress}),t.connection.remoteAddress&&(s["Remote Address"]={Value:t.connection.remoteAddress}),this.log.info("Session Statistics",s,{format:"table"}),this.ctx.host.dispatchEvent(new CustomEvent("session-stats",{detail:t}))}formatBytes(t){if(0===t)return"0 B";const e=Math.floor(Math.log(t)/Math.log(1024));return`${(t/Math.pow(1024,e)).toFixed(2)} ${["B","KB","MB","GB"][e]}`}reportTiming(t){const e=this.ctx.performanceMetrics,s=e.sessionStarted;if(!s)return;let i;if("session-ready"===t&&e.sessionReady)i=e.sessionReady-s;else{if("first-frame"!==t||!e.firstFrame)return;i=e.firstFrame-s}const n={};e.tokenExchangeStart&&e.tokenExchangeComplete&&(n.token_exchange_ms=e.tokenExchangeComplete-e.tokenExchangeStart),e.sessionWsConnectStart&&e.sessionWsConnectComplete&&(n.ws_connect_ms=e.sessionWsConnectComplete-e.sessionWsConnectStart),e.sessionReady&&(n.session_ready_ms=e.sessionReady-s),e.jobRequested&&(n.job_requested_ms=e.jobRequested-s),e.offerReceived&&(n.offer_received_ms=e.offerReceived-s),e.answerSent&&(n.answer_sent_ms=e.answerSent-s),e.iceConnected&&(n.ice_connected_ms=e.iceConnected-s),e.firstTrack&&(n.first_track_ms=e.firstTrack-s),e.firstFrame&&(n.first_frame_ms=e.firstFrame-s),e.jobRequested&&e.firstFrame&&(n.job_to_first_frame_ms=e.firstFrame-e.jobRequested),this.ctx.sendSessionMessage({type:"report-timing",milestone:t,duration_ms:Math.round(i),breakdown:n})}}E.LATENCY_HISTORY_SIZE=60;const R={"low-latency":{minBitrate:5e3,startBitrate:1e4,maxBitrate:1e5,conferenceFlag:1,audioPtime:10,jitterBufferMs:0},balanced:{minBitrate:3e3,startBitrate:8e3,maxBitrate:1e5,conferenceFlag:1,audioPtime:null,jitterBufferMs:30},quality:{minBitrate:1e3,startBitrate:5e3,maxBitrate:15e4,conferenceFlag:0,audioPtime:null,jitterBufferMs:50}};class T{constructor(t,e){this.ctx=t,this.log=e,this.peerConnection=null,this.dataChannel=null,this.microphoneStream=null,this.h=0,this.l=0,this.videoTracks=new Map,this.activeVideoTrackId=null,this.u=null,this.p=0,this.iceCandidateBatch=[],this.iceBatchTimer=null,this.turnErrorCount=0,this.stunErrorCount=0,this.m=0,this.v=null,this.k=null,this.C=0,this.S=0,this.R=[],this.T=0,this.$=0,this.M=null,this.I=0,this.P=null,this.A=0,this.D=null,this._=0,this.V=null,this.audioElement=document.createElement("audio"),this.audioElement.autoplay=1,this.audioElement.controls=0,this.audioElement.muted=this.ctx.config.mute,this.audioElement.style.display="none"}getPeerConnection(){return this.peerConnection}getDataChannel(){return this.dataChannel}getAudioElement(){return this.audioElement}getVideoTracks(){return this.videoTracks}getActiveVideoTrackId(){return this.activeVideoTrackId}getPcGeneration(){return this.m}resetResilienceState(){this.F(),this.B(),this.C=0,this.R=[],this.S=0,this.m=0,this.M=null,this.T=0,this.$=0,this.I=0,this.D=null,this._=0}onWorkerLeft(){this.log.info("Worker left — immediately closing PeerConnection"),this.F(),this.B(),this.A=0,this.I=0,this.C=0,this.m++,this.peerConnection&&(this.peerConnection.onconnectionstatechange=null,this.peerConnection.oniceconnectionstatechange=null,this.peerConnection.onicecandidate=null,this.peerConnection.ontrack=null,this.peerConnection.onicecandidateerror=null,this.peerConnection.ondatachannel=null,this.peerConnection.onicegatheringstatechange=null),this.closePeerConnection(),this.videoTracks.forEach(({track:t})=>t.stop()),this.videoTracks.clear(),this.activeVideoTrackId=null;const t=this.ctx.getVideoElement();t.srcObject&&(t.srcObject.getTracks().forEach(t=>t.stop()),t.srcObject=null),this.u&&(this.u.getTracks().forEach(t=>t.stop()),this.u=null)}async W(){if(!this.peerConnection)return 0;try{const t=await this.peerConnection.getStats();let e=0;return t.forEach(s=>{if("inbound-rtp"===s.type&&(e+=s.bytesReceived||0),"candidate-pair"===s.type&&"succeeded"===s.state&&s.nominated){const e=s.remoteCandidateId;t.forEach(t=>{t.id===e&&"remote-candidate"===t.type&&(this.$="relay"===t.candidateType)})}}),e}catch{return 0}}async U(){return await this.W()>this.T}async L(t){const e=this.peerConnection;if(e)try{const s=await e.getStats();if(this.m!==t)return;let i=null,n=null,r=null;if(s.forEach(t=>{"candidate-pair"===t.type&&"succeeded"===t.state&&t.nominated&&(r=t)}),!r)return;s.forEach(t=>{t.id===r.localCandidateId&&"local-candidate"===t.type&&(i=t),t.id===r.remoteCandidateId&&"remote-candidate"===t.type&&(n=t)});const a="relay"===n?.candidateType||"relay"===i?.candidateType;this.$=a;const o={turnUsed:a,protocol:i?.protocol||null,localCandidateType:i?.candidateType||null,remoteCandidateType:n?.candidateType||null,localAddress:i?`${i.address||i.ip}:${i.port}`:null,remoteAddress:n?`${n.address||n.ip}:${n.port}`:null,rtt:r.currentRoundTripTime?1e3*r.currentRoundTripTime:null,generation:t};this.log.info(`Transport selected: ${a?"TURN (relay)":"direct"}, local=${o.localCandidateType}/${o.protocol}, remote=${o.remoteCandidateType}`),this.ctx.host.dispatchEvent(new CustomEvent("transport-selected",{detail:o}))}catch(t){this.log.debug("Failed to detect selected transport",t)}}q(){const t=this.ctx.getSessionWs();if(!t||t.readyState!==WebSocket.OPEN)return{allowed:0,reason:"session-ws-not-open"};const e=this.ctx.config.reconnectMode;if("recover"!==e&&"always"!==e)return{allowed:0,reason:"reconnect-mode-"+e};if(this.C)return{allowed:0,reason:"recovery-already-in-flight"};const s=Date.now(),i=s-this.S;if(this.S>0&&i<T.RECOVERY_MIN_INTERVAL_MS)return{allowed:0,reason:`min-interval-not-elapsed (${i}ms < ${T.RECOVERY_MIN_INTERVAL_MS}ms)`};if(this.R=this.R.filter(t=>s-t<T.RECOVERY_WINDOW_MS),this.R.length>=T.MAX_RECOVERY_IN_WINDOW)return{allowed:0,reason:`max-recoveries-in-window (${this.R.length}/${T.MAX_RECOVERY_IN_WINDOW} in ${T.RECOVERY_WINDOW_MS}ms)`};const n=this.ctx.getIceServers();return n&&0!==n.length?{allowed:1}:{allowed:0,reason:"no-ice-servers"}}F(){this.v&&(clearTimeout(this.v),this.v=null),this.k&&(clearInterval(this.k),this.k=null)}B(){this.P&&(clearTimeout(this.P),this.P=null),this.A=0}async N(t){const e=this.peerConnection;if(!e||!e.remoteDescription)return this.log.warn("Cannot ICE restart: no peer connection or remote description"),0;if(this.A)return this.log.debug("ICE restart already in progress"),0;if(this.I>=T.ICE_RESTART_MAX_ATTEMPTS)return this.log.info(`ICE restart exhausted (${this.I} attempts), falling back to full recovery`),0;const s=this.ctx.getSessionWs();if(!s||s.readyState!==WebSocket.OPEN)return this.log.warn("Cannot ICE restart: session WebSocket not open"),0;this.A=1,this.I++;const i=this.m;this.log.info(`Attempting ICE restart ${this.I}/${T.ICE_RESTART_MAX_ATTEMPTS}, reason=${t}, gen=${i}`),this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-started",{detail:{attempt:this.I,reason:t,generation:i}}));try{const s=await e.createOffer({iceRestart:1});if(this.m!==i)return this.A=0,0;const n=this.mungeSDP(s.sdp),r={...s,sdp:n};await e.setLocalDescription(r);const a=this.ctx.getPeerAgentId();return this.ctx.sendSessionMessage({type:"webrtc-signaling",signal_type:"offer",payload:{sdp:r.sdp,type:r.type},ice_restart:1,...a&&{target_agent_id:a}}),this.P=setTimeout(()=>{this.P=null,this.m===i&&(this.log.warn(`ICE restart timeout expired (attempt ${this.I})`),this.A=0,this.I<T.ICE_RESTART_MAX_ATTEMPTS?this.N("timeout-retry-"+this.I):(this.log.info("ICE restart failed after max attempts, falling back to full media recovery"),this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-failed",{detail:{attempts:this.I,reason:t,generation:i}})),this.O("ice-restart-exhausted:"+t)))},T.ICE_RESTART_TIMEOUT_MS),1}catch(t){return this.log.error("ICE restart failed",t),this.A=0,this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-error",{detail:{error:t instanceof Error?t.message:t+"",attempt:this.I,generation:i}})),0}}async j(){const t=this.ctx.getIceServers().filter(t=>(Array.isArray(t.urls)?t.urls:[t.urls]).some(t=>t.startsWith("turn:")||t.startsWith("turns:")));if(0===t.length)return this.log.debug("No TURN servers configured, skipping health check"),1;const e=Date.now();if(null!==this.D&&e-this._<T.TURN_HEALTH_CHECK_CACHE_MS)return this.log.debug("Using cached TURN health: "+this.D),this.D;this.log.info("Probing TURN server health...");const s=performance.now();return new Promise(e=>{let i=0,n=0;const r=new RTCPeerConnection({iceServers:t,iceTransportPolicy:"relay"}),a=()=>{i||(i=1,r.close())},o=setTimeout(()=>{i||(this.log.warn(`TURN health check timed out after ${T.TURN_HEALTH_CHECK_TIMEOUT_MS}ms`),this.D=0,this._=Date.now(),a(),e(0))},T.TURN_HEALTH_CHECK_TIMEOUT_MS);r.onicecandidate=t=>{if(t.candidate){if(("relay"===t.candidate.type||t.candidate.candidate.includes("typ relay"))&&!n){n=1;const t=(performance.now()-s).toFixed(2);this.log.info(`TURN health check passed in ${t}ms`),this.D=1,this._=Date.now(),clearTimeout(o),a(),e(1)}}else n||i||(this.log.warn("TURN health check failed: no relay candidates gathered"),this.D=0,this._=Date.now(),clearTimeout(o),a(),e(0))},r.onicecandidateerror=t=>{this.log.debug("TURN probe ICE error",{errorCode:t.errorCode,errorText:t.errorText,url:t.url})},r.createDataChannel("turn-probe"),r.createOffer().then(t=>r.setLocalDescription(t)).catch(t=>{this.log.debug("TURN probe offer failed",t),i||(this.D=0,this._=Date.now(),clearTimeout(o),a(),e(0))})})}getTurnHealthy(){return this.D}async H(t){this.F(),this.T=await this.W();const e=this.ctx.config.disconnectGraceMs,s=this.m;this.log.info(`Starting disconnect grace (${e}ms). reason=${t}, gen=${s}, bytesSnapshot=${this.T}, turnUsed=${this.$}`),this.ctx.setStreamState("interrupted",{reason:t,generation:s,graceMs:e,bytesSnapshot:this.T,turnUsed:this.$}),this.k=setInterval(async()=>{if(this.m!==s)return void this.F();const t=await this.U();this.log.debug(`Grace poll: flowing=${t}, gen=${s}`),t&&(this.log.info("RTP still flowing during grace - cancelling recovery"),this.F(),this.ctx.setStreamState("streaming",{reason:"grace-resolved",generation:s}))},T.DISCONNECT_POLL_INTERVAL_MS),this.v=setTimeout(async()=>{if(this.k&&(clearInterval(this.k),this.k=null),this.v=null,this.m===s){if(await this.U())return this.log.info("RTP flowing at grace expiry - skipping recovery"),void this.ctx.setStreamState("streaming",{reason:"grace-resolved",generation:s});this.log.info("Grace expired, RTP not flowing - attempting ICE restart first"),await this.N("grace-expired:"+t)||this.O("grace-expired:"+t)}},e)}async O(t){this.F();const e=this.q();if(!e.allowed){if(this.log.info(`Recovery blocked: ${e.reason} (reason=${t})`),e.reason?.startsWith("max-recoveries-in-window")){this.ctx.setStreamState("failed",{reason:"recovery-exhausted",blockReason:e.reason,recoveryTimestamps:[...this.R],turnUsed:this.$});const s=new CustomEvent("media-recovery-exhausted",{cancelable:1,detail:{reason:t,blockReason:e.reason,recoveryTimestamps:[...this.R],turnUsed:this.$}});this.ctx.host.dispatchEvent(s),s.defaultPrevented||(this.log.warn("Media recovery exhausted - leaving session"),this.ctx.leaveSession("media-recovery-exhausted"))}return}this.$&&(await this.j()||(this.log.warn("TURN health check failed, recovery may be slow or unsuccessful"),this.ctx.host.dispatchEvent(new CustomEvent("turn-unhealthy",{detail:{reason:t,lastKnownTurnUsed:this.$}})))),this.B(),this.I=0,this.C=1,this.S=Date.now(),this.R.push(this.S);const s=++this.m;this.log.info(`Starting recovery gen=${s}, reason=${t}, turnUsed=${this.$}, recoveriesInWindow=${this.R.length}`),this.ctx.setStreamState("recovering",{reason:t,generation:s,turnUsed:this.$,recoveriesInWindow:this.R.length});try{this.ctx.getMetricsSessionStartTime()&&this.peerConnection&&this.ctx.collectAndDisplaySessionStats(),this.peerConnection&&(this.peerConnection.onconnectionstatechange=null,this.peerConnection.oniceconnectionstatechange=null,this.peerConnection.onicecandidate=null,this.peerConnection.ontrack=null,this.peerConnection.onicecandidateerror=null,this.peerConnection.ondatachannel=null,this.peerConnection.onicegatheringstatechange=null),this.log.info(`Closing old PeerConnection (gen=${s-1})`),this.closePeerConnection(),this.videoTracks.forEach(({track:t})=>t.stop()),this.videoTracks.clear(),this.activeVideoTrackId=null;const t=this.ctx.getVideoElement();t.srcObject&&(t.srcObject.getTracks().forEach(t=>t.stop()),t.srcObject=null),this.u&&(this.u.getTracks().forEach(t=>t.stop()),this.u=null);const e=this.ctx.getIceServers();this.log.info(`Creating new PeerConnection (gen=${s})`),this.createPeerConnection({iceServers:e});const i=this.ctx.getSubscribedStreamerId();i?(this.log.info("Re-asserting streamer preference: "+i),this.ctx.sendSessionMessage({type:"update-presence",preferred_streamer_id:i})):this.log.warn("No subscribed streamer ID for recovery"),this.log.info(`Recovery initiated gen=${s}, waiting for server offer`)}catch(e){this.log.error("Recovery failed gen="+s,e),this.ctx.host.dispatchEvent(new CustomEvent("media-recovery-error",{detail:{error:e instanceof Error?e.message:e+"",generation:s,reason:t}}))}finally{this.C=0}}sendRawBinary(t){if(this.dataChannel&&"open"===this.dataChannel.readyState){const e=new ArrayBuffer(t.length);new Uint8Array(e).set(t),this.dataChannel.send(e)}}sendKeyDownSimple(t,e=0){const s=new Uint8Array([2,t,e?1:0]);this.sendRawBinary(s)}sendKeyUpSimple(t){const e=new Uint8Array([3,t,0]);this.sendRawBinary(e)}sendMouseDownSimple(t,e,s=0){const i=new Uint8Array(9);i[0]=4,i[1]=255&t,i[2]=t>>8&255,i[3]=255&e,i[4]=e>>8&255,i[5]=s,this.sendRawBinary(i)}sendMouseUpSimple(t,e,s=0){const i=new Uint8Array(9);i[0]=5,i[1]=255&t,i[2]=t>>8&255,i[3]=255&e,i[4]=e>>8&255,i[5]=s,this.sendRawBinary(i)}sendMouseMoveSimple(t,e){const s=new Uint8Array(9);s[0]=6,s[1]=255&t,s[2]=t>>8&255,s[3]=255&e,s[4]=e>>8&255,s[5]=0,this.sendRawBinary(s)}sendMouseWheelSimple(t){const e=new Uint8Array(4);e[0]=7,e[1]=255&t,e[2]=t>>8&255,this.sendRawBinary(e)}sendToStreamer(t,e=[]){const s=a.encode(t,e);this.dataChannel&&"open"===this.dataChannel.readyState?this.dataChannel.send(s):this.log.warn(`Cannot send ${t}: data channel not open`),this.ctx.host.dispatchEvent(new CustomEvent("streamer-message",{detail:{type:t,data:e}}))}createPeerConnection(t){this.C||this.ctx.setStreamState("starting",{generation:this.m}),this.warmVideoElement(),this.isAndroid()&&!this.p&&this.verifyCodecAvailability().then(t=>{t?this.p=1:this.ctx.host.dispatchEvent(new CustomEvent("codec-verification-failed"))});const e={iceServers:t?.iceServers||[{urls:"stun:stun.cloudflare.com:3478"}],iceCandidatePoolSize:10,iceTransportPolicy:"all"};(t?.forceRelay||this.ctx.config.forceRelay)&&(e.iceTransportPolicy="relay"),this.peerConnection=new RTCPeerConnection(e),this.turnErrorCount=0,this.stunErrorCount=0,this.ctx.performanceMetrics.peerConnectionCreated||(this.ctx.performanceMetrics.peerConnectionCreated=performance.now()),this.peerConnection.onicecandidate=t=>{if(t.candidate)this.iceCandidateBatch.push({candidate:t.candidate.candidate,sdpMid:t.candidate.sdpMid,sdpMLineIndex:t.candidate.sdpMLineIndex,usernameFragment:t.candidate.usernameFragment}),this.iceBatchTimer&&clearTimeout(this.iceBatchTimer),this.iceBatchTimer=setTimeout(()=>{if(this.iceCandidateBatch.length>0){this.log.debug(`Sending ${this.iceCandidateBatch.length} ICE candidates in batch`);const t=this.ctx.getPeerAgentId();this.iceCandidateBatch.forEach(e=>{const s=this.ctx.getSessionWs();s&&s.readyState===WebSocket.OPEN?this.ctx.sendSessionMessage({type:"webrtc-signaling",signal_type:"iceCandidate",payload:e,...t&&{target_agent_id:t}}):this.ctx.host.dispatchEvent(new CustomEvent("ice-candidate",{detail:e}))}),this.iceCandidateBatch=[]}},this.ctx.config.iceBatchDelay);else if(this.iceBatchTimer&&clearTimeout(this.iceBatchTimer),this.iceCandidateBatch.length>0){this.log.debug(`Sending final ${this.iceCandidateBatch.length} ICE candidates`);const t=this.ctx.getPeerAgentId();this.iceCandidateBatch.forEach(e=>{const s=this.ctx.getSessionWs();s&&s.readyState===WebSocket.OPEN?this.ctx.sendSessionMessage({type:"webrtc-signaling",signal_type:"iceCandidate",payload:e,...t&&{target_agent_id:t}}):this.ctx.host.dispatchEvent(new CustomEvent("ice-candidate",{detail:e}))}),this.iceCandidateBatch=[]}},this.peerConnection.ontrack=t=>{this.handleTrack(t)},this.peerConnection.onconnectionstatechange=()=>{const t=this.peerConnection;if(!t)return;const e=t.connectionState,s=this.m;if(this.log.info(`Connection state: ${e} (gen=${s})`),this.ctx.host.dispatchEvent(new CustomEvent("connection-state-change",{detail:e})),"connected"===e){if(this.F(),this.B(),this.ctx.setStreamState("streaming",{reason:"connected",generation:s}),this.I>0&&this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-success",{detail:{attempts:this.I,generation:s}})),this.I=0,!this.ctx.performanceMetrics.connectionComplete){if(this.ctx.performanceMetrics.connectionComplete=performance.now(),this.ctx.performanceMetrics.subscribeStart){const t=this.ctx.performanceMetrics.connectionComplete-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to connection complete: ${t.toFixed(2)}ms`)}this.ctx.setMetricsSessionStartTime(Date.now()),this.ctx.sendSessionMessage({type:"report-event",event_type:"webrtc.connected",data:{generation:this.m}})}}else"failed"===e?(this.log.error(`Connection failed (gen=${s})`),this.ctx.getMetricsSessionStartTime()&&this.ctx.collectAndDisplaySessionStats(),this.N("connection-failed").then(t=>{t||this.O("connection-failed")})):"disconnected"===e?(this.log.warn(`Connection disconnected (gen=${s})`),this.H("connection-disconnected")):"closed"===e&&(this.log.info(`Connection closed (gen=${s})`),this.F(),this.ctx.getMetricsSessionStartTime()&&this.ctx.collectAndDisplaySessionStats())},this.peerConnection.oniceconnectionstatechange=()=>{const t=this.peerConnection;if(!t)return;const e=t.iceConnectionState,s=this.M;this.M=e;const i=this.m;if(this.log.info(`ICE connection state: ${s||"null"} -> ${e} (gen=${i})`),"connected"===e||"completed"===e){if(this.F(),this.B(),this.I>0&&(this.log.info(`ICE restart succeeded after ${this.I} attempt(s)`),this.ctx.host.dispatchEvent(new CustomEvent("ice-restart-success",{detail:{attempts:this.I,generation:i}}))),this.I=0,!this.ctx.performanceMetrics.iceConnected&&(this.ctx.performanceMetrics.iceConnected=performance.now(),this.ctx.performanceMetrics.subscribeStart)){const t=this.ctx.performanceMetrics.iceConnected-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to ICE connected: ${t.toFixed(2)}ms`)}this.L(i)}else"failed"===e?(this.log.error(`ICE connection failed (gen=${i})`),this.N("ice-failed").then(t=>{t||this.O("ice-failed")})):"disconnected"===e&&(this.log.warn(`ICE connection disconnected (gen=${i})`),this.v||this.H("ice-disconnected"));this.ctx.host.dispatchEvent(new CustomEvent("ice-connection-state-change",{detail:{prevState:s,newState:e,generation:i}}))},this.peerConnection.onicecandidateerror=t=>{const e={errorCode:t.errorCode,errorText:t.errorText,url:t.url,address:t.address,port:t.port},s=t.errorText?.toLowerCase()||"";s.includes("turn")||s.includes("relay")?this.turnErrorCount++:(s.includes("stun")||s.includes("binding"))&&this.stunErrorCount++;const i=this.turnErrorCount+this.stunErrorCount;i<=3?this.log.error("ICE candidate error",e):4===i&&(this.log.warn("Multiple ICE errors detected, suppressing further error logs"),this.log.warn(`STUN errors: ${this.stunErrorCount}, TURN errors: ${this.turnErrorCount}`)),this.ctx.host.dispatchEvent(new CustomEvent("ice-candidate-error",{detail:e}))},this.peerConnection.onicegatheringstatechange=()=>{this.ctx.host.dispatchEvent(new CustomEvent("ice-gathering-state-change",{detail:this.peerConnection.iceGatheringState}))},this.peerConnection.ondatachannel=t=>{this.handleDataChannel(t)};const s=this.peerConnection.addTransceiver("video",{direction:"recvonly"});this.peerConnection.addTransceiver("audio",{direction:"recvonly"}),this.setVideoCodecPreferences(s),t?.useMic&&(this.h=1),t?.forceMonoAudio&&(this.l=1)}closePeerConnection(){this.C||this.ctx.setStreamState("idle"),this.iceBatchTimer&&(clearTimeout(this.iceBatchTimer),this.iceBatchTimer=null),this.iceCandidateBatch=[],this.V&&(this.V.terminate(),this.V=null),this.dataChannel&&(this.dataChannel.onopen=null,this.dataChannel.onclose=null,this.dataChannel.onmessage=null,this.dataChannel.onerror=null,this.dataChannel.close(),this.dataChannel=null),this.peerConnection&&(this.peerConnection.onconnectionstatechange=null,this.peerConnection.oniceconnectionstatechange=null,this.peerConnection.onicecandidate=null,this.peerConnection.ontrack=null,this.peerConnection.onicecandidateerror=null,this.peerConnection.ondatachannel=null,this.peerConnection.onicegatheringstatechange=null,this.peerConnection.onnegotiationneeded=null,this.peerConnection.close(),this.peerConnection=null)}preCreatePeerConnection(t){this.peerConnection?this.log.debug("Peer connection already exists, skipping pre-creation"):(this.log.debug("Pre-creating peer connection for faster setup..."),this.createPeerConnection(t))}isPeerConnectionReady(){return null!==this.peerConnection}warmVideoElement(){const t=this.ctx.getVideoElement();if(!t.srcObject){try{const e=document.createElement("canvas");e.width=2,e.height=2,e.getContext("2d"),this.u=e.captureStream(0);const s=this.u.getVideoTracks()[0];s&&"requestFrame"in s&&s.requestFrame(),t.srcObject=this.u,t.play().catch(()=>{}),this.log.debug("Video pipeline warmed with placeholder stream")}catch(t){this.log.debug("Failed to warm video pipeline",t)}this.warmVideoDecoder()}}warmVideoDecoder(){const t=globalThis.VideoDecoder;if(!t)return;const e=["av01.0.08M.08","vp09.00.10.08","avc1.640028"];for(const s of e)try{const e=new t({output:()=>{},error:()=>{}});return e.configure({codec:s}),e.close(),void this.log.debug("Warmed VideoDecoder for "+s)}catch{}}async verifyCodecAvailability(t=10){const e=["H264","VP9","AV1"];for(let s=0;s<t;s++){const i=new RTCPeerConnection({});try{i.addTransceiver("video");const t=(await i.createOffer()).sdp||"";for(const i of e)if(RegExp("rtpmap:\\d+ "+i,"i").test(t))return this.log.debug(`Codec ${i} verified in SDP (attempt ${s+1})`),i}finally{i.close()}s<t-1&&await new Promise(t=>setTimeout(t,250))}return this.log.warn(`No preferred codecs found in SDP after ${t} attempts`),null}isAndroid(){return/android/i.test(navigator.userAgent)}handleTrack(t){const e=t.track,s=t.streams.length>0?t.streams[0]:new MediaStream([e]);if(!this.ctx.performanceMetrics.firstTrack&&(this.ctx.performanceMetrics.firstTrack=performance.now(),this.ctx.performanceMetrics.subscribeStart)){const t=this.ctx.performanceMetrics.firstTrack-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to first track: ${t.toFixed(2)}ms`)}if("video"===e.kind){const i=(R[this.ctx.config.streamQuality]??R["low-latency"]).jitterBufferMs,n=t.receiver;"jitterBufferTarget"in n&&(n.jitterBufferTarget=i,this.log.debug(`Video jitterBufferTarget set to ${i}ms`)),"playoutDelayHint"in n&&(n.playoutDelayHint=i/1e3,this.log.debug(`Video playoutDelayHint set to ${i/1e3}s`)),this.attachKeyframeTransform(n);const r=e.id;if(this.videoTracks.set(r,{track:e,stream:s}),1===this.videoTracks.size){this.activeVideoTrackId=r;const t=this.ctx.getVideoElement();if(t.srcObject=s,this.u&&(this.u.getTracks().forEach(t=>t.stop()),this.u=null),"function"==typeof t.requestVideoFrameCallback){let e=0;const s=(i,n)=>{if(this.ctx.performanceMetrics.firstFrame||(this.ctx.performanceMetrics.firstFrame=i,this.ctx.performanceMetrics.sessionStarted&&this.log.info(`SESSION START -> FIRST FRAME: ${(i-this.ctx.performanceMetrics.sessionStarted).toFixed(2)}ms`),this.ctx.performanceMetrics.subscribeStart&&this.log.info(`SUBSCRIBE -> FIRST FRAME: ${(i-this.ctx.performanceMetrics.subscribeStart).toFixed(2)}ms`),this.ctx.performanceMetrics.jobRequested&&this.log.info(`JOB REQUEST -> FIRST FRAME: ${(i-this.ctx.performanceMetrics.jobRequested).toFixed(2)}ms`),n.captureTime&&this.log.info(`End-to-end latency: ${(i-n.captureTime).toFixed(1)}ms`),n.receiveTime&&n.presentationTime&&this.log.info(`Local decode latency: ${(n.presentationTime-n.receiveTime).toFixed(1)}ms`),this.ctx.logPerformanceSummary(),this.ctx.reportTiming("first-frame")),e>0){const t=n.presentedFrames-e-1;t>=3&&(this.log.info(`Dropped ${t} frame(s)`),this.ctx.host.dispatchEvent(new CustomEvent("frames-dropped",{detail:{count:t,timestamp:i}})))}e=n.presentedFrames,t.requestVideoFrameCallback(s)};t.requestVideoFrameCallback(s)}else{const e=()=>{this.ctx.performanceMetrics.firstFrame||(this.ctx.performanceMetrics.firstFrame=performance.now(),this.ctx.performanceMetrics.sessionStarted&&this.log.info(`SESSION START -> FIRST FRAME: ${(performance.now()-this.ctx.performanceMetrics.sessionStarted).toFixed(2)}ms`),this.ctx.performanceMetrics.subscribeStart&&this.log.info(`SUBSCRIBE -> FIRST FRAME: ${(performance.now()-this.ctx.performanceMetrics.subscribeStart).toFixed(2)}ms`),this.ctx.performanceMetrics.jobRequested&&this.log.info(`JOB REQUEST -> FIRST FRAME: ${(performance.now()-this.ctx.performanceMetrics.jobRequested).toFixed(2)}ms`),this.ctx.logPerformanceSummary(),this.ctx.reportTiming("first-frame")),t.removeEventListener("loadeddata",e)};t.addEventListener("loadeddata",e)}t.play().catch(t=>{this.log.warn("Autoplay failed",t),this.ctx.setAutoplayBlocked(1),this.ctx.setJobState("ready")})}this.ctx.host.dispatchEvent(new CustomEvent("video-track-received",{detail:{track:e,stream:s,trackId:r,label:e.label,isActive:r===this.activeVideoTrackId,totalTracks:this.videoTracks.size}})),this.ctx.detectXRStream(),e.onended=()=>{if(this.videoTracks.delete(r),this.ctx.host.dispatchEvent(new CustomEvent("video-track-removed",{detail:{trackId:r,label:e.label}})),r===this.activeVideoTrackId&&this.videoTracks.size>0){const t=this.videoTracks.keys().next().value;this.switchVideoTrack(t)}}}else if("audio"===e.kind){if(this.ctx.getVideoElement().srcObject===s)return;this.audioElement.srcObject=s,this.audioElement.play().catch(t=>{this.log.warn("Audio autoplay failed",t)}),this.ctx.host.dispatchEvent(new CustomEvent("audio-track-received",{detail:{track:e,stream:s}}))}}switchVideoTrack(t){const e=this.videoTracks.get(t);if(!e)return void this.log.warn(`Video track ${t} not found`);this.activeVideoTrackId=t;const s=this.ctx.getVideoElement();s.srcObject=e.stream,s.play().catch(t=>{this.log.warn("Failed to play switched video track",t)}),this.ctx.host.dispatchEvent(new CustomEvent("video-track-switched",{detail:{trackId:t,label:e.track.label}}))}setVideoCodecPreferences(t){if("function"!=typeof t.setCodecPreferences)return void this.log.debug("setCodecPreferences not supported, relying on SDP codec ordering");const e=RTCRtpReceiver.getCapabilities?.("video");if(!e)return void this.log.debug("RTCRtpReceiver.getCapabilities not available");const s=["video/H265","video/H264","video/AV1","video/VP9"],i=[...e.codecs].sort((t,e)=>{const i=s.indexOf(t.mimeType),n=s.indexOf(e.mimeType),r=i>=0?i:s.length,a=n>=0?n:s.length;return r!==a?r-a:"video/H264"===t.mimeType&&"video/H264"===e.mimeType?(/profile-level-id=64/.test(t.sdpFmtpLine||"")?0:1)-(/profile-level-id=64/.test(e.sdpFmtpLine||"")?0:1):0});try{t.setCodecPreferences(i);const e=i.filter(t=>s.includes(t.mimeType)).map(t=>t.mimeType.replace("video/",""));this.log.info("Video codec preferences set",{order:e})}catch(t){this.log.warn("Failed to set codec preferences",t)}}mungeSDP(t){const e=this.ctx.config,s=R[e.streamQuality]??R["low-latency"],i=e.videoBitrateMin??s.minBitrate,n=e.videoBitrateStart??s.startBitrate,r=e.videoBitrateMax??s.maxBitrate;let a=t.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm,`$1;x-google-min-bitrate=${i};x-google-start-bitrate=${n};x-google-max-bitrate=${r}\r\n`);s.conferenceFlag&&(a=a.replace(/(m=video[^\r\n]*\r\n)/g,"$1a=x-google-flag:conference\r\n")),a=a.replace(/b=(AS|TIAS):[^\r\n]*\r\n/g,""),null!==s.audioPtime&&(a=a.replace(/(m=audio[^\r\n]*\r\n)/g,`$1a=ptime:${s.audioPtime}\r\na=maxptime:${s.audioPtime}\r\n`));const o=a.match(/a=rtpmap:(\d+) opus\/48000/);if(o){const t=o[1],e=RegExp(`(a=fmtp:${t} .+?)(\\r\\n)`);let s="maxaveragebitrate=510000";this.h&&(s+=";sprop-maxcapturerate=48000"),s+=this.l?";stereo=0":";stereo=1",s+=";useinbandfec=1",a=e.test(a)?a.replace(e,`$1;${s}$2`):a.replace(RegExp(`(a=rtpmap:${t} opus/48000/2\\r\\n)`),`$1a=fmtp:${t} ${s}\r\n`)}return a}async createOffer(){if(!this.peerConnection)throw Error("Peer connection not created");const t=await this.peerConnection.createOffer();return t.sdp=this.mungeSDP(t.sdp),await this.peerConnection.setLocalDescription(t),t}async handleOffer(t){if(!this.ctx.performanceMetrics.offerReceived&&(this.ctx.performanceMetrics.offerReceived=performance.now(),this.ctx.performanceMetrics.subscribeStart)){const t=this.ctx.performanceMetrics.offerReceived-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to receive offer: ${t.toFixed(2)}ms`)}this.peerConnection?this.log.debug("Using pre-created peer connection"):(this.log.debug("Peer connection not pre-created, creating now..."),this.createPeerConnection()),await this.peerConnection.setRemoteDescription(t);for(const t of this.peerConnection.getTransceivers())"video"===t.receiver?.track?.kind&&this.setVideoCodecPreferences(t);const e=await this.peerConnection.createAnswer();if(e.sdp=this.mungeSDP(e.sdp),await this.peerConnection.setLocalDescription(e),this.ctx.performanceMetrics.answerSent=performance.now(),this.ctx.performanceMetrics.subscribeStart){const t=this.ctx.performanceMetrics.answerSent-this.ctx.performanceMetrics.subscribeStart;this.log.info(`Time to create answer: ${t.toFixed(2)}ms`)}return this.ctx.host.dispatchEvent(new CustomEvent("answer-created",{detail:e})),e}async handleAnswer(t){if(!this.peerConnection)throw Error("Peer connection not created");await this.peerConnection.setRemoteDescription(t)}async addIceCandidate(t){if(!this.peerConnection)throw Error("Peer connection not created");await this.peerConnection.addIceCandidate(t)}createDataChannel(t="cirrus"){if(!this.peerConnection)throw Error("Peer connection not created");this.dataChannel=this.peerConnection.createDataChannel(t,{ordered:1}),this.setupDataChannelHandlers()}handleDataChannel(t){this.dataChannel=t.channel,this.setupDataChannelHandlers()}setupDataChannelHandlers(){this.dataChannel&&(this.dataChannel.binaryType="arraybuffer",this.dataChannel.onopen=()=>{this.log.info("Data channel opened"),this.ctx.host.dispatchEvent(new CustomEvent("data-channel-open")),this.sendToStreamer("RequestInitialSettings",[])},this.dataChannel.onclose=()=>{this.log.info("Data channel closed"),this.ctx.host.dispatchEvent(new CustomEvent("data-channel-close"))},this.dataChannel.onerror=t=>{this.log.error("Data channel error",t),this.ctx.host.dispatchEvent(new CustomEvent("data-channel-error",{detail:t}))},this.dataChannel.onmessage=t=>{this.handleDataChannelMessage(t)})}handleDataChannelMessage(t){const e=a.decode(t.data);switch(this.log.debug("Received from UE",{type:e.type,payload:e.payload}),this.ctx.host.dispatchEvent(new CustomEvent("message-from-streamer",{detail:e})),e.type){case"QualityControlOwnership":this.ctx.host.dispatchEvent(new CustomEvent("quality-control-ownership",{detail:e.payload}));break;case"InputControlOwnership":this.ctx.host.dispatchEvent(new CustomEvent("input-control-ownership",{detail:e.payload}));break;case"Response":case"Command":this.ctx.host.dispatchEvent(new CustomEvent("ue-command-response",{detail:e.payload}));break;case"VideoEncoderAvgQP":this.ctx.host.dispatchEvent(new CustomEvent("video-encoder-qp",{detail:e.payload}));break;case"LatencyTest":case"DataChannelLatencyTest":e.payload&&"object"==typeof e.payload&&this.ctx.handleLatencyTestResult(e.payload),this.sendToStreamer("LatencyTest",[JSON.stringify(e.payload)]);break;case"InitialSettings":this.ctx.host.dispatchEvent(new CustomEvent("initial-settings",{detail:e.payload}));break;case"GamepadResponse":this.ctx.host.dispatchEvent(new CustomEvent("gamepad-response",{detail:e.payload}));break;case"FileExtension":this.ctx.handleFileExtension(t.data);break;case"FileMimeType":this.ctx.handleFileMimeType(t.data);break;case"FileContents":this.ctx.handleFileContents(t.data);break;case"FreezeFrame":this.ctx.host.dispatchEvent(new CustomEvent("freeze-frame",{detail:e.payload}));break;case"UnfreezeFrame":this.ctx.host.dispatchEvent(new CustomEvent("unfreeze-frame",{detail:e.payload}));break;case"TestEcho":this.ctx.host.dispatchEvent(new CustomEvent("test-echo",{detail:e.payload}));break;case"Protocol":this.ctx.host.dispatchEvent(new CustomEvent("protocol",{detail:e.payload}))}}requestIFrame(){this.sendToStreamer("IFrameRequest",[]),this.requestKeyFrame()}requestKeyFrame(){this.V&&this.V.postMessage({type:"keyframe"})}attachKeyframeTransform(t){if("function"==typeof globalThis.RTCRtpScriptTransform)try{if(!this.V){const t=new Blob([T.KEYFRAME_WORKER_SRC],{type:"application/javascript"});this.V=new Worker(URL.createObjectURL(t))}t.transform=new globalThis.RTCRtpScriptTransform(this.V),this.log.debug("Keyframe encoded transform attached to video receiver"),this.V.postMessage({type:"keyframe"})}catch(t){this.log.warn("Failed to attach keyframe transform",t)}else this.log.debug("RTCRtpScriptTransform not available, skipping keyframe transform")}disconnectMedia(){this.F(),this.ctx.setStreamState("idle");const t=this.ctx.getVideoElement();t.srcObject&&(t.srcObject.getTracks().forEach(t=>t.stop()),t.srcObject=null),this.videoTracks.forEach(({track:t})=>t.stop()),this.videoTracks.clear(),this.activeVideoTrackId=null,this.audioElement.srcObject&&(this.audioElement.srcObject.getTracks().forEach(t=>t.stop()),this.audioElement.srcObject=null),this.microphoneStream&&(this.microphoneStream.getTracks().forEach(t=>t.stop()),this.microphoneStream=null),this.closePeerConnection()}async connect(t){this.createPeerConnection(t);const e=await this.createOffer();return this.ctx.host.dispatchEvent(new CustomEvent("offer-created",{detail:e})),e}async switchStreamer(t){return this.disconnectMedia(),await new Promise(t=>setTimeout(t,100)),await this.connect(t)}async requestMicrophone(){if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia)return this.log.warn("Microphone unavailable: mediaDevices API not supported"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-unavailable",{detail:{reason:"api-not-supported"}})),0;const t=await this.ctx.checkPermission("microphone");if("denied"===t)return this.log.warn("Microphone permission previously denied"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-permission-denied",{detail:{reason:"previously-denied",state:t}})),0;const e={autoGainControl:0,channelCount:1,echoCancellation:0,latency:0,noiseSuppression:0,sampleRate:48e3,sampleSize:16};try{if(this.microphoneStream=await navigator.mediaDevices.getUserMedia({video:0,audio:e}),this.peerConnection){const t=this.microphoneStream.getAudioTracks()[0],e=this.peerConnection.getSenders().find(t=>"audio"===t.track?.kind);e?await e.replaceTrack(t):this.peerConnection.addTrack(t,this.microphoneStream);const s=this.peerConnection.getTransceivers().find(t=>"audio"===t.receiver.track.kind);s&&(s.direction="sendrecv")}return this.h=1,this.ctx.host.dispatchEvent(new CustomEvent("microphone-enabled")),this.ctx.watchPermission("microphone"),1}catch(t){const e=t?.name||"UnknownError",s=t?.message||"Unknown error";return"NotAllowedError"===e||"PermissionDeniedError"===e?(this.log.warn("Microphone permission denied by user"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-permission-denied",{detail:{reason:"user-denied",error:e,message:s}}))):"NotFoundError"===e||"DevicesNotFoundError"===e?(this.log.warn("No microphone device found"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-unavailable",{detail:{reason:"no-device",error:e,message:s}}))):"NotReadableError"===e||"TrackStartError"===e?(this.log.warn("Microphone not readable (may be in use by another application)"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-unavailable",{detail:{reason:"hardware-error",error:e,message:s}}))):"OverconstrainedError"===e?(this.log.warn("Microphone constraints cannot be satisfied"),this.ctx.host.dispatchEvent(new CustomEvent("microphone-error",{detail:{reason:"overconstrained",error:e,message:s}}))):(this.log.error("Microphone error",t),this.ctx.host.dispatchEvent(new CustomEvent("microphone-error",{detail:{reason:"unknown",error:e,message:s}}))),0}}async enableMicrophone(){if(!await this.requestMicrophone())throw Error("Failed to enable microphone")}disableMicrophone(){if(this.microphoneStream&&(this.microphoneStream.getTracks().forEach(t=>t.stop()),this.microphoneStream=null),this.peerConnection){const t=this.peerConnection.getSenders().find(t=>"audio"===t.track?.kind);t&&t.replaceTrack(null);const e=this.peerConnection.getTransceivers().find(t=>"audio"===t.receiver.track.kind);e&&(e.direction="recvonly")}this.h=0,this.ctx.host.dispatchEvent(new CustomEvent("microphone-disabled"))}setMicrophoneMuted(t){this.microphoneStream&&(this.microphoneStream.getAudioTracks().forEach(e=>{e.enabled=!t}),this.ctx.host.dispatchEvent(new CustomEvent("microphone-muted-change",{detail:t})))}get isMicrophoneEnabled(){return this.h&&null!==this.microphoneStream}get isMicrophoneMuted(){if(!this.microphoneStream)return 1;const t=this.microphoneStream.getAudioTracks();return 0===t.length||!t[0].enabled}sendCommand(t){this.sendToStreamer("Command",[JSON.stringify(t)])}sendUIInteraction(t){this.sendToStreamer("UIInteraction",[JSON.stringify(t)])}}T.DISCONNECT_POLL_INTERVAL_MS=3e3,T.RECOVERY_MIN_INTERVAL_MS=1e4,T.RECOVERY_WINDOW_MS=6e4,T.MAX_RECOVERY_IN_WINDOW=3,T.ICE_RESTART_TIMEOUT_MS=5e3,T.ICE_RESTART_MAX_ATTEMPTS=2,T.TURN_HEALTH_CHECK_TIMEOUT_MS=3e3,T.TURN_HEALTH_CHECK_CACHE_MS=3e4,T.KEYFRAME_WORKER_SRC="\n let transformer = null;\n self.onrtctransform = (event) => {\n transformer = event.transformer;\n // Passthrough: pipe readable straight to writable (zero-copy)\n transformer.readable.pipeTo(transformer.writable);\n };\n self.onmessage = (event) => {\n if (event.data.type === 'keyframe' && transformer) {\n transformer.sendKeyFrameRequest().catch(() => {});\n }\n };\n ";class $ extends Error{constructor(t,e,s){super(t),this.code=e,this.statusCode=s,this.name="SessionError"}}class x{constructor(t,e){this.ctx=t,this.log=e,this.G=null,this.X="idle",this.K=null,this.J=0,this.Y=null,this.Z=null,this.tt=0,this.et=null,this.st=null,this.it=null,this.nt=null,this.rt=null,this.ot=[],this.ht=0,this.ct=null,this.lt=new Map}get ws(){return this.G}get state(){return this.X}get accessToken(){return this.st}get sessionId(){return this.nt}get agentId(){return this.rt}get iceServers(){return this.ot}get subscribedStreamerId(){return this.ct}get isCollaborator(){return this.ht}get peerAgentId(){return this.ut}get availableStreamers(){return Array.from(this.lt.keys())}async startSession(){const t=this.ctx.config;if(!t.admissionToken){const t=Error("admission-token is required to start a session");throw this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t.message,phase:"validation"}})),t}if("idle"!==this.X&&"error"!==this.X)return void this.log.info(`Already in state '${this.X}', ignoring startSession call`);this.ctx.performanceMetrics.sessionStarted=performance.now(),this.ctx.setSessionState("connecting"),this.et?.abort(),this.et=new AbortController;const e=this.et.signal;try{this.log.info("Exchanging admission token for access token..."),this.ctx.performanceMetrics.tokenExchangeStart=performance.now();const s=await this.exchangeToken(e);this.st=s.accessToken,this.it=s.expiresAt,this.nt=s.sessionId,this.rt=s.agentId,this.ot=s.iceServers||[],this.ctx.performanceMetrics.tokenExchangeComplete=performance.now();const i=this.ctx.performanceMetrics.tokenExchangeComplete-this.ctx.performanceMetrics.tokenExchangeStart;this.log.info(`Token exchange successful in ${i.toFixed(2)}ms. Session: ${this.nt}, Agent: ${this.rt}`),this.log.debug(`Received ${this.ot.length} ICE server(s)`,this.ot),this.ctx.host.dispatchEvent(new CustomEvent("session-token-exchanged",{detail:{sessionId:this.nt,agentId:this.rt,iceServers:this.ot}})),t.swiftJobRequest&&!this.ht&&(this.ctx.performanceMetrics.jobRequested=this.ctx.performanceMetrics.tokenExchangeStart,this.ctx.setJobState("pending")),this.ht&&this.ctx.setJobState("pending"),this.ot.length>0&&this.ctx.preCreatePeerConnection({iceServers:this.ot}),this.ctx.performanceMetrics.sessionWsConnectStart=performance.now();const n=3;for(let t=0;t<n;t++){if(e.aborted)throw Error("Session start aborted");try{await this.connectWs();break}catch(s){if(e.aborted)throw Error("Session start aborted");if(!(t<n-1))throw s;{const e=500*(t+1);this.log.warn(`WebSocket connection failed (attempt ${t+1}/${n}), retrying in ${e}ms...`),await new Promise(t=>setTimeout(t,e))}}}}catch(t){this.log.error("Failed to start session:",t);const e=t instanceof $?t.code:"connection_failed";throw this.ctx.setSessionState("error"),this.ctx.setStatusFailed(e),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t instanceof Error?t.message:t+"",code:t instanceof $?t.code:void 0,phase:"connection"}})),t}}leaveSession(t){this.et&&(this.et.abort(),this.et=null),"idle"!==this.X&&this.G?(this.log.info("Client-initiated disconnect"+(t?": "+t:"")),this.sendMessage({type:"report-event",event_type:"client.disconnect",data:t?{reason:t}:void 0}),this.sendMessage({type:"leave-session"}),this.terminateSession(t)):this.terminateSession()}terminateSession(t){this.log.info("Ending session"),this.X="idle",this.ctx.setSessionState("idle"),this.ctx.updateDebugIndicator(),this.stopKeepalive(),this.stopHeartbeat(),this.Z&&(clearTimeout(this.Z),this.Z=null),this.ct=null,this.lt.clear(),this.G&&(this.G.onclose=null,this.G.close(1e3,t||"client-terminated"),this.G=null),this.et&&(this.et.abort(),this.et=null),this.st=null,this.it=null,this.nt=null,this.rt=null,this.tt=0,this.ctx.setJobState("unsubmitted"),this.ctx.setAutoplayBlocked(0),this.ctx.host.dispatchEvent(new CustomEvent("session-ended"))}requestInvite(){this.G&&this.G.readyState===WebSocket.OPEN?this.ht?this.log.warn("session-collaborator role cannot request invite tokens"):(this.log.info("Requesting session invite token..."),this.sendMessage({type:"request-invite"})):this.log.warn("Cannot request invite — WebSocket not connected")}sendMessage(t){this.G&&this.G.readyState===WebSocket.OPEN?this.G.send(JSON.stringify(t)):this.log.warn("Cannot send message - WebSocket not connected")}async subscribeToStreamer(t){this.log.info("Subscribing to streamer: "+t),this.ct=t,this.ctx.resetPerformanceMetrics();const e=this.lt.get(t),s=e?.metadata?.offer_mode;if(e?.contributorAgentId&&(this.ut=e.contributorAgentId),this.log.info(`Streamer ${t} offer_mode: ${s||"not specified (defaulting to streamer)"} peer=${this.ut??"unknown"}`),this.sendMessage({type:"update-presence",preferred_streamer_id:t}),"browser"===s){this.log.info("Browser-initiated offer mode: creating WebRTC offer");try{this.ctx.getPeerConnection()||this.ctx.preCreatePeerConnection({iceServers:this.ot}),this.ctx.createDataChannel("cirrus");const t=await this.ctx.createOffer();this.ctx.setNegotiatingStarted(),this.sendMessage({type:"webrtc-signaling",signal_type:"offer",payload:{sdp:t.sdp},...this.ut&&{target_agent_id:this.ut}}),this.log.info("Browser-initiated offer sent to "+(this.ut??"broadcast"))}catch(t){this.log.error("Failed to create browser-initiated offer:",t),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t instanceof Error?t.message:t+"",phase:"webrtc-offer-creation"}}))}}}evaluateSubscription(){if(!this.G||this.G.readyState!==WebSocket.OPEN)return;if(0===this.lt.size)return;const t=this.ctx.config.streamerId;let e=null;t&&"default"!==t?this.lt.has(t)&&(e=t):e=this.lt.keys().next().value??null,e&&e!==this.ct&&this.subscribeToStreamer(e)}async exchangeToken(t){const e=this.ctx.config,s=`https://${e.apiEndpoint}/agent/token`,i=new AbortController,n=setTimeout(()=>i.abort(),15e3),r=()=>i.abort();t?.addEventListener("abort",r);const a=function(t){try{const e=JSON.parse(atob(t.split(".")[1])),s=e.typ??e.token_type;return"inv"===s||"invite"===s}catch{return 0}}(e.admissionToken);this.ht=a;const o=a?{grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer",invite_token:e.admissionToken,include_ice_servers:1}:{grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer",assertion:e.admissionToken,agent_role:"browser-agent",include_ice_servers:1,...e.swiftJobRequest&&{swift_job_request:1},preferred_streamer_id:e.streamerId||"default",...null!=e.lat&&{lat:e.lat},...null!=e.lng&&{lng:e.lng},...(null!=e.queueWaitTolerance||null!=e.webrtcNegotiationTolerance)&&{tolerances:{...null!=e.queueWaitTolerance&&{queue_wait_tolerance_seconds:e.queueWaitTolerance},...null!=e.webrtcNegotiationTolerance&&{webrtc_negotiation_tolerance_seconds:e.webrtcNegotiationTolerance}}},...e.appId&&{application_request:{app_id:e.appId,...e.appVersion&&{version_id:e.appVersion}}}};let h;try{h=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json"},signal:i.signal,body:JSON.stringify(o)})}finally{clearTimeout(n),t?.removeEventListener("abort",r)}if(!h.ok){const t=await h.text().catch(()=>"Unknown error"),e=401===(c=h.status)?/expired/i.test(t)?"token_expired":"unauthorized":c>=500?"server_error":"request_failed";throw new $(`Token exchange failed (${h.status}): ${t}`,e,h.status)}var c;const l=await h.json();return l.ice_servers&&0!==l.ice_servers.length||this.log.warn("No ICE servers received from gateway"),{accessToken:l.access_token,expiresAt:Date.now()+1e3*l.expires_in,sessionId:l.session_id,agentId:l.agent_id,iceServers:l.ice_servers||[]}}async connectWs(){if(!this.st)throw Error("No access token available for WebSocket connection");this.ctx.setSessionState("authenticating");const t=`wss://${this.ctx.config.apiEndpoint}/ws/session`;return this.log.info(`Connecting WebSocket to ${this.ctx.config.apiEndpoint}...`),new Promise((e,s)=>{try{this.G=new WebSocket(t,["Bearer."+this.st]);let i=0;const n=setTimeout(()=>{this.G&&this.G.readyState===WebSocket.CONNECTING&&(this.G.close(),i||(i=1,s(Error("WebSocket connection timeout"))))},1e4);this.G.onopen=()=>{if(clearTimeout(n),i=1,this.ctx.performanceMetrics.sessionWsConnectComplete=performance.now(),this.ctx.performanceMetrics.sessionWsConnectStart){const t=this.ctx.performanceMetrics.sessionWsConnectComplete-this.ctx.performanceMetrics.sessionWsConnectStart;this.log.info(`WebSocket connected in ${t.toFixed(2)}ms`)}else this.log.info("WebSocket connected");this.tt=0,this.X="connected",this.ctx.setSessionState("connected"),this.startKeepalive(),this.startHeartbeat(),this.ctx.host.dispatchEvent(new CustomEvent("session-connected",{detail:{sessionId:this.nt,agentId:this.rt}})),e()},this.G.onmessage=t=>{this.handleMessage(t.data)},this.G.onclose=t=>{if(clearTimeout(n),!i)return i=1,void s(Error(`WebSocket upgrade failed (code=${t.code})`));this.handleWsClose(t)},this.G.onerror=t=>{this.log.error("WebSocket error:",t)}}catch(t){s(t)}})}handleMessage(t){if("pong"===t)return;let e;try{e=JSON.parse(t)}catch{return void this.log.error("Failed to parse WebSocket message:",t)}switch(this.log.debug("Received message: "+e.type),e.type){case"offer":this.handleSignallingOffer(e);break;case"iceCandidate":this.handleSignallingIceCandidate(e);break;case"streamer-list":this.ctx.host.dispatchEvent(new CustomEvent("streamer-list",{detail:{streamers:e.streamers}}));break;case"streamer-joined":this.ctx.cancelRendezvousTimer(),this.ctx.host.dispatchEvent(new CustomEvent("streamer-joined",{detail:{streamerId:e.streamerId}}));break;case"streamer-left":this.ctx.host.dispatchEvent(new CustomEvent("streamer-left",{detail:{streamerId:e.streamerId}}));break;case"pong":case"heartbeat":case"presence-changed":break;case"error":this.log.error("Server error:",e),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:e.message||"Unknown server error",code:e.code,phase:"signalling"}}));break;case"webrtc-signaling":{const t=e;switch(this.log.info(`WebRTC signaling: ${t.signal_type} from ${t.from_agent_id}`),t.signal_type){case"offer":{const e=this.ctx.getPeerConnection();if(e&&"connected"===e.connectionState){this.log.info(`Ignoring offer from ${t.from_agent_id} — already connected (offer is for another participant)`);break}this.ut=t.from_agent_id,this.ctx.setNegotiatingStarted(),this.handleSignallingOffer({type:"offer",sdp:t.payload.sdp});break}case"iceCandidate":this.handleSignallingIceCandidate({type:"iceCandidate",candidate:t.payload.candidate,sdpMid:t.payload.sdpMid,sdpMLineIndex:t.payload.sdpMLineIndex});break;case"answer":this.ut=t.from_agent_id,this.handleSignallingAnswer({type:"answer",sdp:t.payload.sdp})}break}case"session-ready":{const t=e;this.ctx.performanceMetrics.sessionReady=performance.now();const s=t.participants?.map(t=>`${t.agent_id}(${t.agent_role})`).join(", ")||"none";if(this.ctx.performanceMetrics.sessionStarted){const e=this.ctx.performanceMetrics.sessionReady-this.ctx.performanceMetrics.sessionStarted;this.log.info(`Session ready in ${e.toFixed(2)}ms: ${t.session_id}, participants: ${s}`)}else this.log.info(`Session ready: ${t.session_id}, participants: ${s}`);this.nt=t.session_id,this.rt=t.agent_id,this.ctx.host.dispatchEvent(new CustomEvent("session-ready",{detail:{sessionId:t.session_id,agentId:t.agent_id,projectId:t.project_id,appId:t.app_id,versionId:t.version_id,participants:t.participants}})),t.participants?.some(t=>"worker-agent"===t.agent_role)&&this.ctx.setJobState("fulfilled"),this.ctx.reportTiming("session-ready"),this.ctx.config.streamerId&&!this.ct&&(this.ct="default"===this.ctx.config.streamerId?null:this.ctx.config.streamerId);break}case"participant-joined":{const t=e;this.log.info(`Participant joined: ${t.agent_id} (${t.agent_role})`),this.ctx.host.dispatchEvent(new CustomEvent("participant-joined",{detail:{agentId:t.agent_id,agentRole:t.agent_role,metadata:t.metadata}})),"worker-agent"===t.agent_role&&this.ctx.setJobState("fulfilled");break}case"participant-left":{const t=e;this.log.info(`Participant left: ${t.agent_id} (${t.agent_role}), reason: ${t.reason||"none"}`),"worker-agent"===t.agent_role&&(this.ut===t.agent_id&&(this.ut=void 0),this.ctx.setJobState("pending")),this.ctx.host.dispatchEvent(new CustomEvent("participant-left",{detail:{agentId:t.agent_id,agentRole:t.agent_role,reason:t.reason}}));break}case"session-ended":{const t=e;this.log.info("Session ended by server: "+(t.reason||"unknown reason"));const s=["queue_wait_timeout","rendezvous_timeout","webrtc_negotiation_timeout"];t.reason&&s.includes(t.reason)?this.ctx.setStatusFailed(t.reason):this.ctx.setStatusEnded(),this.ctx.host.dispatchEvent(new CustomEvent("session-ended",{detail:{reason:t.reason}})),this.terminateSession();break}case"heartbeat-ack":this.J=Date.now();break;case"job-requested":{const t=e;this.log.info("Job requested: "+t.job_id),this.ctx.setJobState("pending"),this.ctx.host.dispatchEvent(new CustomEvent("job-requested",{detail:{jobId:t.job_id}}));break}case"job-cancelled":{const t=e;this.log.info(`Job cancelled: success=${t.success}, reason=${t.reason||"none"}`),t.success&&this.ctx.setJobState("unsubmitted"),this.ctx.host.dispatchEvent(new CustomEvent("job-cancelled",{detail:{jobId:t.job_id,success:t.success,reason:t.reason}}));break}case"invite-token":{const t=e;this.log.info("Received invite token from session"),this.ctx.host.dispatchEvent(new CustomEvent("invite-token",{detail:{token:t.token}}));break}case"contribution-available":{const t=e;"pixel-streaming"===t.contribution_type&&(this.ctx.setRendezvoused(),this.lt.set(t.contribution_id,{contributorAgentId:t.contributor_agent_id,metadata:t.metadata}),this.ctx.host.dispatchEvent(new CustomEvent("streamer-available",{detail:{streamerId:t.contribution_id,contributorAgentId:t.contributor_agent_id,metadata:t.metadata}})),this.ctx.host.dispatchEvent(new CustomEvent("streamer-list-changed",{detail:{streamers:this.availableStreamers}})),this.evaluateSubscription());break}case"contribution-unavailable":{const t=e;this.lt.has(t.contribution_id)&&(this.lt.delete(t.contribution_id),this.ctx.host.dispatchEvent(new CustomEvent("streamer-unavailable",{detail:{streamerId:t.contribution_id}})),this.ctx.host.dispatchEvent(new CustomEvent("streamer-list-changed",{detail:{streamers:this.availableStreamers}})),this.ct===t.contribution_id&&(this.ct=null,this.ctx.onWorkerLeft(),this.evaluateSubscription()));break}default:this.log.debug("Unknown message type: "+e.type),this.ctx.host.dispatchEvent(new CustomEvent("session-message",{detail:e}))}}async handleSignallingOffer(t){const e=this.ctx.getPcGeneration();try{this.log.info(`Processing SDP offer from streamer (gen=${e})...`);const s=await this.ctx.handleOffer({type:"offer",sdp:t.sdp});if(this.ctx.getPcGeneration()!==e)return void this.log.warn(`Discarding stale answer (gen=${e}, current=${this.ctx.getPcGeneration()})`);this.sendMessage({type:"webrtc-signaling",signal_type:"answer",payload:{sdp:s.sdp},...this.ut&&{target_agent_id:this.ut}}),this.log.info(`Sent SDP answer (gen=${e}) target=${this.ut??"*"}`)}catch(t){if(this.ctx.getPcGeneration()!==e)return void this.log.debug("Ignoring offer error from stale gen="+e);this.log.error("Failed to handle offer:",t),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t instanceof Error?t.message:t+"",phase:"webrtc-offer"}}))}}async handleSignallingAnswer(t){const e=this.ctx.getPcGeneration();try{if(this.log.info(`Processing SDP answer from streamer (gen=${e})...`),await this.ctx.handleAnswer({type:"answer",sdp:t.sdp}),this.ctx.getPcGeneration()!==e)return void this.log.warn(`Answer processed but gen changed (gen=${e}, current=${this.ctx.getPcGeneration()})`);this.log.info(`SDP answer processed successfully (gen=${e})`)}catch(t){if(this.ctx.getPcGeneration()!==e)return void this.log.debug("Ignoring answer error from stale gen="+e);this.log.error("Failed to handle answer:",t),this.ctx.host.dispatchEvent(new CustomEvent("session-error",{detail:{error:t instanceof Error?t.message:t+"",phase:"webrtc-answer"}}))}}async handleSignallingIceCandidate(t){const e=this.ctx.getPcGeneration();try{await this.ctx.addIceCandidate({candidate:t.candidate,sdpMid:t.sdpMid,sdpMLineIndex:t.sdpMLineIndex})}catch(t){if(this.ctx.getPcGeneration()!==e)return void this.log.debug("Ignoring ICE candidate error from stale gen="+e);this.log.error("Failed to add ICE candidate:",t)}}handleWsClose(t){const e=this.getCloseCodeDescription(t.code);if(this.log.info(`WebSocket closed: code=${t.code} (${e}), reason="${t.reason||"none"}"`),this.stopKeepalive(),this.stopHeartbeat(),"idle"===this.X)return void this.log.info("Not reconnecting - session was intentionally ended");const s=this.ctx.config;if("none"===s.reconnectMode)return this.log.info('Reconnect mode is "none" - not attempting reconnection'),this.X="error",this.ctx.setSessionState("error"),this.ctx.setStatusFailed("disconnected"),void this.ctx.host.dispatchEvent(new CustomEvent("session-disconnected",{detail:{code:t.code,reason:t.reason||e,wasClean:t.wasClean,willReconnect:0}}));const i="connected"===this.X||"reconnecting"===this.X,n=s.reconnectAttempts===1/0||-1===s.reconnectAttempts||this.tt<s.reconnectAttempts,r="recover"===s.reconnectMode&&this.st,a="always"===s.reconnectMode;if(i&&n&&(r||a))this.log.info(`Will attempt reconnection (mode=${s.reconnectMode}, attempts=${this.tt+1}/${s.reconnectAttempts===1/0?"∞":s.reconnectAttempts})`),this.X="reconnecting",this.ctx.setSessionState("reconnecting"),this.scheduleReconnect();else{const i=n?r||"recover"!==s.reconnectMode?"session was not in connected state":"no access token for recovery":"max reconnection attempts reached";this.log.info("Not reconnecting: "+i),this.X="error",this.ctx.setSessionState("error");const a=n?r||"recover"!==s.reconnectMode?"not_connected":"no_access_token":"reconnect_exhausted";this.ctx.setStatusFailed(a),this.ctx.host.dispatchEvent(new CustomEvent("session-disconnected",{detail:{code:t.code,reason:t.reason||e,wasClean:t.wasClean,willReconnect:0,failureReason:i}}))}}getCloseCodeDescription(t){return{1e3:"Normal closure",1001:"Going away",1002:"Protocol error",1003:"Unsupported data",1005:"No status received",1006:"Abnormal closure",1007:"Invalid frame payload data",1008:"Policy violation",1009:"Message too big",1010:"Mandatory extension",1011:"Internal server error",1012:"Service restart",1013:"Try again later",1014:"Bad gateway",1015:"TLS handshake failure"}[t]||`Unknown (${t})`}scheduleReconnect(){this.Z&&clearTimeout(this.Z);const t=this.ctx.config;let e;e="periodic"===t.reconnectStrategy?t.reconnectInterval:Math.min(x.DEFAULT_MIN_RECONNECT_DELAY*Math.pow(x.DEFAULT_RECONNECT_BACKOFF,this.tt),x.DEFAULT_MAX_RECONNECT_DELAY);const s=t.reconnectAttempts===1/0||-1===t.reconnectAttempts?"∞":t.reconnectAttempts;this.log.info(`Scheduling reconnect attempt ${this.tt+1}/${s} in ${e}ms (strategy=${t.reconnectStrategy})`),this.ctx.host.dispatchEvent(new CustomEvent("session-reconnecting",{detail:{attempt:this.tt+1,maxAttempts:t.reconnectAttempts,delay:e,strategy:t.reconnectStrategy}})),this.Z=setTimeout(async()=>{this.tt++;try{if(this.it&&Date.now()>=this.it){this.log.info("Access token expired, re-exchanging before reconnect...");const t=await this.exchangeToken();this.st=t.accessToken,this.it=t.expiresAt,this.log.info("Token re-exchange successful")}this.log.info(`Attempting reconnection ${this.tt}...`),await this.connectWs()}catch(e){this.log.error("Reconnection failed:",e),t.reconnectAttempts===1/0||-1===t.reconnectAttempts||this.tt<t.reconnectAttempts?(this.X="reconnecting",this.ctx.setSessionState("reconnecting"),this.scheduleReconnect()):(this.log.info("Max reconnection attempts reached"),this.X="error",this.ctx.setSessionState("error"),this.ctx.setStatusFailed("reconnect_exhausted"),this.ctx.host.dispatchEvent(new CustomEvent("session-disconnected",{detail:{code:1006,reason:"Max reconnection attempts reached",wasClean:0,willReconnect:0,failureReason:"max reconnection attempts reached"}})))}},e)}startKeepalive(){this.stopKeepalive(),this.Y=setInterval(()=>{this.G&&this.G.readyState===WebSocket.OPEN&&this.G.send("ping")},x.SESSION_KEEPALIVE_INTERVAL)}stopKeepalive(){this.Y&&(clearInterval(this.Y),this.Y=null)}startHeartbeat(){this.stopHeartbeat(),this.J=Date.now(),this.K=setInterval(()=>{if(this.G&&this.G.readyState===WebSocket.OPEN){const t=Date.now()-this.J;if(t>x.SESSION_HEARTBEAT_TIMEOUT)return this.log.warn(`Heartbeat timeout — no ack for ${Math.round(t/1e3)}s, closing stale WebSocket`),void this.G.close(4e3,"heartbeat timeout");this.sendMessage({type:"heartbeat"})}},x.SESSION_HEARTBEAT_INTERVAL)}stopHeartbeat(){this.K&&(clearInterval(this.K),this.K=null)}}x.DEFAULT_MIN_RECONNECT_DELAY=1e3,x.DEFAULT_MAX_RECONNECT_DELAY=3e4,x.DEFAULT_RECONNECT_BACKOFF=2,x.SESSION_HEARTBEAT_INTERVAL=3e4,x.SESSION_HEARTBEAT_TIMEOUT=9e4,x.SESSION_KEEPALIVE_INTERVAL=25e3;const M={sxga:{maxWidth:1280,maxHeight:1024},hd:{maxWidth:1366,maxHeight:768},hdplus:{maxWidth:1600,maxHeight:900},fhd:{maxWidth:1920,maxHeight:1080},wuxga:{maxWidth:1920,maxHeight:1200},qhd:{maxWidth:2560,maxHeight:1440},wqhd:{maxWidth:3440,maxHeight:1440},uhd:{maxWidth:3840,maxHeight:2160}},I="undefined"!=typeof HTMLElement?HTMLElement:class{};class P extends I{get version(){return P.VERSION}get config(){return{nativeTouch:this.dt,pointerLocked:this.gt,pointerLockRelease:this.ft,suppressBrowserKeys:this.vt,apiEndpoint:this.wt,admissionToken:this.yt,appId:this.bt,appVersion:this.kt,noAutoConnect:this.Ct,mute:this.St,volume:this.Et,rendezvousPreference:this.Rt,lingerPreference:this.Tt,leftGracePeriod:this.$t,reconnectMode:this.xt,reconnectAttempts:this.tt,reconnectStrategy:this.Mt,reconnectInterval:this.It,disconnectGraceMs:this.Pt,resizeMode:this.At,dprCap:this.Dt,resolutionClamp:this._t,queueWaitTolerance:this.Vt,webrtcNegotiationTolerance:this.Ft,debug:this.Bt,controls:this.Wt,swiftJobRequest:this.Ut,streamerId:this.Lt,lat:this.qt,lng:this.zt,forceRelay:this.Nt,useMic:this.webrtc?.isMicrophoneEnabled??0,forceMonoAudio:0,iceBatchDelay:this.Ot,streamQuality:this.jt,videoBitrateMin:this.Ht,videoBitrateStart:this.Gt,videoBitrateMax:this.Xt,logLevel:this.Kt}}static get observedAttributes(){return["native-touch","pointer-lock","pointer-lock-release","enable-gamepad","enable-xr","suppress-browser-keys","admission-token","app-id","app-version","no-auto-connect","mute","volume","rendezvous-preference","linger-preference","left-grace-period","api-endpoint","reconnect-mode","reconnect-attempts","reconnect-strategy","reconnect-interval","disconnect-grace-ms","resize-mode","dpr-cap","resolution-clamp","debug","controls","swift-job-request","force-relay","queue-wait-tolerance","webrtc-negotiation-tolerance","streamer-id","lat","lng","stream-quality","video-bitrate-min","video-bitrate-start","video-bitrate-max","log-level"]}get nativeTouch(){return this.dt}set nativeTouch(t){this.dt=t,this.toggleAttribute("native-touch",t)}get pointerLock(){return this.gt}set pointerLock(t){this.gt=t,this.toggleAttribute("pointer-lock",t)}get pointerLockRelease(){return this.ft}set pointerLockRelease(t){this.ft=t,this.toggleAttribute("pointer-lock-release",t)}get suppressBrowserKeys(){return this.vt}set suppressBrowserKeys(t){this.vt=t,this.toggleAttribute("suppress-browser-keys",t)}get debug(){return this.Bt}set debug(t){this.Bt=t,this.toggleAttribute("debug",t),this.Jt()}get logLevel(){return this.Kt}set logLevel(t){this.Kt=t,this.setAttribute("log-level",t)}get iceBatchDelay(){return this.Ot}set iceBatchDelay(t){this.Ot=Math.max(0,t)}get admissionToken(){return this.yt}set admissionToken(t){const e=this.yt;this.yt=t,null!==t?this.setAttribute("admission-token",t):this.removeAttribute("admission-token"),e!==t&&this.dispatchEvent(new CustomEvent("admission-token-change",{detail:{oldValue:e,newValue:t}}))}get appId(){return this.bt}set appId(t){const e=this.bt;this.bt=t,null!==t?this.setAttribute("app-id",t):this.removeAttribute("app-id"),e!==t&&this.dispatchEvent(new CustomEvent("app-id-change",{detail:{oldValue:e,newValue:t}}))}get appVersion(){return this.kt}set appVersion(t){const e=this.kt;this.kt=t,null!==t?this.setAttribute("app-version",t):this.removeAttribute("app-version"),e!==t&&this.dispatchEvent(new CustomEvent("app-version-change",{detail:{oldValue:e,newValue:t}}))}get noAutoConnect(){return this.Ct}set noAutoConnect(t){const e=this.Ct;this.Ct=t,this.toggleAttribute("no-auto-connect",t),e!==t&&this.dispatchEvent(new CustomEvent("no-auto-connect-change",{detail:{oldValue:e,newValue:t}}))}get mute(){return this.St}set mute(t){const e=this.St;this.St=t,this.toggleAttribute("mute",t),this.video&&(this.video.muted=t),this.webrtc.getAudioElement().muted=t,e!==t&&this.dispatchEvent(new CustomEvent("mute-change",{detail:{oldValue:e,newValue:t,muted:t}}))}get volume(){return this.Et}set volume(t){const e=Math.max(0,Math.min(1,+t||0)),s=this.Et;this.Et=e,this.setAttribute("volume",e+""),this.video&&(this.video.volume=e),this.webrtc.getAudioElement().volume=e,s!==e&&this.dispatchEvent(new CustomEvent("volume-change",{detail:{oldValue:s,newValue:e}}))}get rendezvousPreference(){return this.Rt}set rendezvousPreference(t){const e=this.Rt;this.Rt=t,null!==t?this.setAttribute("rendezvous-preference",t.toString()):this.removeAttribute("rendezvous-preference"),e!==t&&this.dispatchEvent(new CustomEvent("rendezvous-preference-change",{detail:{oldValue:e,newValue:t}}))}get lingerPreference(){return this.Tt}set lingerPreference(t){const e=this.Tt;this.Tt=t,null!==t?this.setAttribute("linger-preference",t.toString()):this.removeAttribute("linger-preference"),e!==t&&this.dispatchEvent(new CustomEvent("linger-preference-change",{detail:{oldValue:e,newValue:t}}))}get leftGracePeriod(){return this.$t}set leftGracePeriod(t){const e=this.$t;this.$t=t,null!==t?this.setAttribute("left-grace-period",t.toString()):this.removeAttribute("left-grace-period"),e!==t&&this.dispatchEvent(new CustomEvent("left-grace-period-change",{detail:{oldValue:e,newValue:t}}))}get apiEndpoint(){return this.wt}set apiEndpoint(t){const e=this.wt;this.wt=t,t?this.setAttribute("api-endpoint",t):this.removeAttribute("api-endpoint"),e!==t&&this.dispatchEvent(new CustomEvent("api-endpoint-change",{detail:{oldValue:e,newValue:t}}))}get reconnectMode(){return this.xt}set reconnectMode(t){this.xt=t,this.setAttribute("reconnect-mode",t)}get reconnectAttempts(){return this.tt}set reconnectAttempts(t){this.tt=t,t===1/0||-1===t?this.removeAttribute("reconnect-attempts"):this.setAttribute("reconnect-attempts",t+"")}get reconnectStrategy(){return this.Mt}set reconnectStrategy(t){this.Mt=t,this.setAttribute("reconnect-strategy",t)}get reconnectInterval(){return this.It}set reconnectInterval(t){this.It=t,this.setAttribute("reconnect-interval",t+"")}get disconnectGraceMs(){return this.Pt}set disconnectGraceMs(t){this.Pt=Math.max(0,t),this.setAttribute("disconnect-grace-ms",this.Pt+"")}get resizeMode(){return this.At}set resizeMode(t){this.At=t,this.Qt=null,this.Yt=0,this.Zt=0,this.setAttribute("resize-mode",t)}get dprCap(){return this.Dt}set dprCap(t){this.Dt=Math.max(.1,t),this.setAttribute("dpr-cap",this.Dt+"")}get resolutionClamp(){return this._t}set resolutionClamp(t){null===t?(this._t=null,this.removeAttribute("resolution-clamp")):"string"==typeof t?this.setAttribute("resolution-clamp",t):this._t=t}get isXRStream(){return this.te}get sessionState(){return this.ee}get streamState(){return this.se}get status(){return this.ie}get failureReason(){return this.ne}get lastUserInteraction(){return this.re}get streamStartedAt(){return this.ae}get accessToken(){return this.session.accessToken}get sessionId(){return this.session.sessionId}get agentId(){return this.session.agentId}get iceServers(){return this.session.iceServers}getAdmissionConfig(){return this.yt?{admissionToken:this.yt,appId:this.bt||void 0,appVersion:this.kt||void 0}:null}getSessionPreferences(){return{rendezvousPreference:this.Rt||void 0,lingerPreference:this.Tt||void 0,leftGracePeriod:this.$t||void 0}}get isAdmitted(){return this.oe}he(){const t=this;return{get host(){return t},get config(){return t.config},get performanceMetrics(){return t.performanceMetrics},getVideoElement:()=>t.video,getAudioElement:()=>t.webrtc.getAudioElement(),getCoordTranslator:()=>t.coordTranslator,getDataChannel:()=>t.webrtc.getDataChannel(),sendToStreamer:(e,s)=>t.sendToStreamer(e,s??[]),sendRawBinary:e=>t.webrtc.sendRawBinary(e),getPeerConnection:()=>t.webrtc.getPeerConnection(),getSessionWs:()=>t.session.ws,sendSessionMessage:e=>t.session.sendMessage(e),getSessionState:()=>t.ee,getAccessToken:()=>t.session.accessToken,getSessionId:()=>t.session.sessionId,getAgentId:()=>t.session.agentId,getIceServers:()=>t.session.iceServers,getPeerAgentId:()=>t.session.peerAgentId,logPerformanceSummary:()=>t.metrics.logPerformanceSummary(),reportTiming:e=>t.metrics.reportTiming(e),collectAndDisplaySessionStats:()=>t.metrics.collectAndDisplaySessionStats(),getMetricsSessionStartTime:()=>t.metrics.sessionStartTime,setMetricsSessionStartTime:e=>{t.metrics.sessionStartTime=e},handleFileExtension:e=>t.fileTransfer.handleExtension(e),handleFileMimeType:e=>t.fileTransfer.handleMimeType(e),handleFileContents:e=>t.fileTransfer.handleContents(e),leaveSession:e=>t.stop(e),getSubscribedStreamerId:()=>t.session.subscribedStreamerId,setAutoplayBlocked:e=>{t.ce=e},setJobState:e=>t.le(e),detectXRStream:()=>t.ue(),onWorkerLeft:()=>t.webrtc.onWorkerLeft(),checkPermission:e=>t.checkPermission(e),watchPermission:e=>t.watchPermission(e),handleOffer:e=>t.webrtc.handleOffer(e),handleAnswer:e=>t.webrtc.handleAnswer(e),createOffer:()=>t.webrtc.createOffer(),createDataChannel:e=>t.webrtc.createDataChannel(e),addIceCandidate:e=>t.webrtc.addIceCandidate(e),preCreatePeerConnection:e=>t.webrtc.preCreatePeerConnection(e),getPcGeneration:()=>t.webrtc.getPcGeneration(),handleLatencyTestResult:e=>t.metrics.handleLatencyTestResult(e),getLatencyBreakdown:()=>t.metrics.getLatencyBreakdown(),setStreamState:(e,s)=>t.de(e,s),setSessionState:e=>t.pe(e),updateDebugIndicator:()=>t.Jt(),cancelRendezvousTimer:()=>t.me(),resetPerformanceMetrics:()=>t.metrics.resetPerformanceMetrics(),setRendezvoused:()=>{t.ge=1,t.fe()},setNegotiatingStarted:()=>{t.ve=1,t.fe()},setStatusFailed:e=>{t.we("failed",e),t.webrtc.closePeerConnection(),t.webrtc.resetResilienceState()},setStatusEnded:()=>{t.le("unsubmitted"),t.we("ended"),t.webrtc.closePeerConnection(),t.webrtc.resetResilienceState()}}}constructor(){super(),this.ye=null,this.be=0,this.dt=0,this.vt=0,this.gt=0,this.ft=0,this.resizeObserver=null,this.Ot=0,this.performanceMetrics={sessionStarted:null,tokenExchangeStart:null,tokenExchangeComplete:null,peerConnectionCreated:null,sessionWsConnectStart:null,sessionWsConnectComplete:null,sessionReady:null,jobRequested:null,subscribeStart:null,offerReceived:null,answerSent:null,iceConnected:null,firstTrack:null,firstFrame:null,connectionComplete:null},this.yt=null,this.bt=null,this.kt=null,this.Ct=0,this.ke=0,this.St=0,this.Et=1,this.Rt=null,this.Tt=null,this.$t=null,this.Vt=null,this.Ft=null,this.oe=0,this.Ce=null,this.Se=null,this.wt="api.interlucent.ai",this.ee="idle",this.se="idle",this.xt="none",this.tt=1/0,this.Mt="exponential-backoff",this.It=P.DEFAULT_RECONNECT_INTERVAL,this.Pt=9e3,this.Lt=null,this.qt=null,this.zt=null,this.Ee=null,this.Re=null,this.Te=null,this.At="5.4+",this.Dt=1,this._t=null,this.Qt=null,this.Yt=0,this.$e=null,this.xe=null,this.Zt=0,this.te=0,this.Bt=0,this.jt="balanced",this.Ht=null,this.Gt=null,this.Xt=null,this.Kt="warn",this.Me=c(this,"pixel-stream","host",d(()=>this.Kt)),this.Nt=0,this.Ie="unsubmitted",this.Wt=1,this.Ut=0,this.ce=0,this.re=0,this.ae=0,this.ie="idle",this.ne=null,this.ge=0,this.ve=0,this.Pe=t=>{this.Ae(t.detail)};const t=this.attachShadow({mode:"open"});this.De=this.he(),this.keyboardInput=new v(this.De,this.Me),this.mouseInput=new w(this.De,this.Me,{onPointerUnlocked:()=>this.keyboardInput.releaseAllKeys()}),this.touchInput=new y(this.De,this.Me),this.gamepadInput=new b(this.De,this.Me),this.xrInput=new C(this.De,this.Me),this.fileTransfer=new S(this.De,this.Me),this.metrics=new E(this.De,c(this,"pixel-stream","metrics",d(()=>this.Kt))),this.webrtc=new T(this.De,c(this,"pixel-stream","webrtc",d(()=>this.Kt))),this.session=new x(this.De,c(this,"pixel-stream","session",d(()=>this.Kt))),t.innerHTML='\n <style>\n :host {\n display: block;\n width: 100%;\n height: 100%;\n position: relative;\n transition: box-shadow 0.3s ease;\n\n /* ═══ Overlay theme tokens (dark default) ═══ */\n --ps-overlay-bg: rgba(10, 10, 10, 0.88);\n --ps-overlay-text: #e8e4df;\n --ps-overlay-text-dim: rgba(255, 255, 255, 0.45);\n --ps-overlay-btn-bg: rgba(255, 255, 255, 0.1);\n --ps-overlay-btn-border: rgba(255, 255, 255, 0.15);\n --ps-overlay-btn-hover: rgba(255, 255, 255, 0.18);\n --ps-overlay-spinner-track: rgba(255, 255, 255, 0.1);\n --ps-overlay-spinner-fill: rgba(255, 255, 255, 0.7);\n --ps-overlay-pill-border: rgba(255, 255, 255, 0.15);\n --ps-overlay-pill-text: rgba(255, 255, 255, 0.6);\n --ps-overlay-pill-hover: rgba(255, 255, 255, 0.06);\n }\n\n @media (prefers-color-scheme: light) {\n :host {\n --ps-overlay-bg: rgba(245, 245, 245, 0.88);\n --ps-overlay-text: #1a1a1a;\n --ps-overlay-text-dim: rgba(0, 0, 0, 0.45);\n --ps-overlay-btn-bg: rgba(0, 0, 0, 0.08);\n --ps-overlay-btn-border: rgba(0, 0, 0, 0.15);\n --ps-overlay-btn-hover: rgba(0, 0, 0, 0.14);\n --ps-overlay-spinner-track: rgba(0, 0, 0, 0.1);\n --ps-overlay-spinner-fill: rgba(0, 0, 0, 0.6);\n --ps-overlay-pill-border: rgba(0, 0, 0, 0.15);\n --ps-overlay-pill-text: rgba(0, 0, 0, 0.5);\n --ps-overlay-pill-hover: rgba(0, 0, 0, 0.05);\n }\n }\n\n /* Debug mode - session connectivity indicators */\n :host([debug]) {\n box-shadow:\n inset 0 0 0 4px rgba(128, 128, 128, 0.8),\n 0 0 0 4px rgba(128, 128, 128, 0.8);\n }\n\n :host([debug].session-connecting) {\n box-shadow:\n inset 0 0 0 4px rgba(255, 180, 0, 1),\n 0 0 0 4px rgba(255, 180, 0, 1),\n 0 0 30px 10px rgba(255, 180, 0, 0.6);\n }\n\n :host([debug].session-connected) {\n box-shadow:\n inset 0 0 0 4px rgba(0, 255, 80, 1),\n 0 0 0 4px rgba(0, 255, 80, 1),\n 0 0 30px 10px rgba(0, 255, 80, 0.6);\n }\n\n :host([debug].session-error) {\n box-shadow:\n inset 0 0 0 4px rgba(255, 50, 50, 1),\n 0 0 0 4px rgba(255, 50, 50, 1),\n 0 0 30px 10px rgba(255, 50, 50, 0.6);\n }\n\n video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n pointer-events: all;\n /* Pre-allocate a GPU compositing layer so the first\n real frame can be texture-uploaded without waiting\n for layer promotion. */\n will-change: transform;\n }\n\n /* ═══ OVERLAY ═══ */\n #controls-overlay {\n position: absolute;\n inset: 0;\n display: none;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: var(--ps-overlay-bg);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n font-family: system-ui, -apple-system, sans-serif;\n color: var(--ps-overlay-text);\n transition: opacity 280ms cubic-bezier(0.16, 1, 0.3, 1);\n text-align: center;\n gap: 0.75rem;\n z-index: 1;\n }\n\n #controls-overlay.visible {\n display: flex;\n }\n\n :host([status="streaming"]) #controls-overlay {\n opacity: 0;\n pointer-events: none;\n }\n\n #controls-overlay.slotted {\n background: transparent;\n backdrop-filter: none;\n -webkit-backdrop-filter: none;\n pointer-events: none;\n }\n\n #controls-overlay.slotted ::slotted(*) {\n pointer-events: auto;\n }\n\n ::slotted([slot="overlay"]) {\n width: 100%;\n height: 100%;\n }\n\n /* ═══ STATE VISIBILITY ═══ */\n .state { display: none; flex-direction: column; align-items: center; gap: 0.75rem; }\n\n :host([status="idle"]) .state-idle,\n :host([status="connected"]) .state-idle,\n :host([status="connecting"]) .state-pending,\n :host([status="authenticating"]) .state-pending,\n :host([status="queued"]) .state-pending,\n :host([status="rendezvoused"]) .state-pending,\n :host([status="negotiating"]) .state-pending,\n :host([status="ready"]) .state-ready,\n :host([status="failed"]) .state-error,\n :host([status="ended"]) .state-ended {\n display: flex;\n }\n\n /* ═══ PLAY BUTTON ═══ */\n .play-btn {\n width: 52px;\n height: 52px;\n border: 1px solid var(--ps-overlay-btn-border);\n border-radius: 50%;\n background: var(--ps-overlay-btn-bg);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: transform 0.2s, background 0.2s;\n }\n .play-btn:hover { background: var(--ps-overlay-btn-hover); transform: scale(1.06); }\n .play-btn:active { transform: scale(0.95); }\n .play-btn svg { width: 22px; height: 22px; fill: var(--ps-overlay-text); margin-left: 2px; }\n\n /* ═══ SPINNER ═══ */\n .spinner {\n width: 32px;\n height: 32px;\n border: 2px solid var(--ps-overlay-spinner-track);\n border-top-color: var(--ps-overlay-spinner-fill);\n border-radius: 50%;\n animation: controls-spin 0.8s linear infinite;\n }\n @keyframes controls-spin { to { transform: rotate(360deg); } }\n\n /* ═══ STATUS MESSAGE ═══ */\n .status-msg {\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--ps-overlay-text-dim);\n }\n\n /* ═══ PILL BUTTON ═══ */\n .pill-btn {\n background: none;\n border: 1px solid var(--ps-overlay-pill-border);\n color: var(--ps-overlay-pill-text);\n padding: 0.3rem 0.9rem;\n border-radius: 16px;\n cursor: pointer;\n font-size: 0.7rem;\n font-family: inherit;\n transition: background 0.2s;\n }\n .pill-btn:hover { background: var(--ps-overlay-pill-hover); }\n\n /* ═══ ERROR MESSAGE ═══ */\n .error-message {\n font-size: 0.7rem;\n color: var(--ps-overlay-text-dim);\n max-width: 280px;\n text-align: center;\n word-break: break-word;\n }\n .error-message:empty { display: none; }\n </style>\n\n <video playsinline disablepictureinpicture></video>\n <div id="controls-overlay">\n <slot name="overlay">\n <div class="state state-idle">\n <button class="play-btn" data-action="play" aria-label="Start session">\n <svg viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21"/></svg>\n </button>\n <span class="status-msg">Start</span>\n </div>\n <div class="state state-pending">\n <div class="spinner"></div>\n <span class="status-msg" data-pending-msg>Connecting</span>\n <button class="pill-btn" data-action="cancel">Cancel</button>\n </div>\n <div class="state state-ready">\n <button class="play-btn" data-action="play" aria-label="Play stream">\n <svg viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21"/></svg>\n </button>\n <span class="status-msg">Tap to play</span>\n </div>\n <div class="state state-error">\n <span class="status-msg">Failed</span>\n <span class="error-message"></span>\n <button class="pill-btn" data-action="retry">Retry</button>\n </div>\n <div class="state state-ended">\n <span class="status-msg">Ended</span>\n <button class="pill-btn" data-action="restart">Restart</button>\n </div>\n </slot>\n </div>\n ',this.video=t.querySelector("video"),this.coordTranslator=new o(this.video),this.controlsOverlay=t.querySelector("#controls-overlay"),this.ye=t.querySelector('slot[name="overlay"]'),this.ye?.addEventListener("slotchange",()=>{this.be=(this.ye?.assignedNodes({flatten:0}).length??0)>0,this.controlsOverlay.classList.toggle("slotted",this.be),this._e()}),this.controlsOverlay.addEventListener("click",t=>{const e=t.target.closest("[data-action]");if(e)switch(e.dataset.action){case"play":case"retry":case"restart":this.play();break;case"cancel":this.cancel()}})}attributeChangedCallback(t,e,s){if(e!==s)switch(t){case"native-touch":this.dt=null!==s;break;case"pointer-lock":this.gt=null!==s;break;case"pointer-lock-release":this.ft=null!==s;break;case"suppress-browser-keys":this.vt=null!==s;break;case"enable-gamepad":null!==s&&null===e?this.gamepadInput.setup():null===s&&null!==e&&this.gamepadInput.teardown();break;case"enable-xr":null!==s&&null===e?this.xrInput.setup():null===s&&null!==e&&this.xrInput.teardown();break;case"admission-token":this.yt=s,this.dispatchEvent(new CustomEvent("admission-token-change",{detail:{oldValue:e,newValue:s}})),s&&this.isConnected&&this.Ve();break;case"app-id":this.bt=s,this.dispatchEvent(new CustomEvent("app-id-change",{detail:{oldValue:e,newValue:s}}));break;case"app-version":this.kt=s,this.dispatchEvent(new CustomEvent("app-version-change",{detail:{oldValue:e,newValue:s}}));break;case"no-auto-connect":this.Ct=null!==s,this.dispatchEvent(new CustomEvent("no-auto-connect-change",{detail:{oldValue:null!==e,newValue:null!==s}}));break;case"mute":this.St=null!==s,this.video&&(this.video.muted=this.St),this.webrtc.getAudioElement().muted=this.St,this.dispatchEvent(new CustomEvent("mute-change",{detail:{oldValue:null!==e,newValue:null!==s,muted:this.St}}));break;case"volume":{const t=Math.max(0,Math.min(1,+s||0));this.Et=t,this.video&&(this.video.volume=t),this.webrtc.getAudioElement().volume=t,this.dispatchEvent(new CustomEvent("volume-change",{detail:{oldValue:e?+e:1,newValue:t}}));break}case"rendezvous-preference":this.Rt=null!==s?parseInt(s,10):null,this.dispatchEvent(new CustomEvent("rendezvous-preference-change",{detail:{oldValue:e?parseInt(e,10):null,newValue:this.Rt}}));break;case"linger-preference":this.Tt=null!==s?parseInt(s,10):null,this.dispatchEvent(new CustomEvent("linger-preference-change",{detail:{oldValue:e?parseInt(e,10):null,newValue:this.Tt}}));break;case"left-grace-period":this.$t=null!==s?parseInt(s,10):null,this.dispatchEvent(new CustomEvent("left-grace-period-change",{detail:{oldValue:e?parseInt(e,10):null,newValue:this.$t}}));break;case"force-relay":this.Nt=null!==s;break;case"queue-wait-tolerance":this.Vt=null!==s?parseInt(s,10):null;break;case"webrtc-negotiation-tolerance":this.Ft=null!==s?parseInt(s,10):null;break;case"api-endpoint":this.wt=s||"api.interlucent.ai",this.dispatchEvent(new CustomEvent("api-endpoint-change",{detail:{oldValue:e,newValue:this.wt}}));break;case"reconnect-mode":"none"===s||"recover"===s||"always"===s?this.xt=s:(null===s||this.Me.warn(`Invalid reconnect-mode: ${s}, using "none"`),this.xt="none"),this.Me.debug("Reconnect mode set to: "+this.xt);break;case"reconnect-attempts":if(null===s)this.tt=1/0;else{const t=parseInt(s,10);isNaN(t)||t<-1?(this.Me.warn(`Invalid reconnect-attempts: ${s}, using Infinity`),this.tt=1/0):this.tt=-1===t||0===t?1/0:t}this.Me.debug("Reconnect attempts set to: "+(this.tt===1/0?"Infinity":this.tt));break;case"reconnect-strategy":"periodic"===s||"exponential-backoff"===s?this.Mt=s:(null===s||this.Me.warn(`Invalid reconnect-strategy: ${s}, using "exponential-backoff"`),this.Mt="exponential-backoff"),this.Me.debug("Reconnect strategy set to: "+this.Mt);break;case"reconnect-interval":if(null===s)this.It=P.DEFAULT_RECONNECT_INTERVAL;else{const t=parseInt(s,10);isNaN(t)||t<100?(this.Me.warn(`Invalid reconnect-interval: ${s}, using ${P.DEFAULT_RECONNECT_INTERVAL}ms`),this.It=P.DEFAULT_RECONNECT_INTERVAL):this.It=t}this.Me.debug(`Reconnect interval set to: ${this.It}ms`);break;case"disconnect-grace-ms":if(null===s)this.Pt=9e3;else{const t=parseInt(s,10);isNaN(t)||t<0?(this.Me.warn(`Invalid disconnect-grace-ms: ${s}, using 9000ms`),this.Pt=9e3):this.Pt=t}this.Me.debug(`Disconnect grace period set to: ${this.Pt}ms`);break;case"resize-mode":"none"===s||"auto"===s||"pureweb"===s||"pre-5.4"===s||"5.4+"===s?this.At=s:(null===s||this.Me.warn(`Invalid resize-mode: ${s}, using "5.4+"`),this.At="5.4+"),this.Qt=null,this.Yt=0,this.Zt=0,this.Me.debug("Resize mode set to: "+this.At);break;case"dpr-cap":if(null===s)this.Dt=1;else{const t=parseFloat(s);isNaN(t)||t<=0?(this.Me.warn(`Invalid dpr-cap: ${s}, using 1`),this.Dt=1):this.Dt=t}this.Me.debug("DPR cap set to: "+this.Dt);break;case"resolution-clamp":if(null===s)this._t=null;else{const t=s.toLowerCase().trim(),e=M[t];e?this._t=e:(this.Me.warn(`Invalid resolution-clamp: "${s}". Valid values: ${Object.keys(M).join(", ")}`),this._t=null)}this.Me.debug("Resolution clamp set to: "+(this._t?`${this._t.maxWidth}x${this._t.maxHeight}`:"none"));break;case"debug":this.Bt=null!==s,this.Jt();break;case"controls":this.Wt=null!==s&&"false"!==s,this._e();break;case"swift-job-request":this.Ut=null!==s;break;case"streamer-id":this.Lt=s,this.Fe();break;case"lat":this.qt=null!==s?parseFloat(s):null,null!==this.qt&&isNaN(this.qt)&&(this.qt=null);break;case"lng":this.zt=null!==s?parseFloat(s):null,null!==this.zt&&isNaN(this.zt)&&(this.zt=null);break;case"stream-quality":"low-latency"===s||"balanced"===s||"quality"===s?this.jt=s:(null===s||this.Me.warn(`Invalid stream-quality: ${s}, using "balanced"`),this.jt="balanced");break;case"video-bitrate-min":this.Ht=null!==s&&parseInt(s,10)||null;break;case"video-bitrate-start":this.Gt=null!==s&&parseInt(s,10)||null;break;case"video-bitrate-max":this.Xt=null!==s&&parseInt(s,10)||null;break;case"log-level":"error"===s||"warn"===s||"info"===s||"debug"===s?this.Kt=s:(null===s||this.Me.warn(`Invalid log-level: ${s}, using "warn"`),this.Kt="warn")}}connectedCallback(){if(this.Me.info(`pixel-stream v${P.VERSION} mounted`),this.hasAttribute("tabindex")||this.setAttribute("tabindex","0"),this.setAttribute("job-state",this.Ie),this.setAttribute("session-state",this.ee),this.setAttribute("stream-state",this.se),this.setAttribute("status",this.ie),this.mouseInput.setup(),this.touchInput.setup(),this.keyboardInput.setup(),this.hasAttribute("enable-gamepad")&&this.gamepadInput.setup(),this.hasAttribute("enable-xr")&&this.xrInput.setup(),this.hasAttribute("admission-token")&&(this.yt=this.getAttribute("admission-token")),this.hasAttribute("app-id")&&(this.bt=this.getAttribute("app-id")),this.hasAttribute("app-version")&&(this.kt=this.getAttribute("app-version")),this.hasAttribute("no-auto-connect")&&(this.Ct=1),this.hasAttribute("mute")&&(this.St=1,this.video.muted=1,this.webrtc.getAudioElement().muted=1),this.hasAttribute("volume")){const t=Math.max(0,Math.min(1,+this.getAttribute("volume")||0));this.Et=t,this.video.volume=t,this.webrtc.getAudioElement().volume=t}this.hasAttribute("rendezvous-preference")&&(this.Rt=parseInt(this.getAttribute("rendezvous-preference"),10)),this.hasAttribute("linger-preference")&&(this.Tt=parseInt(this.getAttribute("linger-preference"),10)),this.hasAttribute("left-grace-period")&&(this.$t=parseInt(this.getAttribute("left-grace-period"),10)),this.hasAttribute("queue-wait-tolerance")&&(this.Vt=parseInt(this.getAttribute("queue-wait-tolerance"),10)),this.hasAttribute("webrtc-negotiation-tolerance")&&(this.Ft=parseInt(this.getAttribute("webrtc-negotiation-tolerance"),10)),this.hasAttribute("api-endpoint")&&(this.wt=this.getAttribute("api-endpoint")||"api.interlucent.ai"),this.hasAttribute("controls")&&(this.Wt="false"!==this.getAttribute("controls")),this._e(),this.hasAttribute("swift-job-request")&&(this.Ut=1),this.hasAttribute("force-relay")&&(this.Nt=1),this.hasAttribute("streamer-id")&&(this.Lt=this.getAttribute("streamer-id")),this.video.addEventListener("loadedmetadata",()=>{this.updateCoordTranslator({final:1,cssW:this.video.clientWidth,cssH:this.video.clientHeight,...this.Be(this.video.clientWidth,this.video.clientHeight)})}),this.video.addEventListener("resize",()=>{this.dispatchEvent(new CustomEvent("stream-resolution-change",{detail:{width:this.video.videoWidth,height:this.video.videoHeight,...this._t?{clamp:{maxWidth:this._t.maxWidth,maxHeight:this._t.maxHeight}}:{}}}))}),this.resizeObserver=A({el:this.video,dprCap:()=>this.Dt,onUpdate:t=>this.updateCoordTranslator(t)}),this.Re=()=>{this.updateCoordTranslator({final:1,cssW:this.video.clientWidth,cssH:this.video.clientHeight,...this.Be(this.video.clientWidth,this.video.clientHeight)})},document.addEventListener("fullscreenchange",this.Re),this.Te=()=>{this.updateCoordTranslator({final:1,cssW:this.video.clientWidth,cssH:this.video.clientHeight,...this.Be(this.video.clientWidth,this.video.clientHeight)})},document.addEventListener("webkitfullscreenchange",this.Te),this.We(),this.addEventListener("initial-settings",this.Pe),this.dispatchEvent(new CustomEvent("ready",{detail:{admissionConfig:this.getAdmissionConfig(),sessionPreferences:this.getSessionPreferences(),noAutoConnect:this.Ct,mute:this.St}})),this.yt&&this.Ve()}disconnectedCallback(){this.Ue(),this.Re&&(document.removeEventListener("fullscreenchange",this.Re),this.Re=null),this.Te&&(document.removeEventListener("webkitfullscreenchange",this.Te),this.Te=null),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.Le(),this.removeEventListener("initial-settings",this.Pe),this.mouseInput.teardown(),this.gamepadInput.teardown(),this.xrInput.teardown(),this.cleanupEventListeners(),this.webrtc.closePeerConnection(),this.webrtc.disableMicrophone(),this.session.terminateSession("component-unmounted")}We(){this.Ee||(this.Ee=()=>{this.session.terminateSession("page-hide")},window.addEventListener("pagehide",this.Ee))}Ue(){this.Ee&&(window.removeEventListener("pagehide",this.Ee),this.Ee=null)}cleanupEventListeners(){this.keyboardInput.teardown(),this.mouseInput.teardown(),this.touchInput.teardown()}async startXRSession(){return this.xrInput.startSession()}endXRSession(){this.xrInput.endSession()}static async isXRSupported(){return C.isSupported()}resetPerformanceMetrics(){this.metrics.resetPerformanceMetrics()}getPerformanceMetrics(){return this.metrics.getPerformanceMetrics()}async collectAndDisplaySessionStats(){return this.metrics.collectAndDisplaySessionStats()}Be(t,e){const s=Math.min(window.devicePixelRatio||1,this.Dt);let i=Math.round(t*s),n=Math.round(e*s);return i&=-2,n&=-2,i=Math.max(2,i),n=Math.max(2,n),{dpr:s,devW:i,devH:n}}qe(t,e){const s=this._t;if(!s)return{devW:t,devH:e};const i=t>=e,n=i?s.maxWidth:s.maxHeight,r=i?s.maxHeight:s.maxWidth;if(t<=n&&e<=r)return{devW:t,devH:e};const a=t/e;let o,h;return a>n/r?(o=Math.min(t,n),h=Math.round(o/a)):(h=Math.min(e,r),o=Math.round(h*a)),o&=-2,h&=-2,o=Math.max(2,o),h=Math.max(2,h),{devW:o,devH:h}}updateCoordTranslator({final:t,devW:e,devH:s}){if(!t)return;const i=this.qe(e,s),n=i.devW,r=i.devH,a=n!==e||r!==s;this.dispatchEvent(new CustomEvent("stream-resolution-change",{detail:{width:n,height:r,viewport:{width:e,height:s},...this._t?{clamp:{maxWidth:this._t.maxWidth,maxHeight:this._t.maxHeight,applied:a}}:{}}})),a&&this.Me.debug(`Resolution clamped: ${e}x${s} → ${n}x${r} (max ${this._t.maxWidth}x${this._t.maxHeight})`),"none"!==this.At&&"open"===this.webrtc.getDataChannel()?.readyState&&("auto"===this.At?this.Zt?this.Qt?this.ze(this.Qt,n,r):this.ze("5.4+",n,r):(this.Me.info(`Sending initial resize shotgun: ${n}x${r} (all formats)`),this.ze("5.4+",n,r),this.ze("pre-5.4",n,r),this.ze("pureweb",n,r),this.Zt=1,this.Qt||this.Yt||this.Ne(n,r)):this.ze(this.At,n,r))}ze(t,e,s){"pureweb"===t?(this.sendUIInteraction({type:50,width:e,height:s,action:"1"}),this.Me.debug(`Sent Pureweb resize: ${e}x${s}`)):"pre-5.4"===t?(this.sendUIInteraction({Console:`r.setres ${e}x${s}w`}),this.Me.debug(`Sent legacy resize command: r.setres ${e}x${s}w`)):"5.4+"===t&&(this.sendCommand({"Resolution.Width":e,"Resolution.Height":s}),this.Me.debug(`Sent modern resize command: ${e}x${s}`))}Ne(t,e){if(this.Yt)return;this.Yt=1,this.$e={width:t,height:e};const s=["5.4+","pre-5.4","pureweb"];let i=0;const n=this.video.videoWidth,r=this.video.videoHeight;this.Me.info(`Starting resize auto-detection. Target: ${t}x${e}, Initial video: ${n}x${r}`);const a=()=>{if(i>=s.length)return this.Me.warn("Resize auto-detection: no mode verified, defaulting to 5.4+"),this.Qt="5.4+",this.Yt=0,void this.Oe("5.4+",0);const o=s[i];this.Me.debug(`Resize auto-detection: trying ${o} (attempt ${i+1}/${s.length})`),this.ze(o,t,e),this.xe=setTimeout(()=>{const s=this.video.videoWidth,h=this.video.videoHeight;(s!==n||h!==r)&&(Math.abs(s-t)<.1*t||s!==n)&&(Math.abs(h-e)<.1*e||h!==r)?(this.Me.info(`Resize auto-detection: ${o} verified (video: ${s}x${h})`),this.Qt=o,this.Yt=0,this.Oe(o,1)):(this.Me.debug(`Resize auto-detection: ${o} not verified (video: ${s}x${h})`),i++,a())},500)};a()}Oe(t,e){this.dispatchEvent(new CustomEvent("resize-mode-detected",{detail:{mode:t,verified:e}}))}Le(){this.xe&&(clearTimeout(this.xe),this.xe=null),this.Yt=0,this.Zt=0,this.Qt=null}Ae(t){if("auto"!==this.At||this.Qt)return;const e=t?.PixelStreamingSettings?.EngineVersion||t?.EngineVersion||t?.UEVersion;if(e){this.Me.debug("InitialSettings contains version: "+e);const t=this.je(e);t&&(this.Me.info(`Inferred resize mode from UE version ${e}: ${t}`),this.Qt=t,this.Oe(t,0))}const s=t?.PixelStreamingSettings?.WebRTCDisableResolutionChange;0==s&&(this.Me.debug("InitialSettings indicates WebRTC resize is enabled"),this.Qt||(this.Qt="5.4+",this.Oe("5.4+",0)))}je(t){const e=t.match(/(\d+)\.(\d+)/);if(!e)return null;const s=parseInt(e[1],10),i=parseInt(e[2],10);return s>=5&&i>=4?"5.4+":s>=4?"pre-5.4":null}ue(){const t=Array.from(this.webrtc.getVideoTracks().entries()),e=this.te,s=t.some(([t,{track:e}])=>/left|right|stereo|xr|hmd|vr/i.test(e.label)),i=2===t.length,n=()=>{if(0===this.video.videoWidth||0===this.video.videoHeight)return void this.video.addEventListener("loadedmetadata",n,{once:1});const r=this.video.videoWidth/this.video.videoHeight,a=r>3.5,o=s||i&&a;o&&!e?(this.te=1,this.Me.debug("Stream detected as XR/stereo",{tracks:t.length,aspectRatio:r.toFixed(2),resolution:`${this.video.videoWidth}x${this.video.videoHeight}`,hasXRLabels:s,hasStereoTracks:i,hasStereoAspect:a}),this.dispatchEvent(new CustomEvent("xr-stream-detected",{detail:{trackCount:this.webrtc.getVideoTracks().size,resolution:`${this.video.videoWidth}x${this.video.videoHeight}`,aspectRatio:r,hasXRLabels:s,hasStereoTracks:i,hasStereoAspect:a}}))):!o&&e&&(this.te=0,this.Me.debug("Stream no longer detected as XR"))};n()}sendToStreamer(t,e=[]){const s=Date.now();s-this.re>1e3&&this.dispatchEvent(new CustomEvent("interlucent:user-interaction")),this.re=s,this.webrtc.sendToStreamer(t,e)}sendRawBinary(t){this.webrtc.sendRawBinary(t)}sendCommand(t){this.webrtc.sendCommand(t)}sendUIInteraction(t){this.webrtc.sendUIInteraction(t)}requestIFrame(){this.webrtc.requestIFrame()}requestKeyFrame(){this.webrtc.requestKeyFrame()}requestQualityControl(){this.sendToStreamer("RequestQualityControl",[])}requestFps(t){this.sendToStreamer("FpsRequest",[t])}requestBitrate(){this.sendToStreamer("AverageBitrateRequest",[])}startStreaming(){this.sendToStreamer("StartStreaming",[])}stopStreaming(){this.sendToStreamer("StopStreaming",[])}sendTestEcho(t){this.sendToStreamer("TestEcho",[t])}async checkPermission(t){if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia)return"unavailable";if(navigator.permissions&&navigator.permissions.query)try{return(await navigator.permissions.query({name:t})).state}catch{}return"prompt"}async checkPermissions(t){const e={};for(const s of t)e[s]=await this.checkPermission(s);return e}async watchPermission(t){if(navigator.permissions&&navigator.permissions.query)try{const e=await navigator.permissions.query({name:t});e.addEventListener("change",()=>{this.dispatchEvent(new CustomEvent("permission-change",{detail:{permission:t,state:e.state}}))})}catch{}}async requestMicrophone(){return this.webrtc.requestMicrophone()}async enableMicrophone(){return this.webrtc.enableMicrophone()}disableMicrophone(){this.webrtc.disableMicrophone()}setMicrophoneMuted(t){this.webrtc.setMicrophoneMuted(t)}get isMicrophoneEnabled(){return this.webrtc.isMicrophoneEnabled}get isMicrophoneMuted(){return this.webrtc.isMicrophoneMuted}getVideoTracks(){const t=this.webrtc.getActiveVideoTrackId();return Array.from(this.webrtc.getVideoTracks().entries()).map(([e,{track:s}])=>({trackId:e,label:s.label||"Camera "+e,isActive:e===t}))}switchVideoTrack(t){this.webrtc.switchVideoTrack(t)}getActiveVideoTrack(){const t=this.webrtc.getActiveVideoTrackId();if(!t)return null;const e=this.webrtc.getVideoTracks().get(t);return e?{trackId:t,label:e.track.label||"Camera "+t}:null}He(t){const e=this.oe;this.oe=t,t&&!e?(this.dispatchEvent(new CustomEvent("admitted",{detail:{admissionConfig:this.getAdmissionConfig(),sessionPreferences:this.getSessionPreferences()}})),this.Rt&&this.Rt>0&&this.Ge()):!t&&e&&(this.dispatchEvent(new CustomEvent("admission-revoked",{detail:{reason:"revoked"}})),this.me(),this.Xe())}Ge(){this.me(),!this.Rt||this.Rt<=0||(this.Me.info(`Starting rendezvous timer: ${this.Rt}s`),this.dispatchEvent(new CustomEvent("rendezvous-started",{detail:{timeoutSeconds:this.Rt}})),this.Ce=setTimeout(()=>{this.Me.info("Rendezvous timeout - no peer connected"),this.dispatchEvent(new CustomEvent("rendezvous-timeout",{detail:{timeoutSeconds:this.Rt,admissionConfig:this.getAdmissionConfig()}}))},1e3*this.Rt))}me(){this.Ce&&(clearTimeout(this.Ce),this.Ce=null,this.dispatchEvent(new CustomEvent("rendezvous-cancelled")))}Ke(){this.Xe(),!this.Tt||this.Tt<=0?this.dispatchEvent(new CustomEvent("linger-timeout",{detail:{timeout:0,reason:"no-linger-preference"}})):(this.Me.info(`Starting linger timer: ${this.Tt}s`),this.dispatchEvent(new CustomEvent("linger-started",{detail:{timeoutSeconds:this.Tt}})),this.Se=setTimeout(()=>{this.Me.info("Linger timeout - disconnecting"),this.dispatchEvent(new CustomEvent("linger-timeout",{detail:{timeoutSeconds:this.Tt,reason:"timeout"}}))},1e3*this.Tt))}Xe(){this.Se&&(clearTimeout(this.Se),this.Se=null,this.dispatchEvent(new CustomEvent("linger-cancelled")))}Je(t){this.me(),this.Xe(),this.dispatchEvent(new CustomEvent("peer-connected",{detail:{peerId:t}}))}Qe(t,e=1){this.dispatchEvent(new CustomEvent("peer-disconnected",{detail:{peerId:t,isLastPeer:e}})),e&&this.Ke()}Ye(){if(!this.yt){const t=Error("admission-token is required but not set");throw this.dispatchEvent(new CustomEvent("admission-error",{detail:{error:t.message}})),t}return this.getAdmissionConfig()}getLastReceivedFile(){return this.fileTransfer.getLastReceivedFile()}async waitForFile(){return this.fileTransfer.waitForFile()}setVideoSource(t){this.video.srcObject=t}async startSession(){await this.session.startSession()}Ve(){this.Ct||"idle"!==this.ee&&"error"!==this.ee||this.startSession().catch(t=>this.Me.error("Auto session start failed",t))}sendSessionMessage(t){this.session.sendMessage(t)}le(t){if(this.Ie===t)return;const e=this.Ie;this.Ie=t,this.Me.debug(`Job state: ${e} -> ${t}`),this.setAttribute("job-state",t),this.dispatchEvent(new CustomEvent("job-state-change",{detail:{oldState:e,newState:t}})),this._e(),this.fe()}_e(){if(this.be)return void this.controlsOverlay.classList.add("visible");const t="failed"===this.ie||"ended"===this.ie||"ready"===this.Ie;this.Wt||t?(this.controlsOverlay.classList.add("visible"),this.Ze(),this.ts()):this.controlsOverlay.classList.remove("visible")}es(t){switch(t){case"token_expired":return"Admission token has expired";case"unauthorized":return"Invalid admission token";case"server_error":return"Server error — try again later";case"request_failed":return"Could not reach server";case"connection_failed":return"Connection failed";case"disconnected":return"Connection lost";case"reconnect_exhausted":return"Reconnection attempts exhausted";case"no_access_token":return"No access token for recovery";case"not_connected":return"Session was not connected";case"media_pipeline_failure":return"Media pipeline error";default:return t||"An unexpected error occurred"}}Ze(){const t=this.controlsOverlay.querySelector("[data-pending-msg]");if(t)switch(this.ie){case"connecting":default:t.textContent="Connecting";break;case"authenticating":t.textContent="Authenticating";break;case"queued":t.textContent="Queued";break;case"rendezvoused":t.textContent="Agent matched";break;case"negotiating":t.textContent="Starting stream"}}ts(){const t=this.controlsOverlay.querySelector(".error-message");t&&(t.textContent=this.ne?this.es(this.ne):"")}async requestJob(){if(this.session.isCollaborator)return void this.Me.warn("session-collaborator cannot request jobs");if("pending"===this.Ie)return void this.Me.debug("Job request already in flight — ignoring");const t="failed"===this.ie;if(t&&(this.le("unsubmitted"),this.pe("idle")),this.session.ws?.readyState===WebSocket.OPEN)return this.Me.info("Requesting job..."),this.le("pending"),this.performanceMetrics.jobRequested=performance.now(),void this.sendSessionMessage({type:"request-job"});if("connecting"!==this.ee&&"authenticating"!==this.ee){if(this.config.admissionToken){if(t){this.Me.info("Error recovery — restarting session with swift job request...");const t=this.Ut;this.Ut=1;try{return void await this.session.startSession()}catch(t){return void this.Me.error("Failed to restart session",t)}finally{this.Ut=t}}return this.Me.info("Starting session — job will be requested on connect"),void this.startSession().catch(t=>this.Me.error("Failed to start session",t))}this.Me.warn("Cannot request job — no active session and no admission token"),this.dispatchEvent(new CustomEvent("session-error",{detail:{error:"No active session",phase:"job-request"}}))}else this.Me.info("Session connecting — job will be requested on connect")}async play(){if(!this.ce)return this.ke=1,this.requestJob();try{await this.video.play(),this.ce=0,this.le("fulfilled")}catch(t){throw this.Me.warn("Play still blocked",t),t}}cancel(){if(!this.session.ws||this.session.ws.readyState!==WebSocket.OPEN)return this.Me.warn("Cannot cancel job - WebSocket not connected"),void this.dispatchEvent(new CustomEvent("session-error",{detail:{error:"WebSocket not connected",phase:"job-cancel"}}));this.Me.info("Cancelling job..."),this.sendSessionMessage({type:"cancel-job"})}requestInvite(){this.session.requestInvite()}stop(t){this.le("unsubmitted"),this.we("ended"),this.webrtc.closePeerConnection(),this.session.leaveSession(t),this.webrtc.resetResilienceState()}async resumePlayback(){return this.play()}cancelJob(){this.cancel()}leaveSession(t){this.stop(t)}subscribeToStreamer(t){this.session.subscribeToStreamer(t)}get availableStreamers(){return this.session.availableStreamers}Fe(){this.session.evaluateSubscription()}Jt(){if(this.classList.remove("session-connecting","session-connected","session-error"),this.Bt)switch(this.ee){case"connecting":case"authenticating":case"reconnecting":this.classList.add("session-connecting");break;case"connected":this.classList.add("session-connected");break;case"error":this.classList.add("session-error")}}de(t,e){if(this.se===t)return;const s=this.se;this.se=t,"streaming"===t&&"streaming"!==s&&(this.re=Date.now(),this.ae=Date.now(),this.ke=0),this.Me.debug(`Stream state: ${s} -> ${t}`),this.setAttribute("stream-state",t),this.dispatchEvent(new CustomEvent("stream-state-change",{detail:{oldState:s,newState:t,...e}})),this.Jt(),this.fe()}pe(t){const e=this.ee;this.ee=t,e!==t&&("connecting"===t&&(this.ge=0,this.ve=0,this.ie="idle",this.ne=null,this.ae=0),"connected"!==t||this.oe?"error"!==t&&"idle"!==t||!this.oe||this.He(0):this.He(1),("error"===t||"idle"===t&&"idle"!==e)&&(this.ke=0),this.Me.debug(`Session state: ${e} -> ${t}`),this.setAttribute("session-state",t),this.dispatchEvent(new CustomEvent("session-state-change",{detail:{oldState:e,newState:t}})),this.Jt(),this._e(),this.fe(),"connected"===t&&this.ke&&"unsubmitted"===this.Ie&&this.requestJob())}we(t,e){if(this.ie===t)return;const s=this.ie;this.ie=t,this.ne=e??null,this.Me.debug(`Status: ${s} -> ${t}${e?` (${e})`:""}`),this.setAttribute("status",t),this.dispatchEvent(new CustomEvent("status-change",{detail:{oldStatus:s,newStatus:t,failureReason:this.ne}})),this._e()}fe(){"failed"!==this.ie&&"ended"!==this.ie&&("failed"===this.se?this.we("failed","media_pipeline_failure"):"recovering"===this.se?this.we("recovering"):"interrupted"===this.se?this.we("interrupted"):"ready"===this.Ie?this.we("ready"):"streaming"===this.se?this.we("streaming"):this.ve?this.we("negotiating"):this.ge?this.we("rendezvoused"):"pending"===this.Ie&&"connected"===this.ee?this.we("queued"):"connected"===this.ee&&"unsubmitted"===this.Ie?this.we("connected"):"reconnecting"===this.ee?this.we("connecting"):"authenticating"===this.ee?this.we("authenticating"):"connecting"===this.ee?this.we("connecting"):"idle"===this.ee&&this.we("idle"))}terminateSession(){this.session.terminateSession(),this.webrtc.resetResilienceState()}}if(P.VERSION="0.0.79",P.DEFAULT_RECONNECT_INTERVAL=3e4,"undefined"!=typeof document){const t=[],e=document.querySelector('meta[name="ps-signaling-server"]');if(e){const s=e.getAttribute("content");s&&t.push(s)}const s=document.querySelector('meta[name="ps-ice-servers"]');if(s){const e=s.getAttribute("content");e&&t.push(...e.split(",").map(t=>t.trim()).filter(t=>t))}0===t.length&&t.push("stun:stun.cloudflare.com:3478"),t.forEach(t=>{try{let e=t;const s=t.startsWith("stun:")||t.startsWith("turn:")||t.startsWith("turns:");t.startsWith("stun:")?e=t.replace("stun:","https://"):t.startsWith("turn:")?e=t.replace("turn:","https://"):t.startsWith("turns:")&&(e=t.replace("turns:","https://"));const i=new URL(e).origin;if(document.querySelector(`link[rel="dns-prefetch"][href="${i}"]`))return;const n=document.createElement("link");if(n.rel="dns-prefetch",n.href=i,document.head.appendChild(n),!s){const t=document.createElement("link");t.rel="preconnect",t.href=i,document.head.appendChild(t)}}catch(t){}})}function A(t){const{el:e,onUpdate:s,settleMs:i=200,dprCap:n=1/0,even:r=1,cssJitterPx:a=0}=t;let o=null,h=null,c=-1,l=-1,u=-1,d=-1,p=-1,m=-1;const g=(t,e,s)=>Math.abs(t-e)<=s;function f(){return{cssW:e.clientWidth,cssH:e.clientHeight}}function v(t){const{cssW:e,cssH:i}=f();if(e<=0||i<=0)return;const{dpr:a,devW:o,devH:h}=function(t,e){const s="function"==typeof n?n():n,i=Math.min(window.devicePixelRatio||1,s);let a=Math.round(t*i),o=Math.round(e*i);return r&&(a&=-2,o&=-2),a=Math.max(r?2:1,a),o=Math.max(r?2:1,o),{dpr:i,devW:a,devH:o}}(e,i);s({final:t,cssW:e,cssH:i,dpr:a,devW:o,devH:h})}const w=new ResizeObserver(()=>{const{cssW:t,cssH:e}=f();(a>0?g(t,c,a)&&g(e,l,a):t===c&&e===l)||(c=t,l=e,null===o&&(o=requestAnimationFrame(()=>{o=null;const{cssW:t,cssH:e}=f();(a>0?g(t,u,a)&&g(e,d,a):t===u&&e===d)||(u=t,d=e,v(0))})),null!==h&&clearTimeout(h),h=window.setTimeout(()=>{h=null;const{cssW:t,cssH:e}=f();(a>0?g(t,p,a)&&g(e,m,a):t===p&&e===m)||(p=t,m=e,v(1))},i))});return w.observe(e),w.dispose=()=>{w.disconnect(),null!==o&&cancelAnimationFrame(o),o=null,null!==h&&clearTimeout(h),h=null},w}return g("setLogLevel",function(t){u=t;try{localStorage.setItem(l,t)}catch{}}),g("clearLogLevel",function(){u=null;try{localStorage.removeItem(l)}catch{}}),function(t){const e=(m.versions||Object.defineProperty(m,"versions",{value:{},writable:0,enumerable:1,configurable:0}),m.versions);Object.getOwnPropertyDescriptor(e,t)||Object.defineProperty(e,t,{value:"0.0.79",writable:0,enumerable:1,configurable:0})}("pixel-stream"),"undefined"!=typeof customElements&&customElements.define("pixel-stream",P),t.PixelStream=P,t.PixelStreamResizeObserverForVideo=A,t}({});
|
package/dist/pixel-stream.umd.js
CHANGED
|
@@ -5108,7 +5108,13 @@
|
|
|
5108
5108
|
uhd: { maxWidth: 3840, maxHeight: 2160 },
|
|
5109
5109
|
};
|
|
5110
5110
|
// ===== MAIN WEB COMPONENT =====
|
|
5111
|
-
|
|
5111
|
+
// SSR guard: allow the module to be imported in Node.js (e.g. Next.js SSR)
|
|
5112
|
+
// without crashing on missing browser globals.
|
|
5113
|
+
const HTMLElementBase = typeof HTMLElement !== 'undefined'
|
|
5114
|
+
? HTMLElement
|
|
5115
|
+
: class {
|
|
5116
|
+
};
|
|
5117
|
+
class PixelStream extends HTMLElementBase {
|
|
5112
5118
|
/** Instance accessor for the package version. */
|
|
5113
5119
|
get version() {
|
|
5114
5120
|
return PixelStream.VERSION;
|
|
@@ -7715,7 +7721,7 @@
|
|
|
7715
7721
|
}
|
|
7716
7722
|
}
|
|
7717
7723
|
/** Package version, injected at build time. */
|
|
7718
|
-
PixelStream.VERSION = "0.0.
|
|
7724
|
+
PixelStream.VERSION = "0.0.79";
|
|
7719
7725
|
// Reconnection defaults
|
|
7720
7726
|
PixelStream.DEFAULT_RECONNECT_INTERVAL = 30000; // 30 seconds
|
|
7721
7727
|
// ===== DNS PREFETCH SETUP =====
|
|
@@ -7792,9 +7798,11 @@
|
|
|
7792
7798
|
}
|
|
7793
7799
|
// Expose DevTools namespace for standalone users: interlucent.setLogLevel('debug'), interlucent.versions, etc.
|
|
7794
7800
|
exposeDevTools();
|
|
7795
|
-
registerVersion('pixel-stream', "0.0.
|
|
7796
|
-
// Register the custom element
|
|
7797
|
-
customElements
|
|
7801
|
+
registerVersion('pixel-stream', "0.0.79");
|
|
7802
|
+
// Register the custom element (skip in non-browser environments like SSR)
|
|
7803
|
+
if (typeof customElements !== 'undefined') {
|
|
7804
|
+
customElements.define('pixel-stream', PixelStream);
|
|
7805
|
+
}
|
|
7798
7806
|
function PixelStreamResizeObserverForVideo(opts) {
|
|
7799
7807
|
const { el, onUpdate, settleMs = 200, dprCap = Infinity, even = true, cssJitterPx = 0, } = opts;
|
|
7800
7808
|
let rafId = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interlucent/pixel-stream",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.79",
|
|
4
4
|
"description": "Self-contained W3C Web Component for Pixel Streaming with WebRTC, input handling, and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/pixel-stream.umd.js",
|