whepts 1.1.8 → 1.1.9

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.
@@ -1,4 +1,5 @@
1
1
  import type { EventEmitter } from 'eventemitter3';
2
+ import type { MonitorScheduler } from '~/monitors/scheduler';
2
3
  export declare class TrackManager {
3
4
  private container;
4
5
  private lazyLoad;
@@ -7,7 +8,7 @@ export declare class TrackManager {
7
8
  private showStore;
8
9
  private playMonitor;
9
10
  private flowMonitor;
10
- constructor(container: HTMLMediaElement, eventEmitter: EventEmitter, lazyLoad?: boolean);
11
+ constructor(container: HTMLMediaElement, eventEmitter: EventEmitter, lazyLoad: boolean | undefined, scheduler: MonitorScheduler);
11
12
  onTrack(evt: RTCTrackEvent, pc?: RTCPeerConnection): void;
12
13
  get paused(): boolean;
13
14
  pause(): 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 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};
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))}}var h,l;!function(t){t[t.LOW=0]="LOW",t[t.NORMAL=1]="NORMAL",t[t.HIGH=2]="HIGH",t[t.CRITICAL=3]="CRITICAL"}(h||(h={})),function(t){t.SCHEDULED="scheduled",t.RUNNING="running",t.PAUSED="paused"}(l||(l={}));class p{constructor(t={}){this.tasks=new Map,this.currentTick=0,this.lastTickTime=0,this.throttleLevel=0,this.isRunning=!1,this.baseInterval=t.baseInterval??1e3,this.maxTasksPerTick=t.maxTasksPerTick??10,this.performanceThreshold=t.performanceThreshold??16,this.enableAdaptiveThrottling=t.enableAdaptiveThrottling??!0}register(t){const{interval:e,owner:s,priority:i=h.NORMAL,callback:r}=t,n=`${s}-${e}`,o=Date.now();return this.tasks.set(n,{id:n,interval:e,priority:i,status:l.SCHEDULED,callback:r,lastRun:o-e,nextRun:o}),this.ensureStarted(),()=>{this.tasks.delete(n),0===this.tasks.size&&this.stop()}}pauseByOwner(t){for(const e of this.tasks.values())e.id.startsWith(t)&&e.status===l.SCHEDULED&&(e.status=l.PAUSED)}resumeByOwner(t){const e=Date.now();for(const s of this.tasks.values())s.id.startsWith(t)&&s.status===l.PAUSED&&(s.status=l.SCHEDULED,s.nextRun=e+s.interval)}ensureStarted(){this.isRunning||(this.isRunning=!0,this.lastTickTime=Date.now(),this.timer=setInterval(()=>{this.tick()},this.baseInterval))}stop(){this.timer&&(clearInterval(this.timer),this.timer=void 0),this.isRunning=!1,this.throttleLevel=0}tick(){const t=Date.now(),e=this.lastTickTime>0?t-this.lastTickTime:0;this.lastTickTime=t,this.enableAdaptiveThrottling&&this.updateThrottleLevel(e);const s=this.getTasksToRun(t);0!==s.length&&(this.runTasks(s,t),this.currentTick++)}updateThrottleLevel(t){t>2*this.performanceThreshold?this.throttleLevel=Math.min(this.throttleLevel+1,2):t>this.performanceThreshold?this.throttleLevel=Math.max(this.throttleLevel,1):t<.5*this.performanceThreshold&&(this.throttleLevel=Math.max(this.throttleLevel-1,0))}getTasksToRun(t){const e=[],s=2**this.throttleLevel;for(const i of this.tasks.values()){if(i.status!==l.SCHEDULED)continue;const r=i.interval*s;t-i.lastRun>=r&&e.push(i)}return e.sort((t,e)=>e.priority-t.priority),e}runTasks(t,e){const s=Math.min(t.length,this.maxTasksPerTick);for(let i=0;i<s;i++){const s=t[i];this.runTask(s,e)}}async runTask(t,e){t.status=l.RUNNING;try{await t.callback(),t.lastRun=e,t.nextRun=e+t.interval,t.status=l.SCHEDULED}catch(e){t.status=l.SCHEDULED}}destroy(){this.stop(),this.tasks.clear(),this.currentTick=0}getStatus(){return{taskCount:this.tasks.size,throttleLevel:this.throttleLevel,currentTick:this.currentTick}}}class d{constructor(t,e){this.options=t,this.lastBytesReceived=0,this.isFirstCheck=!0,this.consecutiveNoProgress=0,this.startTime=0,this.scheduler=e,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.scheduleCheck())}stop(){this.unregisterMonitor&&(this.unregisterMonitor(),this.unregisterMonitor=void 0),this.pc=void 0}reset(){this.consecutiveNoProgress=0,this.isFirstCheck=!0,this.lastBytesReceived=0}scheduleCheck(){this.unregisterMonitor&&this.unregisterMonitor();const t=this.getNextCheckInterval();this.unregisterMonitor=this.scheduler.register({interval:t,owner:"flow-monitor",priority:h.HIGH,callback:()=>this.performCheck()})}performCheck(){this.shouldCheck()&&(this.checkFlowState().catch(()=>{}),this.shouldContinueMonitoring()&&this.scheduleCheck())}shouldContinueMonitoring(){return!(!this.pc||"connected"!==this.pc.connectionState)}shouldCheck(){return!(!this.pc||"connected"!==this.pc.connectionState)}getNextCheckInterval(){return Date.now()-this.startTime>this.stabilizationTime?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,e){this.options=t,this.lastCurrentTime=0,this.scheduler=e}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=this.scheduler.register({interval:5e3,owner:"play-monitor",priority:h.NORMAL,callback:()=>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,r){this.container=t,this.lazyLoad=i,this.showStore=e(!1),this.playMonitor=new u({container:t,emitter:s},r),this.flowMonitor=new d({interval:5e3,emitter:s},r),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,this.observer&&(this.observer.disconnect(),this.observer=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.scheduler=new p,this.trackManager=new m(this.conf.container,this,this.conf.lazyLoad,this.scheduler),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.scheduler.destroy(),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};
@@ -1,4 +1,5 @@
1
1
  import type EventEmitter from 'eventemitter3';
2
+ import type { MonitorScheduler } from './scheduler';
2
3
  /**
3
4
  * FlowMonitor 配置选项
4
5
  */
@@ -22,17 +23,7 @@ export interface FlowMonitorOptions {
22
23
  * 性能优化特性:
23
24
  * - 自适应轮询:初始阶段高频检查,稳定后降低频率
24
25
  * - 容错机制:连续多次无进度才判定断流,避免误判
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
- * ```
26
+ * - 持续监控:即使视频不可见,也保持监控以确保连接健康
36
27
  */
37
28
  export declare class FlowMonitor {
38
29
  private options;
@@ -40,18 +31,16 @@ export declare class FlowMonitor {
40
31
  private readonly stableInterval;
41
32
  private readonly maxNoProgress;
42
33
  private readonly stabilizationTime;
34
+ private readonly scheduler;
43
35
  private lastBytesReceived;
44
36
  private isFirstCheck;
45
37
  private unregisterMonitor?;
46
38
  private pc?;
47
39
  private consecutiveNoProgress;
48
40
  private startTime;
49
- private isStable;
50
- constructor(options: FlowMonitorOptions);
41
+ constructor(options: FlowMonitorOptions, scheduler: MonitorScheduler);
51
42
  /**
52
43
  * 启动断流检测
53
- *
54
- * 重置所有状态并开始监控。如果已有正在运行的检测,会先停止。
55
44
  */
56
45
  start(pc?: RTCPeerConnection): void;
57
46
  /**
@@ -60,15 +49,12 @@ export declare class FlowMonitor {
60
49
  stop(): void;
61
50
  /**
62
51
  * 重置检测状态
63
- *
64
- * 清除计数器和标志,但不停止正在运行的检测。
65
- * 用于恢复检测时使用。
66
52
  */
67
53
  reset(): void;
68
54
  /**
69
- * 调度下一次检查
55
+ * 调度检查任务
70
56
  */
71
- private scheduleNextCheck;
57
+ private scheduleCheck;
72
58
  /**
73
59
  * 执行流状态检查
74
60
  */
@@ -83,8 +69,6 @@ export declare class FlowMonitor {
83
69
  private shouldCheck;
84
70
  /**
85
71
  * 计算下次检查间隔
86
- *
87
- * 根据经过时间决定使用基础间隔还是稳定间隔。
88
72
  */
89
73
  private getNextCheckInterval;
90
74
  /**
@@ -93,9 +77,6 @@ export declare class FlowMonitor {
93
77
  private checkFlowState;
94
78
  /**
95
79
  * 获取当前已接收的字节数
96
- *
97
- * 优先检测视频流,如果不存在则检测音频流。
98
- * 如果都不存在,返回 0。
99
80
  */
100
81
  private getReceivedBytes;
101
82
  /**
@@ -104,8 +85,6 @@ export declare class FlowMonitor {
104
85
  private hasDataProgress;
105
86
  /**
106
87
  * 处理无进度情况
107
- *
108
- * 连续多次无进度时触发断流错误。
109
88
  */
110
89
  private handleNoProgress;
111
90
  /**
@@ -1,4 +1,5 @@
1
1
  import type { EventEmitter } from 'eventemitter3';
2
+ import type { MonitorScheduler } from './scheduler';
2
3
  /**
3
4
  * PlayMonitor 配置选项
4
5
  */
@@ -14,22 +15,14 @@ export interface PlayMonitorOptions {
14
15
  * 处理媒体元素播放逻辑,包括自动播放、错误处理、
15
16
  * 播放验证和停滞检测。
16
17
  *
17
- * 使用全局调度器进行定时检查,与其他监控器共享计时器资源。
18
- *
19
- * @example
20
- * ```ts
21
- * const playMonitor = new PlayMonitor({
22
- * container: videoElement,
23
- * emitter: this
24
- * })
25
- * await playMonitor.play()
26
- * ```
18
+ * 使用调度器进行定时检查,与其他监控器共享计时器资源。
27
19
  */
28
20
  export declare class PlayMonitor {
29
21
  private options;
30
22
  private lastCurrentTime;
31
23
  private unregisterMonitor?;
32
- constructor(options: PlayMonitorOptions);
24
+ private readonly scheduler;
25
+ constructor(options: PlayMonitorOptions, scheduler: MonitorScheduler);
33
26
  /**
34
27
  * 获取媒体容器
35
28
  */
@@ -39,9 +32,7 @@ export declare class PlayMonitor {
39
32
  */
40
33
  private get emitter();
41
34
  /**
42
- * 尝试播放媒体。通过自动静音并重试来处理自动播放策略。
43
- * 如果首次尝试失败,会自动静音后重试。
44
- * 同时验证播放状态并启动停滞监控。
35
+ * 尝试播放媒体
45
36
  */
46
37
  play(): Promise<void>;
47
38
  /**
@@ -49,17 +40,15 @@ export declare class PlayMonitor {
49
40
  */
50
41
  private verifyPlayback;
51
42
  /**
52
- * 启动播放停滞监控。
53
- *
54
- * 使用全局调度器,每 5 秒检查一次。
43
+ * 启动播放停滞监控
55
44
  */
56
45
  private startMonitoring;
57
46
  /**
58
- * 停止播放停滞监控。
47
+ * 停止播放停滞监控
59
48
  */
60
49
  stopMonitoring(): void;
61
50
  /**
62
- * 检查播放是否停滞(currentTime 未前进)。
51
+ * 检查播放是否停滞
63
52
  */
64
53
  private checkStalled;
65
54
  }
@@ -1,47 +1,119 @@
1
1
  /**
2
- * 监控调度器
2
+ * 任务优先级
3
+ */
4
+ export declare enum TaskPriority {
5
+ LOW = 0,
6
+ NORMAL = 1,
7
+ HIGH = 2,
8
+ CRITICAL = 3
9
+ }
10
+ /**
11
+ * 监控调度器配置
12
+ */
13
+ export interface SchedulerOptions {
14
+ /** 基础调度间隔(毫秒),默认 1000ms */
15
+ baseInterval?: number;
16
+ /** 单次调度最大执行任务数,默认 10 */
17
+ maxTasksPerTick?: number;
18
+ /** 性能阈值:如果一帧执行超过此时间(毫秒),启用自适应降频,默认 16ms */
19
+ performanceThreshold?: number;
20
+ /** 是否启用自适应降频,默认 true */
21
+ enableAdaptiveThrottling?: boolean;
22
+ }
23
+ /**
24
+ * 任务注册选项
25
+ */
26
+ export interface RegisterTaskOptions {
27
+ /** 执行间隔(毫秒) */
28
+ interval: number;
29
+ /** 任务所有者标识(用于批量暂停/恢复) */
30
+ owner: string;
31
+ /** 任务优先级,默认 NORMAL */
32
+ priority?: TaskPriority;
33
+ /** 执行回调 */
34
+ callback: () => void | Promise<void>;
35
+ }
36
+ /**
37
+ * 高性能监控调度器
3
38
  *
4
- * 统一管理所有监控任务的定时器,避免多个定时器造成的资源浪费。
5
- * 所有监控器共享同一个定时器,按配置的间隔执行各自的检查逻辑。
39
+ * 轻量级设计,专为播放器内部少量监控任务优化。
6
40
  *
7
- * @example
8
- * ```ts
9
- * const scheduler = new MonitorScheduler()
10
- * scheduler.register(5000, () => {
11
- * // 5 秒执行一次
12
- * })
13
- * ```
41
+ * 核心特性:
42
+ * - 优先级队列:高优先级任务优先执行
43
+ * - 智能降频:检测性能压力,自动降低全局频率
44
+ * - 防重复注册:相同 owner + interval 自动替换旧任务
45
+ * - 暂停/恢复:支持按 owner 批量暂停任务
14
46
  */
15
47
  export declare class MonitorScheduler {
16
48
  private timer?;
17
49
  private tasks;
18
- private nextTaskId;
50
+ private currentTick;
51
+ private lastTickTime;
52
+ private throttleLevel;
53
+ private isRunning;
54
+ private readonly baseInterval;
55
+ private readonly maxTasksPerTick;
56
+ private readonly performanceThreshold;
57
+ private readonly enableAdaptiveThrottling;
58
+ constructor(options?: SchedulerOptions);
59
+ /**
60
+ * 注册任务
61
+ *
62
+ * 如果已存在相同 owner 的任务,会自动替换。
63
+ *
64
+ * @returns 取消注册函数
65
+ */
66
+ register(options: RegisterTaskOptions): () => void;
67
+ /**
68
+ * 暂停指定 owner 的所有任务
69
+ */
70
+ pauseByOwner(owner: string): void;
19
71
  /**
20
- * 注册一个监控任务
72
+ * 恢复指定 owner 的所有任务
73
+ */
74
+ resumeByOwner(owner: string): void;
75
+ /**
76
+ * 启动调度器
77
+ */
78
+ private ensureStarted;
79
+ /**
80
+ * 停止调度器
81
+ */
82
+ private stop;
83
+ /**
84
+ * 主调度循环
85
+ */
86
+ private tick;
87
+ /**
88
+ * 更新降频级别
89
+ */
90
+ private updateThrottleLevel;
91
+ /**
92
+ * 获取当前需要执行的任务
21
93
  *
22
- * @param interval - 检查间隔(毫秒)
23
- * @param callback - 检查回调函数
24
- * @returns 取消注册的函数
94
+ * 按优先级排序,并考虑降频级别
25
95
  */
26
- register(interval: number, callback: () => void): () => void;
96
+ private getTasksToRun;
27
97
  /**
28
- * 启动定时器
98
+ * 执行任务列表
29
99
  *
30
- * 使用最小的间隔作为定时器间隔,确保所有任务都能按时执行。
100
+ * 限制每次执行的任务数量,避免阻塞
31
101
  */
32
- private startTimer;
102
+ private runTasks;
33
103
  /**
34
- * 停止定时器
104
+ * 执行单个任务
35
105
  */
36
- private stopTimer;
106
+ private runTask;
37
107
  /**
38
- * 清理所有任务和定时器
108
+ * 清理所有任务
39
109
  */
40
110
  destroy(): void;
111
+ /**
112
+ * 获取调度器状态(用于调试)
113
+ */
114
+ getStatus(): {
115
+ taskCount: number;
116
+ throttleLevel: number;
117
+ currentTick: number;
118
+ };
41
119
  }
42
- /**
43
- * 获取全局监控调度器
44
- *
45
- * 所有监控器共享同一个调度器实例,节省资源。
46
- */
47
- export declare function getGlobalScheduler(): MonitorScheduler;
package/dist/whep.d.ts CHANGED
@@ -9,6 +9,7 @@ export default class WebRTCWhep extends EventEmitter<WhepEvents> {
9
9
  private sessionUrl?;
10
10
  private queuedCandidates;
11
11
  private nonAdvertisedCodecs;
12
+ private scheduler;
12
13
  private httpClient;
13
14
  private connectionManager;
14
15
  private trackManager;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "whepts",
3
3
  "type": "module",
4
- "version": "1.1.8",
4
+ "version": "1.1.9",
5
5
  "packageManager": "pnpm@10.28.2",
6
6
  "description": "基于 mediamtx 的 WebRTC WHEP 播放器,支持 ZLM 和 Mediamtx 的播放地址",
7
7
  "author": "mapleafgo",