UNPKG

foxconn-yumeeting-sdk-js

Version:

A js sdk for ion yumeeting

260 lines (259 loc) 10.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Transport = void 0; const stream_1 = require("./stream"); const API_CHANNEL = 'ion-sfu'; const ERR_NO_SESSION = 'no active session, join first'; var Role; (function (Role) { Role[Role["pub"] = 0] = "pub"; Role[Role["sub"] = 1] = "sub"; })(Role || (Role = {})); class Transport { constructor(role, signal, config) { this.signal = signal; this.pc = new RTCPeerConnection(config); this.candidates = []; if (role === Role.pub) { this.pc.createDataChannel(API_CHANNEL); } this.pc.onicecandidate = ({ candidate }) => { if (candidate) { this.signal.trickle({ target: role, candidate }); } }; this.pc.oniceconnectionstatechange = async (e) => { if (this.pc.iceConnectionState === 'disconnected') { if (this.pc.restartIce) { // this will trigger onNegotiationNeeded this.pc.restartIce(); if (this.onrestartice && role === Role.pub) { this.onrestartice(); } } } if (this.pc.iceConnectionState === 'failed' || this.pc.iceConnectionState === 'closed') { if (this.onrestartice && role === Role.pub) { this.onrestartice(); } // this.signal.close(); } }; } } exports.Transport = Transport; class Client { constructor(signal, config = { codec: 'vp8', iceServers: [ { urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'], }, ], }, audioSDP = { DTX: 0, SampleRate: 16000 }, opusRed = true, subStream = true) { this.signal = signal; this.config = config; this.audioSDP = audioSDP; this.opusRed = opusRed; this.subStream = subStream; signal.onnegotiate = this.negotiate.bind(this); signal.ontrickle = this.trickle.bind(this); } async join(sid, uid) { this.transports = { [Role.pub]: new Transport(Role.pub, this.signal, this.config), [Role.sub]: new Transport(Role.sub, this.signal, this.config), }; this.transports[Role.pub].onrestartice = () => { var _a; return (_a = this.onrestartice) === null || _a === void 0 ? void 0 : _a.call(this); }; this.transports[Role.sub].pc.ontrack = (ev) => { const stream = ev.streams[0]; const remote = stream_1.makeRemote(stream, this.transports[Role.sub]); if (this.ontrack) { this.ontrack(ev.track, remote); } }; const apiReady = new Promise((resolve) => { this.transports[Role.sub].pc.ondatachannel = (ev) => { if (ev.channel.label === API_CHANNEL) { this.transports[Role.sub].api = ev.channel; this.transports[Role.pub].api = ev.channel; ev.channel.onmessage = (e) => { try { const msg = JSON.parse(e.data); this.processChannelMessage(msg); } catch (err) { /* tslint:disable-next-line:no-console */ console.error(err); } }; resolve(); return; } if (this.ondatachannel) { this.ondatachannel(ev); } }; }); const offer = await this.transports[Role.pub].pc.createOffer(); await this.transports[Role.pub].pc.setLocalDescription(offer); const answer = await this.signal.join(sid, uid, offer); await this.transports[Role.pub].pc.setRemoteDescription(answer); this.transports[Role.pub].candidates.forEach((c) => this.transports[Role.pub].pc.addIceCandidate(c)); this.transports[Role.pub].pc.onnegotiationneeded = this.onNegotiationNeeded.bind(this); return apiReady; } leave() { if (this.transports) { Object.values(this.transports).forEach((t) => t.pc.close()); delete this.transports; } } getPubStats(selector) { if (!this.transports) { throw Error(ERR_NO_SESSION); } return this.transports[Role.pub].pc.getStats(selector); } getSubStats(selector) { if (!this.transports) { throw Error(ERR_NO_SESSION); } return this.transports[Role.sub].pc.getStats(selector); } getPubConnection() { if (!this.transports) { throw Error(ERR_NO_SESSION); } return this.transports[Role.pub].pc; } getSubConnection() { if (!this.transports) { throw Error(ERR_NO_SESSION); } return this.transports[Role.sub].pc; } publish(stream, motion) { if (!this.transports) { throw Error(ERR_NO_SESSION); } stream.publish(this.transports[Role.pub], motion); } createDataChannel(label) { if (!this.transports) { throw Error(ERR_NO_SESSION); } return this.transports[Role.pub].pc.createDataChannel(label); } close() { if (this.transports) { Object.values(this.transports).forEach((t) => t.pc.close()); } this.signal.close(); } trickle({ candidate, target }) { if (!this.transports) { throw Error(ERR_NO_SESSION); } if (this.transports[target].pc.remoteDescription) { this.transports[target].pc.addIceCandidate(candidate); } else { this.transports[target].candidates.push(candidate); } } async negotiate(description) { // 只推流,不接受流 if (!this.subStream) return; if (!this.transports) { throw Error(ERR_NO_SESSION); } let answer; try { if (this.opusRed) { description.sdp = description.sdp ? description.sdp.replace(/m=audio 9 UDP\/TLS\/RTP\/SAVPF 63/g, 'm=audio 9 UDP/TLS/RTP/SAVPF 63 111') : description.sdp; description.sdp = description.sdp ? description.sdp.replace(/a=fmtp:63 111\/111/g, 'a=fmtp:63 111/111\na=rtpmap:111 opus/48000/2') : description.sdp; } await this.transports[Role.sub].pc.setRemoteDescription(description); this.transports[Role.sub].candidates.forEach((c) => this.transports[Role.sub].pc.addIceCandidate(c)); this.transports[Role.sub].candidates = []; answer = await this.transports[Role.sub].pc.createAnswer(); // answer.sdp = answer.sdp ? answer.sdp.replace('useinbandfec=1', 'useinbandfec=1;stereo=1') : answer.sdp; // answer.sdp = answer.sdp ? answer.sdp.replace('useinbandfec=1', 'useinbandfec=1;usedtx=1;maxplaybackrate=16000;sprop-maxcapturerate=16000') : answer.sdp; // answer.sdp = answer.sdp ? answer.sdp.replace(/useinbandfec=1/g, this.audioSdpReplace()) : answer.sdp; await this.transports[Role.sub].pc.setLocalDescription(answer); this.signal.answer(answer); } catch (err) { /* tslint:disable-next-line:no-console */ console.error(err); if (this.onerrnegotiate) this.onerrnegotiate(Role.sub, err, description, answer); } } async onNegotiationNeeded() { if (!this.transports) { throw Error(ERR_NO_SESSION); } let offer; let answer; try { offer = await this.transports[Role.pub].pc.createOffer(); // offer.sdp = offer.sdp ? offer.sdp.replace('useinbandfec=1', 'useinbandfec=1;stereo=1') : offer.sdp; // offer.sdp = offer.sdp ? offer.sdp.replace('useinbandfec=1', 'useinbandfec=1;usedtx=1;maxplaybackrate=16000;sprop-maxcapturerate=16000') : offer.sdp; const audioSDPReplace = this.audioSdpReplace(); offer.sdp = offer.sdp ? offer.sdp.replace(/useinbandfec=1/g, audioSDPReplace) : offer.sdp; if (this.opusRed) { offer.sdp = offer.sdp ? offer.sdp.replace(/m=audio 9 UDP\/TLS\/RTP\/SAVPF 111/g, 'm=audio 9 UDP/TLS/RTP/SAVPF 63 111') : offer.sdp; offer.sdp = offer.sdp ? offer.sdp.replace(/a=rtpmap:111 opus\/48000\/2/g, 'a=rtpmap:63 red/48000/2\na=fmtp:63 111/111\na=rtpmap:111 opus/48000/2') : offer.sdp; } await this.transports[Role.pub].pc.setLocalDescription(offer); answer = await this.signal.offer(offer); await this.transports[Role.pub].pc.setRemoteDescription(answer); } catch (err) { /* tslint:disable-next-line:no-console */ console.error(err); if (this.onerrnegotiate) this.onerrnegotiate(Role.pub, err, offer, answer); } } processChannelMessage(msg) { if (msg.method !== undefined && msg.params !== undefined) { switch (msg.method) { case 'audioLevels': if (this.onspeaker) { this.onspeaker(msg.params); } break; case 'activeLayer': if (this.onactivelayer) { this.onactivelayer(msg.params); } break; default: // do nothing } } else { // legacy channel message - payload contains audio levels if (this.onspeaker) { this.onspeaker(msg); } } } audioSdpReplace() { let sdpReplace = 'useinbandfec=1'; if (this.audioSDP.DTX === 1) { sdpReplace += ';usedtx=1'; } const sampleRate = ';maxplaybackrate=' + String(this.audioSDP.SampleRate) + ';sprop-maxcapturerate=' + String(this.audioSDP.SampleRate); sdpReplace += sampleRate; return sdpReplace; } } exports.default = Client;