-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbundle.min.js
More file actions
20 lines (17 loc) · 67 KB
/
Copy pathbundle.min.js
File metadata and controls
20 lines (17 loc) · 67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function lt(n){let t=window.SERVER_CONFIG?.baseUrl||"";return t?`${t.endsWith("/")?t.slice(0,-1):t}${n}`:n}var V=class{constructor(){this.eventListeners={},this.pc=null,this.dataChannel=null,this.packets={},this.testRunning=!1,this.serverRxCount=0,this.isCleanedUp=!1,this.activeTimeouts=new Set,this.lossCheckInterval=null,this.pcId=null,this.connectionTimeoutId=null,this.wasDisconnected=!1,this.reconnectAttempt=0,this.maxReconnectAttempts=10,this.reconnectTimer=null,this.sendQueue=[],this.isSendingPaused=!1,this.BUFFER_THRESHOLD=16*1024*1024,this.BUFFER_LOW_WATERMARK=8*1024*1024,this.sendingComplete=!1,this.sendingCompleteTime=null,this.GRACE_PERIOD_MS=3e3,this.stats={sent:0,received:0,lost:0,currentLoss:0,c2sLost:0,s2cLost:0,c2sLossPercent:0,s2cLossPercent:0,jitter:0,sentPPS:0,receivedPPS:0,outOfOrder:0,outOfOrderPercent:0,rtt:0},this.ppsWindow=1e3,this.lastPPSUpdate=0,this.lastSentCount=0,this.lastReceivedCount=0,this.ppsHistory={sent:[],received:[]},this.SMA_WINDOW_SIZE=5,this.lastReceivedSeq=-1,this.outOfOrderHistory=[],this.isMobile=/iPhone|iPad|iPod|Android/i.test(navigator.userAgent),this.PACKET_TIMEOUT=this.isMobile?5e3:2e3,this.smoothedRTT=null,this.rttVariance=0,this.rttSampleCount=0,this.lastRTT=null,this.BOOTSTRAP_PACKETS=10,this.BOOTSTRAP_TIMEOUT=5e3,this.MIN_TIMEOUT=1e3,this.rttHistory=[],this.jitterHistory=[],this.displayRTT=0,this.displayJitter=0,this.autoIntervalEnabled=!1,this.baseInterval=null,this.autoIntervalAdjusted=!1,this.lastIntervalAdjustmentTime=0,this.lossTrackingReady=!1,this.baselineStats={sent:0,received:0,serverRxCount:0,priorC2SLost:0,priorS2CLost:0},this.calculationMode="cumulative",this.realtimeWindowMs=1e4,this.statsHistory=[]}getStunUrl(){if(window.STUN_CONFIG&&window.STUN_CONFIG.enabled===!1)return null;if(window.STUN_CONFIG&&window.STUN_CONFIG.url)return window.STUN_CONFIG.url;let t=window.SERVER_CONFIG?.baseUrl||window.location.origin;try{return`stun:${new URL(t).hostname}:3478`}catch{return"stun:stun.l.google.com:19302"}}on(t,e){this.eventListeners[t]||(this.eventListeners[t]=[]),this.eventListeners[t].push(e)}once(t,e){let s=i=>{this.off(t,s),e(i)};this.on(t,s)}off(t,e){this.eventListeners[t]&&(this.eventListeners[t]=this.eventListeners[t].filter(s=>s!==e))}emit(t,e){this.eventListeners[t]&&this.eventListeners[t].forEach(s=>s(e))}trimHistory(t,e){t.length>e&&t.shift()}calculateSMA(t,e=!0){if(!t||t.length===0)return 0;let i=t.reduce((o,r)=>o+r,0)/t.length;return e?Math.round(i):i}calculatePPS(){if(!this.testRunning||this.testConfig&&this.testConfig.count!==1/0&&this.stats.sent>=this.testConfig.count||this.sendingComplete&&this.sendingCompleteTime&&Date.now()-this.sendingCompleteTime>this.GRACE_PERIOD_MS)return;let t=Date.now(),e=t-this.lastPPSUpdate;if(e>=this.ppsWindow){let s=this.stats.sent-this.lastSentCount,i=this.stats.received-this.lastReceivedCount,o=Math.round(s/e*1e3),r=Math.round(i/e*1e3);this.ppsHistory.sent.push(o),this.ppsHistory.received.push(r),this.trimHistory(this.ppsHistory.sent,this.SMA_WINDOW_SIZE),this.trimHistory(this.ppsHistory.received,this.SMA_WINDOW_SIZE),this.stats.sentPPS=this.calculateSMA(this.ppsHistory.sent),this.stats.receivedPPS=this.calculateSMA(this.ppsHistory.received),this.lastPPSUpdate=t,this.lastSentCount=this.stats.sent,this.lastReceivedCount=this.stats.received}}calculateOutOfOrderStats(){if(this.testRunning&&!(this.testConfig&&this.testConfig.count!==1/0&&this.stats.sent>=this.testConfig.count)&&!(this.sendingComplete&&this.sendingCompleteTime&&Date.now()-this.sendingCompleteTime>this.GRACE_PERIOD_MS))if(this.calculationMode==="realtime")if(this.statsHistory.length>=2){let t=this.statsHistory[0],s=this.statsHistory[this.statsHistory.length-1].received-t.received;s>0?this.stats.outOfOrderPercent=this.stats.outOfOrder/s*100:this.stats.outOfOrderPercent=0}else this.stats.outOfOrderPercent=0;else this.stats.received>0?this.stats.outOfOrderPercent=this.stats.outOfOrder/this.stats.received*100:this.stats.outOfOrderPercent=0}calculateRealtimeLoss(){let t=Date.now(),e={timestamp:t,sent:this.stats.sent-this.baselineStats.sent,received:this.stats.received-this.baselineStats.received,serverRxCount:this.serverRxCount-this.baselineStats.serverRxCount};this.statsHistory.push(e);let s=t-this.realtimeWindowMs;if(this.statsHistory=this.statsHistory.filter(P=>P.timestamp>=s),this.calculationMode==="realtime"&&this.outOfOrderHistory&&this.outOfOrderHistory.length>0&&(this.outOfOrderHistory=this.outOfOrderHistory.filter(P=>P.timestamp>=s)),this.calculationMode==="realtime"&&this.outOfOrderHistory&&(this.stats.outOfOrder=this.outOfOrderHistory.length),this.calculateOutOfOrderStats(),this.autoIntervalEnabled&&this.smoothedRTT&&t-(this.lastIntervalAdjustmentTime||0)>=this.realtimeWindowMs&&this.recheckAndAdjustInterval(),this.statsHistory.length<2){this.stats.c2sLossPercent=0,this.stats.s2cLossPercent=0,this.stats.currentLoss=0,this.stats.windowSent=0,this.stats.windowServerRx=0,this.stats.windowLost=0,this.stats.windowC2SLost=0,this.stats.windowS2CLost=0;return}let i=this.statsHistory[0],o=this.statsHistory[this.statsHistory.length-1],r=o.sent-i.sent,a=o.serverRxCount-i.serverRxCount,l=o.received-i.received,h=this.getAdaptiveTimeout(),m=Date.now(),d=0;for(let P in this.packets){let X=this.packets[P];if(X.sent<s){delete this.packets[P];continue}m-X.sent>=h||X.received||d++}let p=Math.max(0,r-a-d),f=Math.max(0,a-l),w=0,C=0;r>0&&(w=p/r,C=a>0?f/a:0);let A=this.stats.sent-this.baselineStats.sent,_=this.serverRxCount-this.baselineStats.serverRxCount,z=this.stats.received-this.baselineStats.received,q=Math.max(0,A-_-d)+(this.baselineStats.priorC2SLost||0),H=Math.max(0,_-z)+(this.baselineStats.priorS2CLost||0);this.stats.c2sLost=Math.max(this.stats.c2sLost,q),this.stats.s2cLost=Math.max(this.stats.s2cLost,H),this.stats.lost=this.stats.c2sLost+this.stats.s2cLost,this.stats.windowSent=r,this.stats.windowServerRx=a,this.stats.serverRxCount=this.serverRxCount,this.stats.windowLost=p+f,this.stats.windowC2SLost=p,this.stats.windowS2CLost=f,this.stats.c2sLossPercent=Math.min(100,w*100),this.stats.s2cLossPercent=Math.min(100,C*100);let pt=r>0?(p+f)/r*100:0;this.stats.currentLoss=Math.min(100,pt)}calculateLossStats(){if(!this.lossTrackingReady){this.stats.c2sLost=0,this.stats.s2cLost=0,this.stats.lost=0,this.stats.c2sLossPercent=0,this.stats.s2cLossPercent=0,this.stats.currentLoss=0;return}if(this.calculationMode==="realtime"){this.calculateRealtimeLoss();return}let t=this.getAdaptiveTimeout(),e=Date.now(),s=0;for(let w in this.packets){if(w<this.baselineStats.sent)continue;let C=this.packets[w];e-C.sent>=t||C.received||s++}let i=this.stats.sent-this.baselineStats.sent,o=this.serverRxCount-this.baselineStats.serverRxCount,r=this.stats.received-this.baselineStats.received,a=Math.max(0,i-o-s),l=Math.max(0,o-r),h=0,m=0;i>0&&(h=a/i,m=o>0?l/o:0);let d=Math.round(this.baselineStats.sent*h),p=Math.round(this.baselineStats.serverRxCount*m);this.stats.c2sLost=a+d,this.stats.s2cLost=l+p,this.stats.lost=this.stats.c2sLost+this.stats.s2cLost,this.stats.c2sLossPercent=Math.min(100,h*100),this.stats.s2cLossPercent=Math.min(100,m*100);let f=i>0?(a+l)/i*100:0;this.stats.currentLoss=Math.min(100,f),this.stats.serverRxCount=this.serverRxCount}getAdaptiveTimeout(){if(this.rttSampleCount<this.BOOTSTRAP_PACKETS)return this.BOOTSTRAP_TIMEOUT;let t=this.smoothedRTT+4*this.rttVariance;return Math.max(this.MIN_TIMEOUT,Math.min(t,this.BOOTSTRAP_TIMEOUT))}updateRTT(t){if(this.rttSampleCount++,this.lastRTT!==null){let e=Math.abs(t-this.lastRTT);this.stats.jitter+=(e-this.stats.jitter)/16}if(this.lastRTT=t,this.smoothedRTT===null)this.smoothedRTT=t,this.rttVariance=t/2;else{let i=Math.abs(t-this.smoothedRTT);this.rttVariance=(1-.25)*this.rttVariance+.25*i,this.smoothedRTT=(1-.125)*this.smoothedRTT+.125*t}this.rttHistory.push(this.smoothedRTT),this.trimHistory(this.rttHistory,this.SMA_WINDOW_SIZE),this.jitterHistory.push(this.stats.jitter),this.trimHistory(this.jitterHistory,this.SMA_WINDOW_SIZE),this.displayRTT=this.calculateSMA(this.rttHistory,!1),this.displayJitter=this.calculateSMA(this.jitterHistory,!1),this.rttSampleCount===this.BOOTSTRAP_PACKETS?this.autoIntervalEnabled&&!this.autoIntervalAdjusted?this.adjustPacketInterval():this.enableLossTracking():this.rttSampleCount%10}enableLossTracking(){this.lossTrackingReady||(this.lossTrackingReady=!0,this.baselineStats={sent:this.stats.sent,received:this.stats.received,serverRxCount:this.serverRxCount,priorC2SLost:0,priorS2CLost:0},this.statsHistory=[],this.outOfOrderHistory=[],this.emit("loss-tracking:ready"))}getOptimalInterval(){if(!this.smoothedRTT)return this.baseInterval;let t=15,e=500,i=Math.round(this.smoothedRTT*.5);return Math.max(t,Math.min(e,i))}adjustPacketInterval(){if(this.autoIntervalAdjusted||!this.testRunning)return;let t=this.getOptimalInterval(),e=this.baseInterval;if(Math.abs(t-e)/e<.2){this.enableLossTracking();return}this.autoIntervalAdjusted=!0,this.lastIntervalAdjustmentTime=Date.now(),this.sendInterval&&clearInterval(this.sendInterval),this.emit("interval:adjusted",{oldInterval:e,newInterval:t,rtt:this.smoothedRTT}),this.restartSendingWithInterval(t),this.enableLossTracking()}recheckAndAdjustInterval(){if(!this.testRunning||!this.autoIntervalEnabled)return;let t=this.getOptimalInterval(),e=this.baseInterval;if(Math.abs(t-e)/e<.2){this.lastIntervalAdjustmentTime=Date.now();return}this.sendInterval&&clearInterval(this.sendInterval),this.emit("interval:adjusted",{oldInterval:e,newInterval:t,rtt:this.smoothedRTT}),this.restartSendingWithInterval(t),this.baselineStats={sent:this.stats.sent,received:this.stats.received,serverRxCount:this.serverRxCount,priorC2SLost:this.stats.c2sLost||0,priorS2CLost:this.stats.s2cLost||0},this.statsHistory=[],this.outOfOrderHistory=[],this.lastIntervalAdjustmentTime=Date.now()}completeTest(){if(!this.testRunning||this.isCleanedUp)return;this.testRunning=!1,this.calculateLossStats(),this.stats.rtt=this.displayRTT||0,this.stats.jitter=this.displayJitter||0;let t={...this.stats,interval:this.baseInterval||0};this.emit("metrics:update",t),this.emit("test:completed",t),this.cleanup()}restartSendingWithInterval(t){!this.testRunning||!this.dataChannel||(this.baseInterval=t,this.sendInterval=setInterval(()=>{if(!this.testRunning){clearInterval(this.sendInterval);return}if(this.testConfig.count!==1/0&&this.stats.sent>=this.testConfig.count){clearInterval(this.sendInterval);return}this.sendPacket(this.stats.sent)},t))}checkBackpressure(){if(!this.dataChannel)return!1;let t=this.dataChannel.bufferedAmount;return!this.isSendingPaused&&t>=this.BUFFER_THRESHOLD?(this.isSendingPaused=!0,!0):(this.isSendingPaused&&t<this.BUFFER_LOW_WATERMARK&&(this.isSendingPaused=!1,this.processSendQueue()),this.isSendingPaused)}processSendQueue(){for(;this.sendQueue.length>0&&!this.checkBackpressure();){let t=this.sendQueue.shift();this.sendPacketDirect(t)}}sendPacket(t){if(this.checkBackpressure()){this.sendQueue.push(t),this.sendQueue.length>1e3&&this.sendQueue.shift();return}this.sendPacketDirect(t)}sendPacketDirect(t){if(!this.dataChannel||this.dataChannel.readyState!=="open")return;let e={seq:t,timestamp:Date.now()};this.packetPadding&&(e.data=this.packetPadding);let s=JSON.stringify(e);try{let i=Date.now();if(this.dataChannel.send(s),this.stats.sent++,this.packets[t]={sent:i,received:!1,timedOut:!1},this.testConfig.count!==1/0&&this.stats.sent>=this.testConfig.count&&!this.sendingComplete){this.sendingComplete=!0,this.sendingCompleteTime=Date.now(),this.emit("test:sending-complete",{count:this.testConfig.count});let a=setTimeout(()=>{this.testRunning&&!this.isCleanedUp&&this.completeTest(),this.activeTimeouts.delete(a)},this.GRACE_PERIOD_MS);this.activeTimeouts.add(a)}this.emit("metrics:update",{...this.stats,rtt:this.displayRTT||0,interval:this.baseInterval||0,jitter:this.displayJitter||0});let o=this.getAdaptiveTimeout(),r=setTimeout(()=>{if(this.packets[t]){if(!this.packets[t].received&&!this.packets[t].timedOut&&(this.packets[t].timedOut=!0),this.testConfig.count===1/0){let l=setTimeout(()=>{this.packets[t]&&!this.packets[t].received&&delete this.packets[t],this.activeTimeouts.delete(l)},5e3);this.activeTimeouts.add(l)}this.activeTimeouts.delete(r)}},o);this.activeTimeouts.add(r)}catch(i){i.message.includes("buffer")}}async startTest(t,e,s="both",i=null,o=100,r=!1,a="cumulative",l=10){if(this.testRunning)return;this.cleanup(),this.isCleanedUp=!1,this.testRunning=!0,this.direction=s,this.testConfig={count:t,interval:e,direction:s,duration:i,packetSize:o,calculationMode:a,realtimeWindow:l},this.baseInterval=e,this.autoIntervalEnabled=r,this.autoIntervalAdjusted=!1,this.calculationMode=a,this.realtimeWindowMs=l*1e3,this.statsHistory=[];let m=Math.max(0,o-30);this.packetPadding=m>0?"x".repeat(m):null,this.sendingComplete=!1,this.sendingCompleteTime=null,this.packets={},this.stats={sent:0,received:0,lost:0,currentLoss:0,c2sLost:0,s2cLost:0,c2sLossPercent:0,s2cLossPercent:0,jitter:0,sentPPS:0,receivedPPS:0,outOfOrder:0,outOfOrderPercent:0},this.serverRxCount=0,this.lastPPSUpdate=Date.now(),this.lastSentCount=0,this.lastReceivedCount=0,this.ppsHistory={sent:[],received:[]},this.lastReceivedSeq=-1,this.smoothedRTT=null,this.rttVariance=0,this.rttSampleCount=0,this.lastRTT=null,this.rttHistory=[],this.jitterHistory=[],this.displayRTT=0,this.displayJitter=0,this.testStartTime=Date.now(),this.lossTrackingReady=!1,this.emit("test:started",{count:t,interval:e,direction:s,duration:i,packetSize:o,calculationMode:a,realtimeWindow:l}),this.emit("status",{message:"Preparing network test"}),this.emit("metrics:update",{...this.stats,rtt:this.smoothedRTT||0,interval:this.baseInterval||0}),await new Promise(d=>setTimeout(d,100));try{this.connectionTimeoutId=setTimeout(()=>{this.testRunning&&!this.isCleanedUp&&(this.testRunning=!1,this.emit("test:error",{message:"Connection timeout - please check your network and firewall settings"}),this.cleanup())},3e4),await this.setupPeerConnection(),this.setupDataChannel(t,e,i),this.emit("status",{message:"Gathering network paths"}),this.restartSendingWithInterval(e);let p=await this.pc.createOffer();await this.pc.setLocalDescription(p),this.emit("status",{message:"Exploring network paths"}),this.pc.iceGatheringState!=="complete"&&await new Promise(C=>{let A=!1,_=H=>{A||(A=!0,this.pc.removeEventListener("icegatheringstatechange",z),this.pc.removeEventListener("icecandidate",q),C())},z=()=>{this.pc.iceGatheringState==="complete"&&_("complete")},q=H=>{H.candidate&&H.candidate.candidate.includes("srflx")&&_("early (srflx found)")};this.pc.addEventListener("icegatheringstatechange",z),this.pc.addEventListener("icecandidate",q),setTimeout(()=>{_("timeout")},2e3)}),this.emit("status",{message:"Establishing handshake"});let f=await fetch(lt("/webrtc/offer"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sdp:this.pc.localDescription})});if(!f.ok)throw new Error(`Server rejected offer: ${f.status}`);let w=await f.json();this.pcId=w.pc_id,await this.pc.setRemoteDescription(new RTCSessionDescription(w.sdp)),this.emit("status",{message:"Handshake complete"}),this.emit("status",{message:"Starting test"})}catch(d){this.testRunning=!1,this.emit("test:error",{message:`Failed to start: ${d.message}`}),this.cleanup()}}async setupPeerConnection(){let t=this.getStunUrl();this.pc=new RTCPeerConnection({iceServers:t?[{urls:t}]:[],iceTransportPolicy:"all",iceCandidatePoolSize:2}),this.pc.onconnectionstatechange=()=>{let e=this.pc.connectionState;e==="failed"||e==="closed"?!this.isCleanedUp&&this.testRunning&&(this.calculationMode==="realtime"&&this.testConfig.duration===1/0?this.handleDisconnectionForRealtimeMax():(this.testRunning=!1,this.emit("test:error",{message:`Connection ${e}`}),this.cleanup())):e==="connected"&&(clearTimeout(this.connectionTimeoutId),this.connectionTimeoutId=null)}}setupDataChannel(t,e,s){this.dataChannel=this.pc.createDataChannel("udp-test",{ordered:!1,maxRetransmits:0}),this.dataChannel.onopen=()=>{let i=s??(t===1/0?1/0:t*e/1e3);if(this.lossCheckInterval&&clearInterval(this.lossCheckInterval),this.lossCheckInterval=setInterval(()=>{if(!this.testRunning){this.lossCheckInterval&&(clearInterval(this.lossCheckInterval),this.lossCheckInterval=null);return}this.calculateLossStats(),this.calculatePPS(),this.calculateOutOfOrderStats(),this.emit("metrics:update",{...this.stats,rtt:this.displayRTT||0,interval:this.baseInterval||0,jitter:this.displayJitter||0})},200),i!==1/0&&isFinite(i)){let o=i*1e3+this.BOOTSTRAP_TIMEOUT,r=setTimeout(()=>{this.testRunning&&!this.isCleanedUp&&this.completeTest(),this.activeTimeouts.delete(r)},o);this.activeTimeouts.add(r)}},this.dataChannel.onmessage=async i=>{if(!this.isCleanedUp)try{let o;if(typeof i.data=="string"?o=i.data:i.data instanceof ArrayBuffer?o=new TextDecoder().decode(i.data):i.data instanceof Blob&&(o=await i.data.text()),o){let r=JSON.parse(o);if(r.s_rx!==void 0&&r.s_rx>this.serverRxCount){let a=r.s_rx-this.serverRxCount;this.serverRxCount=r.s_rx,this.testRunning&&this.emit("packet:sent",{count:a,type:"c2s"})}if(this.packets[r.seq]!==void 0&&!this.packets[r.seq].received){this.packets[r.seq].received=!0,this.stats.received++;let a=Date.now(),l=this.packets[r.seq].sent,h=a-l;if(this.updateRTT(h),r.seq<this.lastReceivedSeq){let m=this.lastReceivedSeq-r.seq;this.calculationMode==="realtime"&&m>1e3||(this.stats.outOfOrder++,this.calculationMode==="realtime"&&this.outOfOrderHistory.push({timestamp:Date.now(),seq:r.seq}))}else this.lastReceivedSeq=Math.max(this.lastReceivedSeq,r.seq);this.testConfig.count===1/0&&delete this.packets[r.seq],this.testRunning&&this.emit("packet:received",{count:1,type:"s2c"})}}}catch{this.isCleanedUp}},this.dataChannel.onclose=()=>{!this.isCleanedUp&&this.testRunning&&(this.calculationMode==="realtime"&&this.testConfig.duration===1/0?this.handleDisconnectionForRealtimeMax():(this.testRunning=!1,this.emit("test:error",{message:"Data channel closed"}),this.cleanup()))},this.dataChannel.onerror=i=>{!this.isCleanedUp&&this.testRunning&&(this.calculationMode==="realtime"&&this.testConfig.duration===1/0?this.handleDisconnectionForRealtimeMax():(this.testRunning=!1,this.emit("test:error",{message:`Data channel error: ${i.message}`}),this.cleanup()))}}handleDisconnectionForRealtimeMax(){if(this.wasDisconnected)return;this.wasDisconnected=!0,this.reconnectAttempt=0,this.sendInterval&&(clearInterval(this.sendInterval),this.sendInterval=null);let t=this.stats.sent,e=this.stats.lost;this.emit("test:disconnected",{sent:t,lost:e}),this.attemptReconnection()}attemptReconnection(){if(!this.wasDisconnected||!this.testRunning)return;if(this.reconnectAttempt++,this.reconnectAttempt>this.maxReconnectAttempts){this.testRunning=!1,this.emit("test:error",{message:`Connection lost. Failed to reconnect after ${this.maxReconnectAttempts} attempts.`}),this.cleanup();return}let t=Math.min(1e3*Math.pow(2,this.reconnectAttempt-1),3e4);this.reconnectTimer=setTimeout(async()=>{try{await this.resumeTestAfterReconnection()}catch{this.attemptReconnection()}},t)}async resumeTestAfterReconnection(){if(this.wasDisconnected)try{this.pc&&(this.pc.close(),this.pc=null),this.dataChannel&&(this.dataChannel.close(),this.dataChannel=null),this.pcId=null,this.candidateQueue=[];let t=this.stats.sent,e=this.stats.lost,s=this.stats.c2sLost,i=this.stats.s2cLost;this.packets={},this.serverRxCount=0,this.stats.received=0,this.stats.c2sLossPercent=0,this.stats.s2cLossPercent=0,this.stats.currentLoss=0,this.stats.outOfOrder=0,this.stats.outOfOrderPercent=0,this.statsHistory=[],this.outOfOrderHistory=[],this.lastReceivedSeq=-1,this.lastPPSUpdate=Date.now(),this.lastSentCount=t,this.lastReceivedCount=0,this.ppsHistory.sent=[],this.ppsHistory.received=[],this.stats.sentPPS=0,this.stats.receivedPPS=0,this.stats.sent=t,this.stats.lost=e,this.baselineStats={sent:t,received:0,serverRxCount:0,priorC2SLost:s||0,priorS2CLost:i||0},await this.setupPeerConnection(),this.setupDataChannel(this.testConfig.count,this.testConfig.interval,this.testConfig.duration);let o=await this.pc.createOffer();await this.pc.setLocalDescription(o),this.pc.iceGatheringState!=="complete"&&await new Promise(l=>{let h=()=>{this.pc.iceGatheringState==="complete"&&(this.pc.removeEventListener("icegatheringstatechange",h),l())};this.pc.addEventListener("icegatheringstatechange",h),setTimeout(()=>{this.pc.removeEventListener("icegatheringstatechange",h),l()},5e3)});let r=await fetch(lt("/webrtc/offer"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sdp:this.pc.localDescription})});if(!r.ok)throw new Error(`Server rejected offer: ${r.status}`);let a=await r.json();this.pcId=a.pc_id,await this.pc.setRemoteDescription(new RTCSessionDescription(a.sdp)),this.restartSendingWithInterval(this.baseInterval),this.wasDisconnected=!1,this.reconnectAttempt=0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.smoothedRTT!==null&&this.rttSampleCount>=this.BOOTSTRAP_PACKETS&&(this.lossTrackingReady=!0,this.emit("loss-tracking:ready")),this.emit("test:reconnected",{sent:this.stats.sent,lost:this.stats.lost,c2sLost:this.stats.c2sLost,s2cLost:this.stats.s2cLost})}catch(t){throw t}}cleanup(){this.isCleanedUp=!0,this.testRunning=!1,this.wasDisconnected=!1,this.reconnectAttempt=0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.sendInterval&&(clearInterval(this.sendInterval),this.sendInterval=null),this.lossCheckInterval&&(clearInterval(this.lossCheckInterval),this.lossCheckInterval=null),this.connectionTimeoutId&&(clearTimeout(this.connectionTimeoutId),this.connectionTimeoutId=null),this.activeTimeouts.forEach(t=>clearTimeout(t)),this.activeTimeouts.clear(),this.packetPadding=null,this.testConfig=null,this.baseInterval=null,this.autoIntervalEnabled=!1,this.autoIntervalAdjusted=!1,this.calculationMode="cumulative",this.realtimeWindowMs=0,this.statsHistory=[],this.candidateQueue=[],this.pcId=null,this.sendQueue=[],this.isSendingPaused=!1,this.dataChannel&&(this.dataChannel.close(),this.dataChannel=null),this.pc&&(this.pc.close(),this.pc=null)}};var D={HORIZONTAL:{viewBox:"0 0 710 320",clientPos:{x:60,y:160},serverPos:{x:650,y:160},pathC2S:"M 100 200 L 610 200",pathS2C:"M 610 120 L 100 120",graphPos:{x:130,y:60},graphSize:{w:450,h:200},statsPos:{x:130,y:10},statsSize:{w:450,h:40},packetStartC2S:{x:130,y:200},packetEndC2S:{x:580,y:200},packetStartS2C:{x:580,y:120},packetEndS2C:{x:140,y:120},vertical:!1},VERTICAL:{viewBox:"0 0 360 380",statsPos:{x:30,y:10},statsSize:{w:300,h:40},clientPos:{x:180,y:80},graphPos:{x:50,y:150},graphSize:{w:280,h:120},serverPos:{x:180,y:330},pathC2S:"M 140 100 L 140 310",pathS2C:"M 220 310 L 220 100",packetStartC2S:{x:140,y:150},packetEndC2S:{x:140,y:270},packetStartS2C:{x:220,y:270},packetEndS2C:{x:220,y:150},vertical:!0}},y={packetSpeed:2,graphUpdateInterval:200,maxGraphPoints:40,yScale:1,smaWindowSize:25},K=class{constructor(t,e,s){if(isNaN(t)||isNaN(e)||!isFinite(t)||!isFinite(e)){this.invalid=!0;return}this.x=t,this.y=e,this.vx=(Math.random()-.5)*4,this.vy=(Math.random()-.5)*4,this.life=1,this.decay=.05+Math.random()*.05,this.color=s,this.invalid=!1;let o=document.getElementById("openpacketloss-root").querySelector("#particles-group");this.el=document.createElementNS("http://www.w3.org/2000/svg","circle"),this.el.setAttribute("r",2+Math.random()*2),this.el.setAttribute("fill",s),this.updatePosition(),o.appendChild(this.el)}updatePosition(){this.el.setAttribute("cx",this.x),this.el.setAttribute("cy",this.y),this.el.setAttribute("opacity",this.life)}update(){return this.invalid?!0:(this.x+=this.vx,this.y+=this.vy,this.life-=this.decay,this.updatePosition(),this.life<=0?(this.el.remove(),!1):!0)}},Y=class{constructor(t,e){this.type=t,this.progress=0,this.lost=!1,this.dead=!1,this.success=!1,this.opacity=1,this.visualEngine=e,this.layout=e.currentLayout,this.creationTime=Date.now(),this.updateCoordinates(),this.color=t==="c2s"?"var(--client-color)":"var(--server-color)",t==="c2s"?(this.label=e.c2sCount%2===0?"0":"1",e.c2sCount++):(this.label=e.s2cCount%2===0?"0":"1",e.s2cCount++);let s=document.getElementById("openpacketloss-root"),i=s.querySelector("#packets-c2s"),o=s.querySelector("#packets-s2c");this.group=t==="c2s"?i:o,this.el=document.createElementNS("http://www.w3.org/2000/svg","g"),this.updatePosition();let r=24,a=-r/2,l=document.createElementNS("http://www.w3.org/2000/svg","rect");l.setAttribute("x",a),l.setAttribute("y",a),l.setAttribute("width",r),l.setAttribute("height",r),l.setAttribute("rx",4),l.setAttribute("fill",this.color);let h=document.createElementNS("http://www.w3.org/2000/svg","text");h.setAttribute("class","packet-text"),h.setAttribute("font-size","12"),h.textContent=this.label,this.el.appendChild(l),this.el.appendChild(h),this.group.appendChild(this.el),this.willLose=!1,this.lossPoint=.3+Math.random()*.5}updateCoordinates(){let t=this.visualEngine.currentLayout||D.HORIZONTAL;this.layout=t,this.type==="c2s"?(this.start=t.packetStartC2S||{x:100,y:200},this.end=t.packetEndC2S||{x:610,y:200}):(this.start=t.packetStartS2C||{x:610,y:120},this.end=t.packetEndS2C||{x:100,y:120})}updatePosition(){(!this.start||!this.end)&&this.updateCoordinates();let t=this.start.x+(this.end.x-this.start.x)*this.progress,e=this.start.y+(this.end.y-this.start.y)*this.progress,s=isNaN(t)?this.start.x:t,i=isNaN(e)?this.start.y:e;this.el.setAttribute("transform",`translate(${s.toFixed(1)}, ${i.toFixed(1)})`),this.el.setAttribute("opacity",this.opacity)}forceLoss(){this.willLose=!0,this.lossPoint=this.progress+.02}update(){if(this.dead)return this.el.remove(),!1;if(Date.now()-this.creationTime>2e4)return this.dead=!0,this.el.remove(),!1;if(this.updateCoordinates(),this.lost)return this.lossTimestamp||(this.lossTimestamp=Date.now()),Date.now()-this.lossTimestamp>1e3&&(this.dead=!0),!0;let t=this.end.x-this.start.x,e=this.end.y-this.start.y,s=Math.sqrt(t*t+e*e),i=y.packetSpeed/(s||1);if(this.progress+=i,this.progress>1&&(this.progress=1),this.updatePosition(),this.willLose&&this.progress>=this.lossPoint){this.lost=!0,this.lossTimestamp=Date.now(),this.el.classList.add("packet-lost");let o=document.createElementNS("http://www.w3.org/2000/svg","text");o.setAttribute("x",0),o.setAttribute("y",6),o.setAttribute("text-anchor","middle"),o.setAttribute("fill","#ffffff"),o.setAttribute("font-size","28"),o.setAttribute("font-weight","bold"),o.textContent="\xD7",this.el.appendChild(o);let a=this.el.getAttribute("transform").match(/translate\(([^,]+),\s*([^)]+)\)/);if(a){let l=parseFloat(a[1]),h=parseFloat(a[2]);!isNaN(l)&&!isNaN(h)&&isFinite(l)&&isFinite(h)&&this.spawnParticles(l,h,"#ef4444",8)}else{let l=this.start.x+(this.end.x-this.start.x)*this.progress,h=this.start.y+(this.end.y-this.start.y)*this.progress;!isNaN(l)&&!isNaN(h)&&isFinite(l)&&isFinite(h)&&this.spawnParticles(l,h,"#ef4444",8)}return setTimeout(()=>{this.dead=!0},500),!0}if(this.progress>=1){if(!this.success){this.success=!0,this.spawnParticles(this.end.x,this.end.y,"#10b981"),this.el.classList.add("packet-success");let o=this.el.querySelector("text");o&&(o.textContent="\u2713"),setTimeout(()=>{this.dead=!0},300)}return!0}return!0}spawnParticles(t,e,s,i=8){if(!(this.visualEngine.particles.length>50))for(let o=0;o<i;o++){let r=new K(t,e,s);r.invalid||this.visualEngine.particles.push(r)}}},W=class{constructor(){this.running=!1,this.testCompleted=!1,this.tabVisible=!0,this.packets=[],this.particles=[],this.graphData=new Array(y.maxGraphPoints).fill(0),this.currentLayout=D.HORIZONTAL,this.yAxisMax=5,this.yAxisMin=-5,this.c2sCount=0,this.s2cCount=0,this.c2sBucketCount=0,this.s2cBucketCount=0,this.bucketStartTime=0,this.visualSchedule=[],this.lastGraphUpdate=0,this.animationInterval=null,this.lossHistory=[],this.c2sLossHistory=[],this.s2cLossHistory=[],this.completeGraphHistory=[],this.metricsBuffer=[],this.metricsBufferProcessedCount=0,this.MAX_METRICS_BUFFER_SIZE=1e3,this.lastC2SLossTotal=0,this.lastS2CLossTotal=0,this.c2sLossAccumulator=0,this.s2cLossAccumulator=0,this.realPacketsPerVisual=1,this.stats={sent:0,received:0,lost:0,currentLoss:0,c2sLost:0,s2cLost:0,c2sLossPercent:0,s2cLossPercent:0,sentPPS:0,receivedPPS:0,outOfOrder:0,outOfOrderPercent:0},this.currentRTT=0,this.currentInterval=0,this.currentJitter=0,this.showNetworkMetrics=!1,this.lossTrackingReady=!1,this.calculationMode="cumulative",this.realtimeWindowMs=1e4,this.root=document.getElementById("openpacketloss-root"),this.dom={svg:this.root.querySelector("#main-svg"),clientGroup:this.root.querySelector("#client-group"),serverGroup:this.root.querySelector("#server-group"),pathC2S:this.root.querySelector("#path-c2s"),pathS2C:this.root.querySelector("#path-s2c"),graphGroup:this.root.querySelector("#graph-group"),graphBg:this.root.querySelector("#graph-bg"),gridLines:this.root.querySelector("#grid-lines"),axisLabels:this.root.querySelector("#axis-labels"),graphLine:this.root.querySelector("#graph-line"),graphArea:this.root.querySelector("#graph-area"),watermarkText:this.root.querySelector("#watermark-text"),statsGroup:this.root.querySelector("#stats-group")},this.initializeDOM(),this.setupEventListeners()}setShowNetworkMetrics(t){this.showNetworkMetrics=t,this.updateNetworkLabels()}initializeDOM(){this.applyLayout(D.HORIZONTAL),this.updateLayout()}setupEventListeners(){window.matchMedia("(orientation: portrait)").addEventListener("change",()=>this.updateLayout()),window.addEventListener("resize",()=>this.updateLayout()),document.addEventListener("visibilitychange",()=>{if(document.hidden)this.tabVisible=!1,this.stopAnimationLoop();else if(this.tabVisible=!0,this.running){let t=Date.now();this.bucketStartTime=t,this.lastGraphUpdate=t,this.visualSchedule=[],this.c2sBucketCount=0,this.s2cBucketCount=0,this.testCompleted?(this.lossHistory=[],this.c2sLossHistory=[],this.s2cLossHistory=[]):this.calculationMode==="realtime"?(this.metricsBuffer=[],this.metricsBufferProcessedCount=0,this.lossHistory=[],this.c2sLossHistory=[],this.s2cLossHistory=[]):this.metricsBuffer.length>this.metricsBufferProcessedCount&&(this.backfillGraphFromBuffer(),this.metricsBuffer=[],this.metricsBufferProcessedCount=0),this.startAnimationLoop()}})}onTestStarted(t){this.running=!0,this.testCompleted=!1,this.direction=t.direction||"both",this.calculationMode=t.calculationMode||"cumulative",this.realtimeWindowMs=(t.realtimeWindow||10)*1e3,this.packets.forEach(s=>s.el.remove()),this.packets=[],this.particles.forEach(s=>s.el.remove()),this.particles=[],this.graphData.fill(0),this.c2sCount=0,this.s2cCount=0,this.c2sBucketCount=0,this.s2cBucketCount=0,this.bucketStartTime=0,this.visualSchedule=[],this.lossHistory=[],this.c2sLossHistory=[],this.s2cLossHistory=[],this.completeGraphHistory=[],this.metricsBuffer=[],this.metricsBufferProcessedCount=0,this.lossTrackingReady=!1,this.lastC2SLossTotal=0,this.lastS2CLossTotal=0,this.c2sLossAccumulator=0,this.s2cLossAccumulator=0;let e=t.interval||20;this.updateVisualScaling(e),this.startAnimationLoop()}onLossTrackingReady(){this.lossTrackingReady=!0}updateVisualScaling(t){let e=Math.max(1,Math.ceil(300/t));e!==this.realPacketsPerVisual&&(this.realPacketsPerVisual=e)}onDisconnected(t){this.metricsBuffer=[],this.metricsBufferProcessedCount=0,this.lossHistory=[],this.c2sLossHistory=[],this.s2cLossHistory=[],this.lossTrackingReady=!1}onReconnected(t){this.lastC2SLossTotal=t.c2sLost||0,this.lastS2CLossTotal=t.s2cLost||0,this.c2sLossAccumulator=0,this.s2cLossAccumulator=0}onTestCompleted(t){this.stats=t,this.testCompleted=!0,this.updateStatsUI();let e=setInterval(()=>{this.packets.length===0&&this.particles.length===0&&(clearInterval(e),this.running=!1,this.stopAnimationLoop())},100)}onPacketSent(t){this.running&&t.type==="c2s"&&(this.direction==="both"||this.direction==="c2s")&&(this.c2sBucketCount+=t.count)}onPacketReceived(t){this.running&&t.type==="s2c"&&(this.direction==="both"||this.direction==="s2c")&&(this.s2cBucketCount+=t.count)}onMetricsUpdate(t){this.stats=t;let e=Date.now();if(t.rtt!==void 0&&(this.currentRTT=t.rtt),t.interval!==void 0&&t.interval!==this.currentInterval&&(this.currentInterval=t.interval,this.updateVisualScaling(this.currentInterval)),t.jitter!==void 0&&(this.currentJitter=t.jitter),this.running&&!this.testCompleted){if(this.metricsBuffer.length>=this.MAX_METRICS_BUFFER_SIZE){let s=Math.floor(this.MAX_METRICS_BUFFER_SIZE*.2);this.metricsBuffer.splice(0,s),this.metricsBufferProcessedCount=Math.max(0,this.metricsBufferProcessedCount-s)}if(this.metricsBuffer.push({timestamp:e,stats:{...t}}),this.calculationMode==="realtime"&&this.metricsBuffer.length>100){let s=e-this.realtimeWindowMs*2,i=this.metricsBuffer.length;this.metricsBuffer=this.metricsBuffer.filter(r=>r.timestamp>=s);let o=i-this.metricsBuffer.length;o>0&&(this.metricsBufferProcessedCount=Math.max(0,this.metricsBufferProcessedCount-o))}}if(this.running&&!this.testCompleted&&e-this.lastGraphUpdate>=y.graphUpdateInterval){let s=this.getDirectionalLoss(t),i=s<.01?0:s;this.completeGraphHistory.push({timestamp:e,loss:i,c2sLoss:t.c2sLossPercent||0,s2cLoss:t.s2cLossPercent||0}),this.lossHistory.push(s),this.lossHistory.length>y.smaWindowSize&&this.lossHistory.shift();let o=t.sent>=20&&t.c2sLossPercent||0;this.c2sLossHistory.push(o),this.c2sLossHistory.length>y.smaWindowSize&&this.c2sLossHistory.shift();let r=t.sent>=20&&t.s2cLossPercent||0;this.s2cLossHistory.push(r),this.s2cLossHistory.length>y.smaWindowSize&&this.s2cLossHistory.shift();let a;if(this.calculationMode==="realtime"){let l=Math.min(3,this.lossHistory.length);if(l>0){let h=this.lossHistory.slice(-l),d=h.reduce((p,f)=>p+f,0)/h.length;a=d<.01?0:d}else a=i}else a=this.calculateSMA();this.graphData.shift(),this.graphData.push(a),this.lastGraphUpdate=e,this.metricsBufferProcessedCount=this.metricsBuffer.length,this.tabVisible&&this.updateGraphVisuals()}if(!this.testCompleted&&this.tabVisible&&(this.updateStatsUI(),this.updateNetworkLabels()),this.running&&this.lossTrackingReady){let s=(t.c2sLost||0)-this.lastC2SLossTotal;if(s>0){for(this.c2sLossAccumulator+=s;this.c2sLossAccumulator>=this.realPacketsPerVisual&&this.triggerVisualLoss("c2s");)this.c2sLossAccumulator-=this.realPacketsPerVisual;this.lastC2SLossTotal=t.c2sLost}let i=(t.s2cLost||0)-this.lastS2CLossTotal;if(i>0){for(this.s2cLossAccumulator+=i;this.s2cLossAccumulator>=this.realPacketsPerVisual&&this.triggerVisualLoss("s2c");)this.s2cLossAccumulator-=this.realPacketsPerVisual;this.lastS2CLossTotal=t.s2cLost}}}triggerVisualLoss(t){let e=this.packets.filter(s=>s.type===t&&!s.lost&&!s.success&&!s.dead&&s.progress>.1&&s.progress<.9&&!s.willLose);if(e.length>0){let s=Math.floor(Math.random()*e.length),i=e[s];if(i&&!i.willLose)return i.forceLoss(),!0}return!1}startAnimationLoop(){this.animationInterval||(this.animationInterval=setInterval(()=>this.loop(),16))}stopAnimationLoop(){this.animationInterval&&(clearInterval(this.animationInterval),this.animationInterval=null)}loop(){try{let t=Date.now();if(this.bucketStartTime===0&&(this.bucketStartTime=t),t-this.bucketStartTime>=500)if(!this.running)this.c2sBucketCount=0,this.s2cBucketCount=0,this.bucketStartTime=t;else{let e=Math.min(this.c2sBucketCount,1);if(e>0&&(this.direction==="both"||this.direction==="c2s")){let i=500/e;for(let o=0;o<e;o++)this.visualSchedule.push({time:t+o*i,type:"c2s"})}let s=Math.min(this.s2cBucketCount,1);if(s>0&&(this.direction==="both"||this.direction==="s2c")){let i=500/s;for(let o=0;o<s;o++)this.visualSchedule.push({time:t+o*i,type:"s2c"})}this.c2sBucketCount=0,this.s2cBucketCount=0,this.bucketStartTime=t}for(this.visualSchedule.sort((e,s)=>e.time-s.time);this.visualSchedule.length>0&&this.visualSchedule[0].time<=t;){let e=this.visualSchedule.shift(),s=new Y(e.type,this);this.packets.push(s)}this.packets=this.packets.filter(e=>e.update()),this.particles=this.particles.filter(e=>e.update())}catch{}}getDirectionalLoss(t){return this.direction==="c2s"?t.sent>=20&&t.c2sLossPercent||0:this.direction==="s2c"?t.sent>=20&&t.s2cLossPercent||0:this.calculationMode==="realtime"?t.sent>=20&&t.currentLoss||0:t.sent>=20?t.lost/t.sent*100:0}calculateSMA(){let t=0;if(this.direction==="c2s"){let e=this.c2sLossHistory.reduce((s,i)=>s+i,0);t=this.c2sLossHistory.length>0?e/this.c2sLossHistory.length:0}else if(this.direction==="s2c"){let e=this.s2cLossHistory.reduce((s,i)=>s+i,0);t=this.s2cLossHistory.length>0?e/this.s2cLossHistory.length:0}else{let e=this.lossHistory.reduce((s,i)=>s+i,0);t=this.lossHistory.length>0?e/this.lossHistory.length:0}return t<.01?0:t}renderStats(t){this.dom.statsGroup.innerHTML="";let e=(t.statsSize.w-20)/3,s=t.statsSize.h,i=t.statsPos.x,o=t.statsPos.y;[{id:"sent",label:"Sent",color:"#4fc3f7",bg:"rgba(79, 195, 247, 0.1)"},{id:"loss",label:"Total Loss",color:"#64ffda",bg:"rgba(100, 255, 218, 0.1)"},{id:"lost",label:"Lost",color:"#ff5252",bg:"rgba(255, 82, 82, 0.1)"}].forEach((a,l)=>{let h=i+l*(e+10),m=document.createElementNS("http://www.w3.org/2000/svg","g"),d=document.createElementNS("http://www.w3.org/2000/svg","rect");d.setAttribute("x",h),d.setAttribute("y",o),d.setAttribute("width",e),d.setAttribute("height",s),d.setAttribute("rx",6),d.setAttribute("ry",6),d.setAttribute("class","stat-box-rect"),d.setAttribute("fill",a.bg),d.setAttribute("stroke",a.color);let p=document.createElementNS("http://www.w3.org/2000/svg","text");p.setAttribute("x",h+e/2),p.setAttribute("y",o+s/2-5),p.setAttribute("class","stat-value-text"),p.setAttribute("fill",a.color),p.setAttribute("id",`svg-stat-${a.id}`),p.textContent=a.id==="loss"?"0%":"0";let f=document.createElementNS("http://www.w3.org/2000/svg","text");f.setAttribute("x",h+e/2),f.setAttribute("y",o+s/2+12),f.setAttribute("class","stat-label-text"),f.textContent=a.label,m.appendChild(d),m.appendChild(p),m.appendChild(f),this.dom.statsGroup.appendChild(m)})}updateStatsUI(){let t=document.getElementById("svg-stat-sent"),e=document.getElementById("svg-stat-loss"),s=document.getElementById("svg-stat-lost"),i,o,r=this.stats.sent>=20&&this.stats.c2sLossPercent||0,a=this.stats.sent>=20&&this.stats.s2cLossPercent||0;if(this.testCompleted)i=r,o=a;else if(this.calculationMode==="realtime"){let h=Math.min(3,this.c2sLossHistory.length);if(h>0){let m=this.c2sLossHistory.slice(-h);i=m.reduce((w,C)=>w+C,0)/m.length;let p=this.s2cLossHistory.slice(-h);o=p.reduce((w,C)=>w+C,0)/p.length}else i=r,o=a}else{let h=this.c2sLossHistory.reduce((d,p)=>d+p,0);i=this.c2sLossHistory.length>0?h/this.c2sLossHistory.length:0;let m=this.s2cLossHistory.reduce((d,p)=>d+p,0);o=this.s2cLossHistory.length>0?m/this.s2cLossHistory.length:0}this.direction==="c2s"?this.stats.currentLoss=i:this.direction==="s2c"&&(this.stats.currentLoss=o),t&&(this.direction==="s2c"?t.textContent=this.calculationMode==="realtime"?this.stats.windowServerRx??0:this.stats.serverRxCount??0:t.textContent=this.calculationMode==="realtime"?this.stats.windowSent??0:this.stats.sent);let l=!this.testCompleted&&this.stats.currentLoss<.01;if(s&&(this.direction==="c2s"?s.textContent=this.calculationMode==="realtime"?this.stats.windowC2SLost??0:this.stats.c2sLost??0:this.direction==="s2c"?s.textContent=this.calculationMode==="realtime"?this.stats.windowS2CLost??0:this.stats.s2cLost??0:s.textContent=this.calculationMode==="realtime"?this.stats.windowLost??0:this.stats.lost),e){let h=l?0:this.stats.currentLoss,m=Number(h.toFixed(1));e.textContent=m+"%",h>20?e.setAttribute("fill","var(--loss-color)"):e.setAttribute("fill","var(--text-color)")}this.updateDirectionalLossLabels(i,o)}updateNetworkLabels(){let t=document.getElementById("rtt-label");if(t||(t=document.createElementNS("http://www.w3.org/2000/svg","text"),t.setAttribute("id","rtt-label"),t.setAttribute("class","network-metric-label"),t.setAttribute("text-anchor","start"),t.setAttribute("font-size","10"),t.setAttribute("font-weight","600"),t.setAttribute("fill","#9ca3af"),t.setAttribute("opacity","0.6"),this.dom.graphGroup.appendChild(t)),this.running&&this.showNetworkMetrics){t.setAttribute("visibility","visible"),t.setAttribute("x",5),t.setAttribute("y",10);let a=this.currentRTT>0?Math.round(this.currentRTT):0;t.textContent=`RTT: ${a}ms`}else t.setAttribute("visibility","hidden");let e=document.getElementById("interval-label");if(e||(e=document.createElementNS("http://www.w3.org/2000/svg","text"),e.setAttribute("id","interval-label"),e.setAttribute("class","network-metric-label"),e.setAttribute("text-anchor","start"),e.setAttribute("font-size","10"),e.setAttribute("font-weight","600"),e.setAttribute("fill","#9ca3af"),e.setAttribute("opacity","0.6"),this.dom.graphGroup.appendChild(e)),this.running&&this.showNetworkMetrics){e.setAttribute("visibility","visible"),e.setAttribute("x",5),e.setAttribute("y",36);let a=this.currentInterval>0?Math.round(this.currentInterval*10)/10:0;e.textContent=`Interval: ${a}ms`}else e.setAttribute("visibility","hidden");let s=document.getElementById("jitter-label");if(s||(s=document.createElementNS("http://www.w3.org/2000/svg","text"),s.setAttribute("id","jitter-label"),s.setAttribute("class","network-metric-label"),s.setAttribute("text-anchor","start"),s.setAttribute("font-size","10"),s.setAttribute("font-weight","600"),s.setAttribute("fill","#9ca3af"),s.setAttribute("opacity","0.6"),this.dom.graphGroup.appendChild(s)),this.running&&this.showNetworkMetrics){s.setAttribute("visibility","visible"),s.setAttribute("x",5),s.setAttribute("y",23);let a=this.currentJitter>0?this.currentJitter.toFixed(1):"0.0";s.textContent=`Jitter: ${a}ms`}else s.setAttribute("visibility","hidden");let i=document.getElementById("sent-pps-label");if(i||(i=document.createElementNS("http://www.w3.org/2000/svg","text"),i.setAttribute("id","sent-pps-label"),i.setAttribute("class","network-metric-label"),i.setAttribute("text-anchor","end"),i.setAttribute("font-size","10"),i.setAttribute("font-weight","600"),i.setAttribute("fill","#9ca3af"),i.setAttribute("opacity","0.6"),this.dom.graphGroup.appendChild(i)),this.running&&this.showNetworkMetrics){i.setAttribute("visibility","visible");let a=this.currentLayout?.graphSize?.w||450;i.setAttribute("x",a-5),i.setAttribute("y",10);let l=this.stats.sentPPS||0;i.textContent=`Sent: ${l} pps`}else i.setAttribute("visibility","hidden");let o=document.getElementById("received-pps-label");if(o||(o=document.createElementNS("http://www.w3.org/2000/svg","text"),o.setAttribute("id","received-pps-label"),o.setAttribute("class","network-metric-label"),o.setAttribute("text-anchor","end"),o.setAttribute("font-size","10"),o.setAttribute("font-weight","600"),o.setAttribute("fill","#9ca3af"),o.setAttribute("opacity","0.6"),this.dom.graphGroup.appendChild(o)),this.running&&this.showNetworkMetrics){o.setAttribute("visibility","visible");let a=this.currentLayout?.graphSize?.w||450;o.setAttribute("x",a-5),o.setAttribute("y",23);let l=this.stats.receivedPPS||0;o.textContent=`Received: ${l} pps`}else o.setAttribute("visibility","hidden");let r=document.getElementById("out-of-order-label");if(r||(r=document.createElementNS("http://www.w3.org/2000/svg","text"),r.setAttribute("id","out-of-order-label"),r.setAttribute("class","network-metric-label"),r.setAttribute("text-anchor","end"),r.setAttribute("font-size","10"),r.setAttribute("font-weight","600"),r.setAttribute("fill","#9ca3af"),r.setAttribute("opacity","0.6"),this.dom.graphGroup.appendChild(r)),this.running&&this.showNetworkMetrics){r.setAttribute("visibility","visible");let a=this.currentLayout?.graphSize?.w||450;r.setAttribute("x",a-5),r.setAttribute("y",36);let l=this.stats.outOfOrderPercent||0;l>=5?(r.setAttribute("fill","#ef4444"),r.setAttribute("opacity","0.9")):l>=1?(r.setAttribute("fill","#f59e0b"),r.setAttribute("opacity","0.8")):(r.setAttribute("fill","#9ca3af"),r.setAttribute("opacity","0.6")),r.textContent=`Out-of-Order: ${l.toFixed(1)}%`}else r.setAttribute("visibility","hidden")}updateDirectionalLossLabels(t,e){let s=document.getElementById("c2s-loss-label");if(s||(s=document.createElementNS("http://www.w3.org/2000/svg","text"),s.setAttribute("id","c2s-loss-label"),s.setAttribute("class","directional-loss-label"),s.setAttribute("text-anchor","middle"),s.setAttribute("font-size","11"),s.setAttribute("font-weight","bold"),s.setAttribute("fill","#4fc3f7"),this.dom.clientGroup.appendChild(s)),this.direction==="both"){s.setAttribute("visibility","visible");let o=this.currentLayout.vertical?65:-40;s.setAttribute("y",o),s.setAttribute("x",0);let r=!this.testCompleted&&t<.01?0:t;s.textContent=`\u2191 ${r.toFixed(1)}%`,s.setAttribute("fill",r>20?"#ff5252":"#4fc3f7")}else s.setAttribute("visibility","hidden");let i=document.getElementById("s2c-loss-label");if(i||(i=document.createElementNS("http://www.w3.org/2000/svg","text"),i.setAttribute("id","s2c-loss-label"),i.setAttribute("class","directional-loss-label"),i.setAttribute("text-anchor","middle"),i.setAttribute("font-size","11"),i.setAttribute("font-weight","bold"),i.setAttribute("fill","#64ffda"),this.dom.serverGroup.appendChild(i)),this.direction==="both"){i.setAttribute("visibility","visible");let o=this.currentLayout.vertical?-35:-40;i.setAttribute("y",o),i.setAttribute("x",0);let r=!this.testCompleted&&e<.01?0:e;i.textContent=`\u2193 ${r.toFixed(1)}%`,i.setAttribute("fill",r>20?"#ff5252":"#64ffda")}else i.setAttribute("visibility","hidden")}updateGraphVisuals(t=!1){this.updateYAxisMax();let e=this.currentLayout?.graphSize?.w||450,s=this.currentLayout?.graphSize?.h||200,i=this.graphData,o;if(t){let h=-1;for(let m=0;m<this.graphData.length;m++)if(this.graphData[m]>0){h=m;break}if(h>0&&h<this.graphData.length-5){let m=Math.max(0,h-1);i=this.graphData.slice(m);let d=Math.max(2,i.length-1);o=e/d}else o=e/(y.maxGraphPoints-1)}else o=e/(y.maxGraphPoints-1);let r="M ";i.forEach((h,m)=>{let d=m*o,p=s-(h-this.yAxisMin)*y.yScale;r+=`${d.toFixed(1)} ${p.toFixed(1)} `,m<i.length-1&&(r+="L ")}),this.dom.graphLine.setAttribute("d",r);let a=(i.length-1)*o,l=r+`L ${a.toFixed(1)} ${s} L 0 ${s} Z`;this.dom.graphArea.setAttribute("d",l)}updateYAxisMax(){let t=Math.max(...this.graphData),e=Math.min(...this.graphData),s=5,i=-5;t>2&&(i=0,t<=5?s=10:t<=10?s=15:t<=15?s=20:t<=20?s=30:t<=30?s=40:t<=50?s=60:s=100),(s!==this.yAxisMax||i!==this.yAxisMin)&&(this.yAxisMax=s,this.yAxisMin=i,this.rebuildGrid())}updateLayout(){let e=window.matchMedia("(orientation: portrait)").matches?D.VERTICAL:D.HORIZONTAL;this.currentLayout!==e&&(this.currentLayout=e,this.applyLayout(e))}applyLayout(t){if(this.dom.svg.setAttribute("viewBox",t.viewBox),this.dom.clientGroup.setAttribute("transform",`translate(${t.clientPos.x}, ${t.clientPos.y})`),this.dom.serverGroup.setAttribute("transform",`translate(${t.serverPos.x}, ${t.serverPos.y})`),this.dom.pathC2S.setAttribute("d",t.pathC2S),this.dom.pathS2C.setAttribute("d",t.pathS2C),this.dom.graphGroup.setAttribute("transform",`translate(${t.graphPos.x}, ${t.graphPos.y})`),this.dom.graphBg.setAttribute("width",t.graphSize.w),this.dom.graphBg.setAttribute("height",t.graphSize.h),this.renderStats(t),this.updateStatsUI(),this.updateNetworkLabels(),this.rebuildGrid(),this.dom.watermarkText){this.dom.watermarkText.setAttribute("x",t.graphSize.w/2);let e=t.vertical?1.45:1.6;this.dom.watermarkText.setAttribute("y",t.graphSize.h/e)}}rebuildGrid(){this.dom.gridLines.innerHTML="",this.dom.axisLabels.innerHTML="";let t=this.currentLayout?.graphSize?.w||450,e=this.currentLayout?.graphSize?.h||200,s=this.yAxisMax,i=this.yAxisMin,o=s-i;for(let r=0;r<=4;r++){let a=e-r*(e/4),l=document.createElementNS("http://www.w3.org/2000/svg","line");if(l.setAttribute("x1",0),l.setAttribute("y1",a),l.setAttribute("x2",t),l.setAttribute("y2",a),l.setAttribute("class","grid-line"),this.dom.gridLines.appendChild(l),!this.currentLayout.vertical||r%2===0){let h=document.createElementNS("http://www.w3.org/2000/svg","text");h.setAttribute("x",-10),h.setAttribute("y",a),h.setAttribute("text-anchor","end"),h.setAttribute("class","axis-label"),h.setAttribute("dominant-baseline","middle");let m=i+r*(o/4);h.textContent=m.toFixed(0)+"%",this.dom.axisLabels.appendChild(h)}}y.yScale=e/o}reconstructGraphFromHistory(){this.completeGraphHistory.length!==0&&this.updateGraphVisuals(!0)}backfillGraphFromBuffer(){if(this.metricsBuffer.length-this.metricsBufferProcessedCount<=0)return;let e=this.metricsBufferProcessedCount;if(this.calculationMode==="realtime"){let m=Date.now()-this.realtimeWindowMs;for(let p=this.metricsBufferProcessedCount;p<this.metricsBuffer.length;p++)if(this.metricsBuffer[p].timestamp>=m){e=p;break}e-this.metricsBufferProcessedCount>0&&(this.lossHistory=[],this.c2sLossHistory=[],this.s2cLossHistory=[])}let s=0,i=0;for(let h=e;h<this.metricsBuffer.length;h++){let m=this.metricsBuffer[h],d=m.stats,p=m.timestamp,f=this.getDirectionalLoss(d);i+=f,s++,this.lossHistory.push(f),this.lossHistory.length>y.smaWindowSize&&this.lossHistory.shift();let w=d.sent>=20&&d.c2sLossPercent||0;this.c2sLossHistory.push(w),this.c2sLossHistory.length>y.smaWindowSize&&this.c2sLossHistory.shift();let C=d.sent>=20&&d.s2cLossPercent||0;this.s2cLossHistory.push(C),this.s2cLossHistory.length>y.smaWindowSize&&this.s2cLossHistory.shift();let A=this.calculateSMA();this.completeGraphHistory.push({timestamp:p,loss:A,c2sLoss:d.c2sLossPercent||0,s2cLoss:d.s2cLossPercent||0}),this.graphData.shift(),this.graphData.push(A)}let o=s>0?i/s:0;this.metricsBufferProcessedCount=this.metricsBuffer.length,this.updateGraphVisuals(),this.updateStatsUI();let r=this.graphData[this.graphData.length-1],a=this.calculateSMA(),l=this.graphData.slice(-5).map(h=>h.toFixed(1)).join(", ")}};var k=class{static ERROR_MESSAGES={OUT_OF_RANGE:"Value must be between {min} and {max}",POSITIVE_REQUIRED:"Value must be greater than 0",PLATFORM_LIMIT:"This value exceeds the platform limit of {max}",NAN_ERROR:"Invalid input - not a number"};static _error(t,e={}){let s=this.ERROR_MESSAGES[t];for(let[i,o]of Object.entries(e))s=s.replace(`{${i}}`,o);return{valid:!1,value:null,error:s}}static validatePacketInterval(t){let e=parseFloat(t);if(isNaN(e))return this._error("NAN_ERROR");if(e<=0)return this._error("POSITIVE_REQUIRED");let s=1e4;return e>s?this._error("OUT_OF_RANGE",{min:0,max:s}):{valid:!0,value:e,error:null}}static validatePacketSize(t,e="web"){let s=parseInt(t);if(isNaN(s))return this._error("NAN_ERROR");let i=50,o=e==="web"?1500:9e3;return s<i||s>o?this._error("OUT_OF_RANGE",{min:i,max:o}):{valid:!0,value:s,error:null}}static validatePacketCount(t,e="web"){if(t===1/0||t==="Infinity")return e==="web"?this._error("PLATFORM_LIMIT",{max:"6000"}):{valid:!0,value:1/0,error:null};let s=parseInt(t);if(isNaN(s))return this._error("NAN_ERROR");if(s<=0)return this._error("POSITIVE_REQUIRED");let i=e==="web"?6e3:1/0;return i!==1/0&&s>i?this._error("PLATFORM_LIMIT",{max:i}):{valid:!0,value:s,error:null}}static validateDuration(t,e="web"){if(t===1/0||t==="Infinity")return e==="web"?this._error("PLATFORM_LIMIT",{max:"300 seconds"}):{valid:!0,value:1/0,error:null};let s=parseInt(t);if(isNaN(s))return this._error("NAN_ERROR");let i=1,o=e==="web"?300:3600;return s<i||s>o?this._error("OUT_OF_RANGE",{min:i,max:o}):{valid:!0,value:s,error:null}}static validateRealtimeWindow(t){let e=parseInt(t);if(isNaN(e))return this._error("NAN_ERROR");let s=5,i=60;return e<s||e>i?this._error("OUT_OF_RANGE",{min:s,max:i}):{valid:!0,value:e,error:null}}static validateSettings(t,e="web"){let s=[],i={packetInterval:o=>this.validatePacketInterval(o),packetSize:o=>this.validatePacketSize(o,e),packetCount:o=>this.validatePacketCount(o,e),duration:o=>this.validateDuration(o,e),realtimeWindow:o=>this.validateRealtimeWindow(o)};for(let[o,r]of Object.entries(i))if(t[o]!==void 0){let a=r(t[o]);a.valid||s.push(`${o}: ${a.error}`)}return{valid:s.length===0,errors:s}}};var M=class{static ERROR_CATALOG={CONNECTION_FAILED:{title:"Connection Failed",message:"Unable to establish WebRTC connection.",technicalHelp:"WebRTC requires UDP traffic. Common ports: 3478 (STUN), 49152-65535 (media)"},CONNECTION_TIMEOUT:{title:"Connection Timeout",message:"Connection attempt timed out.",technicalHelp:"ICE negotiation took longer than 30 seconds"},DATA_CHANNEL_ERROR:{title:"Data Transfer Error",message:"Lost connection during test.",technicalHelp:"DataChannel failed due to network instability"}};static showError(t,e=null,s=null){let i=this.ERROR_CATALOG[t]||{title:"Error",message:t,causes:[],solutions:["Try again","Contact support"],technicalHelp:""},o=document.getElementById("openpacketloss-root"),r=o.querySelector("#error-detail-modal");r&&r.remove();let a=document.createElement("div");a.id="error-detail-modal",a.className="error-modal-overlay";let l=document.createElement("div");l.className="error-modal-compact",l.innerHTML=`
<div class="error-modal-header">
<span class="error-icon">\u26A0\uFE0F</span>
<h2>${i.title}</h2>
</div>
<p class="error-modal-message">${i.message}</p>
${i.technicalHelp||e?`
<div class="error-modal-technical-hint">
${i.technicalHelp||""}
${e?`<div class="error-detail">${e}</div>`:""}
</div>
`:""}
<div class="error-modal-actions">
${s?'<button class="error-btn error-btn-retry">Retry</button>':""}
<a href="https://openpacketloss.com/faq.php#${t}" target="_blank" class="error-btn error-btn-learn">Learn More</a>
</div>
`,a.appendChild(l),o.appendChild(a),s&&l.querySelector(".error-btn-retry").addEventListener("click",()=>{window.location.reload()})}static mapErrorMessage(t){let e=t.toLowerCase();return e.includes("timeout")?"CONNECTION_TIMEOUT":e.includes("datachannel")||e.includes("data channel")?"DATA_CHANNEL_ERROR":"CONNECTION_FAILED"}static showToast(t,e="info"){let s=document.createElement("div");s.className=`error-toast error-toast-${e}`,s.textContent=t,document.getElementById("openpacketloss-root").appendChild(s),setTimeout(()=>{s.classList.add("fade-out"),setTimeout(()=>s.remove(),300)},3e3)}};var v=new V,T=new W;v.on("test:started",n=>T.onTestStarted(n));v.on("status",n=>yt(n.message));v.once("packet:sent",()=>ot());v.once("packet:received",()=>ot());v.on("test:completed",n=>T.onTestCompleted(n));v.on("packet:sent",n=>T.onPacketSent(n));v.on("packet:received",n=>T.onPacketReceived(n));v.on("metrics:update",n=>T.onMetricsUpdate(n));v.on("test:error",n=>{let t=M.mapErrorMessage(n.message);M.showError(t,n.message,()=>{rt()})});v.on("loss-tracking:ready",()=>T.onLossTrackingReady());v.on("test:disconnected",n=>T.onDisconnected(n));v.on("test:reconnected",n=>T.onReconnected(n));v.on("interval:adjusted",n=>{});var st=3,c={version:st,preset:"default",duration:30,direction:"both",packetInterval:15.625,packetCount:1920,packetSize:100,autoInterval:!0,showNetworkMetrics:!1,calculationMode:null,realtimeWindow:10},it=!1,b=null,nt={quick:{duration:window.TEST_CONFIG?.presets?.quick?.duration??10,count:window.TEST_CONFIG?.presets?.quick?.packetCount??200,interval:window.TEST_CONFIG?.presets?.quick?.interval??50,label:"QUICK"},default:{duration:window.TEST_CONFIG?.presets?.default?.duration??30,count:window.TEST_CONFIG?.presets?.default?.packetCount??1920,interval:window.TEST_CONFIG?.presets?.default?.interval??15.625,label:"DEFAULT"},stress:{duration:window.TEST_CONFIG?.presets?.max?.duration??300,count:window.TEST_CONFIG?.presets?.max?.packetCount??6e3,interval:window.TEST_CONFIG?.presets?.max?.interval??50,label:"MAX"}},u=document.getElementById("openpacketloss-root"),ft=u.querySelector("#start-overlay"),j=u.querySelector("#settings-modal"),ut=u.querySelector("#btn-test"),gt=u.querySelector("#btn-settings"),vt=u.querySelector("#close-settings"),St=u.querySelector("#start-test-from-modal"),et=u.querySelector("#theme-btn"),g=u.querySelector("#duration-slider"),O=u.querySelector("#duration-value"),F=u.querySelector("#packet-interval"),N=u.querySelector("#auto-interval"),U=u.querySelector("#show-network-metrics"),ht=u.querySelector("#packet-count-display"),G=u.querySelector("#packet-size"),wt=u.querySelector("#reset-settings"),tt=u.querySelector("#calculation-mode-group"),S=u.querySelector("#status-text"),$=u.querySelector("#status-content"),zt=u.querySelector("#status-cursor");function dt(){ut.textContent="TEST PACKET LOSS"}function L(){c.packetCount===1/0?ht.textContent="\u221E Unlimited packets":ht.textContent=`${c.packetCount.toLocaleString()} packets`}function Q(){if(!tt)return;let n=c.preset==="stress",t=c.duration>=300||c.duration===1/0;n&&t?(tt.classList.remove("hidden"),c.calculationMode||(c.calculationMode="realtime",u.querySelectorAll('input[name="calculation-mode"]').forEach(e=>{e.checked=e.value==="realtime"}))):(tt.classList.add("hidden"),c.calculationMode="cumulative")}function Z(n,t){let e=u.querySelector(`#${n}`);if(!e)return;let s=e.parentElement.querySelector(".field-error-message");s||(s=document.createElement("div"),s.className="field-error-message",e.parentElement.appendChild(s)),t.valid?(e.classList.remove("field-error"),s.style.display="none"):(e.classList.add("field-error"),s.textContent=t.error,s.style.display="block")}function J(){let n=u.querySelector("#start-test-from-modal");if(!n)return;u.querySelectorAll(".field-error").length>0?(n.disabled=!0,n.style.opacity="0.5",n.style.cursor="not-allowed",n.title="Please fix validation errors before starting test"):(n.disabled=!1,n.style.opacity="1",n.style.cursor="pointer",n.title="")}var x=null,R=null,E=[],B=!1;async function mt(){if(!(B||E.length===0)){for(B=!0;E.length>0;){let n=E.shift();if(S&&S.classList.contains("hidden")&&!R&&S.classList.remove("hidden"),S&&S.classList.contains("hidden")){B=!1;return}await new Promise(e=>{Ct(n,$,10,e)});let t=E.length>0?800:1200;await new Promise(e=>setTimeout(e,t))}B=!1}}function Ct(n,t,e=10,s){x&&clearInterval(x),S&&S.classList.remove("cursor-blinking"),$&&($.textContent="");let i=0;x=setInterval(()=>{i<n.length?($&&($.textContent=n.substring(0,i+1)),i++):(clearInterval(x),x=null,S&&S.classList.add("cursor-blinking"),s&&s())},e)}function yt(n){S&&(E.push(n),mt())}function bt(){S&&(R&&clearTimeout(R),R=setTimeout(()=>{S&&S.classList.remove("hidden"),E.length===0&&E.push("Gathering network paths"),mt(),R=null},500))}function ot(){S&&(R&&(clearTimeout(R),R=null),E=[],B=!1,x&&(clearInterval(x),x=null),S.classList.add("hidden"))}async function rt(){let n=c.direction,t=c.packetCount,e=c.packetInterval,s=c.duration,i=c.packetSize,o=c.autoInterval,r=c.calculationMode,a=c.realtimeWindow;ft.classList.add("hidden"),bt(),it=!1,u.classList.remove("clickable");try{await v.startTest(t,e,n,s,i,o,r,a)}catch(l){throw ot(),l}}function Tt(){j.classList.add("active")}function at(){j.classList.remove("active")}u.querySelectorAll(".preset-btn").forEach(n=>{n.addEventListener("click",()=>{let t=n.dataset.preset;c.preset=t,u.setAttribute("data-preset",t),u.querySelectorAll(".preset-btn").forEach(r=>r.classList.remove("active")),n.classList.add("active");let e=nt[t];c.packetInterval=e.interval,c.calculationMode=null;let s=u.querySelector(".slider-labels span:last-child");(window.TEST_CONFIG?.platform||"web")==="self"&&e.duration===1/0?(g.disabled=!1,g.max="3660",g.value="3660",c.duration=1/0,c.packetCount=1/0,O.textContent="Unlimited",L(),s&&(s.textContent="Unlimited")):(g.disabled=!1,g.max="300",g.value=e.duration,c.duration=e.duration,c.packetCount=e.count,O.textContent=e.duration>=60?`${Math.floor(e.duration/60)}m ${e.duration%60}s`.replace(" 0s",""):`${e.duration}s`,L(),s&&(s.textContent="5m")),F.value=e.interval,dt(),L(),Q(),I()})});u.querySelectorAll('input[name="direction"]').forEach(n=>{n.addEventListener("change",()=>{c.direction=n.value,I()})});u.querySelectorAll('input[name="calculation-mode"]').forEach(n=>{n.addEventListener("change",()=>{c.calculationMode=n.value,I()})});g.addEventListener("input",n=>{let t=parseInt(n.target.value),e=u.querySelector(".slider-labels span:last-child");if((window.TEST_CONFIG?.platform||"web")==="self"&&t>=3660)c.duration=1/0,c.packetCount=1/0,O.textContent="Unlimited",L(),e&&(e.textContent="Unlimited");else{if(c.duration=t,c.packetInterval>0){let o=Math.ceil(t*1e3/c.packetInterval);c.packetCount=o,L()}if(O.textContent=t>=60?`${Math.floor(t/60)}m ${t%60}s`.replace(" 0s",""):`${t}s`,e&&parseInt(g.max)<=300)e.textContent="5m";else if(e&&parseInt(g.max)>300){let o=parseInt(g.max);o>=3600&&(e.textContent=`${Math.floor(o/60)}m`)}}Q(),I()});F.addEventListener("input",n=>{c.autoInterval&&(c.autoInterval=!1,N&&(N.checked=!1));let t=window.TEST_CONFIG?.platform||"web",e=k.validatePacketInterval(n.target.value,t);Z("packet-interval",e),J()});F.addEventListener("change",n=>{let t=window.TEST_CONFIG?.platform||"web",e=k.validatePacketInterval(n.target.value,t);if(e.valid){if(c.packetInterval=e.value,n.target.value=e.value,c.duration!==1/0){let s=Math.ceil(c.duration*1e3/e.value);c.packetCount=s,L()}I()}else n.target.value=c.packetInterval});u.querySelectorAll(".size-preset-btn").forEach(n=>{n.addEventListener("click",()=>{let t=parseInt(n.dataset.size);c.packetSize=t,G.value=t,u.querySelectorAll(".size-preset-btn").forEach(s=>s.classList.remove("active")),n.classList.add("active"),Z("packet-size",{valid:!0}),J(),I()})});G.addEventListener("input",n=>{let t=window.TEST_CONFIG?.platform||"web",e=k.validatePacketSize(n.target.value,t);Z("packet-size",e),J()});G.addEventListener("change",n=>{let t=window.TEST_CONFIG?.platform||"web",e=k.validatePacketSize(n.target.value,t);e.valid?(c.packetSize=e.value,n.target.value=e.value,u.querySelectorAll(".size-preset-btn").forEach(s=>s.classList.remove("active")),I()):n.target.value=c.packetSize});N.addEventListener("change",n=>{if(c.autoInterval=n.target.checked,n.target.checked){let t=nt[c.preset].interval;c.packetInterval=t,F.value=t,Z("packet-interval",{valid:!0}),J()}I()});U.addEventListener("change",n=>{c.showNetworkMetrics=n.target.checked,T.setShowNetworkMetrics(n.target.checked),I()});function Lt(n,t,e=365){let s=new Date(Date.now()+e*864e5).toUTCString();document.cookie=`${n}=${t}; expires=${s}; path=/; SameSite=Lax`}function It(n){try{let t=document.cookie.split("; ").reduce((e,s)=>{let i=s.split("=");return i[0]===n?decodeURIComponent(i[1]):e},"");return t&&typeof t=="string"?t:""}catch{return""}}function I(){try{let n={...c,duration:c.duration===1/0?"Infinity":c.duration,packetCount:c.packetCount===1/0?"Infinity":c.packetCount};localStorage.setItem("testSettings",JSON.stringify(n))}catch{M.showToast("Settings could not be saved. Your changes may be lost.","warning")}}function At(){try{let n=localStorage.getItem("testSettings");if(n){let t=JSON.parse(n);if(!t.version||t.version<st)return localStorage.removeItem("testSettings"),null;t.duration==="Infinity"&&(t.duration=1/0),t.packetCount==="Infinity"&&(t.packetCount=1/0);let e=window.TEST_CONFIG?.platform||"web";return k.validateSettings(t,e).valid?t:(localStorage.removeItem("testSettings"),null)}}catch{M.showToast("Could not load saved settings. Using defaults.","warning")}return null}function kt(n){let t={...n},e=window.TEST_CONFIG?.platform||"web",s=nt.stress;e==="web"&&(t.duration===1/0||t.duration>300)&&(t.duration=300),e==="web"&&(t.packetCount===1/0||t.packetCount>6e3)&&(t.packetCount=6e3);let i=e==="web"?1500:9e3;return t.packetSize&&t.packetSize>i&&(t.packetSize=i),t.preset==="stress"&&e==="web"&&((t.duration>300||t.duration===1/0)&&(t.duration=300),(t.packetCount>6e3||t.packetCount===1/0)&&(t.packetCount=6e3)),t}function xt(){let n=At();if(!n){u.setAttribute("data-preset",c.preset),u.querySelectorAll(".preset-btn").forEach(i=>{i.classList.toggle("active",i.dataset.preset===c.preset)});return}let t=kt(n);Object.assign(c,t),u.setAttribute("data-preset",c.preset),u.querySelectorAll(".preset-btn").forEach(i=>{i.classList.toggle("active",i.dataset.preset===c.preset)}),u.querySelectorAll('input[name="direction"]').forEach(i=>{i.checked=i.value===c.direction});let e=u.querySelector(".slider-labels span:last-child"),s=window.TEST_CONFIG?.platform||"web";if(c.duration===1/0?(g.max="3660",g.value="3660",O.textContent="Unlimited",e&&(e.textContent="Unlimited")):(s==="self"&&c.duration>300?g.max="3660":g.max="300",g.value=Math.min(c.duration,parseInt(g.max)),O.textContent=c.duration>=60?`${Math.floor(c.duration/60)}m ${c.duration%60}s`.replace(" 0s",""):`${c.duration}s`,e&&(e.textContent=g.max==="3660"?"60m":"5m")),F.value=c.packetInterval,N&&(N.checked=c.autoInterval!==void 0?c.autoInterval:!0),U){let i=c.showNetworkMetrics!==void 0?c.showNetworkMetrics:!1;U.checked=i,T.setShowNetworkMetrics(i)}L(),G.value=c.packetSize,u.querySelectorAll(".size-preset-btn").forEach(i=>{i.classList.toggle("active",parseInt(i.dataset.size)===c.packetSize)}),c.calculationMode&&u.querySelectorAll('input[name="calculation-mode"]').forEach(i=>{i.checked=i.value===c.calculationMode}),Q()}function Rt(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function ct(n,t=!1){document.documentElement.setAttribute("data-theme",n),n==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark"),et&&(et.textContent=n==="light"?"Dark":"Light"),t||window.dispatchEvent(new CustomEvent("themeChanged",{detail:{theme:n}}))}function Et(n){return new URLSearchParams(window.location.search).get(n)}function Pt(){let n=Et("mode"),t;n==="dark"||n==="light"?t=n:t=It("mode")||Rt(),ct(t)}et.addEventListener("click",()=>{let t=document.documentElement.getAttribute("data-theme")==="dark"?"light":"dark";ct(t),Lt("mode",t)});window.addEventListener("themeChanged",n=>{let t=n.detail.theme;document.documentElement.getAttribute("data-theme")!==t&&ct(t,!0)});ut.addEventListener("click",rt);gt.addEventListener("click",Tt);vt.addEventListener("click",at);St.addEventListener("click",()=>{at(),rt()});wt.addEventListener("click",()=>{if(confirm("Reset all settings to defaults?")){localStorage.removeItem("testSettings"),c={version:st,preset:"default",duration:30,direction:"both",packetInterval:15.625,packetCount:1920,packetSize:100,autoInterval:!0,showNetworkMetrics:!1,calculationMode:null,realtimeWindow:10},u.setAttribute("data-preset","default"),u.querySelectorAll(".preset-btn").forEach(t=>{t.classList.toggle("active",t.dataset.preset==="default")}),u.querySelectorAll('input[name="direction"]').forEach(t=>{t.checked=t.value==="both"}),g.max="300",g.value="30",O.textContent="30s";let n=u.querySelector(".slider-labels span:last-child");n&&(n.textContent="5m"),F.value="15.625",N&&(N.checked=!0),L(),G.value="100",document.querySelectorAll(".size-preset-btn").forEach(t=>t.classList.remove("active")),U&&(U.checked=!1,T.setShowNetworkMetrics(!1)),document.querySelectorAll('input[name="calculation-mode"]').forEach(t=>{t.checked=!1}),Q()}});j.addEventListener("click",n=>{n.target===j&&at()});function Mt(n){if((window.TEST_CONFIG?.platform||"web")!=="web")return;let e=new URL("https://openpacketloss.com/results");e.searchParams.set("sent",n.sent),e.searchParams.set("received",n.received),e.searchParams.set("lost",n.lost),e.searchParams.set("loss",n.currentLoss.toFixed(2)),e.searchParams.set("jitter",n.jitter.toFixed(2)),e.searchParams.set("rtt",(n.rtt||0).toFixed(2)),e.searchParams.set("c2s_loss",(n.c2sLossPercent||0).toFixed(2)),e.searchParams.set("s2c_loss",(n.s2cLossPercent||0).toFixed(2)),e.searchParams.set("ooo",(n.outOfOrderPercent||0).toFixed(2)),setTimeout(()=>{window.location.href=e.toString()},3500)}v.on("test:completed",n=>{it=!0,b=n,(window.TEST_CONFIG?.platform||"web")==="self"&&u.classList.add("clickable"),Mt(n),setTimeout(()=>{},2e3)});window.addEventListener("beforeunload",()=>{v.cleanup()});function Ot(){(window.TEST_CONFIG?.platform||"web")==="self"&&u.addEventListener("click",t=>{if(!it)return;if(!t.target.closest("button, input, select, textarea, .modal-content, a, label, details, summary")){let s=new URL("https://openpacketloss.com/results/");b&&(s.searchParams.set("sent",b.sent),s.searchParams.set("received",b.received),s.searchParams.set("lost",b.lost),s.searchParams.set("loss",b.currentLoss.toFixed(2)),s.searchParams.set("jitter",b.jitter.toFixed(2)),s.searchParams.set("rtt",(b.rtt||0).toFixed(2)),s.searchParams.set("c2s_loss",(b.c2sLossPercent||0).toFixed(2)),s.searchParams.set("s2c_loss",(b.s2cLossPercent||0).toFixed(2)),s.searchParams.set("ooo",(b.outOfOrderPercent||0).toFixed(2))),window.open(s.toString(),"_blank")}})}Pt();xt();Ot();dt();L();if(window.TEST_CONFIG&&window.TEST_CONFIG.platform==="web"){let n=document.getElementById("packet-size");n&&(n.max="1500",parseInt(n.value)>1500&&(n.value="1500",c.packetSize=1500));let t=document.querySelector('.size-preset-btn[data-size="9000"]');t&&(t.style.display="none")}