diff --git a/README.md b/README.md index 0a57b59..9ee9941 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,64 @@ -该仓库是go-chat的前端界面。 -后端是使用go开发的,基于WebSocket的聊天程序。 -后端仓库地址: -https://github.com/kone-net/go-chat +## go-chat +使用Go基于WebSocket的通讯聊天软件。 +### 功能列表: +* 登录注册 +* 修改头像 +* 群聊天 +* 群好友列表 +* 单人聊天 +* 添加好友 +* 添加群组 +* 文本消息 +* 剪切板图片 +* 图片消息 +* 文件发送 +* 语音消息 +* 视频消息 +* 屏幕共享(基于图片) +* 视频通话(基于webrtc的p2p视频通话) + +## 后端 +[代码仓库](https://github.com/kone-net/go-chat) +go中协程是非常轻量级的。在每个client接入的时候,为每一个client开启一个协程,能够在单机实现更大的并发。同时go的channel,可以非常完美的解耦client接入和消息的转发等操作。 + +通过go-chat,可以掌握channel的和Select的配合使用,ORM框架的使用,web框架Gin的使用,配置管理,日志操作,还包括proto buffer协议的使用,等一些列项目中常用的技术。 + + +### 后端技术和框架 +* web框架Gin +* 长连接WebSocket +* 日志框架Uber的zap +* 配置管理viper +* ORM框架gorm +* 通讯协议Google的proto buffer +* makefile 的编写 +* 数据库MySQL +* 图片文件二进制操作 + +## 前端 +基于react,UI和基本组件是使用ant design。可以很方便搭建前端界面。 + +界面选择单页框架可以更加方便写聊天界面,比如像消息提醒,可以在一个界面接受到消息进行提醒,不会因为换页面或者查看其他内容影响消息接受。 +[前端代码仓库](https://github.com/kone-net/go-chat-web): +https://github.com/kone-net/go-chat-web + + +### 前端技术和框架 +* React +* Redux状态管理 +* AntDesign +* proto buffer的使用 +* WebSocket +* 剪切板的文件读取和操作 +* 聊天框发送文字显示底部 +* FileReader对文件操作 +* ArrayBuffer,Blob,Uint8Array之间的转换 +* 获取摄像头视频(mediaDevices) +* 获取麦克风音频(Recorder) +* 获取屏幕共享(mediaDevices) +* WebRTC的p2p视频通话 + + +### 截图 +![go-chat-panel](/public/screenshot/go-chat-panel.jpeg) diff --git a/package-lock.json b/package-lock.json index ce0c6e4..bee9ef8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,10 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-infinite-scroll-component": "^6.1.0", + "react-redux": "^7.2.6", "react-router-dom": "^5.3.0", "react-scripts": "4.0.3", + "redux-thunk": "^2.4.0", "socket.io-client": "^4.3.2", "web-vitals": "^1.0.1" } @@ -3095,6 +3097,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/@types/hoist-non-react-statics/download/@types/hoist-non-react-statics-3.3.1.tgz?cache=0&sync_timestamp=1637265727153&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fhoist-non-react-statics%2Fdownload%2F%40types%2Fhoist-non-react-statics-3.3.1.tgz", + "integrity": "sha1-ESSq/lEYy1kZd66xzqrtEHDrA58=", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.nlark.com/@types/html-minifier-terser/download/@types/html-minifier-terser-5.1.2.tgz?cache=0&sync_timestamp=1631043806613&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fhtml-minifier-terser%2Fdownload%2F%40types%2Fhtml-minifier-terser-5.1.2.tgz", @@ -3273,12 +3284,39 @@ "integrity": "sha1-4TAwSNU4lWPhMPW92J03qZrLdes=", "license": "MIT" }, + "node_modules/@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmmirror.com/@types/prop-types/download/@types/prop-types-15.7.4.tgz", + "integrity": "sha1-/PcgXCXf95Xuea8eMNosl5CAjxE=" + }, "node_modules/@types/q": { "version": "1.5.5", "resolved": "https://registry.nlark.com/@types/q/download/@types/q-1.5.5.tgz", "integrity": "sha1-daKo59irSyMEFFBdkjNdHctTpt8=", "license": "MIT" }, + "node_modules/@types/react": { + "version": "17.0.36", + "resolved": "https://registry.npmmirror.com/@types/react/download/@types/react-17.0.36.tgz?cache=0&sync_timestamp=1637514386557&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Freact%2Fdownload%2F%40types%2Freact-17.0.36.tgz", + "integrity": "sha512-CUFUp01OdfbpN/76v4koqgcpcRGT3sYOq3U3N6q0ZVGcyeP40NUdVU+EWe3hs34RNaTefiYyBzOpxBBidCc5zw==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-redux": { + "version": "7.1.20", + "resolved": "https://registry.npmmirror.com/@types/react-redux/download/@types/react-redux-7.1.20.tgz", + "integrity": "sha1-QvDmGrq7Yh4SxmyW3alMWEI7198=", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/resolve": { "version": "0.0.8", "resolved": "https://registry.nlark.com/@types/resolve/download/@types/resolve-0.0.8.tgz?cache=0&sync_timestamp=1629709391127&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fresolve%2Fdownload%2F%40types%2Fresolve-0.0.8.tgz", @@ -3288,6 +3326,11 @@ "@types/node": "*" } }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmmirror.com/@types/scheduler/download/@types/scheduler-0.16.2.tgz?cache=0&sync_timestamp=1637270013832&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fscheduler%2Fdownload%2F%40types%2Fscheduler-0.16.2.tgz", + "integrity": "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk=" + }, "node_modules/@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.nlark.com/@types/source-list-map/download/@types/source-list-map-0.1.2.tgz", @@ -7544,6 +7587,11 @@ "integrity": "sha1-nxJ29bK0Y/IRTT8sdSUK+MGjb0o=", "license": "MIT" }, + "node_modules/csstype": { + "version": "3.0.10", + "resolved": "https://registry.npmmirror.com/csstype/download/csstype-3.0.10.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcsstype%2Fdownload%2Fcsstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" + }, "node_modules/cyclist": { "version": "1.0.1", "resolved": "https://registry.npm.taobao.org/cyclist/download/cyclist-1.0.1.tgz", @@ -16966,6 +17014,36 @@ "integrity": "sha1-eJcppNw23imZ3BVt1sHZwYzqVqQ=", "license": "MIT" }, + "node_modules/react-redux": { + "version": "7.2.6", + "resolved": "https://registry.npmmirror.com/react-redux/download/react-redux-7.2.6.tgz?cache=0&sync_timestamp=1637424929776&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-redux%2Fdownload%2Freact-redux-7.2.6.tgz", + "integrity": "sha1-SWM6JP5VK1+cr1j+uKE4k23f6ao=", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmmirror.com/react-is/download/react-is-17.0.2.tgz?cache=0&sync_timestamp=1637338596901&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-is%2Fdownload%2Freact-is-17.0.2.tgz", + "integrity": "sha1-5pHUqOnHiTZWVVOas3J2Kw77VPA=" + }, "node_modules/react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmmirror.com/react-refresh/download/react-refresh-0.8.3.tgz", @@ -17583,6 +17661,24 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/redux/download/redux-4.1.2.tgz", + "integrity": "sha1-FA81Qm2Zu0cpr3YK/PeeqqxAcQQ=", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/redux-thunk/download/redux-thunk-2.4.0.tgz?cache=0&sync_timestamp=1635214108732&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fredux-thunk%2Fdownload%2Fredux-thunk-2.4.0.tgz", + "integrity": "sha1-rInh1rm9ue5JzmmmkHG+QbvYLWc=", + "license": "MIT", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.nlark.com/regenerate/download/regenerate-1.4.2.tgz", @@ -24082,6 +24178,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/@types/hoist-non-react-statics/download/@types/hoist-non-react-statics-3.3.1.tgz?cache=0&sync_timestamp=1637265727153&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fhoist-non-react-statics%2Fdownload%2F%40types%2Fhoist-non-react-statics-3.3.1.tgz", + "integrity": "sha1-ESSq/lEYy1kZd66xzqrtEHDrA58=", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.nlark.com/@types/html-minifier-terser/download/@types/html-minifier-terser-5.1.2.tgz?cache=0&sync_timestamp=1631043806613&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fhtml-minifier-terser%2Fdownload%2F%40types%2Fhtml-minifier-terser-5.1.2.tgz", @@ -24221,11 +24326,37 @@ "resolved": "https://registry.npmmirror.com/@types/prettier/download/@types/prettier-2.4.1.tgz", "integrity": "sha1-4TAwSNU4lWPhMPW92J03qZrLdes=" }, + "@types/prop-types": { + "version": "15.7.4", + "resolved": "https://registry.npmmirror.com/@types/prop-types/download/@types/prop-types-15.7.4.tgz", + "integrity": "sha1-/PcgXCXf95Xuea8eMNosl5CAjxE=" + }, "@types/q": { "version": "1.5.5", "resolved": "https://registry.nlark.com/@types/q/download/@types/q-1.5.5.tgz", "integrity": "sha1-daKo59irSyMEFFBdkjNdHctTpt8=" }, + "@types/react": { + "version": "17.0.36", + "resolved": "https://registry.npmmirror.com/@types/react/download/@types/react-17.0.36.tgz?cache=0&sync_timestamp=1637514386557&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Freact%2Fdownload%2F%40types%2Freact-17.0.36.tgz", + "integrity": "sha512-CUFUp01OdfbpN/76v4koqgcpcRGT3sYOq3U3N6q0ZVGcyeP40NUdVU+EWe3hs34RNaTefiYyBzOpxBBidCc5zw==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-redux": { + "version": "7.1.20", + "resolved": "https://registry.npmmirror.com/@types/react-redux/download/@types/react-redux-7.1.20.tgz", + "integrity": "sha1-QvDmGrq7Yh4SxmyW3alMWEI7198=", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/resolve": { "version": "0.0.8", "resolved": "https://registry.nlark.com/@types/resolve/download/@types/resolve-0.0.8.tgz?cache=0&sync_timestamp=1629709391127&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fresolve%2Fdownload%2F%40types%2Fresolve-0.0.8.tgz", @@ -24234,6 +24365,11 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmmirror.com/@types/scheduler/download/@types/scheduler-0.16.2.tgz?cache=0&sync_timestamp=1637270013832&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fscheduler%2Fdownload%2F%40types%2Fscheduler-0.16.2.tgz", + "integrity": "sha1-GmL4lSVyPd4kuhsBsJK/XfitTTk=" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.nlark.com/@types/source-list-map/download/@types/source-list-map-0.1.2.tgz", @@ -27329,6 +27465,11 @@ } } }, + "csstype": { + "version": "3.0.10", + "resolved": "https://registry.npmmirror.com/csstype/download/csstype-3.0.10.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcsstype%2Fdownload%2Fcsstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npm.taobao.org/cyclist/download/cyclist-1.0.1.tgz", @@ -33947,6 +34088,26 @@ "resolved": "https://registry.npmmirror.com/react-is/download/react-is-16.13.1.tgz?cache=0&sync_timestamp=1634573849595&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-is%2Fdownload%2Freact-is-16.13.1.tgz", "integrity": "sha1-eJcppNw23imZ3BVt1sHZwYzqVqQ=" }, + "react-redux": { + "version": "7.2.6", + "resolved": "https://registry.npmmirror.com/react-redux/download/react-redux-7.2.6.tgz?cache=0&sync_timestamp=1637424929776&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-redux%2Fdownload%2Freact-redux-7.2.6.tgz", + "integrity": "sha1-SWM6JP5VK1+cr1j+uKE4k23f6ao=", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "dependencies": { + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmmirror.com/react-is/download/react-is-17.0.2.tgz?cache=0&sync_timestamp=1637338596901&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-is%2Fdownload%2Freact-is-17.0.2.tgz", + "integrity": "sha1-5pHUqOnHiTZWVVOas3J2Kw77VPA=" + } + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmmirror.com/react-refresh/download/react-refresh-0.8.3.tgz", @@ -34392,6 +34553,20 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/redux/download/redux-4.1.2.tgz", + "integrity": "sha1-FA81Qm2Zu0cpr3YK/PeeqqxAcQQ=", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-thunk": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/redux-thunk/download/redux-thunk-2.4.0.tgz?cache=0&sync_timestamp=1635214108732&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fredux-thunk%2Fdownload%2Fredux-thunk-2.4.0.tgz", + "integrity": "sha1-rInh1rm9ue5JzmmmkHG+QbvYLWc=", + "requires": {} + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.nlark.com/regenerate/download/regenerate-1.4.2.tgz", diff --git a/package.json b/package.json index dac76a2..48910a0 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-infinite-scroll-component": "^6.1.0", + "react-redux": "^7.2.6", "react-router-dom": "^5.3.0", "react-scripts": "4.0.3", + "redux-thunk": "^2.4.0", "socket.io-client": "^4.3.2", "web-vitals": "^1.0.1" }, diff --git a/public/screenshot/go-chat-panel.jpeg b/public/screenshot/go-chat-panel.jpeg new file mode 100644 index 0000000..8948d2d Binary files /dev/null and b/public/screenshot/go-chat-panel.jpeg differ diff --git a/src/chat/Login.jsx b/src/chat/Login.jsx index 7fa6b23..7b5cc4f 100755 --- a/src/chat/Login.jsx +++ b/src/chat/Login.jsx @@ -3,6 +3,7 @@ import { Button, Form, Input, + Drawer, message } from 'antd'; import { axiosPostBody } from './util/Request'; @@ -12,6 +13,7 @@ class Login extends React.Component { constructor(props) { super(props) this.state = { + registerDrawerVisible: false } } @@ -37,6 +39,32 @@ class Login extends React.Component { console.log('Failed:', errorInfo); }; + showRegister = () => { + this.setState({ + registerDrawerVisible: true + }) + } + + registerDrawerOnClose = () => { + this.setState({ + registerDrawerVisible: false + }) + } + + onRegister = (values) => { + let data = { + ...values + } + + axiosPostBody(Params.REGISTER_URL, data) + .then(_response => { + message.success("注册成功!"); + this.setState({ + registerDrawerVisible: false + }) + }); + } + render() { return ( @@ -51,7 +79,7 @@ class Login extends React.Component { style={{ marginTop: 150 }} > @@ -59,7 +87,7 @@ class Login extends React.Component { @@ -68,10 +96,65 @@ class Login extends React.Component { + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+
); } diff --git a/src/chat/Panel.jsx b/src/chat/Panel.jsx index 44b0e7c..ef5f8e7 100755 --- a/src/chat/Panel.jsx +++ b/src/chat/Panel.jsx @@ -7,7 +7,7 @@ import { Drawer, Tag, Popover, - Tooltip + Tooltip, } from 'antd'; import { UserOutlined, @@ -21,17 +21,22 @@ import { PhoneOutlined, VideoCameraOutlined, UngroupOutlined, - DesktopOutlined + DesktopOutlined, + FileOutlined } from '@ant-design/icons'; import InfiniteScroll from 'react-infinite-scroll-component'; import moment from 'moment'; import { axiosGet, axiosPostBody } from './util/Request'; import * as Params from './common/param/Params' +import UserInfo from './component/UserInfo' import protobuf from './proto/proto' import Recorder from 'js-audio-recorder'; +import { connect } from 'react-redux' +import { actions } from './redux/module/userInfo' -var socket = null +var socket = null; +var peer = null; const { TextArea } = Input; @@ -105,7 +110,6 @@ var heartCheck = { } } - class Panel extends React.Component { constructor(props) { super(props) @@ -137,12 +141,13 @@ class Panel extends React.Component { }, share: { height: 540, - width: 960 + width: 750 }, currentScreen: { height: 0, width: 0 - } + }, + rtcType: 'answer', } } @@ -194,34 +199,54 @@ class Panel extends React.Component { const messagePB = message.create(data) socket.send(message.encode(messagePB).finish()) - // 将ArrayBuffer转换为base64进行展示 - const str = String.fromCharCode(...new Uint8Array(imgData)); - let base64String = `data:image/jpeg;base64,${window.btoa(str)}`; - - this.setState({ - comments: [ - ...this.state.comments, - { - author: localStorage.username, - avatar: this.state.user.avatar, - content:

, - datetime: moment().fromNow(), - }, - ], - }) + this.appendImgToPanel(imgData) }) }, false) } + /** + * 本地上传后,将图片追加到聊天框 + * @param {Arraybuffer类型图片}} imgData + */ + appendImgToPanel(imgData) { + // 将ArrayBuffer转换为base64进行展示 + var binary = ''; + var bytes = new Uint8Array(imgData); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + let base64String = `data:image/jpeg;base64,${window.btoa(binary)}`; + + this.setState({ + comments: [ + ...this.state.comments, + { + author: localStorage.username, + avatar: this.state.user.avatar, + content:

, + datetime: moment().fromNow(), + }, + ], + }, () => { + setTimeout(this.scrollToBottom(), 3000) + }) + } + /** * 获取用户详情 */ fetchUserDetails = () => { axiosGet(Params.USER_URL + this.state.fromUser) .then(response => { + let user = { + ...response.data, + avatar: Params.HOST + "/file/" + response.data.avatar + } + this.props.setUser(user) this.setState({ - user: response.data, + user: user, }) }); } @@ -230,39 +255,15 @@ class Panel extends React.Component { * websocket连接 */ connection = () => { + console.log("to connect...") + peer = new RTCPeerConnection(); var image = document.getElementById('receiver'); - - let arr = [] - var video = document.getElementById('preview1') - let i = 0 - - // let flag = false - // let sourceBuffer - // let mediaSource = new MediaSource() - // var video = document.getElementById('preview1') - // video.src = URL.createObjectURL(mediaSource) - // mediaSource.addEventListener('sourceopen', sourceOpen); - - // function sourceOpen(e) { - // console.log("sourceOpen", "mediaSource ready state: ", mediaSource.readyState) - // // var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"' - // var mime = 'video/webm; codecs="opus, vp9"'; - // // 新建一个 sourceBuffer - // sourceBuffer = mediaSource.addSourceBuffer(mime); - - // sourceBuffer.addEventListener('updateend', function (_) { - // console.log(mediaSource.readyState); // ended - // // sourceBuffer.appendBuffer(arr) - // }); - // } - - - console.log("to connection") socket = new WebSocket("ws://" + Params.IP_PORT + "/socket.io?user=" + this.props.match.params.user) socket.onopen = () => { heartCheck.start() console.log("connected") + this.webrtcConnection() } socket.onmessage = (message) => { heartCheck.start() @@ -277,6 +278,7 @@ class Panel extends React.Component { return; } + // 视频图像 if (messagePB.contentType === 8) { let currentScreen = { width: this.state.video.width, @@ -289,6 +291,7 @@ class Panel extends React.Component { return; } + // 屏幕共享 if (messagePB.contentType === 9) { let currentScreen = { width: this.state.share.width, @@ -301,28 +304,9 @@ class Panel extends React.Component { return; } - // 接受语音电话或者视频电话 - if (messagePB.contentType === 6 || messagePB.contentType === 7) { - i++ - console.log(i) - // arr.push(messagePB.file) - - if (i % 5 === 0) { - // let fileReader = new FileReader(); - let recordedBlob = new Blob(arr, { type: "video/webm" }); //video/x-matroska;codecs=avc1,opus video/webm - // fileReader.readAsDataURL(recordedBlob) - // fileReader.onload = (e) => { - // console.log(e.target.result) - // video.src = e.target.result; - // this.setState({ - // url: e.target.result - // }) - // } - console.log(recordedBlob) - video.src = URL.createObjectURL(recordedBlob); - } - // sourceBuffer.appendBuffer(messagePB.file.buffer) - + // 接受语音电话或者视频电话 webrtc + if (messagePB.type === "webrtc") { + this.dealWebRtcMessage(messagePB); return; } @@ -331,18 +315,20 @@ class Panel extends React.Component { avatar = messagePB.avatar } + // 文件内容,录制的视频,语音内容 + let content = this.getContentByType(messagePB.contentType, messagePB.url, messagePB.content) this.setState({ comments: [ ...this.state.comments, { author: messagePB.fromUsername, avatar: avatar, - content:

{(messagePB.contentType === 2 || messagePB.contentType === 3) ? : messagePB.content}

, + content:

{content}

, datetime: moment().fromNow(), }, ], }, () => { - this.scrollToBottom() + setTimeout(this.scrollToBottom(), 3000) }) }) } @@ -360,6 +346,98 @@ class Panel extends React.Component { } } + /** + * webrtc 绑定事件 + */ + webrtcConnection = () => { + /** + * 对等方收到ice信息后,通过调用 addIceCandidate 将接收的候选者信息传递给浏览器的ICE代理。 + * @param {候选人信息} e + */ + peer.onicecandidate = (e) => { + if (e.candidate) { + // rtcType参数默认是对端值为answer,如果是发起端,会将值设置为offer + let candidate = { + type: this.state.rtcType + '_ice', + iceCandidate: e.candidate + } + + let data = { + fromUsername: localStorage.username, + from: this.state.fromUser, + to: this.state.toUser, + messageType: this.state.messageType, + content: JSON.stringify(candidate), + type: "webrtc", + } + let message = protobuf.lookup("protocol.Message") + const messagePB = message.create(data) + socket.send(message.encode(messagePB).finish()) + } + + }; + + /** + * 当连接成功后,从里面获取语音视频流 + * @param {包含语音视频流} e + */ + peer.ontrack = (e) => { + if (e && e.streams) { + let remoteVideo = document.getElementById("remoteVideo"); + remoteVideo.srcObject = e.streams[0]; + } + }; + } + + /** + * 处理webrtc消息,包括获取请求方的offer,回应answer等 + * @param {消息内容}} messagePB + */ + dealWebRtcMessage = (messagePB) => { + const { type, sdp, iceCandidate } = JSON.parse(messagePB.content); + + if (type === "answer") { + const offerSdp = new RTCSessionDescription({ type, sdp }); + peer.setRemoteDescription(offerSdp) + } else if (type === "answer_ice") { + peer.addIceCandidate(iceCandidate) + } else if (type === "offer_ice") { + peer.addIceCandidate(iceCandidate) + } else if (type === "offer") { + let preview = document.getElementById("preview1"); + navigator.mediaDevices + .getUserMedia({ + audio: true, + video: true, + }).then((stream) => { + preview.srcObject = stream; + stream.getTracks().forEach(track => { + peer.addTrack(track, stream); + }); + + // 一定注意:需要将该动作,放在这里面,即流获取成功后,再进行answer创建。不然不能获取到流,从而不能播放视频。 + const offerSdp = new RTCSessionDescription({ type, sdp }); + peer.setRemoteDescription(offerSdp) + .then(() => { + peer.createAnswer().then(answer => { + peer.setLocalDescription(answer) + let data = { + fromUsername: localStorage.username, + from: this.state.fromUser, + to: this.state.toUser, + messageType: this.state.messageType, + content: JSON.stringify(answer), + type: "webrtc", + } + let message = protobuf.lookup("protocol.Message") + const messagePB = message.create(data) + socket.send(message.encode(messagePB).finish()) + }) + }); + }); + } + } + /** * 断开连接后重新连接 */ @@ -397,7 +475,7 @@ class Panel extends React.Component { username: users[index].username, uuid: users[index].uuid, messageType: 1, - avatar: users[index].avatar, + avatar: Params.HOST + "/file/" + users[index].avatar, } data.push(d) } @@ -534,16 +612,22 @@ class Panel extends React.Component { return } - axiosGet(Params.USER_URL + value) + let data = { + name: value + } + axiosGet(Params.USER_NAME_URL, data) .then(response => { - console.log(response) - if (response.data.username === 0) { - message.error("无此用户") + let data = response.data + if (data.user.username === "" && data.group.name === "") { + message.error("未查找到群或者用户") return } let queryUser = { - username: response.data.username, - nickname: response.data.nickname, + username: data.user.username, + nickname: data.user.nickname, + + groupUuid: data.group.uuid, + groupName: data.group.name, } this.setState({ hasUser: true, @@ -573,6 +657,18 @@ class Panel extends React.Component { }); }; + joinGroup = () => { + // /group/join/:userUid/:groupUuid + axiosPostBody(Params.GROUP_JOIN_URL + this.state.fromUser + "/" + this.state.queryUser.groupUuid) + .then(_response => { + message.success("添加成功") + this.fetchUserList() + this.setState({ + hasUser: false + }); + }); + } + handleCancel = () => { this.setState({ hasUser: false @@ -601,21 +697,11 @@ class Panel extends React.Component { } for (var i = 0; i < data.length; i++) { let contentType = data[i].contentType - let content = data[i].content; - - if (contentType === 2) { - - } else if (contentType === 3) { - content = - } else if (contentType === 4) { - content =