@16x/webphone-sdk 3.2.5 → 3.2.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.2.6 - 2026-10-07
4
+
5
+ - Fix the multi-egress-office dial delay at its root: a dead STUN/TURN-UDP endpoint dragged JsSIP's
6
+ non-trickle ICE gathering through the browser retransmission tables (~40s measured with both
7
+ STUN binding and TURN allocation retrying in parallel) even though a usable TURN-TCP relay
8
+ candidate was already available at ~150ms.
9
+ - Attach an ICE gathering timeout to every outgoing/incoming session: call JsSIP's built-in
10
+ `icecandidate` `ready()` shortcut as soon as an srflx/relay candidate arrives, and force-close
11
+ the local SDP with the most recent ready callback after 3s so INVITE/200 OK never waits for
12
+ dead servers (mirrors ccbar BUG-013).
13
+ - Add URL-level ICE server probing after each successful registration (`iceServerQuality`):
14
+ every stun/turn/turns URL is probed with its own PeerConnection (credentials inherited);
15
+ URLs that yield no srflx/relay are dropped from the dial-time pcConfig. Healthy networks keep
16
+ the original list unchanged (zero behavior change); when every URL fails the list becomes
17
+ host-only so the INVITE still goes out immediately. Attribution must be per-URL because relay
18
+ candidates always report `protocol: udp` (it describes the relayed port, not the client
19
+ transport) and mixing dead and live URLs in one probe drags them all to timeout.
20
+ - `getDiagnostics()` now includes an `ice.probe` summary (runs / lastMs / udpOk / tcpRelayOk /
21
+ convergedServers) from the tab-local adapter (absent in SharedWorker mode).
22
+ - Correct the `warmIceCandidatePool` comment: `iceCandidatePoolSize` is per-RTCPeerConnection,
23
+ so warmed candidates cannot transfer to call connections — the timeout + convergence above
24
+ are the actual safeguards. `answer()` now also passes `iceCandidatePoolSize`.
25
+
3
26
  ## 3.2.5 - 2026-10-06
4
27
 
5
28
  - Keep held-call state synchronized with the call controls so resume, hangup, and DTMF remain available after a hold transition.
@@ -1 +1 @@
1
- const e=require("./redact-Bq26pObO.cjs");var t=`3.2.5`,n=class{capacity;entries=[];constructor(e){if(this.capacity=e,!Number.isInteger(e)||e<=0)throw RangeError(`RingBuffer capacity must be a positive integer`)}push(e){this.entries.length===this.capacity&&this.entries.shift(),this.entries.push(e)}toArray(){return[...this.entries]}},r=`@16x/webphone-sdk`,i=200,a=class{dependencies;logs=new n(i);now;constructor(e){this.dependencies=e,this.now=e.now??(()=>new Date)}log(t,n,r){this.logs.push({timestamp:this.now().toISOString(),level:t,message:e.t(n),...r===void 0?{}:{context:e.n(r)}})}async getReport(n={}){let i=n.includeRecentLogs!==!1,a=n.includeDeviceNames===!0,u=n.maskPhoneNumbers!==!1,d=this.dependencies.getSnapshot(),f=this.dependencies.getPermission===void 0?d.permission:await o(this.dependencies.getPermission,d.permission),p=a?await s(this.dependencies.getDeviceNames):void 0,m=l(c(this.dependencies.getUserAgent)),h={generatedAt:this.now().toISOString(),sdk:{name:r,version:t},browser:m.browser,os:m.os,...d,permission:f,recentLogs:i?this.logs.toArray():[],...p===void 0?{}:{deviceNames:p}};return e.n(h,{maskPhoneNumbers:u})}};async function o(e,t){try{return await e()}catch{return t}}async function s(e){if(e===void 0)return[];try{return[...await e()]}catch{return[]}}function c(e){if(e!==void 0)try{return e()}catch{return``}return typeof navigator>`u`?``:navigator.userAgent}function l(e){return{browser:u(e),os:d(e)}}function u(e){for(let[t,n]of[[`Edge`,/Edg\/([\d.]+)/],[`Chrome`,/(?:Chrome|CriOS)\/([\d.]+)/],[`Firefox`,/(?:Firefox|FxiOS)\/([\d.]+)/],[`Safari`,/Version\/([\d.]+).*Safari/]]){let r=e.match(n);if(r?.[1]!==void 0)return{name:t,version:r[1]}}return{name:`unknown`,version:``}}function d(e){let t=e.match(/(?:iPhone|iPad).*OS ([\d_]+)/);if(t?.[1]!==void 0)return{name:`iOS`,version:t[1].replaceAll(`_`,`.`)};let n=e.match(/Android ([\d.]+)/);if(n?.[1]!==void 0)return{name:`Android`,version:n[1]};let r=e.match(/Windows NT ([\d.]+)/);if(r?.[1]!==void 0)return{name:`Windows`,version:r[1]};let i=e.match(/Mac OS X ([\d_]+)/);return i?.[1]===void 0?/Linux/.test(e)?{name:`Linux`,version:``}:{name:`unknown`,version:``}:{name:`macOS`,version:i[1].replaceAll(`_`,`.`)}}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return a}});
1
+ const e=require("./redact-Bq26pObO.cjs");var t=`3.2.6`,n=class{capacity;entries=[];constructor(e){if(this.capacity=e,!Number.isInteger(e)||e<=0)throw RangeError(`RingBuffer capacity must be a positive integer`)}push(e){this.entries.length===this.capacity&&this.entries.shift(),this.entries.push(e)}toArray(){return[...this.entries]}},r=`@16x/webphone-sdk`,i=200,a=class{dependencies;logs=new n(i);now;constructor(e){this.dependencies=e,this.now=e.now??(()=>new Date)}log(t,n,r){this.logs.push({timestamp:this.now().toISOString(),level:t,message:e.t(n),...r===void 0?{}:{context:e.n(r)}})}async getReport(n={}){let i=n.includeRecentLogs!==!1,a=n.includeDeviceNames===!0,u=n.maskPhoneNumbers!==!1,d=this.dependencies.getSnapshot(),f=this.dependencies.getPermission===void 0?d.permission:await o(this.dependencies.getPermission,d.permission),p=a?await s(this.dependencies.getDeviceNames):void 0,m=l(c(this.dependencies.getUserAgent)),h={generatedAt:this.now().toISOString(),sdk:{name:r,version:t},browser:m.browser,os:m.os,...d,permission:f,recentLogs:i?this.logs.toArray():[],...p===void 0?{}:{deviceNames:p}};return e.n(h,{maskPhoneNumbers:u})}};async function o(e,t){try{return await e()}catch{return t}}async function s(e){if(e===void 0)return[];try{return[...await e()]}catch{return[]}}function c(e){if(e!==void 0)try{return e()}catch{return``}return typeof navigator>`u`?``:navigator.userAgent}function l(e){return{browser:u(e),os:d(e)}}function u(e){for(let[t,n]of[[`Edge`,/Edg\/([\d.]+)/],[`Chrome`,/(?:Chrome|CriOS)\/([\d.]+)/],[`Firefox`,/(?:Firefox|FxiOS)\/([\d.]+)/],[`Safari`,/Version\/([\d.]+).*Safari/]]){let r=e.match(n);if(r?.[1]!==void 0)return{name:t,version:r[1]}}return{name:`unknown`,version:``}}function d(e){let t=e.match(/(?:iPhone|iPad).*OS ([\d_]+)/);if(t?.[1]!==void 0)return{name:`iOS`,version:t[1].replaceAll(`_`,`.`)};let n=e.match(/Android ([\d.]+)/);if(n?.[1]!==void 0)return{name:`Android`,version:n[1]};let r=e.match(/Windows NT ([\d.]+)/);if(r?.[1]!==void 0)return{name:`Windows`,version:r[1]};let i=e.match(/Mac OS X ([\d_]+)/);return i?.[1]===void 0?/Linux/.test(e)?{name:`Linux`,version:``}:{name:`unknown`,version:``}:{name:`macOS`,version:i[1].replaceAll(`_`,`.`)}}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return a}});
@@ -1,6 +1,6 @@
1
1
  import { n as e, t } from "./redact-DB3CMbsK.js";
