whepts 1.1.6 → 1.1.8

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.
@@ -5,9 +5,10 @@ export declare class TrackManager {
5
5
  private stream?;
6
6
  private observer?;
7
7
  private showStore;
8
- private playController;
8
+ private playMonitor;
9
+ private flowMonitor;
9
10
  constructor(container: HTMLMediaElement, eventEmitter: EventEmitter, lazyLoad?: boolean);
10
- onTrack(evt: RTCTrackEvent): void;
11
+ onTrack(evt: RTCTrackEvent, pc?: RTCPeerConnection): void;
11
12
  get paused(): boolean;
12
13
  pause(): void;
13
14
  resume(): void;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import t from"eventemitter3";import{atom as e}from"nanostores";const s={SIGNAL_ERROR:"SignalError",STATE_ERROR:"StateError",REQUEST_ERROR:"RequestError",NOT_FOUND_ERROR:"NotFoundError",CONNECT_ERROR:"ConnectError",MEDIA_ERROR:"MediaError",OTHER_ERROR:"OtherError"};class n extends Error{constructor(t,e,s){super(e,s),this.type=t}}class i{static async supportsNonAdvertisedCodec(t,e){return new Promise(s=>{const n=new RTCPeerConnection({iceServers:[]}),r="audio";let o="";n.addTransceiver(r,{direction:"recvonly"}),n.createOffer().then(s=>{if(!s.sdp)throw new Error("SDP not present");if(s.sdp.includes(` ${t}`))throw new Error("already present");const a=s.sdp.split(`m=${r}`),c=a.slice(1).map(t=>t.split("\r\n")[0].split(" ").slice(3)).reduce((t,e)=>[...t,...e],[]);o=i.reservePayloadType(c);const h=a[1].split("\r\n");return h[0]+=` ${o}`,h.splice(h.length-1,0,`a=rtpmap:${o} ${t}`),void 0!==e&&h.splice(h.length-1,0,`a=fmtp:${o} ${e}`),a[1]=h.join("\r\n"),s.sdp=a.join(`m=${r}`),n.setLocalDescription(s)}).then(()=>n.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:`v=0\r\no=- 6539324223450680508 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\nm=${r} 9 UDP/TLS/RTP/SAVPF ${o}\r\nc=IN IP4 0.0.0.0\r\na=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\na=ice-ufrag:29e036dc\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:${o} ${t}\r\n${void 0!==e?`a=fmtp:${o} ${e}\r\n`:""}`}))).then(()=>s(!0)).catch(()=>s(!1)).finally(()=>n.close())})}static unquoteCredential(t){return JSON.parse(`"${t}"`)}static linkToIceServers(t){return t?t.split(", ").map(t=>{const e=t.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);if(!e)throw new n(s.SIGNAL_ERROR,"Invalid ICE server link format");const r={urls:[e[1]]};return e[3]&&(r.username=i.unquoteCredential(e[3]),r.credential=i.unquoteCredential(e[4]),r.credentialType="password"),r}):[]}static reservePayloadType(t){for(let e=30;e<=127;e++)if((e<=63||e>=96)&&!t.includes(e.toString())){const s=e.toString();return t.push(s),s}throw new Error("unable to find a free payload type")}}class r{constructor(t){this.options=t}detect(){Promise.all([["pcma/8000/2"],["multiopus/48000/6","channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2"],["L16/48000/2"]].map(t=>i.supportsNonAdvertisedCodec(t[0],t[1]).then(e=>!!e&&t[0]))).then(t=>t.filter(t=>!1!==t)).then(t=>{const e=this.options.getState();if("getting_codecs"!==e)throw new n(s.STATE_ERROR,`State changed to ${e}`);this.options.emitter.emit("codecs:detected",t)}).catch(t=>this.options.emitter.emit("error",t))}}class o{static parseOffer(t){const e={iceUfrag:"",icePwd:"",medias:[]};for(const s of t.split("\r\n"))s.startsWith("m=")?e.medias.push(s.slice(2)):""===e.iceUfrag&&s.startsWith("a=ice-ufrag:")?e.iceUfrag=s.slice(12):""===e.icePwd&&s.startsWith("a=ice-pwd:")&&(e.icePwd=s.slice(10));return e}static reservePayloadType(t){for(let e=30;e<=127;e++)if((e<=63||e>=96)&&!t.includes(e.toString())){const s=e.toString();return t.push(s),s}throw new Error("unable to find a free payload type")}static enableStereoPcmau(t,e){const s=e.split("\r\n");let n=o.reservePayloadType(t);return s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} PCMU/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=o.reservePayloadType(t),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} PCMA/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),s.join("\r\n")}static enableMultichannelOpus(t,e){const s=e.split("\r\n");let n=o.reservePayloadType(t);return s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/3`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=o.reservePayloadType(t),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/4`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=o.reservePayloadType(t),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/5`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=o.reservePayloadType(t),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/6`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=o.reservePayloadType(t),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/7`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=o.reservePayloadType(t),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} multiopus/48000/8`),s.splice(s.length-1,0,`a=fmtp:${n} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),s.join("\r\n")}static enableL16(t,e){const s=e.split("\r\n");let n=o.reservePayloadType(t);return s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} L16/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=o.reservePayloadType(t),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} L16/16000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),n=o.reservePayloadType(t),s[0]+=` ${n}`,s.splice(s.length-1,0,`a=rtpmap:${n} L16/48000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${n} transport-cc`),s.join("\r\n")}static enableStereoOpus(t){let e="";const s=t.split("\r\n");for(let t=0;t<s.length;t++)if(s[t].startsWith("a=rtpmap:")&&s[t].toLowerCase().includes("opus/")){e=s[t].slice(9).split(" ")[0];break}if(""===e)return t;for(let t=0;t<s.length;t++)s[t].startsWith(`a=fmtp:${e} `)&&(s[t].includes("stereo")||(s[t]+=";stereo=1"),s[t].includes("sprop-stereo")||(s[t]+=";sprop-stereo=1"));return s.join("\r\n")}static editOffer(t,e){const s=t.split("m="),n=s.slice(1).map(t=>t.split("\r\n")[0].split(" ").slice(3)).reduce((t,e)=>[...t,...e],[]);for(let t=1;t<s.length;t++)if(s[t].startsWith("audio")){s[t]=o.enableStereoOpus(s[t]),e.includes("pcma/8000/2")&&(s[t]=o.enableStereoPcmau(n,s[t])),e.includes("multiopus/48000/6")&&(s[t]=o.enableMultichannelOpus(n,s[t])),e.includes("L16/48000/2")&&(s[t]=o.enableL16(n,s[t]));break}return s.join("m=")}static generateSdpFragment(t,e){const s={};for(const t of e){const e=t.sdpMLineIndex;e&&(void 0===s[e]&&(s[e]=[]),s[e].push(t))}let n=`a=ice-ufrag:${t.iceUfrag}\r\na=ice-pwd:${t.icePwd}\r\n`,i=0;for(const e of t.medias){if(void 0!==s[i]){n+=`m=${e}\r\na=mid:${i}\r\n`;for(const t of s[i])n+=`a=${t.candidate}\r\n`}i++}return n}}class a{constructor(t){this.options=t}async setupPeerConnection(t){if("running"!==this.options.getState())throw new n(s.STATE_ERROR,"closed");const e=new RTCPeerConnection({iceServers:t,sdpSemantics:"unified-plan"});this.pc=e;const i="recvonly";return e.addTransceiver("video",{direction:i}),e.addTransceiver("audio",{direction:i}),e.onicecandidate=t=>this.onLocalCandidate(t),e.onconnectionstatechange=()=>this.onConnectionState(),e.oniceconnectionstatechange=()=>this.onIceConnectionState(),e.ontrack=t=>this.options.emitter.emit("track",t),e.createOffer().then(t=>{if(!t.sdp)throw new n(s.SIGNAL_ERROR,"Failed to create offer SDP");return t.sdp=o.editOffer(t.sdp,this.options.getNonAdvertisedCodecs()),this.offerData=o.parseOffer(t.sdp),e.setLocalDescription(t).then(()=>t.sdp)})}async setAnswer(t){if("running"!==this.options.getState())throw new n(s.STATE_ERROR,"closed");return this.pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:t}))}getPeerConnection(){return this.pc}getOfferData(){return this.offerData}close(){this.pc?.close(),this.pc=void 0,this.offerData=void 0}onLocalCandidate(t){"running"===this.options.getState()&&t.candidate&&this.options.emitter.emit("candidate",t.candidate)}onConnectionState(){"running"===this.options.getState()&&this.pc&&("failed"!==this.pc.connectionState&&"closed"!==this.pc.connectionState||this.options.emitter.emit("error",new n(s.OTHER_ERROR,"peer connection closed")))}onIceConnectionState(){"running"===this.options.getState()&&this.pc&&"failed"===this.pc.iceConnectionState&&this.pc.restartIce()}}class c{constructor(t){this.options=t}authHeader(){if(this.options.conf.user&&""!==this.options.conf.user){return{Authorization:`Basic ${btoa(`${this.options.conf.user}:${this.options.conf.pass}`)}`}}return this.options.conf.token&&""!==this.options.conf.token?{Authorization:`Bearer ${this.options.conf.token}`}:{}}async requestICEServers(){return this.options.conf.iceServers&&this.options.conf.iceServers.length>0?this.options.conf.iceServers:fetch(this.options.conf.url,{method:"OPTIONS",headers:{...this.authHeader()}}).then(t=>i.linkToIceServers(t.headers.get("Link")))}async sendOffer(t){if("running"!==this.options.getState())throw new n(s.STATE_ERROR,"closed");return fetch(this.options.conf.url,{method:"POST",headers:{...this.authHeader(),"Content-Type":"application/sdp"},body:t}).then(t=>{switch(t.status){case 201:break;case 404:case 406:throw new n(s.NOT_FOUND_ERROR,"stream not found");case 400:return t.json().then(t=>{throw new n(s.REQUEST_ERROR,t.error)});default:throw new n(s.REQUEST_ERROR,`bad status code ${t.status}`)}const e=t.headers.get("Location"),i=e?new URL(e,this.options.conf.url).toString():void 0;return t.text().then(t=>({sessionUrl:i,answer:t}))})}sendLocalCandidates(t,e,i){fetch(t,{method:"PATCH",headers:{"Content-Type":"application/trickle-ice-sdpfrag","If-Match":"*"},body:o.generateSdpFragment(e,i)}).then(t=>{switch(t.status){case 204:break;case 404:throw new n(s.NOT_FOUND_ERROR,"stream not found");default:throw new n(s.REQUEST_ERROR,`bad status code ${t.status}`)}}).catch(t=>this.options.emitter.emit("error",t))}}class h{constructor(t,e){this.container=t,this.eventEmitter=e}async play(){try{await this.container.play(),this.eventEmitter.emit("play",{success:!0,muted:this.container.muted})}catch(t){if(!this.container.muted){this.container.muted=!0;try{return await this.container.play(),void this.eventEmitter.emit("play",{success:!0,muted:!0})}catch{}}this.eventEmitter.emit("play",{success:!1,reason:t instanceof Error?t.message:"Autoplay failed",muted:this.container.muted})}}}class l{constructor(t,s,n=!0){this.container=t,this.lazyLoad=n,this.showStore=e(!1),this.playController=new h(t,s),this.showStore.subscribe(t=>{t?this.resume():this.pause()}),this.lazyLoad?(this.observer=new IntersectionObserver(([t])=>{t.isIntersecting?this.showStore.set(!0):this.showStore.set(!1)},{threshold:.5}),this.observer.observe(this.container)):this.showStore.set(!0)}onTrack(t){this.stream=t.streams[0],this.showStore.get()&&(this.container.srcObject=this.stream,this.playController.play())}get paused(){return null===this.container.srcObject}pause(){this.container.srcObject=null}resume(){this.stream&&(this.container.srcObject=this.stream)}stop(){this.stream=void 0}}class p{constructor(t){this.options=t,this.lastBytesReceived=0,this.consecutiveNoProgress=0,this.startTime=0,this.isStable=!1,this.baseInterval=t.interval,this.stableInterval=t.stableInterval||2*t.interval,this.maxNoProgress=t.maxNoProgress||3,this.stabilizationTime=t.stabilizationTime||3e4}setPeerConnection(t){this.pc=t}start(){this.close(),this.startTime=Date.now(),this.isStable=!1,this.consecutiveNoProgress=0,this.scheduleNextCheck()}close(){this.checkTimer&&(clearTimeout(this.checkTimer),this.checkTimer=void 0),this.pc=void 0}scheduleNextCheck(){const t=this.getNextCheckInterval();this.checkTimer=setTimeout(()=>{this.checkFlowState().then(()=>{this.pc&&"connected"===this.pc.connectionState&&this.scheduleNextCheck()})},t)}getNextCheckInterval(){const t=Date.now()-this.startTime;return this.isStable=t>this.stabilizationTime,this.isStable?this.stableInterval:this.baseInterval}async checkFlowState(){if(!this.pc)return;if("connected"!==this.pc.connectionState)return;const t=await this.pc.getStats();let e=0;if(t.forEach(t=>{const s=t;"inbound-rtp"===t.type&&"video"===s.kind&&(e=s.bytesReceived||0)}),e===this.lastBytesReceived){if(this.consecutiveNoProgress++,this.consecutiveNoProgress>=this.maxNoProgress)return this.options.emitter.emit("error",new n(s.CONNECT_ERROR,"data stream interruption")),void(this.consecutiveNoProgress=0)}else this.consecutiveNoProgress=0;this.lastBytesReceived=e}}class d extends t{constructor(t){super(),this.retryPause=2e3,this.stateStore=e("getting_codecs"),this.queuedCandidates=[],this.nonAdvertisedCodecs=[],this.conf=t,this.stateStore.subscribe((t,e)=>{this.emit("state:change",{from:e,to:t})}),this.trackManager=new l(this.conf.container,this,this.conf.lazyLoad),this.flowCheck=new p({interval:5e3,emitter:this}),this.httpClient=new c({conf:this.conf,getState:()=>this.stateStore.get(),emitter:this}),this.connectionManager=new a({getState:()=>this.stateStore.get(),emitter:this,getNonAdvertisedCodecs:()=>this.nonAdvertisedCodecs}),this.codecDetector=new r({getState:()=>this.stateStore.get(),emitter:this}),this.on("codecs:detected",t=>{this.handleCodecsDetected(t)}),this.on("candidate",t=>this.handleCandidate(t)),this.on("track",t=>{this.trackManager.onTrack(t),this.flowCheck.start()}),this.on("error",t=>this.handleError(t)),this.codecDetector.detect()}get state(){return this.stateStore.get()}close(){this.stateStore.set("closed"),this.connectionManager.close(),this.trackManager.stop(),this.flowCheck.close(),this.restartTimeout&&clearTimeout(this.restartTimeout),this.emit("close")}cleanupSession(){this.restartTimeout&&(clearTimeout(this.restartTimeout),this.restartTimeout=void 0),this.connectionManager.close(),this.flowCheck.close(),this.queuedCandidates=[],this.sessionUrl&&(fetch(this.sessionUrl,{method:"DELETE"}).catch(()=>{}),this.sessionUrl=void 0)}handleError(t){this.flowCheck.close(),"getting_codecs"===this.stateStore.get()||t instanceof n&&[s.SIGNAL_ERROR,s.NOT_FOUND_ERROR,s.REQUEST_ERROR].includes(t.type)?this.stateStore.set("failed"):"running"===this.stateStore.get()&&(this.cleanupSession(),this.stateStore.set("restarting"),this.emit("restart"),this.restartTimeout=setTimeout(()=>{this.restartTimeout=void 0,this.stateStore.set("running"),this.start()},this.retryPause))}handleCodecsDetected(t){this.nonAdvertisedCodecs=t,this.stateStore.set("running"),this.start()}start(){this.httpClient.requestICEServers().then(t=>this.connectionManager.setupPeerConnection(t)).then(t=>{const e=this.connectionManager.getPeerConnection();return e&&this.flowCheck.setPeerConnection(e),t}).then(t=>this.httpClient.sendOffer(t)).then(({sessionUrl:t,answer:e})=>this.handleOfferResponse(t,e)).catch(t=>this.handleError(t))}handleOfferResponse(t,e){return t&&(this.sessionUrl=t),this.connectionManager.setAnswer(e).then(()=>{if("running"===this.stateStore.get()&&0!==this.queuedCandidates.length){const t=this.connectionManager.getOfferData();t&&this.sessionUrl&&(this.httpClient.sendLocalCandidates(this.sessionUrl,t,this.queuedCandidates),this.queuedCandidates=[])}})}handleCandidate(t){if(this.sessionUrl){const e=this.connectionManager.getOfferData();e&&this.httpClient.sendLocalCandidates(this.sessionUrl,e,[t])}else this.queuedCandidates.push(t)}get paused(){return this.trackManager.paused}pause(){this.trackManager.pause()}resume(){this.trackManager.resume()}updateUrl(t){"closed"!==this.state?(this.conf.url=t,this.cleanupSession(),this.stateStore.set("running"),this.start()):this.emit("error",new n(s.OTHER_ERROR,"Cannot update URL: instance is closed"))}}export{s as ErrorTypes,n as WebRTCError,d as default};
1
+ import t from"eventemitter3";import{atom as e}from"nanostores";const s={SIGNAL_ERROR:"SignalError",STATE_ERROR:"StateError",REQUEST_ERROR:"RequestError",NOT_FOUND_ERROR:"NotFoundError",CONNECT_ERROR:"ConnectError",MEDIA_ERROR:"MediaError",OTHER_ERROR:"OtherError"};class i extends Error{constructor(t,e,s){super(e,s),this.type=t}}class r{static async supportsNonAdvertisedCodec(t,e){return new Promise(s=>{const i=new RTCPeerConnection({iceServers:[]}),n="audio";let o="";i.addTransceiver(n,{direction:"recvonly"}),i.createOffer().then(s=>{if(!s.sdp)throw new Error("SDP not present");if(s.sdp.includes(` ${t}`))throw new Error("already present");const a=s.sdp.split(`m=${n}`),c=a.slice(1).map(t=>t.split("\r\n")[0].split(" ").slice(3)).reduce((t,e)=>[...t,...e],[]);o=r.reservePayloadType(c);const h=a[1].split("\r\n");return h[0]+=` ${o}`,h.splice(h.length-1,0,`a=rtpmap:${o} ${t}`),void 0!==e&&h.splice(h.length-1,0,`a=fmtp:${o} ${e}`),a[1]=h.join("\r\n"),s.sdp=a.join(`m=${n}`),i.setLocalDescription(s)}).then(()=>i.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:`v=0\r\no=- 6539324223450680508 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\nm=${n} 9 UDP/TLS/RTP/SAVPF ${o}\r\nc=IN IP4 0.0.0.0\r\na=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\na=ice-ufrag:29e036dc\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:${o} ${t}\r\n${void 0!==e?`a=fmtp:${o} ${e}\r\n`:""}`}))).then(()=>s(!0)).catch(()=>s(!1)).finally(()=>i.close())})}static unquoteCredential(t){return JSON.parse(`"${t}"`)}static linkToIceServers(t){return t?t.split(", ").map(t=>{const e=t.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);if(!e)throw new i(s.SIGNAL_ERROR,"Invalid ICE server link format");const n={urls:[e[1]]};return e[3]&&(n.username=r.unquoteCredential(e[3]),n.credential=r.unquoteCredential(e[4]),n.credentialType="password"),n}):[]}static reservePayloadType(t){for(let e=30;e<=127;e++)if((e<=63||e>=96)&&!t.includes(e.toString())){const s=e.toString();return t.push(s),s}throw new Error("unable to find a free payload type")}}class n{constructor(t){this.options=t}detect(){Promise.all([["pcma/8000/2"],["multiopus/48000/6","channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2"],["L16/48000/2"]].map(t=>r.supportsNonAdvertisedCodec(t[0],t[1]).then(e=>!!e&&t[0]))).then(t=>t.filter(t=>!1!==t)).then(t=>{const e=this.options.getState();if("getting_codecs"!==e)throw new i(s.STATE_ERROR,`State changed to ${e}`);this.options.emitter.emit("codecs:detected",t)}).catch(t=>this.options.emitter.emit("error",t))}}class o{static parseOffer(t){const e={iceUfrag:"",icePwd:"",medias:[]};for(const s of t.split("\r\n"))s.startsWith("m=")?e.medias.push(s.slice(2)):""===e.iceUfrag&&s.startsWith("a=ice-ufrag:")?e.iceUfrag=s.slice(12):""===e.icePwd&&s.startsWith("a=ice-pwd:")&&(e.icePwd=s.slice(10));return e}static reservePayloadType(t){for(let e=30;e<=127;e++)if((e<=63||e>=96)&&!t.includes(e.toString())){const s=e.toString();return t.push(s),s}throw new Error("unable to find a free payload type")}static enableStereoPcmau(t,e){const s=e.split("\r\n");let i=o.reservePayloadType(t);return s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} PCMU/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} PCMA/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),s.join("\r\n")}static enableMultichannelOpus(t,e){const s=e.split("\r\n");let i=o.reservePayloadType(t);return s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/3`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/4`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/5`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/6`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/7`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} multiopus/48000/8`),s.splice(s.length-1,0,`a=fmtp:${i} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),s.join("\r\n")}static enableL16(t,e){const s=e.split("\r\n");let i=o.reservePayloadType(t);return s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} L16/8000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} L16/16000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),i=o.reservePayloadType(t),s[0]+=` ${i}`,s.splice(s.length-1,0,`a=rtpmap:${i} L16/48000/2`),s.splice(s.length-1,0,`a=rtcp-fb:${i} transport-cc`),s.join("\r\n")}static enableStereoOpus(t){let e="";const s=t.split("\r\n");for(let t=0;t<s.length;t++)if(s[t].startsWith("a=rtpmap:")&&s[t].toLowerCase().includes("opus/")){e=s[t].slice(9).split(" ")[0];break}if(""===e)return t;for(let t=0;t<s.length;t++)s[t].startsWith(`a=fmtp:${e} `)&&(s[t].includes("stereo")||(s[t]+=";stereo=1"),s[t].includes("sprop-stereo")||(s[t]+=";sprop-stereo=1"));return s.join("\r\n")}static editOffer(t,e){const s=t.split("m="),i=s.slice(1).map(t=>t.split("\r\n")[0].split(" ").slice(3)).reduce((t,e)=>[...t,...e],[]);for(let t=1;t<s.length;t++)if(s[t].startsWith("audio")){s[t]=o.enableStereoOpus(s[t]),e.includes("pcma/8000/2")&&(s[t]=o.enableStereoPcmau(i,s[t])),e.includes("multiopus/48000/6")&&(s[t]=o.enableMultichannelOpus(i,s[t])),e.includes("L16/48000/2")&&(s[t]=o.enableL16(i,s[t]));break}return s.join("m=")}static generateSdpFragment(t,e){const s={};for(const t of e){const e=t.sdpMLineIndex;e&&(void 0===s[e]&&(s[e]=[]),s[e].push(t))}let i=`a=ice-ufrag:${t.iceUfrag}\r\na=ice-pwd:${t.icePwd}\r\n`,r=0;for(const e of t.medias){if(void 0!==s[r]){i+=`m=${e}\r\na=mid:${r}\r\n`;for(const t of s[r])i+=`a=${t.candidate}\r\n`}r++}return i}}class a{constructor(t){this.options=t}async setupPeerConnection(t){if("running"!==this.options.getState())throw new i(s.STATE_ERROR,"closed");const e=new RTCPeerConnection({iceServers:t,sdpSemantics:"unified-plan"});this.pc=e;const r="recvonly";return e.addTransceiver("video",{direction:r}),e.addTransceiver("audio",{direction:r}),e.onicecandidate=t=>this.onLocalCandidate(t),e.onconnectionstatechange=()=>this.onConnectionState(),e.oniceconnectionstatechange=()=>this.onIceConnectionState(),e.ontrack=t=>this.options.emitter.emit("track",t),e.createOffer().then(t=>{if(!t.sdp)throw new i(s.SIGNAL_ERROR,"Failed to create offer SDP");return t.sdp=o.editOffer(t.sdp,this.options.getNonAdvertisedCodecs()),this.offerData=o.parseOffer(t.sdp),e.setLocalDescription(t).then(()=>t.sdp)})}async setAnswer(t){if("running"!==this.options.getState())throw new i(s.STATE_ERROR,"closed");return this.pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:t}))}getPeerConnection(){return this.pc}getOfferData(){return this.offerData}close(){this.pc?.close(),this.pc=void 0,this.offerData=void 0}onLocalCandidate(t){"running"===this.options.getState()&&t.candidate&&this.options.emitter.emit("candidate",t.candidate)}onConnectionState(){"running"===this.options.getState()&&this.pc&&("failed"!==this.pc.connectionState&&"closed"!==this.pc.connectionState||this.options.emitter.emit("error",new i(s.OTHER_ERROR,"peer connection closed")))}onIceConnectionState(){"running"===this.options.getState()&&this.pc&&"failed"===this.pc.iceConnectionState&&this.pc.restartIce()}}class c{constructor(t){this.options=t}authHeader(){if(this.options.conf.user&&""!==this.options.conf.user){return{Authorization:`Basic ${btoa(`${this.options.conf.user}:${this.options.conf.pass}`)}`}}return this.options.conf.token&&""!==this.options.conf.token?{Authorization:`Bearer ${this.options.conf.token}`}:{}}async requestICEServers(){return this.options.conf.iceServers&&this.options.conf.iceServers.length>0?this.options.conf.iceServers:fetch(this.options.conf.url,{method:"OPTIONS",headers:{...this.authHeader()}}).then(t=>r.linkToIceServers(t.headers.get("Link")))}async sendOffer(t){if("running"!==this.options.getState())throw new i(s.STATE_ERROR,"closed");return fetch(this.options.conf.url,{method:"POST",headers:{...this.authHeader(),"Content-Type":"application/sdp"},body:t}).then(t=>{switch(t.status){case 201:break;case 404:case 406:throw new i(s.NOT_FOUND_ERROR,"stream not found");case 400:return t.json().then(t=>{throw new i(s.REQUEST_ERROR,t.error)});default:throw new i(s.REQUEST_ERROR,`bad status code ${t.status}`)}const e=t.headers.get("Location"),r=e?new URL(e,this.options.conf.url).toString():void 0;return t.text().then(t=>({sessionUrl:r,answer:t}))})}sendLocalCandidates(t,e,r){fetch(t,{method:"PATCH",headers:{"Content-Type":"application/trickle-ice-sdpfrag","If-Match":"*"},body:o.generateSdpFragment(e,r)}).then(t=>{switch(t.status){case 204:break;case 404:throw new i(s.NOT_FOUND_ERROR,"stream not found");default:throw new i(s.REQUEST_ERROR,`bad status code ${t.status}`)}}).catch(t=>this.options.emitter.emit("error",t))}}class h{constructor(){this.tasks=new Map,this.nextTaskId=0}register(t,e){const s=this.nextTaskId++;return this.tasks.set(s,e),1===this.tasks.size&&this.startTimer(t),()=>{this.tasks.delete(s),0===this.tasks.size&&this.stopTimer()}}startTimer(t){this.stopTimer(),this.timer=setInterval(()=>{this.tasks.forEach(t=>t())},t)}stopTimer(){this.timer&&(clearInterval(this.timer),this.timer=void 0)}destroy(){this.tasks.clear(),this.stopTimer()}}let l=null;function p(){return l||(l=new h),l}class d{constructor(t){this.options=t,this.lastBytesReceived=0,this.isFirstCheck=!0,this.consecutiveNoProgress=0,this.startTime=0,this.isStable=!1,this.baseInterval=t.interval,this.stableInterval=t.stableInterval||2*t.interval,this.maxNoProgress=t.maxNoProgress||3,this.stabilizationTime=t.stabilizationTime||3e4}start(t){t&&(this.pc=t,this.reset(),this.startTime=Date.now(),this.isStable=!1,this.scheduleNextCheck())}stop(){this.unregisterMonitor&&(this.unregisterMonitor(),this.unregisterMonitor=void 0),this.pc=void 0}reset(){this.consecutiveNoProgress=0,this.isFirstCheck=!0,this.lastBytesReceived=0}scheduleNextCheck(){const t=this.getNextCheckInterval();this.unregisterMonitor=p().register(t,()=>{this.performCheck(),this.shouldContinueMonitoring()&&this.scheduleNextCheck()})}performCheck(){this.shouldCheck()&&this.checkFlowState().catch(()=>{})}shouldContinueMonitoring(){return!(!this.pc||"connected"!==this.pc.connectionState)}shouldCheck(){return!(!this.pc||"connected"!==this.pc.connectionState)}getNextCheckInterval(){const t=Date.now()-this.startTime;return this.isStable=t>this.stabilizationTime,this.isStable?this.stableInterval:this.baseInterval}async checkFlowState(){const t=await this.getReceivedBytes();if(this.isFirstCheck)return this.lastBytesReceived=t,void(this.isFirstCheck=!1);this.hasDataProgress(t)?this.consecutiveNoProgress=0:this.handleNoProgress(),this.lastBytesReceived=t}async getReceivedBytes(){if(!this.pc)return 0;try{const t=await this.pc.getStats();let e=0,s=0;return t.forEach(t=>{if("inbound-rtp"===t.type){const i=t;"video"===i.kind&&void 0!==i.bytesReceived?e=i.bytesReceived:"audio"===i.kind&&void 0!==i.bytesReceived&&(s=i.bytesReceived)}}),e>0?e:s}catch{return 0}}hasDataProgress(t){return t>this.lastBytesReceived}handleNoProgress(){this.consecutiveNoProgress++,this.consecutiveNoProgress>=this.maxNoProgress&&(this.triggerStreamError(),this.consecutiveNoProgress=0)}triggerStreamError(){const t=new i(s.CONNECT_ERROR,"数据流中断:检测到流已停滞");this.options.emitter.emit("error",t)}}class u{constructor(t){this.options=t,this.lastCurrentTime=0}get container(){return this.options.container}get emitter(){return this.options.emitter}async play(){if(this.container.paused)try{await this.container.play(),this.emitter.emit("play:success",{muted:this.container.muted}),setTimeout(()=>this.verifyPlayback(),500),this.startMonitoring()}catch(t){if(!this.container.muted){this.container.muted=!0;try{return await this.container.play(),this.emitter.emit("play:success",{muted:!0}),setTimeout(()=>this.verifyPlayback(),500),void this.startMonitoring()}catch{}}this.emitter.emit("play:failed",{reason:t instanceof Error?t.message:"Autoplay failed",muted:this.container.muted})}else this.startMonitoring()}verifyPlayback(){this.container.paused?this.emitter.emit("play:stalled",{reason:"Playback verification failed: media is paused"}):0===this.container.currentTime&&this.emitter.emit("play:stalled",{reason:"Playback verification failed: currentTime is not advancing"})}startMonitoring(){this.stopMonitoring(),this.lastCurrentTime=this.container.currentTime,this.unregisterMonitor=p().register(5e3,()=>this.checkStalled())}stopMonitoring(){this.unregisterMonitor&&(this.unregisterMonitor(),this.unregisterMonitor=void 0)}checkStalled(){const t=this.container.currentTime,e=t-this.lastCurrentTime;e<.1&&!this.container.paused&&this.emitter.emit("play:stalled",{reason:`Playback stalled: currentTime not advancing (advanced ${e.toFixed(2)}s in 5s)`}),this.lastCurrentTime=t}}class m{constructor(t,s,i=!0){this.container=t,this.lazyLoad=i,this.showStore=e(!1),this.playMonitor=new u({container:t,emitter:s}),this.flowMonitor=new d({interval:5e3,emitter:s}),this.showStore.subscribe(t=>{t?this.resume():this.pause()}),this.lazyLoad?(this.observer=new IntersectionObserver(([t])=>{t.isIntersecting?this.showStore.set(!0):this.showStore.set(!1)},{threshold:.5}),this.observer.observe(this.container)):this.showStore.set(!0)}onTrack(t,e){this.stream=t.streams[0],this.showStore.get()&&(this.container.srcObject=this.stream,this.playMonitor.play()),this.flowMonitor.start(e)}get paused(){return null===this.container.srcObject}pause(){this.playMonitor.stopMonitoring(),this.container.srcObject=null}resume(){this.stream&&(this.container.srcObject=this.stream,this.playMonitor.play())}stop(){this.playMonitor.stopMonitoring(),this.flowMonitor.stop(),this.stream=void 0}}class g extends t{constructor(t){super(),this.retryPause=2e3,this.stateStore=e("getting_codecs"),this.queuedCandidates=[],this.nonAdvertisedCodecs=[],this.conf=t,this.stateStore.subscribe((t,e)=>{this.emit("state:change",{from:e,to:t})}),this.trackManager=new m(this.conf.container,this,this.conf.lazyLoad),this.httpClient=new c({conf:this.conf,getState:()=>this.stateStore.get(),emitter:this}),this.connectionManager=new a({getState:()=>this.stateStore.get(),emitter:this,getNonAdvertisedCodecs:()=>this.nonAdvertisedCodecs}),this.codecDetector=new n({getState:()=>this.stateStore.get(),emitter:this}),this.on("codecs:detected",t=>this.handleCodecsDetected(t)),this.on("candidate",t=>this.handleCandidate(t)),this.on("track",t=>this.trackManager.onTrack(t,this.connectionManager.getPeerConnection())),this.on("error",t=>this.handleError(t)),this.codecDetector.detect()}get state(){return this.stateStore.get()}close(){this.stateStore.set("closed"),this.connectionManager.close(),this.trackManager.stop(),this.restartTimeout&&clearTimeout(this.restartTimeout),this.emit("close")}cleanupSession(){this.restartTimeout&&(clearTimeout(this.restartTimeout),this.restartTimeout=void 0),this.connectionManager.close(),this.trackManager.stop(),this.queuedCandidates=[],this.sessionUrl&&(fetch(this.sessionUrl,{method:"DELETE"}).catch(()=>{}),this.sessionUrl=void 0)}handleError(t){this.trackManager.stop(),"getting_codecs"===this.stateStore.get()||t instanceof i&&[s.SIGNAL_ERROR,s.NOT_FOUND_ERROR,s.REQUEST_ERROR].includes(t.type)?this.stateStore.set("failed"):"running"===this.stateStore.get()&&(this.cleanupSession(),this.stateStore.set("restarting"),this.emit("restart"),this.restartTimeout=setTimeout(()=>{this.restartTimeout=void 0,this.stateStore.set("running"),this.start()},this.retryPause))}handleCodecsDetected(t){this.nonAdvertisedCodecs=t,this.stateStore.set("running"),this.start()}start(){this.httpClient.requestICEServers().then(t=>this.connectionManager.setupPeerConnection(t)).then(t=>this.httpClient.sendOffer(t)).then(({sessionUrl:t,answer:e})=>this.handleOfferResponse(t,e)).catch(t=>this.handleError(t))}handleOfferResponse(t,e){return t&&(this.sessionUrl=t),this.connectionManager.setAnswer(e).then(()=>{if("running"===this.stateStore.get()&&0!==this.queuedCandidates.length){const t=this.connectionManager.getOfferData();t&&this.sessionUrl&&(this.httpClient.sendLocalCandidates(this.sessionUrl,t,this.queuedCandidates),this.queuedCandidates=[])}})}handleCandidate(t){if(this.sessionUrl){const e=this.connectionManager.getOfferData();e&&this.httpClient.sendLocalCandidates(this.sessionUrl,e,[t])}else this.queuedCandidates.push(t)}get paused(){return this.trackManager.paused}pause(){this.trackManager.pause()}resume(){this.trackManager.resume()}updateUrl(t){"closed"!==this.state?(this.conf.url=t,this.cleanupSession(),this.stateStore.set("running"),this.start()):this.emit("error",new i(s.OTHER_ERROR,"Cannot update URL: instance is closed"))}}export{s as ErrorTypes,i as WebRTCError,g as default};
@@ -0,0 +1,115 @@
1
+ import type EventEmitter from 'eventemitter3';
2
+ /**
3
+ * FlowMonitor 配置选项
4
+ */
5
+ export interface FlowMonitorOptions {
6
+ /** 基础检查间隔(毫秒),初始阶段使用 */
7
+ interval: number;
8
+ /** 稳定后的检查间隔(毫秒),默认为 interval 的 2 倍 */
9
+ stableInterval?: number;
10
+ /** 连续无进度的最大次数,超过则判定为断流,默认为 3 */
11
+ maxNoProgress?: number;
12
+ /** 稳定时间(毫秒),超过此时间后降低检查频率,默认为 30000(30秒) */
13
+ stabilizationTime?: number;
14
+ /** 事件发射器 */
15
+ emitter: EventEmitter;
16
+ }
17
+ /**
18
+ * 流量监控器
19
+ *
20
+ * 监控 WebRTC 连接的数据流状态,通过检测接收字节数是否增长来判定是否断流。
21
+ *
22
+ * 性能优化特性:
23
+ * - 自适应轮询:初始阶段高频检查,稳定后降低频率
24
+ * - 容错机制:连续多次无进度才判定断流,避免误判
25
+ * - 资源共享:使用全局调度器,与其他监控器共享计时器
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const flowMonitor = new FlowMonitor({
30
+ * interval: 5000,
31
+ * emitter: this
32
+ * })
33
+ * flowMonitor.setPeerConnection(pc)
34
+ * flowMonitor.start()
35
+ * ```
36
+ */
37
+ export declare class FlowMonitor {
38
+ private options;
39
+ private readonly baseInterval;
40
+ private readonly stableInterval;
41
+ private readonly maxNoProgress;
42
+ private readonly stabilizationTime;
43
+ private lastBytesReceived;
44
+ private isFirstCheck;
45
+ private unregisterMonitor?;
46
+ private pc?;
47
+ private consecutiveNoProgress;
48
+ private startTime;
49
+ private isStable;
50
+ constructor(options: FlowMonitorOptions);
51
+ /**
52
+ * 启动断流检测
53
+ *
54
+ * 重置所有状态并开始监控。如果已有正在运行的检测,会先停止。
55
+ */
56
+ start(pc?: RTCPeerConnection): void;
57
+ /**
58
+ * 停止断流检测并清理资源
59
+ */
60
+ stop(): void;
61
+ /**
62
+ * 重置检测状态
63
+ *
64
+ * 清除计数器和标志,但不停止正在运行的检测。
65
+ * 用于恢复检测时使用。
66
+ */
67
+ reset(): void;
68
+ /**
69
+ * 调度下一次检查
70
+ */
71
+ private scheduleNextCheck;
72
+ /**
73
+ * 执行流状态检查
74
+ */
75
+ private performCheck;
76
+ /**
77
+ * 判断是否应该继续监控
78
+ */
79
+ private shouldContinueMonitoring;
80
+ /**
81
+ * 判断是否应该进行检查
82
+ */
83
+ private shouldCheck;
84
+ /**
85
+ * 计算下次检查间隔
86
+ *
87
+ * 根据经过时间决定使用基础间隔还是稳定间隔。
88
+ */
89
+ private getNextCheckInterval;
90
+ /**
91
+ * 检测流状态(通过接收字节数判断是否断流)
92
+ */
93
+ private checkFlowState;
94
+ /**
95
+ * 获取当前已接收的字节数
96
+ *
97
+ * 优先检测视频流,如果不存在则检测音频流。
98
+ * 如果都不存在,返回 0。
99
+ */
100
+ private getReceivedBytes;
101
+ /**
102
+ * 判断是否有数据增长
103
+ */
104
+ private hasDataProgress;
105
+ /**
106
+ * 处理无进度情况
107
+ *
108
+ * 连续多次无进度时触发断流错误。
109
+ */
110
+ private handleNoProgress;
111
+ /**
112
+ * 触发断流错误
113
+ */
114
+ private triggerStreamError;
115
+ }
@@ -0,0 +1,65 @@
1
+ import type { EventEmitter } from 'eventemitter3';
2
+ /**
3
+ * PlayMonitor 配置选项
4
+ */
5
+ export interface PlayMonitorOptions {
6
+ /** 媒体容器元素 */
7
+ container: HTMLMediaElement;
8
+ /** 事件发射器 */
9
+ emitter: EventEmitter;
10
+ }
11
+ /**
12
+ * 播放监控器
13
+ *
14
+ * 处理媒体元素播放逻辑,包括自动播放、错误处理、
15
+ * 播放验证和停滞检测。
16
+ *
17
+ * 使用全局调度器进行定时检查,与其他监控器共享计时器资源。
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const playMonitor = new PlayMonitor({
22
+ * container: videoElement,
23
+ * emitter: this
24
+ * })
25
+ * await playMonitor.play()
26
+ * ```
27
+ */
28
+ export declare class PlayMonitor {
29
+ private options;
30
+ private lastCurrentTime;
31
+ private unregisterMonitor?;
32
+ constructor(options: PlayMonitorOptions);
33
+ /**
34
+ * 获取媒体容器
35
+ */
36
+ private get container();
37
+ /**
38
+ * 获取事件发射器
39
+ */
40
+ private get emitter();
41
+ /**
42
+ * 尝试播放媒体。通过自动静音并重试来处理自动播放策略。
43
+ * 如果首次尝试失败,会自动静音后重试。
44
+ * 同时验证播放状态并启动停滞监控。
45
+ */
46
+ play(): Promise<void>;
47
+ /**
48
+ * 验证 play() 调用后播放是否真正开始。
49
+ */
50
+ private verifyPlayback;
51
+ /**
52
+ * 启动播放停滞监控。
53
+ *
54
+ * 使用全局调度器,每 5 秒检查一次。
55
+ */
56
+ private startMonitoring;
57
+ /**
58
+ * 停止播放停滞监控。
59
+ */
60
+ stopMonitoring(): void;
61
+ /**
62
+ * 检查播放是否停滞(currentTime 未前进)。
63
+ */
64
+ private checkStalled;
65
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 监控调度器
3
+ *
4
+ * 统一管理所有监控任务的定时器,避免多个定时器造成的资源浪费。
5
+ * 所有监控器共享同一个定时器,按配置的间隔执行各自的检查逻辑。
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const scheduler = new MonitorScheduler()
10
+ * scheduler.register(5000, () => {
11
+ * // 每 5 秒执行一次
12
+ * })
13
+ * ```
14
+ */
15
+ export declare class MonitorScheduler {
16
+ private timer?;
17
+ private tasks;
18
+ private nextTaskId;
19
+ /**
20
+ * 注册一个监控任务
21
+ *
22
+ * @param interval - 检查间隔(毫秒)
23
+ * @param callback - 检查回调函数
24
+ * @returns 取消注册的函数
25
+ */
26
+ register(interval: number, callback: () => void): () => void;
27
+ /**
28
+ * 启动定时器
29
+ *
30
+ * 使用最小的间隔作为定时器间隔,确保所有任务都能按时执行。
31
+ */
32
+ private startTimer;
33
+ /**
34
+ * 停止定时器
35
+ */
36
+ private stopTimer;
37
+ /**
38
+ * 清理所有任务和定时器
39
+ */
40
+ destroy(): void;
41
+ }
42
+ /**
43
+ * 获取全局监控调度器
44
+ *
45
+ * 所有监控器共享同一个调度器实例,节省资源。
46
+ */
47
+ export declare function getGlobalScheduler(): MonitorScheduler;
package/dist/types.d.ts CHANGED
@@ -29,11 +29,16 @@ export interface WhepEvents {
29
29
  }) => void;
30
30
  'candidate': (candidate: RTCIceCandidate) => void;
31
31
  'track': (evt: RTCTrackEvent) => void;
32
- 'play': (payload: {
33
- success: boolean;
34
- reason?: string;
32
+ 'play:success': (payload: {
35
33
  muted: boolean;
36
34
  }) => void;
35
+ 'play:failed': (payload: {
36
+ reason: string;
37
+ muted: boolean;
38
+ }) => void;
39
+ 'play:stalled': (payload: {
40
+ reason: string;
41
+ }) => void;
37
42
  'error': (err: WebRTCError) => void;
38
43
  'close': () => void;
39
44
  'restart': () => void;
package/dist/whep.d.ts CHANGED
@@ -9,7 +9,6 @@ export default class WebRTCWhep extends EventEmitter<WhepEvents> {
9
9
  private sessionUrl?;
10
10
  private queuedCandidates;
11
11
  private nonAdvertisedCodecs;
12
- private flowCheck;
13
12
  private httpClient;
14
13
  private connectionManager;
15
14
  private trackManager;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "whepts",
3
3
  "type": "module",
4
- "version": "1.1.6",
4
+ "version": "1.1.8",
5
5
  "packageManager": "pnpm@10.28.2",
6
6
  "description": "基于 mediamtx 的 WebRTC WHEP 播放器,支持 ZLM 和 Mediamtx 的播放地址",
7
7
  "author": "mapleafgo",
@@ -1,15 +0,0 @@
1
- import type { EventEmitter } from 'eventemitter3';
2
- /**
3
- * PlayController handles media element playback logic including auto-play,
4
- * error handling, and muted retry.
5
- */
6
- export declare class PlayController {
7
- private container;
8
- private eventEmitter;
9
- constructor(container: HTMLMediaElement, eventEmitter: EventEmitter);
10
- /**
11
- * Attempt to play the media. Handles auto-play policy by automatically
12
- * muting and retrying if the first attempt fails.
13
- */
14
- play(): Promise<void>;
15
- }
@@ -1,52 +0,0 @@
1
- import type EventEmitter from 'eventemitter3';
2
- export interface FlowCheckOptions {
3
- interval: number;
4
- stableInterval?: number;
5
- maxNoProgress?: number;
6
- stabilizationTime?: number;
7
- emitter: EventEmitter;
8
- }
9
- /**
10
- * Flow checking logic (断流检测)
11
- *
12
- * 性能优化:自适应轮询机制
13
- * - 初始阶段(stabilizationTime):高频检查(interval)
14
- * - 稳定阶段:降低频率(stableInterval,默认为 interval 的 2 倍)
15
- * - 使用 setTimeout 而非 setInterval,便于动态调整间隔
16
- * - 连续多次(maxNoProgress)无进展才判定断流,避免误判
17
- */
18
- export declare class FlowCheck {
19
- private options;
20
- private baseInterval;
21
- private stableInterval;
22
- private maxNoProgress;
23
- private stabilizationTime;
24
- private lastBytesReceived;
25
- private checkTimer?;
26
- private pc?;
27
- private consecutiveNoProgress;
28
- private startTime;
29
- private isStable;
30
- constructor(options: FlowCheckOptions);
31
- setPeerConnection(pc: RTCPeerConnection): void;
32
- /**
33
- * 启动断流检测
34
- */
35
- start(): void;
36
- /**
37
- * 停止断流检测并清理资源
38
- */
39
- close(): void;
40
- /**
41
- * 调度下一次检查
42
- */
43
- private scheduleNextCheck;
44
- /**
45
- * 计算下次检查间隔
46
- */
47
- private getNextCheckInterval;
48
- /**
49
- * 检测流状态(通过接收字节数判断是否断流)
50
- */
51
- private checkFlowState;
52
- }