/* * Copyright (c) 2020 WildFireChat. All rights reserved. */ import EventType from "../../client/wfcEvent"; import {BrowserWindow, ipcRenderer, isElectron, remote} from "../../../platform"; import ConversationType from "../../model/conversationType"; import MessageContentType from "../../messages/messageContentType"; import wfc from "../../client/wfc"; import MessageConfig from "../../client/messageConfig"; import DetectRTC from 'detectrtc'; import Config from "../../../config"; import {longValue, numberValue} from '../../util/longUtil' import Conversation from "../../../wfc/model/conversation"; import CallEndReason from "./callEndReason"; import CallByeMessageContent from "../messages/callByeMessageContent"; // main window renderer process -> voip window renderer process // voip window renderer process -> main process -> main window renderer process export class AvEngineKitProxy { wfc; queueEvents = []; callWin; isVoipWindowReady = false; type; conference = false; conversation; callId; inviteMessageUid; participants = []; isSupportVoip = false; hasMicrophone = false; hasSpeaker = false; hasWebcam = false; /** * 无法正常弹出音视频通话窗口是的回调 * 回到参数说明:-1,有音视频通话正在进行中;-2,设备不支持音视频通话,可能原因是不支持webrtc,没有摄像头或麦克风等 * @type {(Number) => {}} */ onVoipCallErrorCallback; /** * 音视频通话通话状态回调 */ onVoipCallStatusCallback = (covnersation, ongonging) => { }; /** * 应用初始化的时候调用 * @param wfc */ setup(wfc) { if (this.wfc === wfc) { console.log('re-setup, just ignore'); return; } this.wfc = wfc; DetectRTC.load(() => { this.isSupportVoip = DetectRTC.isWebRTCSupported; this.hasMicrophone = DetectRTC.hasMicrophone; // Safari 14.0 版本,hasSpeakers一直为false,先全部置为true //this.hasSpeaker = DetectRTC.hasSpeakers; this.hasSpeaker = true; this.hasWebcam = DetectRTC.hasWebcam; console.log(`detectRTC, isWebRTCSupported: ${DetectRTC.isWebRTCSupported}, hasWebcam: ${DetectRTC.hasWebcam}, hasSpeakers: ${DetectRTC.hasSpeakers}, hasMicrophone: ${DetectRTC.hasMicrophone}`, this.isSupportVoip); }); this.event = wfc.eventEmitter; this.event.on(EventType.ReceiveMessage, this.onReceiveMessage); this.event.on(EventType.ConferenceEvent, this.onReceiveConferenceEvent); this.event.on(EventType.ConnectionStatusChanged, this.onConnectionStatusChange) if (!isElectron()) { const EventEmitter = require("events").EventEmitter; this.events = new EventEmitter(); this.events.on('voip-message', this.sendVoipListener) this.events.on('conference-request', this.sendConferenceRequestListener); this.events.on('update-call-start-message', this.updateCallStartMessageContentListener) } } updateCallStartMessageContentListener = (event, message) => { let messageUid = message.messageUid; let content = message.content; let msg = wfc.getMessageByUid(messageUid); let orgContent = msg.messageContent; orgContent.connectTime = content.connectTime ? content.connectTime : orgContent.connectTime; orgContent.endTime = content.endTime ? content.endTime : orgContent.endTime; orgContent.status = content.status; orgContent.audioOnly = content.audioOnly; wfc.updateMessageContent(msg.messageId, orgContent); } sendConferenceRequestListener = (event, request) => { console.log('to send conference request', request) wfc.sendConferenceRequestEx(request.sessionId ? request.sessionId : 0, request.roomId ? request.roomId : '', request.request, request.data, request.advance, (errorCode, res) => { this.emitToVoip('sendConferenceRequestResult', { error: errorCode, sendConferenceRequestId: request.sendConferenceRequestId, response: res }) }); } // 发送消息时,返回的timestamp,已经过修正,后面使用时,不用考虑和服务器的时间差 sendVoipListener = (event, msg) => { let contentClazz = MessageConfig.getMessageContentClazz(msg.content.type); let content = new contentClazz(); content.decode(msg.content); console.log('to send voip message', content.type, content.callId, content); let delta = wfc.getServerDeltaTime(); if (content.type === MessageContentType.VOIP_CONTENT_TYPE_ADD_PARTICIPANT) { this.participants.push(content.participants); } else if (content.type === MessageContentType.VOIP_CONTENT_TYPE_END) { console.log('to send end message', content.reason, content); this.conversation = null; this.queueEvents = []; this.callId = null; this.inviteMessageUid = null; this.participants = []; // 仅仅为了通知proxy,其他端已经接听电话了,关闭窗口时,不应当发送挂断信令 if (!content.callId) { return; } } let conversation = new Conversation(msg.conversation.type, msg.conversation.target, msg.conversation.line) wfc.sendConversationMessage(conversation, content, msg.toUsers, (messageId, timestamp) => { // do nothing }, (uploaded, total) => { // do nothing }, (messageUid, timestamp) => { this.emitToVoip('sendMessageResult', { error: 0, sendMessageId: msg.sendMessageId, messageUid: messageUid, timestamp: longValue(numberValue(timestamp) - delta) }) if (content.type === MessageContentType.VOIP_CONTENT_TYPE_START) { this.inviteMessageUid = messageUid; } }, (errorCode) => { this.emitToVoip('sendMessageResult', {error: errorCode, sendMessageId: msg.sendMessageId}) }); } onReceiveConferenceEvent = (event) => { this.emitToVoip("conferenceEvent", event); } onConnectionStatusChange = (status) => { this.emitToVoip('connectionStatus', status); } // 收到消息时,timestamp已经过修正,后面使用时,不用考虑和服务器的时间差 onReceiveMessage = (msg) => { if (!Config.ENABLE_MULTI_VOIP_CALL && msg.conversation.type === ConversationType.Group) { console.log('not enable multi call '); return; } if (!Config.ENABLE_SINGLE_VOIP_CALL && msg.conversation.type === ConversationType.Single) { console.log('not enable multi call '); return; } let now = (new Date()).valueOf(); let delta = wfc.getServerDeltaTime(); if (now - (numberValue(msg.timestamp) - delta) >= 90 * 1000) { // 消息已失效,不做处理 return; } let content = msg.messageContent; if (this.callWin && this.conference && content.type !== MessageContentType.CONFERENCE_CONTENT_TYPE_COMMAND) { console.log('in conference, ignore all other msg'); return; } if (msg.direction === 1 && (content.type === MessageContentType.VOIP_CONTENT_TYPE_START || content.type === MessageContentType.VOIP_CONTENT_TYPE_ADD_PARTICIPANT)) { if (this.callWin) { if (content.type === MessageContentType.VOIP_CONTENT_TYPE_START || (content.type === MessageContentType.VOIP_CONTENT_TYPE_ADD_PARTICIPANT && content.participants.indexOf(wfc.getUserId()) >= 0)) { // 已在音视频通话中,其他的音视频通话,又邀请自己,这儿是只是让主界面提示一下,拒绝逻辑在 engine 里面 this.onVoipCallErrorCallback && this.onVoipCallErrorCallback(-1); } } if (!this.isSupportVoip || !this.hasMicrophone || !this.hasSpeaker) { this.onVoipCallErrorCallback && this.onVoipCallErrorCallback(-2); return; } } if ((msg.conversation.type === ConversationType.Single || msg.conversation.type === ConversationType.Group || (this.conference && msg.conversation.type === ConversationType.ChatRoom))) { if (content.type === MessageContentType.VOIP_CONTENT_TYPE_START || content.type === MessageContentType.VOIP_CONTENT_TYPE_END || content.type === MessageContentType.VOIP_CONTENT_TYPE_ACCEPT || content.type === MessageContentType.VOIP_CONTENT_TYPE_SIGNAL || content.type === MessageContentType.VOIP_CONTENT_TYPE_MODIFY || content.type === MessageContentType.VOIP_CONTENT_TYPE_ACCEPT_T || content.type === MessageContentType.VOIP_CONTENT_TYPE_ADD_PARTICIPANT || content.type === MessageContentType.VOIP_CONTENT_TYPE_MUTE_VIDEO || content.type === MessageContentType.VOIP_Join_Call_Request || content.type === MessageContentType.CONFERENCE_CONTENT_TYPE_KICKOFF_MEMBER || content.type === MessageContentType.CONFERENCE_CONTENT_TYPE_CHANGE_MODE || content.type === MessageContentType.CONFERENCE_CONTENT_TYPE_COMMAND ) { console.log("receive voip message", msg.messageContent.type, msg.messageContent.callId, msg.messageUid.toString(), msg); if (msg.direction === 0 && content.type !== MessageContentType.VOIP_CONTENT_TYPE_END && content.type !== MessageContentType.VOIP_CONTENT_TYPE_ACCEPT && content.type !== MessageContentType.VOIP_CONTENT_TYPE_ACCEPT) { return; } let participantUserInfos = []; let selfUserInfo = wfc.getUserInfo(wfc.getUserId()); if (content.type === MessageContentType.VOIP_CONTENT_TYPE_START) { this.conversation = msg.conversation; this.callId = content.callId; this.inviteMessageUid = msg.messageUid; this.participants.push(...content.targetIds); this.participants.push(msg.from); // 参与者不包含自己 this.participants = this.participants.filter(uid => uid !== selfUserInfo.uid) if (msg.conversation.type === ConversationType.Single) { participantUserInfos = [wfc.getUserInfo(msg.from)]; } else { let targetIds = content.targetIds.filter(id => id !== selfUserInfo.uid); targetIds.push(msg.from); participantUserInfos = wfc.getUserInfos(targetIds, msg.conversation.target); } if (!this.callWin) { if (this.conversation) { this.showCallUI(msg.conversation, false, { event: 'message', args: msg }); } else { console.log('call ended') } } } else if (content.type === MessageContentType.VOIP_CONTENT_TYPE_ADD_PARTICIPANT) { let participantIds = [...content.participants]; if (content.existParticipants) { content.existParticipants.forEach(p => { participantIds.push(p.userId); }); } this.conversation = msg.conversation; this.callId = content.callId; this.inviteMessageUid = msg.messageUid; this.participants.push(...participantIds); participantIds = participantIds.filter(u => u.uid !== selfUserInfo.uid); participantUserInfos = wfc.getUserInfos(participantIds, msg.conversation.target); if (!this.callWin && content.participants.indexOf(selfUserInfo.uid) > -1) { if (this.conversation) { this.showCallUI(msg.conversation, false, { event: 'message', args: msg }); } else { console.log('call ended') } } } else if (content.type === MessageContentType.VOIP_CONTENT_TYPE_END) { if (content.callId !== this.callId) { return; } } if (msg.conversation.type === ConversationType.Group && (content.type === MessageContentType.VOIP_CONTENT_TYPE_START || content.type === MessageContentType.VOIP_CONTENT_TYPE_ADD_PARTICIPANT )) { let memberIds = wfc.getGroupMemberIds(msg.conversation.target); msg.groupMemberUserInfos = wfc.getUserInfos(memberIds, msg.conversation.target); } msg.participantUserInfos = participantUserInfos; msg.selfUserInfo = selfUserInfo; msg.timestamp = longValue(numberValue(msg.timestamp) - delta) // 这两条消息,显示 ui 的时候,会传过去,这儿就不用再次传了 if (this.callWin && [MessageContentType.VOIP_CONTENT_TYPE_START, MessageContentType.VOIP_CONTENT_TYPE_ADD_PARTICIPANT].indexOf(msg.messageContent.type) === -1) { this.emitToVoip("message", msg); } } } }; emitToVoip(event, args) { console.log('emitToVoip', event, args) if (isElectron()) { // renderer/main to renderer if (this.isVoipWindowReady) { // fix object of long.js can be send inter-process args = JSON.stringify(args) this.callWin.webContents.send(event, args); } else if (this.queueEvents) { this.queueEvents.push({event, args}); } } else { if (this.events) { this.events.emit(event, event, args); } else if (this.queueEvents) { this.queueEvents.push({event, args}); } } } emitToMain(event, args) { console.log('emit to main', event, args); if (isElectron()) { // renderer to main ipcRenderer.send(event, args); } else { this.events.emit(event, event, args); } } listenVoipEvent = (event, listener) => { console.log('listenVoipEvent ', event, listener) if (isElectron()) { // listen for event from renderer ipcRenderer.on(event, listener); } else { this.events.on(event, listener); } }; /** * 发起音视频通话 * @param {Conversation} conversation 会话 * @param {Boolean} audioOnly 是否是音频通话 * @param {[String]} participants 参与者用户id列表 * @param {string} callExtra 通话附加信息,会议版有效 */ startCall(conversation, audioOnly, participants, callExtra = '') { if (this.callWin) { console.log('voip call is ongoing'); this.onVoipCallErrorCallback && this.onVoipCallErrorCallback(-1); return; } console.log(`startCall speaker、microphone、webcam检测结果分别为:${this.hasSpeaker} , ${this.hasMicrophone}, ${this.hasWebcam},如果不全为true,请检查硬件设备是否正常,否则通话可能存在异常`) if (!this.isSupportVoip || !this.hasSpeaker || !this.hasMicrophone) { console.log('not support voip', this.isSupportVoip, this.hasSpeaker, this.hasMicrophone, this.hasWebcam); this.onVoipCallErrorCallback && this.onVoipCallErrorCallback(-2); return; } let selfUserInfo = wfc.getUserInfo(wfc.getUserId()); participants = participants.filter(uid => uid !== selfUserInfo.uid); let callId = conversation.target + Math.floor(Math.random() * 10000); this.conversation = conversation; this.participants.push(...participants) this.callId = callId; let participantUserInfos = wfc.getUserInfos(participants); let groupMemberUserInfos; if (conversation.type === ConversationType.Group) { let memberIds = wfc.getGroupMemberIds(conversation.target); groupMemberUserInfos = wfc.getUserInfos(memberIds, conversation.target); } delete conversation._target; this.showCallUI(conversation, false, { event: 'startCall', args: { conversation: conversation, audioOnly: audioOnly, callId: callId, selfUserInfo: selfUserInfo, groupMemberUserInfos: groupMemberUserInfos, participantUserInfos: participantUserInfos, callExtra: callExtra, } }); } /** * 开始会议 * @param {string} callId 会议id * @param {boolean} audioOnly 是否仅仅开启音频; true,音频会议;false,视频会议 * @param {string} pin 入会pin码 * @param {string} host 主持人用户id * @param {string} title 会议标题 * @param {string} desc 会议描述 * @param {boolean} audience 其他人加入会议时,是否默认为观众;true,默认为观众;false,默认为互动者 * @param {boolean} advance 是否为高级会议,当预计参与人员很多的时候,开需要开启超级会议 * @param {boolean} record 是否开启服务端录制 * @param {Object} extra 一些额外信息,主要用于将信息传到音视频通话窗口,会议的其他参与者,无法看到该附加信息 * @param {Object} callExtra 通话附件信息,会议的所有参与者都能看到该附加信息 * @param {boolean} muteAudio 是否是静音加入会议 * @param {boolean} muteVideo 是否是关闭摄像头加入会议 */ startConference(callId, audioOnly, pin, host, title, desc, audience, advance, record = false, extra, callExtra, muteAudio = false, muteVideo = false) { if (this.callWin) { console.log('voip call is ongoing'); this.onVoipCallErrorCallback && this.onVoipCallErrorCallback(-1); return; } if (!this.isSupportVoip || !this.hasSpeaker || !this.hasMicrophone) { console.log('not support voip', this.isSupportVoip, this.hasSpeaker); this.onVoipCallErrorCallback && this.onVoipCallErrorCallback(-2); return; } callId = callId ? callId : wfc.getUserId() + Math.floor(Math.random() * 10000); this.callId = callId; this.conversation = null; this.conference = true; wfc.joinChatroom(callId, () => { console.log('join conference chatRoom success', callId) }, (err) => { console.error('join conference chatRoom fail', callId, err); }); let selfUserInfo = wfc.getUserInfo(wfc.getUserId()); this.showCallUI(null, true, { event: 'startConference', args: { audioOnly: audioOnly, callId: callId, pin: pin ? pin : Math.ceil(Math.random() * 1000000) + '', host: host, title: title, desc: desc, audience: audience, advance: advance, record: record, selfUserInfo: selfUserInfo, extra: extra, callExtra: callExtra, muteAudio: muteAudio, muteVideo: muteVideo, } }); } /** * 加入会议 * @param {string} callId 会议id * @param {string} audioOnly 是否只开启音频 * @param {string} pin 会议pin码 * @param {string} host 会议主持人 * @param {string} title 会议标题 * @param {string} desc 会议描述 * @param {boolean} audience 是否是以观众角色入会 * @param {string} advance 是否是高级会议 * @param {boolean} muteAudio 是否是静音加入会议 * @param {boolean} muteVideo 是否是关闭摄像头加入会议 * @param {Object} extra 一些额外信息,主要用于将信息传到音视频通话窗口 * @param {Object} callExtra 通话附加信息,会议的所有参与者都能看到该附加信息 */ joinConference(callId, audioOnly, pin, host, title, desc, audience, advance, muteAudio, muteVideo, extra = null, callExtra = null) { if (this.callWin) { console.log('voip call is ongoing'); this.onVoipCallErrorCallback && this.onVoipCallErrorCallback(-1); return; } if (!this.isSupportVoip || !this.hasSpeaker || !this.hasMicrophone) { console.log('not support voip', this.isSupportVoip, this.hasSpeaker, this.hasMicrophone); this.onVoipCallErrorCallback && this.onVoipCallErrorCallback(-2); return; } this.conversation = null; this.conference = true; this.callId = callId; wfc.joinChatroom(callId, () => { console.log('join conference chatRoom success', callId) }, (err) => { console.error('join conference chatRoom fail', callId, err); }); let selfUserInfo = wfc.getUserInfo(wfc.getUserId()); this.showCallUI(null, true, { event: 'joinConference', args: { audioOnly: audioOnly, callId: callId, pin: pin, host: host, title: title, desc: desc, audience: audience, advance: advance, muteAudio: muteAudio, muteVideo: muteVideo, selfUserInfo: selfUserInfo, extra: extra, callExtra: callExtra, } }); } showCallUI(conversation, isConference, options) { let type = isConference ? 'conference' : (conversation.type === ConversationType.Single ? 'single' : 'multi'); this.type = type; let width = 360; let height = 640; let minWidth = 360; let minHeight = 640; switch (type) { case 'single': width = 360; height = 640; break; case 'multi': case 'conference': width = 960; height = 600; minWidth = 800; minHeight = 480; break; default: break; } if (isElectron()) { let win = new BrowserWindow( { width: width, height: height, minWidth: minWidth, minHeight: minHeight, resizable: true, maximizable: true, transparent: !!isConference, frame: !isConference, webPreferences: { scrollBounce: false, nativeWindowOpen: true, nodeIntegration: true, contextIsolation: false, }, } ); this.callWin = win; this.isVoipWindowReady = false; // const remoteMain = require("@electron/remote").require("@electron/remote/main"); const remoteMain = remote.require("@electron/remote/main"); remoteMain.enable(win.webContents); win.webContents.on('did-finish-load', () => { this.onVoipWindowReady(win); }); if (localStorage.getItem("enable_voip_debug")) { win.webContents.openDevTools(); } win.on('close', () => { this.onVoipWindowClose(); }); // win.loadURL(path.join('file://', AppPath, 'src/index.html?' + type)); let hash = window.location.hash; let url = window.location.origin; if (hash) { url = window.location.href.replace(hash, '#/voip'); } else { url += "/voip" } url += '/' + type + '?t=' + new Date().getTime() win.loadURL(url); console.log('voip windows url', url) win.show(); win.removeMenu(); this.emitToVoip(options.event, options.args); } else { this.callWin = window; console.log('windowEmitter subscribe events'); this.events.once('close-voip-div', () => { this.onVoipCallStatusCallback && this.conversation && this.onVoipCallStatusCallback(this.conversation, false) this.callWin = null; this.isVoipWindowReady = false; if (this.conference) { wfc.quitChatroom(this.callId); this.conference = false; } this.conference = false; this.callId = null; this.conversation = null; }) // 等 voip view mounted setTimeout(() => { this.isVoipWindowReady = true; this.emitToVoip(options.event, options.args); }, 200) this.onVoipCallStatusCallback && this.conversation && this.onVoipCallStatusCallback(this.conversation, true) } } onVoipWindowClose = (event) => { if (event && event.srcElement && event.srcElement.URL === 'about:blank') { // fix safari bug: safari 浏览器,页面刚打开的时候,也会走到这个地方 return; } // 让voip内部先处理关闭事件,内部处理时,可能还需要发消息 console.log('onVoipWindowClose') if (!this.callWin) { return; } setTimeout(() => { this.onVoipCallStatusCallback && this.onVoipCallStatusCallback(this.conversation, false); this.conversation = null; this.queueEvents = []; if (this.conference) { wfc.quitChatroom(this.callId); this.conference = false; } this.callId = null; this.participants = []; this.queueEvents = []; this.callWin = null; this.isVoipWindowReady = false; this.voipEventRemoveAllListeners('voip-message', 'conference-request', 'update-call-start-message', 'start-screen-share'); }, 2000); } onVoipWindowReady() { if (!this.callId) { return; } this.isVoipWindowReady = true; console.log('onVoipWindowReady', this.onVoipCallStatusCallback) this.onVoipCallStatusCallback && this.onVoipCallStatusCallback(this.conversation, true); if (!isElectron()) { // 启动页面的时候就监听,不然太慢了,会丢事件 } else { console.log('ipcRenderer subscribe events'); ipcRenderer.on('voip-message', this.sendVoipListener); ipcRenderer.on('conference-request', this.sendConferenceRequestListener); ipcRenderer.on('update-call-start-message', this.updateCallStartMessageContentListener) ipcRenderer.on(/*IPCEventType.START_SCREEN_SHARE*/'start-screen-share', (event, args) => { if (this.callWin) { let screenWidth = args.width; this.callWin.resizable = true; this.callWin.closable = true; this.callWin.maximizable = false; this.callWin.transparent = true; this.callWin.setMinimumSize(800, 800); this.callWin.setSize(800, 800); // console.log('screen width', screen, screen.width); this.callWin.setPosition((screenWidth - 800) / 2, 0, true); } }); ipcRenderer.on(/*IPCEventType.STOP_SCREEN_SHARE*/'stop-screen-share', (event, args) => { if (this.callWin) { let type = args.type; let width = 360; let height = 640; switch (type) { case 'single': width = 360; height = 640; break; case 'multi': case 'conference': width = 1024; height = 800; break; default: break; } this.callWin.resizable = true; this.callWin.closable = true; this.callWin.maximizable = true; this.callWin.setMinimumSize(width, height); this.callWin.setSize(width, height); this.callWin.center(); } }) } if (this.queueEvents.length > 0) { this.queueEvents.forEach((eventArgs) => { console.log('process queued event', eventArgs); this.emitToVoip(eventArgs.event, eventArgs.args); }) } } voipEventRemoveAllListeners(...events) { if (isElectron()) { // renderer events.forEach(e => ipcRenderer.removeAllListeners(e)); } } forceCloseVoipWindow() { if (isElectron() && this.callWin) { this.callWin.close(); } this.callId = null; this.callWin = null; } // 仅 web 端,单人、多人通话有效 forceCloseVoipWindowAndHangup() { if (this.callWin && !this.conference) { let byeMessage = new CallByeMessageContent(); // 处理用户没有主动挂断,而是直接返回页面时,补偿一个挂断消息 // 现在还无法处理会议的情况,会议只能等待超时 byeMessage.callId = this.callId; byeMessage.inviteMsgUid = this.inviteMessageUid; byeMessage.reason = CallEndReason.REASON_Hangup; wfc.sendConversationMessage(this.conversation, byeMessage); this.onVoipWindowClose() } } } const self = new AvEngineKitProxy(); export default self;