2
2
  //#region src/version.ts
3
- var n = "3.2.5", r = class {
3
+ var n = "3.2.6", r = class {
4
4
  capacity;
5
5
  entries = [];
6
6
  constructor(e) {
@@ -0,0 +1 @@
1
+ const e=require("./SharedWorkerProtocol-C-4KpECM.cjs");require("./createJsSipFactory-B1ORbJiP.cjs");function t(e){let t=new Map;return{peerConnection:{getSenders:()=>e.getSenders().map(n=>{let r=t.get(n);if(r!==void 0)return r;let i={get track(){return n.track},get senderKind(){let t=n.track;if(t!==null)return t.kind===`audio`||t.kind===`video`?t.kind:void 0;let r=e.getTransceivers().find(e=>e.sender===n)?.receiver.track;if(r?.kind===`audio`||r?.kind===`video`)return r.kind;let i=n.getParameters().codecs?.find(e=>typeof e.mimeType==`string`);if(i?.mimeType?.toLowerCase().startsWith(`video/`))return`video`;if(i?.mimeType?.toLowerCase().startsWith(`audio/`))return`audio`},replaceTrack:e=>n.replaceTrack(e)};return t.set(n,i),i}),close:()=>{e.connectionState!==`closed`&&e.close()}},trackSource:{onTrack(t){let n=e=>{let n=e.track;t({streams:[...e.streams],track:n})};return e.addEventListener(`track`,n),()=>e.removeEventListener(`track`,n)}}}}var n=2e3;function r(e){if(!e)return``;let t=String(e.type??``);return t===``?/ typ (\w+)/.exec(String(e.candidate??``))?.[1]??``:t}function i(e,t={}){let i=Number.isFinite(t.timeoutMs)&&t.timeoutMs>0?t.timeoutMs:n;if(typeof RTCPeerConnection>`u`||!Array.isArray(e))return Promise.resolve([]);let a=[];for(let t of e){if(!t)continue;let e=Array.isArray(t.urls)?t.urls:[t.urls];for(let n of e){let e=String(n??``).trim();/^(stun|turn|turns):/i.test(e)&&a.push({url:e,entry:t})}}return Promise.all(a.map(e=>new Promise(t=>{let n=!1,a,o=r=>{if(!n){n=!0,clearTimeout(s);try{a?.close()}catch{}t({url:e.url,ok:r,tcp:/transport=tcp/i.test(e.url)||/^turns:/i.test(e.url)})}},s=setTimeout(()=>o(!1),i);try{let t={urls:[e.url]};e.entry.username&&(t.username=e.entry.username),e.entry.credential&&(t.credential=e.entry.credential),a=new RTCPeerConnection({iceServers:[t]}),a.createDataChannel(`webphone-ice-probe`),a.onicecandidate=e=>{if(!e||!e.candidate)return;let t=r(e.candidate);(t===`srflx`||t===`relay`)&&o(!0)},a.createOffer().then(e=>a?.setLocalDescription(e)).catch(()=>o(!1))}catch{o(!1)}})))}function a(e,t){let n=new Map;for(let e of t)e&&e.url&&n.set(String(e.url),e.ok===!0);let r=[];for(let t of e??[]){if(!t)continue;let e=(Array.isArray(t.urls)?t.urls:[t.urls]).filter(Boolean).filter(e=>n.get(String(e))===!0);if(e.length===0)continue;let i={...t,urls:e};r.push(i)}return r}var o=class{convergedList;lastResults=[];snapshotState={runs:0,lastMs:null,udpOk:!1,tcpRelayOk:!1,convergedServers:null,unchanged:null};running=!1;seq=0;converged(e){return this.convergedList??e}snapshot(){return this.snapshotState}reset(){this.seq++,this.running=!1,this.convergedList=void 0,this.lastResults=[],this.snapshotState={runs:0,lastMs:null,udpOk:!1,tcpRelayOk:!1,convergedServers:null,unchanged:null}}assess(e,t={}){if(e===void 0||e.length===0)return;let n=e.map(e=>({...e,urls:Array.isArray(e.urls)?[...e.urls]:[e.urls]}));if(typeof RTCPeerConnection>`u`||!n.some(e=>(Array.isArray(e.urls)?e.urls:[e.urls]).some(e=>/^(stun|turn|turns):/i.test(String(e??``)))))return;let r=++this.seq;this.running=!0;let o=Date.now();i(n,t).then(e=>{if(r!==this.seq)return;this.lastResults=e;let t=n,i=a(t,e);this.convergedList=i,this.snapshotState={runs:this.snapshotState.runs+1,lastMs:Date.now()-o,udpOk:e.some(e=>e.ok&&!e.tcp),tcpRelayOk:e.some(e=>e.ok&&e.tcp),convergedServers:i.length,unchanged:JSON.stringify(i)===JSON.stringify(t)}}).catch(()=>{this.seq}).finally(()=>{r===this.seq&&(this.running=!1)})}isRunning(){return this.running}},s=3e3;function c(e){if(!e)return``;let t=String(e.type??``);return t===``?/ typ (\w+)/.exec(String(e.candidate??``))?.[1]??``:t}function l(e,t={}){if(typeof e.on!=`function`)return()=>void 0;let n=Number.isFinite(t.timeoutMs)&&t.timeoutMs>0?t.timeoutMs:s,r=!1,i,a=()=>{if(!r){r=!0,clearTimeout(l);try{i?.()}catch{}}},o=e=>{let t=e;if(!t||typeof t.ready!=`function`)return;i=t.ready;let n=c(t.candidate);(n===`srflx`||n===`relay`)&&a()};e.on(`icecandidate`,o);let l=setTimeout(a,n);return()=>{clearTimeout(l);try{e.off?.(`icecandidate`,o)}catch{}}}var u=300,d=25,f=2,p=``,m=120;function h(e){let t=Math.floor(Number(e.sip.registerExpires));return Number.isFinite(t)&&t>0?t:u}var g=class{factory;transportBaseUrl;keepaliveSeconds;agentTerminal;sessionTimers;ua;uaListeners;incomingHandlers;iceServers;pendingConnection;disposed=!1;warmPc;keepaliveTimer;keepaliveBusy=!1;registerExpiresSeconds=0;connectionHandlers=new Set;iceQuality=new o;constructor(e,t,n=d,r=p,i=!1){this.factory=e,this.transportBaseUrl=t,this.keepaliveSeconds=n,this.agentTerminal=r,this.sessionTimers=i}terminalHeaders(){return this.agentTerminal===``?[]:[`X-Agent-Terminal: ${this.agentTerminal}`]}warmIceCandidatePool(){if(this.closeWarmPc(),typeof RTCPeerConnection<`u`&&this.iceServers!==void 0)try{this.warmPc=new RTCPeerConnection({iceServers:[...this.iceServers],iceCandidatePoolSize:f})}catch{this.warmPc=void 0}}closeWarmPc(){try{this.warmPc?.close()}catch{}this.warmPc=void 0}keepaliveTick(e){if(this.isCurrentUa(e)&&!this.keepaliveBusy)try{if(typeof e.isConnected==`function`&&e.isConnected()===!1||typeof e.register!=`function`)return;this.keepaliveBusy=!0,e.register({extraHeaders:this.terminalHeaders()})}catch{}finally{this.keepaliveBusy=!1}}startKeepalive(e,t){if(this.stopKeepalive(),!Number.isFinite(this.keepaliveSeconds)||this.keepaliveSeconds<=0||t>0&&t<=this.keepaliveSeconds)return;let n=Math.max(10,Math.floor(this.keepaliveSeconds))*1e3;this.keepaliveTimer=setInterval(()=>this.keepaliveTick(e),n)}stopKeepalive(){this.keepaliveTimer!==void 0&&clearInterval(this.keepaliveTimer),this.keepaliveTimer=void 0}connect(t){this.assertNotDisposed(),this.disconnect(this.registrationError(`SIP connection replaced`));try{let e=this.factory.createUa({sockets:b(this.factory.createSocket(this.withoutTicket(this.resolveWssUrl(t.transport.wssUrl)),t.transport.ticket)),uri:t.sip.uri,password:t.sip.registerTicket,registrar_server:t.sip.registrar,register:!0,register_expires:h(t),session_timers:this.sessionTimers,...this.agentTerminal===``?{}:{user_agent:this.agentTerminal}});return this.ua=e,this.registerExpiresSeconds=h(t),this.iceServers=t.iceServers,this.uaListeners=[],this.startRegistration(e)}catch(t){let n=t instanceof e.i&&t.code===`CONFIG_INVALID`?t:this.transportError(t);return this.disconnect(n),Promise.reject(n)}}disconnect(e=this.registrationError(`SIP connection disconnected`)){this.stopKeepalive(),this.closeWarmPc(),this.iceQuality.reset();let t=this.pendingConnection;this.pendingConnection=void 0,this.ua!==void 0&&(this.removeUaListeners(this.ua),this.ua.stop()),this.ua=void 0,this.uaListeners=void 0,this.iceServers=void 0,t?.reject(e)}dial(t,n){let r=this.requireUa();if(t.trim()===``)throw new e.i(`CALL_INVALID_DESTINATION`,`call`,!1);try{let e=n===void 0?{mediaConstraints:{audio:!0,video:!1},sessionTimersExpires:m,pcConfig:{iceServers:this.dialIceServers(),iceCandidatePoolSize:f}}:{mediaConstraints:{audio:!0,video:!1},sessionTimersExpires:m,pcConfig:{iceServers:[...n.iceServers],iceCandidatePoolSize:f},extraHeaders:[...this.terminalHeaders(),...n.extraHeaders,...n.type===`extension`?[`X-XCall-Destination-Type: extension`]:[]]};return new _(r.call(t,e))}catch(t){throw new e.i(`CALL_OPERATION_NOT_ALLOWED`,`call`,!1,{cause:t})}}onIncoming(e){this.assertNotDisposed();let t=this.incomingHandlers??new Set;return this.incomingHandlers=t,t.add(e),()=>t.delete(e)}onConnectionStateChanged(e){return this.disposed?()=>void 0:(this.connectionHandlers.add(e),()=>this.connectionHandlers.delete(e))}dispose(){this.disposed||(this.disposed=!0,this.disconnect(),this.incomingHandlers=void 0,this.connectionHandlers.clear())}dialIceServers(){let e=this.iceQuality.converged(this.iceServers);return e===void 0?void 0:[...e]}iceQualitySnapshot(){return this.iceQuality.snapshot()}getResourceCounts(){return{listeners:(this.uaListeners?.length??0)+(this.incomingHandlers?.size??0)+this.connectionHandlers.size,webSockets:this.ua===void 0?0:1}}handleIncoming(e){if(!v(e))return;let t=e.session;if(t===void 0)return;let n=new _(t);this.incomingHandlers?.forEach(e=>e(n))}startRegistration(e){let t=!0,n=!1,r=new Promise((t,n)=>{this.pendingConnection={ua:e,resolve:t,reject:n}});this.listen(e,`registered`,()=>{if(this.isCurrentUa(e)){if(t){n=!0;return}this.resolveConnection(e)}}),this.listen(e,`registrationFailed`,t=>{if(!this.isCurrentUa(e))return;let n=this.registrationError(this.registrationFailureCause(t));this.connectionHandlers.forEach(e=>e(`failed`,n)),this.disconnect(n)}),this.listen(e,`disconnected`,t=>{if(!this.isCurrentUa(e))return;let n=this.transportError(t);this.connectionHandlers.forEach(e=>e(`disconnected`,n)),this.disconnect(n)}),this.listen(e,`newRTCSession`,t=>{this.isCurrentUa(e)&&this.handleIncoming(t)});try{e.start()}catch(e){let t=this.transportError(e);this.connectionHandlers.forEach(e=>e(`failed`,t)),this.disconnect(t)}finally{t=!1}return n&&this.resolveConnection(e),r}listen(e,t,n){e.on(t,n),this.uaListeners?.push({event:t,handler:n})}removeUaListeners(e){this.uaListeners?.forEach(({event:t,handler:n})=>e.off(t,n))}isCurrentUa(e){return this.ua===e}resolveConnection(e){let t=this.pendingConnection;t?.ua===e&&(this.pendingConnection=void 0,t.resolve(),this.warmIceCandidatePool(),this.iceQuality.assess(this.iceServers),this.startKeepalive(e,this.registerExpiresSeconds),this.connectionHandlers.forEach(e=>e(`registered`)))}resolveWssUrl(e){let t=this.transportBaseUrl?.trim()===``||this.transportBaseUrl===void 0?typeof location>`u`?`http://localhost`:location.origin:this.transportBaseUrl,n=e.startsWith(`/`)?new URL(e,t):new URL(e);return n.protocol===`http:`&&(n.protocol=`ws:`),n.protocol===`https:`&&(n.protocol=`wss:`),this.assertAllowedWssUrl(n),n.toString()}assertAllowedWssUrl(t){if(t.protocol!==`wss:`&&!(t.protocol===`ws:`&&x(t.hostname)))throw new e.i(`CONFIG_INVALID`,`configuration`,!1,{cause:t.origin})}withoutTicket(e){let t=new URL(e);return t.searchParams.delete(`ticket`),t.toString()}requireUa(){if(this.assertNotDisposed(),this.ua===void 0)throw new e.i(`CALL_NOT_CONNECTED`,`call`,!1);return this.ua}assertNotDisposed(){if(this.disposed)throw new e.i(`SDK_ALREADY_DISPOSED`,`internal`,!1)}registrationError(t){return new e.i(`REGISTRATION_FAILED`,`registration`,!0,{cause:t})}transportError(t){return new e.i(`SIP_WS_UNAVAILABLE`,`registration`,!0,{cause:t})}registrationFailureCause(e){return typeof e==`object`&&e&&`cause`in e?e.cause:e}},_=class{session;pendingHoldReject;mediaRegistration;mediaConnection;mediaHandlers=new Set;mediaWatchStarted=!1;sessionUnsubscribeFromPeerConnection;detachIceGatheringTimeout;constructor(e){this.session=e,this.detachIceGatheringTimeout=l(e)}get id(){return this.session.id}get from(){return this.session.from??S(this.session.remote_identity)}getMediaRegistration(){let e=this.session.connection;if(e!=null)return this.mediaRegistration!==void 0&&this.mediaConnection===e?this.mediaRegistration:(this.mediaConnection=e,this.mediaRegistration=t(e),this.mediaRegistration)}answer(e){this.session.answer({mediaConstraints:e.mediaConstraints,pcConfig:{iceServers:[...e.pcConfig.iceServers],iceCandidatePoolSize:f}})}terminate(){this.detachIceGatheringTimeout?.(),this.detachIceGatheringTimeout=void 0,this.pendingHoldReject?.(Error(`SIP session terminated`)),this.session.terminate()}hold(){return this.changeHoldState(`hold`)}unhold(){return this.changeHoldState(`unhold`)}mute(){this.session.mute()}unmute(){this.session.unmute()}sendDtmf(e){this.session.sendDTMF(e)}refer(e){this.session.refer(e)}on(e,t){return e===`mediaConnected`?(this.mediaHandlers.add(t),this.watchMediaConnection(),()=>{this.mediaHandlers.delete(t),this.mediaHandlers.size===0&&(this.sessionUnsubscribeFromPeerConnection?.(),this.sessionUnsubscribeFromPeerConnection=void 0)}):(this.session.on(e,t),()=>this.session.off(e,t))}watchMediaConnection(){if(this.mediaWatchStarted)return;this.mediaWatchStarted=!0;let e=!1,t=t=>{if(t==null)return;let n=()=>{if(e)return;let n=t.connectionState||t.iceConnectionState;if(n===`connected`){e=!0;for(let e of this.mediaHandlers)try{e({state:n})}catch{}}};t.addEventListener(`connectionstatechange`,n),n()},n=this.session.connection;if(n!=null){t(n);return}let r=e=>{t(e?.peerconnection??void 0)};this.session.on(`peerconnection`,r),this.sessionUnsubscribeFromPeerConnection=()=>this.session.off(`peerconnection`,r)}changeHoldState(t){return new Promise((n,r)=>{let i=!1,a=e=>{c(e)},o=()=>{try{this.session.off(`ended`,a)}catch{}try{this.session.off(`failed`,a)}catch{}this.pendingHoldReject===c&&(this.pendingHoldReject=void 0)},s=()=>{i||(i=!0,o(),n())},c=t=>{i||(i=!0,o(),r(new e.i(`CALL_OPERATION_NOT_ALLOWED`,`call`,!1,{cause:t})))};try{this.session.on(`ended`,a),this.session.on(`failed`,a)}catch(e){c(e);return}this.pendingHoldReject=c;try{this.session[t](void 0,s)||c({reason:`jssip_operation_rejected`,operation:t})}catch(e){c(e)}})}};function v(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.originator===`remote`&&y(t.session)}function y(e){return typeof e==`object`&&!!e&&`id`in e}function b(e){return typeof e==`object`&&e&&(e.via_transport=`WSS`),e}function x(e){let t=e.replace(/^\[|\]$/g,``).toLowerCase();return t===`localhost`||t===`127.0.0.1`||t===`::1`||t===`0:0:0:0:0:0:0:1`}function S(e){let t=e?.uri;if(t==null)return;if(typeof t.user==`string`&&t.user!==``)return t.user;let n=t.toString();return n.match(/^sips?:([^@;>]+)/i)?.[1]??(n===``?void 0:n)}exports.JsSipAdapter=g;
@@ -41,13 +41,169 @@ function t(e) {
41
41
  };
42
42
  }
43
43
  //#endregion
44
+ //#region src/sip/iceServerQuality.ts
45
+ var n = 2e3;
46
+ function r(e) {
47
+ if (!e) return "";
48
+ let t = String(e.type ?? "");
49
+ return t === "" ? / typ (\w+)/.exec(String(e.candidate ?? ""))?.[1] ?? "" : t;
50
+ }
51
+ function i(e, t = {}) {
52
+ let i = Number.isFinite(t.timeoutMs) && t.timeoutMs > 0 ? t.timeoutMs : n;
53
+ if (typeof RTCPeerConnection > "u" || !Array.isArray(e)) return Promise.resolve([]);
54
+ let a = [];
55
+ for (let t of e) {
56
+ if (!t) continue;
57
+ let e = Array.isArray(t.urls) ? t.urls : [t.urls];
58
+ for (let n of e) {
59
+ let e = String(n ?? "").trim();
60
+ /^(stun|turn|turns):/i.test(e) && a.push({
61
+ url: e,
62
+ entry: t
63
+ });
64
+ }
65
+ }
66
+ return Promise.all(a.map((e) => new Promise((t) => {
67
+ let n = !1, a, o = (r) => {
68
+ if (!n) {
69
+ n = !0, clearTimeout(s);
70
+ try {
71
+ a?.close();
72
+ } catch {}
73
+ t({
74
+ url: e.url,
75
+ ok: r,
76
+ tcp: /transport=tcp/i.test(e.url) || /^turns:/i.test(e.url)
77
+ });
78
+ }
79
+ }, s = setTimeout(() => o(!1), i);
80
+ try {
81
+ let t = { urls: [e.url] };
82
+ e.entry.username && (t.username = e.entry.username), e.entry.credential && (t.credential = e.entry.credential), a = new RTCPeerConnection({ iceServers: [t] }), a.createDataChannel("webphone-ice-probe"), a.onicecandidate = (e) => {
83
+ if (!e || !e.candidate) return;
84
+ let t = r(e.candidate);
85
+ (t === "srflx" || t === "relay") && o(!0);
86
+ }, a.createOffer().then((e) => a?.setLocalDescription(e)).catch(() => o(!1));
87
+ } catch {
88
+ o(!1);
89
+ }
90
+ })));
91
+ }
92
+ function a(e, t) {
93
+ let n = /* @__PURE__ */ new Map();
94
+ for (let e of t) e && e.url && n.set(String(e.url), e.ok === !0);
95
+ let r = [];
96
+ for (let t of e ?? []) {
97
+ if (!t) continue;
98
+ let e = (Array.isArray(t.urls) ? t.urls : [t.urls]).filter(Boolean).filter((e) => n.get(String(e)) === !0);
99
+ if (e.length === 0) continue;
100
+ let i = {
101
+ ...t,
102
+ urls: e
103
+ };
104
+ r.push(i);
105
+ }
106
+ return r;
107
+ }
108
+ var o = class {
109
+ convergedList;
110
+ lastResults = [];
111
+ snapshotState = {
112
+ runs: 0,
113
+ lastMs: null,
114
+ udpOk: !1,
115
+ tcpRelayOk: !1,
116
+ convergedServers: null,
117
+ unchanged: null
118
+ };
119
+ running = !1;
120
+ seq = 0;
121
+ converged(e) {
122
+ return this.convergedList ?? e;
123
+ }
124
+ snapshot() {
125
+ return this.snapshotState;
126
+ }
127
+ reset() {
128
+ this.seq++, this.running = !1, this.convergedList = void 0, this.lastResults = [], this.snapshotState = {
129
+ runs: 0,
130
+ lastMs: null,
131
+ udpOk: !1,
132
+ tcpRelayOk: !1,
133
+ convergedServers: null,
134
+ unchanged: null
135
+ };
136
+ }
137
+ assess(e, t = {}) {
138
+ if (e === void 0 || e.length === 0) return;
139
+ let n = e.map((e) => ({
140
+ ...e,
141
+ urls: Array.isArray(e.urls) ? [...e.urls] : [e.urls]
142
+ }));
143
+ if (typeof RTCPeerConnection > "u" || !n.some((e) => (Array.isArray(e.urls) ? e.urls : [e.urls]).some((e) => /^(stun|turn|turns):/i.test(String(e ?? ""))))) return;
144
+ let r = ++this.seq;
145
+ this.running = !0;
146
+ let o = Date.now();
147
+ i(n, t).then((e) => {
148
+ if (r !== this.seq) return;
149
+ this.lastResults = e;
150
+ let t = n, i = a(t, e);
151
+ this.convergedList = i, this.snapshotState = {
152
+ runs: this.snapshotState.runs + 1,
153
+ lastMs: Date.now() - o,
154
+ udpOk: e.some((e) => e.ok && !e.tcp),
155
+ tcpRelayOk: e.some((e) => e.ok && e.tcp),
156
+ convergedServers: i.length,
157
+ unchanged: JSON.stringify(i) === JSON.stringify(t)
158
+ };
159
+ }).catch(() => {
160
+ this.seq;
161
+ }).finally(() => {
162
+ r === this.seq && (this.running = !1);
163
+ });
164
+ }
165
+ isRunning() {
166
+ return this.running;
167
+ }
168
+ }, s = 3e3;
169
+ function c(e) {
170
+ if (!e) return "";
171
+ let t = String(e.type ?? "");
172
+ return t === "" ? / typ (\w+)/.exec(String(e.candidate ?? ""))?.[1] ?? "" : t;
173
+ }
174
+ function l(e, t = {}) {
175
+ if (typeof e.on != "function") return () => void 0;
176
+ let n = Number.isFinite(t.timeoutMs) && t.timeoutMs > 0 ? t.timeoutMs : s, r = !1, i, a = () => {
177
+ if (!r) {
178
+ r = !0, clearTimeout(l);
179
+ try {
180
+ i?.();
181
+ } catch {}
182
+ }
183
+ }, o = (e) => {
184
+ let t = e;
185
+ if (!t || typeof t.ready != "function") return;
186
+ i = t.ready;
187
+ let n = c(t.candidate);
188
+ (n === "srflx" || n === "relay") && a();
189
+ };
190
+ e.on("icecandidate", o);
191
+ let l = setTimeout(a, n);
192
+ return () => {
193
+ clearTimeout(l);
194
+ try {
195
+ e.off?.("icecandidate", o);
196
+ } catch {}
197
+ };
198
+ }
199
+ //#endregion
44
200
  //#region src/sip/JsSipAdapter.ts
45
- var n = 300, r = 25, i = 2, a = "", o = 120;
46
- function s(e) {
201
+ var u = 300, d = 25, f = 2, p = "", m = 120;
202
+ function h(e) {
47
203
  let t = Math.floor(Number(e.sip.registerExpires));
48
- return Number.isFinite(t) && t > 0 ? t : n;
204
+ return Number.isFinite(t) && t > 0 ? t : u;
49
205
  }
50
- var c = class {
206
+ var g = class {
51
207
  factory;
52
208
  transportBaseUrl;
53
209
  keepaliveSeconds;
@@ -64,8 +220,9 @@ var c = class {
64
220
  keepaliveBusy = !1;
65
221
  registerExpiresSeconds = 0;
66
222
  connectionHandlers = /* @__PURE__ */ new Set();
67
- constructor(e, t, n = r, i = a, o = !1) {
68
- this.factory = e, this.transportBaseUrl = t, this.keepaliveSeconds = n, this.agentTerminal = i, this.sessionTimers = o;
223
+ iceQuality = new o();
224
+ constructor(e, t, n = d, r = p, i = !1) {
225
+ this.factory = e, this.transportBaseUrl = t, this.keepaliveSeconds = n, this.agentTerminal = r, this.sessionTimers = i;
69
226
  }
70
227
  terminalHeaders() {
71
228
  return this.agentTerminal === "" ? [] : [`X-Agent-Terminal: ${this.agentTerminal}`];
@@ -74,7 +231,7 @@ var c = class {
74
231
  if (this.closeWarmPc(), typeof RTCPeerConnection < "u" && this.iceServers !== void 0) try {
75
232
  this.warmPc = new RTCPeerConnection({
76
233
  iceServers: [...this.iceServers],
77
- iceCandidatePoolSize: i
234
+ iceCandidatePoolSize: f
78
235
  });
79
236
  } catch {
80
237
  this.warmPc = void 0;
@@ -106,23 +263,23 @@ var c = class {
106
263
  this.assertNotDisposed(), this.disconnect(this.registrationError("SIP connection replaced"));
107
264
  try {
108
265
  let e = this.factory.createUa({
109
- sockets: f(this.factory.createSocket(this.withoutTicket(this.resolveWssUrl(t.transport.wssUrl)), t.transport.ticket)),
266
+ sockets: b(this.factory.createSocket(this.withoutTicket(this.resolveWssUrl(t.transport.wssUrl)), t.transport.ticket)),
110
267
  uri: t.sip.uri,
111
268
  password: t.sip.registerTicket,
112
269
  registrar_server: t.sip.registrar,
113
270
  register: !0,
114
- register_expires: s(t),
271
+ register_expires: h(t),
115
272
  session_timers: this.sessionTimers,
116
273
  ...this.agentTerminal === "" ? {} : { user_agent: this.agentTerminal }
117
274
  });
118
- return this.ua = e, this.registerExpiresSeconds = s(t), this.iceServers = t.iceServers, this.uaListeners = [], this.startRegistration(e);
275
+ return this.ua = e, this.registerExpiresSeconds = h(t), this.iceServers = t.iceServers, this.uaListeners = [], this.startRegistration(e);
119
276
  } catch (t) {
120
277
  let n = t instanceof e && t.code === "CONFIG_INVALID" ? t : this.transportError(t);
121
278
  return this.disconnect(n), Promise.reject(n);
122
279
  }
123
280
  }
124
281
  disconnect(e = this.registrationError("SIP connection disconnected")) {
125
- this.stopKeepalive(), this.closeWarmPc();
282
+ this.stopKeepalive(), this.closeWarmPc(), this.iceQuality.reset();
126
283
  let t = this.pendingConnection;
127
284
  this.pendingConnection = void 0, this.ua !== void 0 && (this.removeUaListeners(this.ua), this.ua.stop()), this.ua = void 0, this.uaListeners = void 0, this.iceServers = void 0, t?.reject(e);
128
285
  }
@@ -135,20 +292,20 @@ var c = class {
135
292
  audio: !0,
136
293
  video: !1
137
294
  },
138
- sessionTimersExpires: o,
295
+ sessionTimersExpires: m,
139
296
  pcConfig: {
140
- iceServers: this.iceServers,
141
- iceCandidatePoolSize: i
297
+ iceServers: this.dialIceServers(),
298
+ iceCandidatePoolSize: f
142
299
  }
143
300
  } : {
144
301
  mediaConstraints: {
145
302
  audio: !0,
146
303
  video: !1
147
304
  },
148
- sessionTimersExpires: o,
305
+ sessionTimersExpires: m,
149
306
  pcConfig: {
150
307
  iceServers: [...n.iceServers],
151
- iceCandidatePoolSize: i
308
+ iceCandidatePoolSize: f
152
309
  },
153
310
  extraHeaders: [
154
311
  ...this.terminalHeaders(),
@@ -156,7 +313,7 @@ var c = class {
156
313
  ...n.type === "extension" ? ["X-XCall-Destination-Type: extension"] : []
157
314
  ]
158
315
  };
159
- return new l(r.call(t, e));
316
+ return new _(r.call(t, e));
160
317
  } catch (t) {
161
318
  throw new e("CALL_OPERATION_NOT_ALLOWED", "call", !1, { cause: t });
162
319
  }
@@ -172,6 +329,13 @@ var c = class {
172
329
  dispose() {
173
330
  this.disposed || (this.disposed = !0, this.disconnect(), this.incomingHandlers = void 0, this.connectionHandlers.clear());
174
331
  }
332
+ dialIceServers() {
333
+ let e = this.iceQuality.converged(this.iceServers);
334
+ return e === void 0 ? void 0 : [...e];
335
+ }
336
+ iceQualitySnapshot() {
337
+ return this.iceQuality.snapshot();
338
+ }
175
339
  getResourceCounts() {
176
340
  return {
177
341
  listeners: (this.uaListeners?.length ?? 0) + (this.incomingHandlers?.size ?? 0) + this.connectionHandlers.size,
@@ -179,10 +343,10 @@ var c = class {
179
343
  };
180
344
  }
181
345
  handleIncoming(e) {
182
- if (!u(e)) return;
346
+ if (!v(e)) return;
183
347
  let t = e.session;
184
348
  if (t === void 0) return;
185
- let n = new l(t);
349
+ let n = new _(t);
186
350
  this.incomingHandlers?.forEach((e) => e(n));
187
351
  }
188
352
  startRegistration(e) {
@@ -236,14 +400,14 @@ var c = class {
236
400
  }
237
401
  resolveConnection(e) {
238
402
  let t = this.pendingConnection;
239
- t?.ua === e && (this.pendingConnection = void 0, t.resolve(), this.warmIceCandidatePool(), this.startKeepalive(e, this.registerExpiresSeconds), this.connectionHandlers.forEach((e) => e("registered")));
403
+ t?.ua === e && (this.pendingConnection = void 0, t.resolve(), this.warmIceCandidatePool(), this.iceQuality.assess(this.iceServers), this.startKeepalive(e, this.registerExpiresSeconds), this.connectionHandlers.forEach((e) => e("registered")));
240
404
  }
241
405
  resolveWssUrl(e) {
242
406
  let t = this.transportBaseUrl?.trim() === "" || this.transportBaseUrl === void 0 ? typeof location > "u" ? "http://localhost" : location.origin : this.transportBaseUrl, n = e.startsWith("/") ? new URL(e, t) : new URL(e);
243
407
  return n.protocol === "http:" && (n.protocol = "ws:"), n.protocol === "https:" && (n.protocol = "wss:"), this.assertAllowedWssUrl(n), n.toString();
244
408
  }
245
409
  assertAllowedWssUrl(t) {
246
- if (t.protocol !== "wss:" && !(t.protocol === "ws:" && p(t.hostname))) throw new e("CONFIG_INVALID", "configuration", !1, { cause: t.origin });
410
+ if (t.protocol !== "wss:" && !(t.protocol === "ws:" && x(t.hostname))) throw new e("CONFIG_INVALID", "configuration", !1, { cause: t.origin });
247
411
  }
248
412
  withoutTicket(e) {
249
413
  let t = new URL(e);
@@ -265,7 +429,7 @@ var c = class {
265
429
  registrationFailureCause(e) {
266
430
  return typeof e == "object" && e && "cause" in e ? e.cause : e;
267
431
  }
268
- }, l = class {
432
+ }, _ = class {
269
433
  session;
270
434
  pendingHoldReject;
271
435
  mediaRegistration;
@@ -273,14 +437,15 @@ var c = class {
273
437
  mediaHandlers = /* @__PURE__ */ new Set();
274
438
  mediaWatchStarted = !1;
275
439
  sessionUnsubscribeFromPeerConnection;
440
+ detachIceGatheringTimeout;
276
441
  constructor(e) {
277
- this.session = e;
442
+ this.session = e, this.detachIceGatheringTimeout = l(e);
278
443
  }
279
444
  get id() {
280
445
  return this.session.id;
281
446
  }
282
447
  get from() {
283
- return this.session.from ?? m(this.session.remote_identity);
448
+ return this.session.from ?? S(this.session.remote_identity);
284
449
  }
285
450
  getMediaRegistration() {
286
451
  let e = this.session.connection;
@@ -289,11 +454,14 @@ var c = class {
289
454
  answer(e) {
290
455
  this.session.answer({
291
456
  mediaConstraints: e.mediaConstraints,
292
- pcConfig: { iceServers: [...e.pcConfig.iceServers] }
457
+ pcConfig: {
458
+ iceServers: [...e.pcConfig.iceServers],
459
+ iceCandidatePoolSize: f
460
+ }
293
461
  });
294
462
  }
295
463
  terminate() {
296
- this.pendingHoldReject?.(/* @__PURE__ */ Error("SIP session terminated")), this.session.terminate();
464
+ this.detachIceGatheringTimeout?.(), this.detachIceGatheringTimeout = void 0, this.pendingHoldReject?.(/* @__PURE__ */ Error("SIP session terminated")), this.session.terminate();
297
465
  }
298
466
  hold() {
299
467
  return this.changeHoldState("hold");
@@ -379,22 +547,22 @@ var c = class {
379
547
  });
380
548
  }
381
549
  };
382
- function u(e) {
550
+ function v(e) {
383
551
  if (typeof e != "object" || !e) return !1;
384
552
  let t = e;
385
- return t.originator === "remote" && d(t.session);
553
+ return t.originator === "remote" && y(t.session);
386
554
  }
387
- function d(e) {
555
+ function y(e) {
388
556
  return typeof e == "object" && !!e && "id" in e;
389
557
  }
390
- function f(e) {
558
+ function b(e) {
391
559
  return typeof e == "object" && e && (e.via_transport = "WSS"), e;
392
560
  }
393
- function p(e) {
561
+ function x(e) {
394
562
  let t = e.replace(/^\[|\]$/g, "").toLowerCase();
395
563
  return t === "localhost" || t === "127.0.0.1" || t === "::1" || t === "0:0:0:0:0:0:0:1";
396
564
  }
397
- function m(e) {
565
+ function S(e) {
398
566
  let t = e?.uri;
399
567
  if (t == null) return;
400
568
  if (typeof t.user == "string" && t.user !== "") return t.user;
@@ -402,4 +570,4 @@ function m(e) {
402
570
  return n.match(/^sips?:([^@;>]+)/i)?.[1] ?? (n === "" ? void 0 : n);
403
571
  }
404
572
  //#endregion
405
- export { c as JsSipAdapter };
573
+ export { g as JsSipAdapter };
@@ -26,6 +26,14 @@ export interface DiagnosticIceSummary {
26
26
  readonly serverCount: number;
27
27
  readonly stunServerCount: number;
28
28
  readonly turnServerCount: number;
29
+ /** url 级探测收敛快照(3.2.6+;SharedWorker 模式下主页面侧可能缺失) */
30
+ readonly probe?: {
31
+ readonly runs: number;
32
+ readonly lastMs: number | null;
33
+ readonly udpOk: boolean;
34
+ readonly tcpRelayOk: boolean;
35
+ readonly convergedServers: number | null;
36
+ };
29
37
  }
30
38
  export interface DiagnosticWssSummary {
31
39
  readonly configured: boolean;
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../Diagnostics-A7Z5fddO.cjs"),t=require("../redact-Bq26pObO.cjs");require("../createJsSipFactory-B1ORbJiP.cjs"),exports.Diagnostics=e.t,exports.RingBuffer=e.n,exports.diagnosticsVersion=e.r,exports.redactText=t.t,exports.redactValue=t.n;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../Diagnostics-5p1QKAQp.cjs"),t=require("../redact-Bq26pObO.cjs");require("../createJsSipFactory-B1ORbJiP.cjs"),exports.Diagnostics=e.t,exports.RingBuffer=e.n,exports.diagnosticsVersion=e.r,exports.redactText=t.t,exports.redactValue=t.n;
@@ -1,3 +1,3 @@
1
- import { n as e, r as t, t as n } from "../Diagnostics-bC1jC3qZ.js";
1
+ import { n as e, r as t, t as n } from "../Diagnostics-B_v3Ais8.js";
2
2
  import { n as r, t as i } from "../redact-DB3CMbsK.js";
3
3
  export { n as Diagnostics, e as RingBuffer, t as diagnosticsVersion, i as redactText, r as redactValue };
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./SharedWorkerProtocol-C-4KpECM.cjs"),t=require("./legacySessionProvider-DEC6MTLR.cjs"),n=require("./Diagnostics-A7Z5fddO.cjs"),r=require("./redact-Bq26pObO.cjs");require("./diagnostics/index.cjs"),require("./createJsSipFactory-B1ORbJiP.cjs");function i(e){if(e.code!==`OK`)throw TypeError(`Unexpected session response code: ${e.code}`);if(e.data===void 0||e.data===null)throw TypeError(`Unexpected session response: missing data`);return e.data}function a(e={}){return{version:`0.1.0`,options:e}}var o=`0.1.0`;exports.CCBarCall=t.c,exports.CCBarClient=t.r,exports.CCBarError=e.i,exports.CallManager=t.s,exports.CallStateError=t.l,exports.Diagnostics=n.t,exports.RingBuffer=n.n,exports.TypedEventBus=t.m,exports.assertCallTransition=t.u,exports.canTransition=t.d,exports.createSdk=a,exports.createSharedWorkerSelection=t.i,exports.createTokenSessionProvider=t.n,exports.diagnosticsVersion=n.r,exports.parseSessionResponse=i,exports.redactText=r.t,exports.redactValue=r.n,exports.resolveWorkerUrl=t.a,exports.transition=t.f,exports.transitions=t.p,exports.version=o;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./SharedWorkerProtocol-C-4KpECM.cjs"),t=require("./legacySessionProvider-BlkuIB83.cjs"),n=require("./Diagnostics-5p1QKAQp.cjs"),r=require("./redact-Bq26pObO.cjs");require("./diagnostics/index.cjs"),require("./createJsSipFactory-B1ORbJiP.cjs");function i(e){if(e.code!==`OK`)throw TypeError(`Unexpected session response code: ${e.code}`);if(e.data===void 0||e.data===null)throw TypeError(`Unexpected session response: missing data`);return e.data}function a(e={}){return{version:`0.1.0`,options:e}}var o=`0.1.0`;exports.CCBarCall=t.c,exports.CCBarClient=t.r,exports.CCBarError=e.i,exports.CallManager=t.s,exports.CallStateError=t.l,exports.Diagnostics=n.t,exports.RingBuffer=n.n,exports.TypedEventBus=t.m,exports.assertCallTransition=t.u,exports.canTransition=t.d,exports.createSdk=a,exports.createSharedWorkerSelection=t.i,exports.createTokenSessionProvider=t.n,exports.diagnosticsVersion=n.r,exports.parseSessionResponse=i,exports.redactText=r.t,exports.redactValue=r.n,exports.resolveWorkerUrl=t.a,exports.transition=t.f,exports.transitions=t.p,exports.version=o;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { i as e } from "./SharedWorkerProtocol-CffZrUXg.js";
2
- import { a as t, c as n, d as r, f as i, i as a, l as o, m as s, n as c, p as l, r as u, s as d, u as f } from "./legacySessionProvider-BlHM7TTM.js";
3
- import { n as p, r as m, t as h } from "./Diagnostics-bC1jC3qZ.js";
2
+ import { a as t, c as n, d as r, f as i, i as a, l as o, m as s, n as c, p as l, r as u, s as d, u as f } from "./legacySessionProvider-DIPSh55z.js";
3
+ import { n as p, r as m, t as h } from "./Diagnostics-B_v3Ais8.js";
4
4
  import { n as g, t as _ } from "./redact-DB3CMbsK.js";
5
5
  import "./diagnostics/index.js";
6
6
  //#region src/api/contracts.ts