![]() System : Linux absol.cf 5.4.0-198-generic #218-Ubuntu SMP Fri Sep 27 20:18:53 UTC 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.33 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, Directory : /var/www/html/gadevoir/app/ |
Upload File : |
/************* POLYFILL ******************/ import * as mqtt from "mqtt"; if (!navigator.getUserMedia) { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; } /************ABSOL-LIBRARY***************************************/ function mixClass(constructor) { var descriptors = {}; for (var i = 1; i < arguments.length; ++i) { Object.assign(descriptors, Object.getOwnPropertyDescriptors(arguments[i].prototype)); } delete descriptors.constructor; Object.defineProperties(constructor.prototype, descriptors); } function safeThrow(error) { setTimeout(function () { if (error.stack) { try { error.message += '\n' + error.stack; } catch (e) { //can not modify message } } throw error; }, 0); } function EventEmitter() { if (!this._azar_extendEvents) { Object.defineProperty(this, '_azar_extendEvents', { enumerable: false, value: this._azar_extendEvents || { supported: {}, prioritize: {}, nonprioritize: {} } }); Object.defineProperty(this, '__azar_force', { value: !(typeof Node === "object" ? this instanceof Node : this && typeof this === "object" && typeof this.nodeType === "number" && typeof this.nodeName === "string"), enumerable: false }); } } EventEmitter.prototype.defineEvent = function (name) { if (name instanceof Array) { for (var i = 0; i < name.length; ++i) this._azar_extendEvents.supported[name[i]] = true; } else this._azar_extendEvents.supported[name] = true; return this; }; EventEmitter.prototype.isSupportedEvent = function (name) { return this.__azar_force || !!this._azar_extendEvents.supported[name]; }; EventEmitter.prototype.emit = function (eventName, data) { this.fire.apply(this, arguments); }; EventEmitter.prototype.fire = function (eventName, data) { var others = Array.prototype.slice.call(arguments, 1); var listenerList; var i; var startTime, endTime; if (this.isSupportedEvent(eventName)) { if (this._azar_extendEvents.prioritize[eventName]) { listenerList = this._azar_extendEvents.prioritize[eventName].slice(); for (i = 0; i < listenerList.length; ++i) { try { startTime = Date.now(); listenerList[i].wrappedCallback.apply(this, others); endTime = Date.now(); if (endTime - startTime > 200) { console.log('slow function call (' + (endTime - startTime) + ')', listenerList[i]); } } catch (e) { safeThrow(e); } } } if (this._azar_extendEvents.nonprioritize[eventName]) { listenerList = this._azar_extendEvents.nonprioritize[eventName].slice(); for (i = 0; i < listenerList.length; ++i) { try { startTime = Date.now(); listenerList[i].wrappedCallback.apply(this, others); endTime = Date.now(); if (endTime - startTime > 200) { console.log('slow function call (' + (endTime - startTime) + ')', listenerList[i]); } } catch (e) { safeThrow(e); } } } } else { if (this.dispatchEvent) { var event = new Event(eventName); data && Object.assign(event, data); this.dispatchEvent(event); } else throw new Error("Not support event " + eventName); } return this; }; EventEmitter.prototype.eventEmitterOnWithTime = function (isOnce, arg0, arg1, arg2) { if (typeof arg0 == 'object') { for (var key in arg0) { this.eventEmitterOnWithTime(isOnce, key, arg0[key]); } return this; } else { if (typeof arg1 == 'object') { return this.eventEmitterOnWithTime(isOnce, arg0, arg1.callback, arg1.cap); } else { var eventArr = this._azar_extendEvents[arg2 ? 'prioritize' : 'nonprioritize'][arg0] || []; var eventIndex = -1; for (var i = 0; i < eventArr.length; ++i) { if (eventArr[i].wrappedCallback === arg1) { eventIndex = i; break; } } if (eventIndex < 0) { var event = { isOnce: isOnce, eventName: arg0, callback: arg1, cap: !!arg2 }; //wrappedCallback will be call if (isOnce) { event.wrappedCallback = function () { this.off(event.eventName, event.wrappedCallback, event.cap); event.callback.apply(this, arguments); }; } else { event.wrappedCallback = event.callback; } if (!this.isSupportedEvent(arg0)) { if (this.addEventListener) { this.addEventListener(arg0, event.wrappedCallback, !!arg2); } else { this.attachEvent('on' + arg0, arg1, !!arg2); } } eventArr.push(event); this._azar_extendEvents[arg2 ? 'prioritize' : 'nonprioritize'][arg0] = eventArr; } else { console.warn("duplicate event"); } } return this; } }; EventEmitter.prototype.on = function (arg0, arg1, arg2) { this.eventEmitterOnWithTime(false, arg0, arg1, arg2); return this; }; EventEmitter.prototype.once = function (arg0, arg1, arg2) { this.eventEmitterOnWithTime(true, arg0, arg1, arg2); return this; }; EventEmitter.prototype.off = function (arg0, arg1, arg2) { if (typeof arg0 == 'object') { for (var key in arg0) { this.off(key, arg0[key]); } return this; } else { if (typeof arg1 == 'object') { return this.off(arg0, arg1.callback, arg1.cap); } else { var eventArr = this._azar_extendEvents[arg2 ? 'prioritize' : 'nonprioritize'][arg0] || []; var newEventArray = []; for (var i = 0; i < eventArr.length; ++i) { var event = eventArr[i]; if (event.wrappedCallback == arg1) { //Dont add to newEventArray if (this.isSupportedEvent(arg0)) { } else { if (this.removeEventListener) { this.removeEventListener(event.eventName, event.wrappedCallback, !!event.cap); } else { this.detachEvent('on' + event.eventName, event.wrappedCallback, !!event.cap); } } } else { newEventArray.push(event); } } this._azar_extendEvents[arg2 ? 'prioritize' : 'nonprioritize'][arg0] = newEventArray; return this; } } }; var identCharacters = function () { var chars = 'qwertyuiopasdfghjklzxcvbnm'; chars = chars + chars.toUpperCase(); var num = '0123456789'; var spect = '_'; return (chars + spect + num).split(''); }(); function randomIdent(length) { if (!(length > 0)) length = 4; var factor = identCharacters; return [factor[(Math.random() * (factor.length - 10)) >> 0]].concat(Array(length - 1).fill('').map(function () { return factor[(Math.random() * factor.length) >> 0]; })).join(''); } /*****************UTILS**********************/ function isObject(obj) { if (typeof obj == "object") { for (var key in obj) { if (obj.hasOwnProperty(key)) { return true; // search for first object prop } } } return false; } function mergeObject(target, add) { for (var key in add) { if (add.hasOwnProperty(key)) { if (target[key] && isObject(target[key]) && isObject(add[key])) { mergeObject(target[key], add[key]); } else { target[key] = add[key]; } } } return target; } /********************************************************/ var defaultConfig = { media: { audio: { echoCancellationType: 'system', echoCancellation: true, noiseSuppression: true, // sampleRate:24000, // sampleSize:16, // channelCount:2, volume: 0.5 }, video: { facingMode: 'user' } }, ws: 'wss:mqtt.absol.cf:9884', appChannel: 'gadevoir', rtc: { offerToReceiveAudio: true, offerToReceiveVideo: true, iceServers: [{ "urls": "stun:absol.cf:3478" }] } }; /*** * @typedef {{number: string, clientId: string}} GEndPoint */ /*** * @extends EventEmitter * @param {GPhone} phone * @param {GEndPoint} other * @param {string=} id * @param {("outgoing"|"income")=} type * */ function GConversation(phone, other, id, type) { EventEmitter.call(this); this.type = type || "outgoing"; this.phone = phone; /*** * * @type {GEndPoint} */ this.me = { number: phone.number, clientId: phone.clientId }; this.id = id || randomIdent(32); this.other = other; //video=>remote description => ice this.localStream = null; this.localStreamSync = new Promise(function (resolve) { this._localStreamResolveCb = resolve; }.bind(this)); this.remoteStreams = null; this.remoteStreamSync = new Promise(function (resolve) { this._remoteResolveCb = resolve; }.bind(this)); this.remoteDescSync = new Promise(function (resolve) { this._remoteDescResolveCb = resolve; }.bind(this)); // IceCandidate } mixClass(GConversation, EventEmitter); GConversation.prototype.startP2P = function () { this.pc = new RTCPeerConnection(this.phone.config.rtc); //ontrack not work with webrtc-adapter this.pc.addEventListener('track', this.ev_ontrack.bind(this)); this.pc.addEventListener('icecandidate', this.ev_onicecandidate.bind(this)); }; GConversation.prototype.startVideo = function () { }; GConversation.prototype.startAudio = function () { var thisC = this; return navigator.mediaDevices.getUserMedia({ audio: this.phone.config.media.audio }).then(function (stream) { thisC.localStream = stream; stream.getTracks().forEach(function (track) { thisC.pc.addTrack(track, stream); }); thisC._localStreamResolveCb(stream); thisC._localStreamResolveCb = null; }); }; GConversation.prototype._setupAntiNoise = function () { }; GConversation.prototype.getLocalStream = function () { return this.localStreamSync; }; GConversation.prototype.getRemoteStreams = function () { return this.remoteStreamSync; }; GConversation.prototype.close = function () { this.pc.close(); if (this.remoteStreams) this.remoteStreams.forEach(function (stream) { stream.getTracks().forEach(function (track) { track.stop(); }); }); if (this.localStream) this.localStream.getTracks().forEach(function (track) { track.stop(); }); }; GConversation.prototype.setRemoteDescription = function (offer) { var thisC = this; var pc = this.pc; //don't set remoteDescription before audio return this.localStreamSync.then(function () { return pc.setRemoteDescription(new RTCSessionDescription(offer)).then(function () { thisC._remoteDescResolveCb(offer); thisC._remoteDescResolveCb = null; }); }); }; GConversation.prototype.getRemoteDescription = function () { return this.remoteDescSync; }; /*** * * @return {Promise<RTCSessionDescriptionInit>} */ GConversation.prototype.offer = function () { var thisC = this; return this.localStreamSync.then(function () { return thisC.pc.createOffer(); }).then(function (offer) { return thisC.pc.setLocalDescription(offer) .then(function () { return offer; }) }); }; /*** * @returns {Promise<RTCSessionDescriptionInit>} */ GConversation.prototype.answer = function () { var pc = this.pc; return this.remoteDescSync.then(function () { if (pc.remoteDescription.type !== "offer") return null; return pc.createAnswer() .then(function (answer) { return pc.setLocalDescription(answer) .then(function () { return answer; }); }); }); }; GConversation.prototype.addIceCandidate = function (candidate) { var pc = this.pc; this.remoteDescSync.then(function () { return pc.addIceCandidate( new RTCIceCandidate(candidate) ); }); }; GConversation.prototype.ev_ontrack = function (event) { var streams = Array.prototype.slice.call(event.streams); this.emit('remote_stream', { type: 'remote_stream', streams: streams, target: this }, this); this.remoteStreams = streams; if (this._remoteResolveCb) { this._remoteResolveCb(streams); this._remoteResolveCb = null; } }; GConversation.prototype.ev_onicecandidate = function (event) { if (event.candidate) this.emit('icecandidate', { type: 'icecandidate', candidate: event.candidate, target: this }); }; /** * @typedef {Object} GMQTTConfig * @property {string} appChannel * @property {object} [mqttClient] - use existed mqtt client or create new one using ws * @property {string} [ws] * @property {string} clientId */ /** * * @param {GMQTTConfig} config * @constructor */ function GMQTTNetwork(config) { EventEmitter.call(this); this.clientId = config.clientId; this.userId = config.userId; this.appChannel = config.appChannel; this.classChannel = this.appChannel + '/gphone'; this.userChannel = this.classChannel + '/' + this.userId; this.clientChannel = this.classChannel + '/' + this.clientId; if (config.mqttClient) { this.mqttClient = config.mqttClient; } else if (typeof config.ws === "string") { this.mqttClient = mqtt.connect(config.ws, { clientId: this.clientId, connectTimeout: 100000 }); } else { throw new Error("Invalid config: can not config mqtt"); } this.mqttClient.subscribe([this.classChannel, this.userChannel, this.clientChannel]); this.mqttClient.on('connect', this.ev_connect.bind(this)); this.mqttClient.on('error', this.ev_error.bind(this)); this.mqttClient.on('message', this.ev_message.bind(this)); } mixClass(GMQTTNetwork, EventEmitter); GMQTTNetwork.prototype.ready = function () { if (this.mqttClient) return Promise.resolve(); return new Promise((resolve, reject) => { this.mqttClient.once('connect', function () { resolve(); }); this.mqttClient.once('error', function (error) { reject(error); }); }); }; GMQTTNetwork.prototype.ev_connect = function () { }; GMQTTNetwork.prototype.ev_error = function (error) { }; GMQTTNetwork.prototype.packagePrefix = 'gadevoir_'; GMQTTNetwork.prototype.packageSubfix = '_gadevoir'; GMQTTNetwork.prototype.ev_message = function (channel, payload) { payload = payload + ''; var json; if (payload.startsWith(this.packagePrefix) && payload.endsWith(this.packageSubfix)) { payload = payload.substring(this.packagePrefix.length, payload.length - this.packageSubfix.length); console.log(payload) try { json = JSON.parse(payload); this.emit('receive_package', json); } catch (error) { console.error(error); } } }; GMQTTNetwork.prototype.sendToUser = function (userId, type, data) { var json = JSON.stringify({ type: type, data: data, sourceClient: this.clientId, sourceUser: this.userId, from: this.userId, destType: 'user', dest: userId }); this.mqttClient.publish(this.classChannel + '/' + userId, this.packagePrefix + json + this.packageSubfix); }; GMQTTNetwork.prototype.sendToClient = function (client, type, data) { var json = JSON.stringify({ type: type, data: data, sourceClient: this.clientId, sourceUser: this.userId, from: this.clientId, destType: 'client', dest: client }); this.mqttClient.publish(this.classChannel + '/' + client, this.packagePrefix + json + this.packageSubfix); }; GMQTTNetwork.prototype.sendToClass = function (type, data) { var json = JSON.stringify({ type: type, data: data, sourceClient: this.clientId, sourceUser: this.userId, from: this.userId, destType: 'class' }); this.mqttClient.publish(this.classChannel, this.packagePrefix + json + this.packageSubfix); } function GPhone(config) { this.state = "NOT_READY"; EventEmitter.call(this); this.config = mergeObject(defaultConfig, config || {}); this.config.userId = this.config.userId || localStorage.getItem('gphone_id') || this._randomPhoneNumber(); this.config.clientId = this.config.clientId || (this.config.userId + '-' + randomIdent(24)); localStorage.setItem('gphone_id', this.config.userId); this.userId = this.config.userId; this.clientId = this.config.clientId; this.network = new GMQTTNetwork(Object.assign({ userId: this.userId, clientId: this.clientId }, this.config)); // setup receive package from network this.pkgRecv = new EventEmitter(); this.network.on('receive_package', this.ev_package.bind(this)); this.pkgRecv.on('dial', this.recv_dial.bind(this)); this.pkgRecv.on('who_online', this.recv_whoOnline.bind(this)); this.pkgRecv.on('icecandidate', this.recv_icecandidate.bind(this)); this.pkgRecv.on('answer', this.recv_answer.bind(this)); this.pkgRecv.on('hangup', this.recv_hangup.bind(this)); Object.defineProperty(this, 'number', { get: function () { return this.userId; } }); /*** * * @type {null|GConversation} */ this.conversation = null; this.state = "READY"; this.sync = this.network.ready(); } mixClass(GPhone, EventEmitter); GPhone.isSupported = function () { return !!window.RTCPeerConnection && !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) && !!RTCPeerConnection.prototype.createDataChannel }; GPhone.prototype._randomPhoneNumber = function () { return '0908' + Array(6).fill(0).map(function () { return Math.floor(Math.random() * 10); }).join(''); }; GPhone.prototype.ev_package = function (pkg) { this.pkgRecv.emit(pkg.type, pkg); }; GPhone.prototype.sendToPhone = function (number, type, data) { return this.network.sendToUser(number, type, data); }; GPhone.prototype.sendToClient = function (client, type, data) { return this.network.sendToClient(client, type, data); } GPhone.prototype.sendToClass = function (type, data, cb) { return this.network.sendToClass(type, data, cb); }; /*** * * @param {string} phoneNumber * @param {number} timeout * @param {function} onStateChangeCb */ GPhone.prototype.dial = function (phoneNumber, timeout, onStateChangeCb) { if (this.state !== 'READY') { return false; // throw new Error("GPhone is not ready"); } this.state = "OUTGOING_DIAL"; var responseIdx = -1; var answerCount = 0; var thisPhone = this; var conversation = new GConversation(this, { clientId: "undefined", number: phoneNumber }); conversation.on('icecandidate', this.ev_icecandidate.bind(this, conversation)); var id = conversation.id; this.conversation = conversation; var answerVal = 'NOT_RESPONSE'; function onDialResponse(res, pkg) { if (id !== res.id) return; if (responseIdx >= 0) { clearTimeout(responseIdx); } switch (res.answer) { case "ACCEPT": answerVal = "ACCEPT"; finish(); thisPhone.state = "OUTGOING_TRANSMIT"; conversation.other.clientId = pkg.sourceClient; conversation.startP2P(); conversation.startAudio(); conversation.setRemoteDescription(res.offer); conversation.answer().then(function (answer) { thisPhone.sendToPhone(conversation.other.userId, 'answer', { id: conversation.id, answer: answer, targetClient: pkg.sourceClient }); }); onStateChangeCb({ state: "ACCEPT" }); break; case "BUSY": if (answerVal === 'NOT_RESPONSE') { answerVal = 'BUSY'; responseIdx = setTimeout(finish, 4000); } break; case "DENY": answerVal = "DENY"; onStateChangeCb({ state: "DENY" }); finish(); break; case "WAITING": if (answerVal !== "WAITING") { answerVal = "WAITING"; onStateChangeCb({ state: "WAITING" }); } responseIdx = setTimeout(finish, timeout); break; } } function finish() { responseIdx = -1; thisPhone.off('dial_response', onDialResponse); switch (answerVal) { case "NOT_RESPONSE": onStateChangeCb({ state: "NOT_RESPONSE" }); break; case "BUSY": onStateChangeCb({ state: "BUSY" }); break; case "WAITING": onStateChangeCb({ state: "NOT_ANSWER" }); break; } if (answerVal !== 'ACCEPT') { thisPhone.state = "READY"; thisPhone.conversation = null; } } responseIdx = setTimeout(finish, 2000) this.sendToPhone(phoneNumber, 'dial', { id: id }); this.pkgRecv.on('dial_response', onDialResponse); return true; }; GPhone.prototype.recv_dial = function (pkg) { var data = pkg.data; var clientId = pkg.sourceClient; if (this.state === 'READY') { this.sendToPhone(clientId, 'dial_response', { answer: "WAITING", id: data.id }); this.state = "INCOME_DIAL"; var conversation = new GConversation(this, { clientId: pkg.sourceClient, number: pkg.sourceUser }, data.id, 'income'); conversation.on('icecandidate', this.ev_icecandidate.bind(this, conversation)); this.conversation = conversation; this.emit('income', { from: pkg.sourceUser, clientId: clientId, conversationId: conversation.id }); } else { this.sendToClient(clientId, 'dial_response', { answer: "BUSY" }); } }; GPhone.prototype.hangup = function () { var conversation = this.conversation; if (conversation) { switch (this.conversation.type) { case "income": if (conversation.pc) { this.sendToPhone(this.conversation.other.clientId, 'hangup', { id: conversation.id }); conversation.close(); } else { this.sendToClient(this.conversation.other.clientId, 'dial_response', { id: conversation.id, answer: "DENY" }); } break; case "outgoing": if (conversation.pc) { this.sendToPhone(this.conversation.other.clientId, 'hangup', { id: conversation.id }); conversation.close(); } else { this.sendToClient(this.conversation.other.clientId, 'dial_cancel', { id: conversation.id, answer: "DENY" }); } break; } //do something before remove this.conversation = null; } this.state = "READY"; }; GPhone.prototype.pickup = function () { var conversation = this.conversation; if (!conversation) return; var thisPhone = this; if (!conversation.pc) { this.state = "INCOME_TRANSMIT"; conversation.startP2P(); conversation.startAudio(); conversation.offer().then(function (offer) { return thisPhone.sendToClient(conversation.other.clientId, 'dial_response', { answer: "ACCEPT", id: conversation.id, offer: offer }); }); //todo: handle dial cancel after start P2P } }; GPhone.prototype.getOnlineList = function () { var thisPhone = this; this.sendToClass('who_online', {}); return new Promise(function (resolve) { var onlineList = {}; var timeout; function onFinish() { thisPhone.pkgRecv.off('who_online_response', onResponse); resolve(onlineList); } function onResponse(pkg) { var from = pkg.sourceUser; if (from === thisPhone.userId) return; onlineList[from] = true; clearTimeout(timeout); timeout = setTimeout(onFinish, 500); } timeout = setTimeout(onFinish, 1500); thisPhone.pkgRecv.on('who_online_response', onResponse); }); }; GPhone.prototype.recv_whoOnline = function (pkg) { this.sendToClient(pkg.sourceClient, 'who_online_response', {}); }; GPhone.prototype.ev_icecandidate = function (conversation, event) { if (conversation !== this.conversation) return; if (conversation) { this.sendToPhone(conversation.other.clientId, 'icecandidate', { id: conversation.id, candidate: event.candidate }) } }; GPhone.prototype.recv_icecandidate = function (pkg) { var data = pkg.data; var conversation = this.conversation; if (!conversation || conversation.id !== data.id) return; conversation.addIceCandidate(data.candidate); }; GPhone.prototype.recv_answer = function (pkg) { var data = pkg.data; var conversation = this.conversation; if (!conversation || conversation.id !== data.id) return; if (this.clientId === data.targetClient) { conversation.setRemoteDescription(data.answer); } else { //todo: cancel current conversation conversation.close(); this.emit('cancel_income', { type: 'cancel_income', target: this }); console.info("Pickup by other client, cancel!"); this.state = "READY"; } }; GPhone.prototype.recv_hangup = function (pkg) { var data = pkg.data; var conversation = this.conversation; if (!conversation || conversation.id !== data.id) return; if (conversation.pc) { conversation.close(); this.emit('hangup', { from: pkg.sourceUser, conversationId: data.id }, this); } else { this.emit('cancel_income', { type: 'cancel_income', target: this }); } this.emit('hangup', { from: pkg.sourceUser, conversationId: data.id }, this); this.state = "READY"; }; GPhone.prototype.copyRTCIceCandidate = function (obj) { return ['address', 'candidate', "component", "foundation", "port", "priority", "protocol", "relatedAddress", "relatedPort", "sdpMLineIndex", "sdpMid", "tcpType", "usernameFragment" ].reduce(function (ac, key) { ac[key] = obj[key]; return ac; }, {}); }; export default GPhone;