whepts 1.1.2 → 1.1.3

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,10 +1,10 @@
1
1
  export declare class TrackManager {
2
2
  private container;
3
3
  private stream?;
4
- private observer?;
4
+ private observer;
5
+ private showStore;
5
6
  constructor(container: HTMLMediaElement);
6
7
  onTrack(evt: RTCTrackEvent): void;
7
- private stopObserver;
8
8
  get paused(): boolean;
9
9
  pause(): void;
10
10
  resume(): void;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import e from"eventemitter3";import{atom as t}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(e,t,s){super(t,s),this.type=e}}class i{static async supportsNonAdvertisedCodec(e,t){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(` ${e}`))throw new Error("already present");const a=s.sdp.split(`m=${r}`),c=a.slice(1).map(e=>e.split("\r\n")[0].split(" ").slice(3)).reduce((e,t)=>[...e,...t],[]);o=i.reservePayloadType(c);const h=a[1].split("\r\n");return h[0]+=` ${o}`,h.splice(h.length-1,0,`a=rtpmap:${o} ${e}`),void 0!==t&&h.splice(h.length-1,0,`a=fmtp:${o} ${t}`),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} ${e}\r\n${void 0!==t?`a=fmtp:${o} ${t}\r\n`:""}`}))).then(()=>s(!0)).catch(()=>s(!1)).finally(()=>n.close())})}static unquoteCredential(e){return JSON.parse(`"${e}"`)}static linkToIceServers(e){return e?e.split(", ").map(e=>{const t=e.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);if(!t)throw new n(s.SIGNAL_ERROR,"Invalid ICE server link format");const r={urls:[t[1]]};return t[3]&&(r.username=i.unquoteCredential(t[3]),r.credential=i.unquoteCredential(t[4]),r.credentialType="password"),r}):[]}static reservePayloadType(e){for(let t=30;t<=127;t++)if((t<=63||t>=96)&&!e.includes(t.toString())){const s=t.toString();return e.push(s),s}throw new Error("unable to find a free payload type")}}class r{constructor(e){this.options=e}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(e=>i.supportsNonAdvertisedCodec(e[0],e[1]).then(t=>!!t&&e[0]))).then(e=>e.filter(e=>!1!==e)).then(e=>{if("getting_codecs"!==this.options.getState())throw new n(s.STATE_ERROR,"closed");this.options.emitter.emit("codecs:detected",e)}).catch(e=>this.options.emitter.emit("error",e))}}class o{static parseOffer(e){const t={iceUfrag:"",icePwd:"",medias:[]};for(const s of e.split("\r\n"))s.startsWith("m=")?t.medias.push(s.slice(2)):""===t.iceUfrag&&s.startsWith("a=ice-ufrag:")?t.iceUfrag=s.slice(12):""===t.icePwd&&s.startsWith("a=ice-pwd:")&&(t.icePwd=s.slice(10));return t}static reservePayloadType(e){for(let t=30;t<=127;t++)if((t<=63||t>=96)&&!e.includes(t.toString())){const s=t.toString();return e.push(s),s}throw new Error("unable to find a free payload type")}static enableStereoPcmau(e,t){const s=t.split("\r\n");let n=o.reservePayloadType(e);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(e),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(e,t){const s=t.split("\r\n");let n=o.reservePayloadType(e);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(e),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(e),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(e),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(e),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(e),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(e,t){const s=t.split("\r\n");let n=o.reservePayloadType(e);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(e),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(e),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(e){let t="";const s=e.split("\r\n");for(let e=0;e<s.length;e++)if(s[e].startsWith("a=rtpmap:")&&s[e].toLowerCase().includes("opus/")){t=s[e].slice(9).split(" ")[0];break}if(""===t)return e;for(let e=0;e<s.length;e++)s[e].startsWith(`a=fmtp:${t} `)&&(s[e].includes("stereo")||(s[e]+=";stereo=1"),s[e].includes("sprop-stereo")||(s[e]+=";sprop-stereo=1"));return s.join("\r\n")}static editOffer(e,t){const s=e.split("m="),n=s.slice(1).map(e=>e.split("\r\n")[0].split(" ").slice(3)).reduce((e,t)=>[...e,...t],[]);for(let e=1;e<s.length;e++)if(s[e].startsWith("audio")){s[e]=o.enableStereoOpus(s[e]),t.includes("pcma/8000/2")&&(s[e]=o.enableStereoPcmau(n,s[e])),t.includes("multiopus/48000/6")&&(s[e]=o.enableMultichannelOpus(n,s[e])),t.includes("L16/48000/2")&&(s[e]=o.enableL16(n,s[e]));break}return s.join("m=")}static generateSdpFragment(e,t){const s={};for(const e of t){const t=e.sdpMLineIndex;t&&(void 0===s[t]&&(s[t]=[]),s[t].push(e))}let n=`a=ice-ufrag:${e.iceUfrag}\r\na=ice-pwd:${e.icePwd}\r\n`,i=0;for(const t of e.medias){if(void 0!==s[i]){n+=`m=${t}\r\na=mid:${i}\r\n`;for(const e of s[i])n+=`a=${e.candidate}\r\n`}i++}return n}}class a{constructor(e){this.options=e}async setupPeerConnection(e){if("running"!==this.options.getState())throw new n(s.STATE_ERROR,"closed");const t=new RTCPeerConnection({iceServers:e,sdpSemantics:"unified-plan"});this.pc=t;const i="recvonly";return t.addTransceiver("video",{direction:i}),t.addTransceiver("audio",{direction:i}),t.onicecandidate=e=>this.onLocalCandidate(e),t.onconnectionstatechange=()=>this.onConnectionState(),t.oniceconnectionstatechange=()=>this.onIceConnectionState(),t.ontrack=e=>this.options.emitter.emit("track",e),t.createOffer().then(e=>{if(!e.sdp)throw new n(s.SIGNAL_ERROR,"Failed to create offer SDP");return e.sdp=o.editOffer(e.sdp,this.options.getNonAdvertisedCodecs()),this.offerData=o.parseOffer(e.sdp),t.setLocalDescription(e).then(()=>e.sdp)})}async setAnswer(e){if("running"!==this.options.getState())throw new n(s.STATE_ERROR,"closed");return this.pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e}))}getPeerConnection(){return this.pc}getOfferData(){return this.offerData}close(){this.pc?.close(),this.pc=void 0,this.offerData=void 0}onLocalCandidate(e){"running"===this.options.getState()&&e.candidate&&this.options.emitter.emit("candidate",e.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(e){this.options=e}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(e=>i.linkToIceServers(e.headers.get("Link")))}async sendOffer(e){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:e}).then(e=>{switch(e.status){case 201:break;case 404:case 406:throw new n(s.NOT_FOUND_ERROR,"stream not found");case 400:return e.json().then(e=>{throw new n(s.REQUEST_ERROR,e.error)});default:throw new n(s.REQUEST_ERROR,`bad status code ${e.status}`)}const t=e.headers.get("Location"),i=t?new URL(t,this.options.conf.url).toString():void 0;return e.text().then(e=>({sessionUrl:i,answer:e}))})}sendLocalCandidates(e,t,i){fetch(e,{method:"PATCH",headers:{"Content-Type":"application/trickle-ice-sdpfrag","If-Match":"*"},body:o.generateSdpFragment(t,i)}).then(e=>{switch(e.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 ${e.status}`)}}).catch(e=>this.options.emitter.emit("error",e))}}class h{constructor(e){this.container=e,this.observer=new IntersectionObserver(([e])=>{e.isIntersecting?this.resume():this.pause()},{threshold:.5}),this.observer.observe(this.container)}onTrack(e){this.stream=e.streams[0],this.paused||(this.container.srcObject=this.stream)}stopObserver(){this.observer&&(this.observer.disconnect(),this.observer=void 0)}get paused(){return null===this.container.srcObject}pause(){this.container.srcObject=null}resume(){this.stream&&this.paused&&(this.container.srcObject=this.stream)}stop(){this.stopObserver(),this.stream=void 0}}class p{constructor(e){this.options=e,this.lastBytesReceived=0,this.consecutiveNoProgress=0,this.startTime=0,this.isStable=!1,this.baseInterval=e.interval,this.stableInterval=e.stableInterval||2*e.interval,this.maxNoProgress=e.maxNoProgress||3,this.stabilizationTime=e.stabilizationTime||3e4}setPeerConnection(e){this.pc=e}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 e=this.getNextCheckInterval();this.checkTimer=setTimeout(()=>{this.checkFlowState().then(()=>{this.pc&&"connected"===this.pc.connectionState&&this.scheduleNextCheck()})},e)}getNextCheckInterval(){const e=Date.now()-this.startTime;return this.isStable=e>this.stabilizationTime,this.isStable?this.stableInterval:this.baseInterval}async checkFlowState(){if(!this.pc)return;if("connected"!==this.pc.connectionState)return;const e=await this.pc.getStats();let t=0;if(e.forEach(e=>{const s=e;"inbound-rtp"===e.type&&"video"===s.kind&&(t=s.bytesReceived||0)}),t===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=t}}class l extends e{constructor(e){super(),this.retryPause=2e3,this.stateStore=t("getting_codecs"),this.queuedCandidates=[],this.nonAdvertisedCodecs=[],this.conf=e,this.stateStore.subscribe((e,t)=>{this.emit("state:change",{from:t,to:e})}),this.trackManager=new h(this.conf.container),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",e=>{this.handleCodecsDetected(e)}),this.on("candidate",e=>this.handleCandidate(e)),this.on("track",e=>{this.trackManager.onTrack(e),this.flowCheck.start()}),this.codecDetector.detect()}get state(){return this.stateStore.get()}get isRunning(){return"running"===this.state}close(){this.stateStore.set("closed"),this.connectionManager.close(),this.trackManager.stop(),this.flowCheck.close(),this.restartTimeout&&clearTimeout(this.restartTimeout),this.emit("close")}cleanupSession(){this.connectionManager.close(),this.flowCheck.close(),this.queuedCandidates=[],this.sessionUrl&&(fetch(this.sessionUrl,{method:"DELETE"}).catch(()=>{}),this.sessionUrl=void 0)}handleError(e){this.flowCheck.close(),"getting_codecs"===this.stateStore.get()||e instanceof n&&[s.SIGNAL_ERROR,s.NOT_FOUND_ERROR,s.REQUEST_ERROR].includes(e.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),e.message=`${e.message}, retrying in some seconds`),e instanceof n?this.emit("error",e):this.emit("error",new n(s.OTHER_ERROR,e.message))}handleCodecsDetected(e){this.nonAdvertisedCodecs=e,this.stateStore.set("running"),this.start()}start(){this.httpClient.requestICEServers().then(e=>this.connectionManager.setupPeerConnection(e)).then(e=>{const t=this.connectionManager.getPeerConnection();return t&&this.flowCheck.setPeerConnection(t),e}).then(e=>this.httpClient.sendOffer(e)).then(({sessionUrl:e,answer:t})=>this.handleOfferResponse(e,t)).catch(e=>this.handleError(e))}handleOfferResponse(e,t){return e&&(this.sessionUrl=e),this.connectionManager.setAnswer(t).then(()=>{if("running"===this.stateStore.get()&&0!==this.queuedCandidates.length){const e=this.connectionManager.getOfferData();e&&this.sessionUrl&&(this.httpClient.sendLocalCandidates(this.sessionUrl,e,this.queuedCandidates),this.queuedCandidates=[])}})}handleCandidate(e){if(this.sessionUrl){const t=this.connectionManager.getOfferData();t&&this.httpClient.sendLocalCandidates(this.sessionUrl,t,[e])}else this.queuedCandidates.push(e)}get paused(){return this.trackManager.paused}pause(){this.trackManager.pause()}resume(){this.trackManager.resume()}updateUrl(e){"closed"!==this.stateStore.get()?(this.conf.url=e,this.restartTimeout&&(clearTimeout(this.restartTimeout),this.restartTimeout=void 0),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,l as default};
1
+ import e from"eventemitter3";import{atom as t}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(e,t,s){super(t,s),this.type=e}}class i{static async supportsNonAdvertisedCodec(e,t){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(` ${e}`))throw new Error("already present");const a=s.sdp.split(`m=${r}`),c=a.slice(1).map(e=>e.split("\r\n")[0].split(" ").slice(3)).reduce((e,t)=>[...e,...t],[]);o=i.reservePayloadType(c);const h=a[1].split("\r\n");return h[0]+=` ${o}`,h.splice(h.length-1,0,`a=rtpmap:${o} ${e}`),void 0!==t&&h.splice(h.length-1,0,`a=fmtp:${o} ${t}`),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} ${e}\r\n${void 0!==t?`a=fmtp:${o} ${t}\r\n`:""}`}))).then(()=>s(!0)).catch(()=>s(!1)).finally(()=>n.close())})}static unquoteCredential(e){return JSON.parse(`"${e}"`)}static linkToIceServers(e){return e?e.split(", ").map(e=>{const t=e.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);if(!t)throw new n(s.SIGNAL_ERROR,"Invalid ICE server link format");const r={urls:[t[1]]};return t[3]&&(r.username=i.unquoteCredential(t[3]),r.credential=i.unquoteCredential(t[4]),r.credentialType="password"),r}):[]}static reservePayloadType(e){for(let t=30;t<=127;t++)if((t<=63||t>=96)&&!e.includes(t.toString())){const s=t.toString();return e.push(s),s}throw new Error("unable to find a free payload type")}}class r{constructor(e){this.options=e}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(e=>i.supportsNonAdvertisedCodec(e[0],e[1]).then(t=>!!t&&e[0]))).then(e=>e.filter(e=>!1!==e)).then(e=>{if("getting_codecs"!==this.options.getState())throw new n(s.STATE_ERROR,"closed");this.options.emitter.emit("codecs:detected",e)}).catch(e=>this.options.emitter.emit("error",e))}}class o{static parseOffer(e){const t={iceUfrag:"",icePwd:"",medias:[]};for(const s of e.split("\r\n"))s.startsWith("m=")?t.medias.push(s.slice(2)):""===t.iceUfrag&&s.startsWith("a=ice-ufrag:")?t.iceUfrag=s.slice(12):""===t.icePwd&&s.startsWith("a=ice-pwd:")&&(t.icePwd=s.slice(10));return t}static reservePayloadType(e){for(let t=30;t<=127;t++)if((t<=63||t>=96)&&!e.includes(t.toString())){const s=t.toString();return e.push(s),s}throw new Error("unable to find a free payload type")}static enableStereoPcmau(e,t){const s=t.split("\r\n");let n=o.reservePayloadType(e);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(e),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(e,t){const s=t.split("\r\n");let n=o.reservePayloadType(e);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(e),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(e),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(e),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(e),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(e),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(e,t){const s=t.split("\r\n");let n=o.reservePayloadType(e);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(e),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(e),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(e){let t="";const s=e.split("\r\n");for(let e=0;e<s.length;e++)if(s[e].startsWith("a=rtpmap:")&&s[e].toLowerCase().includes("opus/")){t=s[e].slice(9).split(" ")[0];break}if(""===t)return e;for(let e=0;e<s.length;e++)s[e].startsWith(`a=fmtp:${t} `)&&(s[e].includes("stereo")||(s[e]+=";stereo=1"),s[e].includes("sprop-stereo")||(s[e]+=";sprop-stereo=1"));return s.join("\r\n")}static editOffer(e,t){const s=e.split("m="),n=s.slice(1).map(e=>e.split("\r\n")[0].split(" ").slice(3)).reduce((e,t)=>[...e,...t],[]);for(let e=1;e<s.length;e++)if(s[e].startsWith("audio")){s[e]=o.enableStereoOpus(s[e]),t.includes("pcma/8000/2")&&(s[e]=o.enableStereoPcmau(n,s[e])),t.includes("multiopus/48000/6")&&(s[e]=o.enableMultichannelOpus(n,s[e])),t.includes("L16/48000/2")&&(s[e]=o.enableL16(n,s[e]));break}return s.join("m=")}static generateSdpFragment(e,t){const s={};for(const e of t){const t=e.sdpMLineIndex;t&&(void 0===s[t]&&(s[t]=[]),s[t].push(e))}let n=`a=ice-ufrag:${e.iceUfrag}\r\na=ice-pwd:${e.icePwd}\r\n`,i=0;for(const t of e.medias){if(void 0!==s[i]){n+=`m=${t}\r\na=mid:${i}\r\n`;for(const e of s[i])n+=`a=${e.candidate}\r\n`}i++}return n}}class a{constructor(e){this.options=e}async setupPeerConnection(e){if("running"!==this.options.getState())throw new n(s.STATE_ERROR,"closed");const t=new RTCPeerConnection({iceServers:e,sdpSemantics:"unified-plan"});this.pc=t;const i="recvonly";return t.addTransceiver("video",{direction:i}),t.addTransceiver("audio",{direction:i}),t.onicecandidate=e=>this.onLocalCandidate(e),t.onconnectionstatechange=()=>this.onConnectionState(),t.oniceconnectionstatechange=()=>this.onIceConnectionState(),t.ontrack=e=>this.options.emitter.emit("track",e),t.createOffer().then(e=>{if(!e.sdp)throw new n(s.SIGNAL_ERROR,"Failed to create offer SDP");return e.sdp=o.editOffer(e.sdp,this.options.getNonAdvertisedCodecs()),this.offerData=o.parseOffer(e.sdp),t.setLocalDescription(e).then(()=>e.sdp)})}async setAnswer(e){if("running"!==this.options.getState())throw new n(s.STATE_ERROR,"closed");return this.pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e}))}getPeerConnection(){return this.pc}getOfferData(){return this.offerData}close(){this.pc?.close(),this.pc=void 0,this.offerData=void 0}onLocalCandidate(e){"running"===this.options.getState()&&e.candidate&&this.options.emitter.emit("candidate",e.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(e){this.options=e}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(e=>i.linkToIceServers(e.headers.get("Link")))}async sendOffer(e){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:e}).then(e=>{switch(e.status){case 201:break;case 404:case 406:throw new n(s.NOT_FOUND_ERROR,"stream not found");case 400:return e.json().then(e=>{throw new n(s.REQUEST_ERROR,e.error)});default:throw new n(s.REQUEST_ERROR,`bad status code ${e.status}`)}const t=e.headers.get("Location"),i=t?new URL(t,this.options.conf.url).toString():void 0;return e.text().then(e=>({sessionUrl:i,answer:e}))})}sendLocalCandidates(e,t,i){fetch(e,{method:"PATCH",headers:{"Content-Type":"application/trickle-ice-sdpfrag","If-Match":"*"},body:o.generateSdpFragment(t,i)}).then(e=>{switch(e.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 ${e.status}`)}}).catch(e=>this.options.emitter.emit("error",e))}}class h{constructor(e){this.container=e,this.showStore=t(!1),this.showStore.subscribe(e=>{e?this.resume():this.pause()}),this.observer=new IntersectionObserver(([e])=>{e.isIntersecting?this.showStore.set(!0):this.showStore.set(!1)},{threshold:.5}),this.observer.observe(this.container)}onTrack(e){this.stream=e.streams[0],this.showStore.value&&(this.container.srcObject=this.stream)}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 l{constructor(e){this.options=e,this.lastBytesReceived=0,this.consecutiveNoProgress=0,this.startTime=0,this.isStable=!1,this.baseInterval=e.interval,this.stableInterval=e.stableInterval||2*e.interval,this.maxNoProgress=e.maxNoProgress||3,this.stabilizationTime=e.stabilizationTime||3e4}setPeerConnection(e){this.pc=e}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 e=this.getNextCheckInterval();this.checkTimer=setTimeout(()=>{this.checkFlowState().then(()=>{this.pc&&"connected"===this.pc.connectionState&&this.scheduleNextCheck()})},e)}getNextCheckInterval(){const e=Date.now()-this.startTime;return this.isStable=e>this.stabilizationTime,this.isStable?this.stableInterval:this.baseInterval}async checkFlowState(){if(!this.pc)return;if("connected"!==this.pc.connectionState)return;const e=await this.pc.getStats();let t=0;if(e.forEach(e=>{const s=e;"inbound-rtp"===e.type&&"video"===s.kind&&(t=s.bytesReceived||0)}),t===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=t}}class p extends e{constructor(e){super(),this.retryPause=2e3,this.stateStore=t("getting_codecs"),this.queuedCandidates=[],this.nonAdvertisedCodecs=[],this.conf=e,this.stateStore.subscribe((e,t)=>{this.emit("state:change",{from:t,to:e})}),this.trackManager=new h(this.conf.container),this.flowCheck=new l({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",e=>{this.handleCodecsDetected(e)}),this.on("candidate",e=>this.handleCandidate(e)),this.on("track",e=>{this.trackManager.onTrack(e),this.flowCheck.start()}),this.codecDetector.detect()}get state(){return this.stateStore.get()}get isRunning(){return"running"===this.state}close(){this.stateStore.set("closed"),this.connectionManager.close(),this.trackManager.stop(),this.flowCheck.close(),this.restartTimeout&&clearTimeout(this.restartTimeout),this.emit("close")}cleanupSession(){this.connectionManager.close(),this.flowCheck.close(),this.queuedCandidates=[],this.sessionUrl&&(fetch(this.sessionUrl,{method:"DELETE"}).catch(()=>{}),this.sessionUrl=void 0)}handleError(e){this.flowCheck.close(),"getting_codecs"===this.stateStore.get()||e instanceof n&&[s.SIGNAL_ERROR,s.NOT_FOUND_ERROR,s.REQUEST_ERROR].includes(e.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),e.message=`${e.message}, retrying in some seconds`),e instanceof n?this.emit("error",e):this.emit("error",new n(s.OTHER_ERROR,e.message))}handleCodecsDetected(e){this.nonAdvertisedCodecs=e,this.stateStore.set("running"),this.start()}start(){this.httpClient.requestICEServers().then(e=>this.connectionManager.setupPeerConnection(e)).then(e=>{const t=this.connectionManager.getPeerConnection();return t&&this.flowCheck.setPeerConnection(t),e}).then(e=>this.httpClient.sendOffer(e)).then(({sessionUrl:e,answer:t})=>this.handleOfferResponse(e,t)).catch(e=>this.handleError(e))}handleOfferResponse(e,t){return e&&(this.sessionUrl=e),this.connectionManager.setAnswer(t).then(()=>{if("running"===this.stateStore.get()&&0!==this.queuedCandidates.length){const e=this.connectionManager.getOfferData();e&&this.sessionUrl&&(this.httpClient.sendLocalCandidates(this.sessionUrl,e,this.queuedCandidates),this.queuedCandidates=[])}})}handleCandidate(e){if(this.sessionUrl){const t=this.connectionManager.getOfferData();t&&this.httpClient.sendLocalCandidates(this.sessionUrl,t,[e])}else this.queuedCandidates.push(e)}get paused(){return this.trackManager.paused}pause(){this.trackManager.pause()}resume(){this.trackManager.resume()}updateUrl(e){"closed"!==this.stateStore.get()?(this.conf.url=e,this.restartTimeout&&(clearTimeout(this.restartTimeout),this.restartTimeout=void 0),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,p as default};
package/dist/types.d.ts CHANGED
@@ -28,6 +28,7 @@ export interface WhepEvents {
28
28
  'candidate': (candidate: RTCIceCandidate) => void;
29
29
  'track': (evt: RTCTrackEvent) => void;
30
30
  'error': (err: WebRTCError) => void;
31
+ 'observer': (isIntersecting: boolean) => void;
31
32
  'close': () => void;
32
33
  'restart': () => void;
33
34
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "whepts",
3
3
  "type": "module",
4
- "version": "1.1.2",
4
+ "version": "1.1.3",
5
5
  "packageManager": "pnpm@10.28.2",
6
6
  "description": "基于 mediamtx 的 WebRTC WHEP 播放器,支持 ZLM 和 Mediamtx 的播放地址",
7
7
  "author": "mapleafgo",