foxconn-yumeeting-sdk-js
Version:
A js sdk for ion yumeeting
260 lines (259 loc) • 10.6 kB
JavaScript
"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;