src/Remon.js
// import "webrtc-adapter";
import platform from "platform";
import Config from "./Configure";
import Context from "./Context";
import Media from "./Media";
import EventManager from "./EventManager";
import signalingStates from "./SignalingStates";
import bindPeerConnectionEvents from "./PeerConnectionHandler";
import SignalingConnection from "./SignalingConnection";
import bindSignalingConnectionEvents from "./SignalingConnectionHandler";
import RemonRecorder from "./RemonRecorder";
import util from "./Util";
import l from "./Logger";
import adapter from "webrtc-adapter";
/**
* Most important class for using RemoteMonster API. It can be use to P2P communication and broadcast. You can receive callback events from listener.
*/
class Remon {
/**
* create Remon object with config object and listener object.
* example: var v = new Remon({config: rtcConfig, listener: rtcListener});
*/
constructor({ config, listener }) {
this.version = "2.3.0-beta.1";
this.context = new Context();
this.context.sdkVersion = this.version;
this.context.logServer =
config.logServer && config.logServer.url
? config.logServer
: Config.logServer;
this.context.eventManager = EventManager();
this.config = config;
this.context.simulcast =
this.config.rtc && this.config.rtc.simulcast
? this.config.rtc.simulcast
: Config.rtc.simulcast;
this.context.logLevel =
this.config.dev && this.config.dev.logLevel
? this.config.dev.logLevel
: "INFO";
util.validateConfig(this.context, this.config);
this.media = new Media(this.context);
this.context.mediaManager = this.media;
this.uri = Config.appServer.url;
this.key = this.config.credential.key;
this.serviceId = this.config.credential.serviceId;
this.context.key = this.key;
this.context.serviceId = this.serviceId;
this.context.state = "INIT";
l.init(this.context);
if (listener) {
Object.keys(listener).forEach(type => {
const listenerItem = listener[type];
this.context.eventManager.addEventListener({ type, listenerItem });
});
}
if (!this.config.rtc) this.config.rtc = Config.rtc;
if (!this.config.media)
this.config.media = { audio: true, video: true, record: false };
if (this.config.media.record) {
this.context.useRecord = this.config.media.record;
if (this.config.media.recordUrl)
this.context.recordUrl = this.config.media.recordUrl;
else
this.context.recordUrl = "https://demo.remotemonster.com/rest/record";
}
Config.media = this.config.media;
Config.view = this.config.view;
if (this.config.media.video.codec)
this.context.videoCodec = this.config.media.video.codec;
if (this.config.media.audio.codec)
this.context.audioCodec = this.config.media.audio.codec;
if (this.config.media.video === false) this.context.useVideo = false;
if (this.config.media.audio === false) this.context.useAudio = false;
if (this.config.media.video.maxBandwidth)
this.context.videoBandwidth = this.config.media.video.maxBandwidth;
if (this.config.media.audio.maxBandwidth)
this.context.audioBandwidth = this.config.media.audio.maxBandwidth;
if (this.config.credential.resturl) {
this.config.credential.resturl = this.config.credential.resturl.replace(
"/init",
""
);
Config.appServer.url = this.config.credential.resturl;
this.uri = Config.appServer.url;
}
if (this.config.credential.wsurl)
Config.signalingServer.url = this.config.credential.wsurl;
//this.init();
}
async init() {
l.d("init is called");
var that = this;
var ctx = this.context;
var cfg = this.config;
var messageBody = {
credential: { key: this.key, serviceId: this.serviceId },
env: {
os: platform.os.family,
osVersion: platform.os.version || "0",
device: platform.name,
deviceVersion: platform.version || "0",
networkType: Navigator.connection,
sdkVersion: this.version
}
};
if (this.config.sdk && this.config.sdk.country)
messageBody.env.country = this.config.sdk.country;
if (this.config.media.roomid) messageBody.id = this.config.media.roomid;
var message = {
method: "POST",
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json"
},
body: JSON.stringify(messageBody)
};
try {
var response = await fetch(this.uri + "/init", message);
var responseJson = await response.json();
} catch (e) {
if (ctx.eventManager.hasEventListener("onError")) {
ctx.eventManager.dispatchEvent("onError", "WebSocketFailedError");
}
l.e("Init: failed:", error);
l.errorEvt(ctx, "1004", "init failed:" + error);
}
l.d("-> Message:", responseJson);
Object.keys(responseJson).forEach(responseJsonKey => {
switch (responseJsonKey) {
case "iceServers": {
responseJson[responseJsonKey].forEach(x =>
Config.rtc.iceServers.push(x)
);
break;
}
case "token": {
ctx.token = responseJson[responseJsonKey];
break;
}
case "key": {
ctx.channel.id = responseJson[responseJsonKey];
break;
}
case "name": {
ctx.channel.name = responseJson[responseJsonKey];
break;
}
default: {
// l.e("Init: Unknown property:" + responseJsonKey);
}
}
});
var eventMsg = {
topic: "log",
messages: {
log: "Peer Id is created : " + ctx.token,
logLevel: "info",
os: platform.os.family,
osVersion: platform.os.version || "0",
device: platform.name,
deviceVersion: platform.version || "0",
networkType: Navigator.connection,
sdkVersion: this.version,
svcId: ctx.serviceId,
pId: ctx.token,
status: "INIT"
}
};
l.evt(JSON.stringify(eventMsg));
ctx.signalingConnection = new SignalingConnection({
url: Config.signalingServer.url,
context: ctx
});
ctx.signalingConnection.connect();
ctx.signalingConnection.on("reconnect", () => {
this.onReconnectSignalConnection();
});
ctx.signalingConnection.on("disconnect", () => {
this.onDisconnectSignalConnection();
});
window.addEventListener(
"offline",
() => {
l.i("Browser: offline");
// if (this.context.eventManager.hasEventListener("onError")) {
// this.context.eventManager.dispatchEvent("onError", "disconnected");
// }
// this.close("UNKNOWN");
this.context.signalingConnection.onOffline();
},
false
);
if (cfg.rtc.audioType === "music") {
cfg.opt = {
mandatory: {
googHighpassFilter: false,
googEchoCancellation: false,
googNoiseSuppression: false
},
optional: [{ googCpuOveruseDetection: false }]
};
}
ctx.peerConnection = new RTCPeerConnection(Config.rtc, cfg.opt);
ctx.hasAddTrack = ctx.peerConnection.addTrack !== undefined;
bindSignalingConnectionEvents({
context: ctx,
media: that.media,
config: cfg
});
bindPeerConnectionEvents({ context: ctx, media: that.media });
if (cfg.view && typeof cfg.view.local !== "undefined")
Config.rtc.localVideo = document.querySelector(`${cfg.view.local}`);
if (cfg.view && typeof cfg.view.remote !== "undefined") {
ctx.remoteVideo = document.querySelector(`${cfg.view.remote}`);
}
if (cfg.media.recvonly) {
ctx.remoteVideo = document.querySelector(`${cfg.view.remote}`);
}
const MAX_RETRIES = 11;
for (let i = 5; i <= MAX_RETRIES; i++) {
// ctx.signalingConnection connection state와 localmedia 들어왔는지, ctx.peerConnection이 제대로 생성되었는지 체크 후 return
if (
ctx.signalingConnection.isOpened()
// ctx.mediaManager.isLocalPrepared()
) {
return;
} else {
const timeout = Math.pow(2, i);
l.v("wating for init %i", i);
await this.wait(timeout);
}
}
// if (ctx.eventManager.hasEventListener("onError")) {
// ctx.eventManager.dispatchEvent("onError", "Error is successfully dispatched");
// }
try {
ctx.devices = await navigator.mediaDevices.enumerateDevices();
ctx.currentVideoDeviceId = ctx.devices[0].deviceId;
} catch (e) {
console.log(e);
l.errorEvt(ctx, "1007", "failed to get media devices: " + e);
}
}
async connectCall(...args) {
l.d("connect is called");
await this.connectChannel(...args);
}
/**
* Create P2P channel, if there is no P2P channel with the id. Join the P2P channel, if there is P2P channel with the id.
* example: remon.connectChannel("roomname1");
*/
async connectChannel(...args) {
l.d("createChannel is called");
this.config.rtc.audioType = "voice";
await this.init();
return this.context.signalingConnection.connectChannel(...args);
}
/**
* Create a broadcast room
* @param (string) roomname name of broadcast room. it is no id but name. You can take a real room id from onCreateChannel event
*/
async createCast(roomname) {
l.d("createCast is called");
this.config.rtc.audioType = "music";
await this.init();
this.context.signalingConnection.createBroadcastChannel(roomname);
}
/**
* Join a room by room id.
* @param (string) room id
*/
async joinCast(roomid) {
l.d("joinCast is called");
this.config.rtc.audioType = "music";
this.context.channel.type = "VIEWER";
this.config.media.recvonly = true;
await this.init();
this.context.signalingConnection.createViewerChannel(roomid);
}
/**
* retrieve current stream health information
*/
getHealth() {
return this.context.health.result;
}
/**
* retrieve current remon state information
*/
getState() {
return this.context.state;
}
/**
* retrieve current sdk version
*/
getVersion() {
return this.version;
}
/**
* get channel id
*/
getChannelId() {
return this.context.channel.id;
}
/**
* mute local video
* @param (bool)
*/
pauseLocalVideo(bool) {
l.d("pauseLocalVideo is called");
this.media
.mediaStreamTrackSwitch(Config.rtc.localStream)
.type("Video")
.enabled(!!bool);
}
/**
* mute remote video
* @param (bool) bool
*/
pauseRemoteVideo(bool) {
l.d("pauseRemoteVideo is called");
this.media
.mediaStreamTrackSwitch(this.context.remoteStream)
.type("Video")
.enabled(!!bool);
}
/**
* switch camera between fore and back
*/
cameraSwitch() {
l.d("cameraSwitch is called");
this.media.setUserDevices(null, this.context.devices[1].deviceId);
}
/**
* mute local audio and mic stream
* @param {bool} bool
*/
muteLocalAudio(bool) {
this.media
.mediaStreamTrackSwitch(Config.rtc.localStream)
.type("Audio")
.enabled(!!bool);
}
/**
* mute remote audio stream
* @param {*} bool
*/
muteRemoteAudio(bool) {
this.media
.mediaStreamTrackSwitch(this.context.remoteStream)
.type("Audio")
.enabled(!!bool);
}
async fetchCalls(id) {
return await this.search(id);
}
setVideoQulity(quility) {
this.context.signalingConnection.setSimulcastPriority(quility);
}
/**
* search P2P channels by id.
* @param (string) id for search. It can be part of full id
*/
search(id) {
l.d("search by" + id);
// const message = {
// command: "search", token: this.context.token,
// serviceId: this.context.serviceId, body: id
// };
// this.context.signalingConnection.send(JSON.stringify(message));
const message = {
method: "GET",
headers: {
"Content-Type": "application/json"
}
};
return new Promise((resolve, reject) => {
fetch(
this.uri + "/call/" + this.config.credential.serviceId,
message
).then(response => {
response
.json()
.then(responseJson => {
if (this.context.eventManager.hasEventListener("onSearch")) {
this.context.eventManager.dispatchEvent("onSearch", responseJson);
}
resolve(responseJson);
})
.catch(err => {
reject(err);
l.errorEvt(this.context, "1008", "search is failed:" + err);
});
});
});
}
async fetchCasts() {
return await this.liveRooms();
}
/**
* Retrieve all broadcast rooms information
*/
liveRooms() {
const message = {
method: "GET",
headers: {
"Content-Type": "application/json"
}
};
return new Promise((resolve, reject) => {
fetch(
this.uri + "/room/" + this.config.credential.serviceId,
message
).then(response => {
response
.json()
.then(responseJson => {
resolve(responseJson);
})
.catch(err => {
reject(err);
l.errorEvt(this.context, "1008", "search is failed:" + err);
});
});
});
}
/**
* It's only function for P2P communication. send message to peer
* @param (string) userMessage message to peer
*/
sendMessage(userMessage) {
l.g("Signaling: Send user message");
const message = this.context.signalingConnection.createMessage({
command: "message",
body: userMessage,
code: ""
});
l.d("Message ->:", message);
this.context.signalingConnection.send(JSON.stringify(message));
}
onReconnectSignalConnection() {
console.log("event: onReconnectSignalConnection");
this.context.signalingConnection.reconnectChannel();
}
onDisconnectSignalConnection() {
console.log("event: onDisconnectSignalConnection");
if (this.context.eventManager.hasEventListener("onStateChange")) {
this.context.eventManager.dispatchEvent("onStateChange", "CLOSE");
}
this.close("UNKNOWN");
}
/**
* close all Remon's resources
*/
close(closeType) {
l.i("Remon.close");
if (this.context.useRecord && this.context.remoteRecorder) {
this.context.remoteRecorder.stop();
this.context.remoteRecorder = null;
}
if (this.context.useRecord && this.context.localRecorder) {
this.context.localRecorder.stop();
this.context.localRecorder = null;
this.context.useRecord = false;
}
if (this.context.remoteVideo && this.context.remoteVideo.srcObject) {
this.context.remoteVideo.srcObject
.getTracks()
.forEach(track => track.stop());
this.context.remoteVideo.srcObject = null;
}
if (Config.rtc.localVideo && Config.rtc.localVideo.srcObject) {
Config.rtc.localVideo.srcObject
.getTracks()
.forEach(track => track.stop());
}
if (!this.context.signalingConnection) return;
//this.context.localVideo.srcObject = null;
// FIXME: Chrome, adapter does not support addTrack.
if (!this.context.peerConnection) return;
if (this.context.health) this.context.health.stop();
if (this.context.hasAddTrack) {
this.context.peerConnection.ontrack = null;
} else {
this.context.peerConnection.onaddstream = null;
}
this.context.peerConnection.onremovestream = null;
this.context.peerConnection.onicecandidate = null;
this.context.peerConnection.oniceconnectionstatechange = null;
this.context.peerConnection.onsignalingstatechange = null;
this.context.peerConnection.onicegatheringstatechange = null;
this.context.peerConnection.onnegotiationneeded = null;
if (this.context.peerConnection.signalingState !== "closed") {
this.context.peerConnection.close();
}
this.context.peerConnection = null;
this.context.signalingConnection.close();
if (closeType) {
this.context.eventManager.dispatchEvent("onClose", {
closeType
});
} else {
this.context.eventManager.dispatchEvent("onClose", {
closeType: "MINE"
});
}
var eventMsg = {
topic: "log",
messages: {
log: "remon is closed",
logLevel: "info",
sdkVersion: this.version,
svcId: this.context.serviceId,
pId: this.context.token,
chId: this.context.channel.id,
status: "CLOSE"
}
};
l.evt(JSON.stringify(eventMsg));
}
wait(timeout) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, timeout);
});
}
}
export default Remon;