feat:提交代码
This commit is contained in:
commit
edc7fb6507
|
@ -0,0 +1,8 @@
|
||||||
|
# webstorm
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# mac
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# nodejs
|
||||||
|
node_modules
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 zhouxhere
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,3 @@
|
||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
|
||||||
|
parserOptions: {
|
||||||
|
parser: "babel-eslint",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
|
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
# client-vue
|
||||||
|
|
||||||
|
webrtc 客户端 vue 实现 client demo
|
||||||
|
|
||||||
|
问题:navigator.mediaDevices 需要在https下 ( localhost 除外)
|
||||||
|
|
||||||
|
1. 调试时可修改 vue.config.js 中 devServer https,但是后端也需要修改为 https
|
||||||
|
2. chrome 浏览器 设置 chrome://flags,Insecure origins treated as secure,启用填入ip即可
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
@ -0,0 +1,13 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: ["@vue/cli-plugin-babel/preset"],
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
"import",
|
||||||
|
{
|
||||||
|
libraryName: "ant-design-vue",
|
||||||
|
libraryDirectory: "es",
|
||||||
|
style: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "client-vue",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ant-design-vue": "^1.7.7",
|
||||||
|
"babel-plugin-import": "^1.13.3",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"socket.io-client": "^4.1.3",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-router": "^3.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"less": "^3.0.4",
|
||||||
|
"less-loader": "^5.0.0",
|
||||||
|
"lint-staged": "^9.5.0",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
},
|
||||||
|
"gitHooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,vue}": [
|
||||||
|
"vue-cli-service lint",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-align: center;
|
||||||
|
color: #2c3e50;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<video ref="video" controls muted autoplay></video>
|
||||||
|
<p>{{ name }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "VideoCard",
|
||||||
|
props: {
|
||||||
|
srcObject: null,
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs.video.srcObject = this.srcObject;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.card {
|
||||||
|
width: 200px;
|
||||||
|
height: 160px;
|
||||||
|
background: #f3f3f3;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 6px;
|
||||||
|
|
||||||
|
> video {
|
||||||
|
width: 200px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,24 @@
|
||||||
|
import Vue from "vue";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Input,
|
||||||
|
List,
|
||||||
|
Result,
|
||||||
|
Row,
|
||||||
|
} from "ant-design-vue";
|
||||||
|
import { Form } from "ant-design-vue";
|
||||||
|
import { Icon } from "ant-design-vue";
|
||||||
|
|
||||||
|
Vue.use(Button);
|
||||||
|
Vue.use(Form);
|
||||||
|
Vue.use(Icon);
|
||||||
|
Vue.use(Input);
|
||||||
|
Vue.use(Row);
|
||||||
|
Vue.use(Col);
|
||||||
|
Vue.use(List);
|
||||||
|
Vue.use(Avatar);
|
||||||
|
Vue.use(Card);
|
||||||
|
Vue.use(Result);
|
|
@ -0,0 +1,12 @@
|
||||||
|
import Vue from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import router from "./router";
|
||||||
|
|
||||||
|
import "@/core/bootstrap";
|
||||||
|
|
||||||
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
render: (h) => h(App),
|
||||||
|
}).$mount("#app");
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Vue from "vue";
|
||||||
|
import VueRouter from "vue-router";
|
||||||
|
import Home from "../views/Home.vue";
|
||||||
|
|
||||||
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "Home",
|
||||||
|
component: Home,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "*",
|
||||||
|
redirect: "/",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
mode: "history",
|
||||||
|
base: process.env.BASE_URL,
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,745 @@
|
||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<div class="home-header">
|
||||||
|
<template v-if="status === 'login'">
|
||||||
|
<a-input v-model="loginParams.name" placeholder="name" />
|
||||||
|
<a-input v-model="loginParams.unique" placeholder="unique" />
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!loginParams.name || !loginParams.unique"
|
||||||
|
@click="login"
|
||||||
|
>login</a-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="status === 'join'">
|
||||||
|
<a-input v-model="createParam" placeholder="create" />
|
||||||
|
<a-button type="primary" :disabled="!createParam" @click="create"
|
||||||
|
>create</a-button
|
||||||
|
>
|
||||||
|
<a-input v-model="joinParam" placeholder="join" />
|
||||||
|
<a-button type="primary" :disabled="!joinParam" @click="join"
|
||||||
|
>join</a-button
|
||||||
|
>
|
||||||
|
<a-button type="primary" @click="logout">logout</a-button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="status === 'room' || status === 'chat'">
|
||||||
|
<p>{{ user.id }}</p>
|
||||||
|
<p>{{ user.name }}</p>
|
||||||
|
<p>{{ user.unique }}</p>
|
||||||
|
<p>{{ room.id }}</p>
|
||||||
|
<p>{{ room.name }}</p>
|
||||||
|
<a-button type="primary" @click="leave">leave</a-button>
|
||||||
|
<a-button type="primary" @click="logout">logout</a-button>
|
||||||
|
</template>
|
||||||
|
<template v-else> please login first</template>
|
||||||
|
</div>
|
||||||
|
<a-row
|
||||||
|
class="home-content"
|
||||||
|
:style="
|
||||||
|
status !== 'room' &&
|
||||||
|
status !== 'chat' &&
|
||||||
|
'align-items: center; justify-content: center'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-result
|
||||||
|
v-if="status !== 'room' && status !== 'chat'"
|
||||||
|
title="please login and create or join a room!"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<a-icon type="smile" theme="twoTone" />
|
||||||
|
</template>
|
||||||
|
</a-result>
|
||||||
|
<template v-else>
|
||||||
|
<a-col :span="4" class="home-content-user">
|
||||||
|
<a-list item-layout="horizontal" :data-source="users">
|
||||||
|
<a-list-item slot="renderItem" slot-scope="item">
|
||||||
|
<a-list-item-meta :description="item.unique">
|
||||||
|
<p slot="title">{{ item.name }}</p>
|
||||||
|
</a-list-item-meta>
|
||||||
|
</a-list-item>
|
||||||
|
</a-list>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12" class="home-content-message">
|
||||||
|
<a-list
|
||||||
|
item-layout="horizontal"
|
||||||
|
class="home-content-message-list"
|
||||||
|
:data-source="messages"
|
||||||
|
>
|
||||||
|
<a-list-item
|
||||||
|
slot-scope="msg"
|
||||||
|
:class="[
|
||||||
|
`home-content-message-list-${
|
||||||
|
msg.message.userId !== user.id ? 'left' : 'right'
|
||||||
|
}`,
|
||||||
|
]"
|
||||||
|
slot="renderItem"
|
||||||
|
>
|
||||||
|
<div class="home-content-message-list-user">
|
||||||
|
<p>{{ msg.user ? msg.user.name : "none" }}</p>
|
||||||
|
<span>{{ moment(msg.message.timestamp).format("HH:mm") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="home-content-message-list-message">
|
||||||
|
{{ msg.message.content }}
|
||||||
|
</div>
|
||||||
|
</a-list-item>
|
||||||
|
</a-list>
|
||||||
|
<div class="home-content-message-input">
|
||||||
|
<a-textarea
|
||||||
|
class="home-content-message-input-textarea"
|
||||||
|
placeholder="发送消息"
|
||||||
|
v-model="message"
|
||||||
|
/>
|
||||||
|
<div class="home-content-message-input-btn">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!Boolean(message)"
|
||||||
|
@click="send"
|
||||||
|
>发送</a-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8" class="home-content-chat">
|
||||||
|
<div class="home-content-chat-main">
|
||||||
|
<video-card
|
||||||
|
v-if="localStream"
|
||||||
|
:name="user.name"
|
||||||
|
:src-object="localStream"
|
||||||
|
/>
|
||||||
|
<a-result
|
||||||
|
title="leave chat"
|
||||||
|
class="home-content-chat-card"
|
||||||
|
v-if="localStream"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<!-- <a-icon type="smile" theme="twoTone" />-->
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary" @click="leaveChat"> leave </a-button>
|
||||||
|
</template>
|
||||||
|
</a-result>
|
||||||
|
<a-result
|
||||||
|
v-if="!localStream"
|
||||||
|
title="join chat"
|
||||||
|
class="home-content-chat-card"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<!-- <a-icon type="smile" theme="twoTone" />-->
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary" @click="joinChat"> chat </a-button>
|
||||||
|
</template>
|
||||||
|
</a-result>
|
||||||
|
<video-card
|
||||||
|
v-for="chat in chats.filter((p) => p.stream)"
|
||||||
|
:key="chat.user.id"
|
||||||
|
:name="chat.user.name"
|
||||||
|
:src-object="chat.stream"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</template>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { io } from "socket.io-client";
|
||||||
|
import moment from "moment";
|
||||||
|
import VideoCard from "../components/VideoCard";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Home",
|
||||||
|
components: { VideoCard },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null,
|
||||||
|
socket: null,
|
||||||
|
clientType: navigator.userAgent.match(
|
||||||
|
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
|
||||||
|
)
|
||||||
|
? "mobile"
|
||||||
|
: "pc",
|
||||||
|
loginParams: { name: null, unique: null },
|
||||||
|
createParam: null,
|
||||||
|
joinParam: null,
|
||||||
|
message: null,
|
||||||
|
users: [],
|
||||||
|
messages: [],
|
||||||
|
chats: [],
|
||||||
|
user: null,
|
||||||
|
client: null,
|
||||||
|
room: null,
|
||||||
|
localStream: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 页面刷新
|
||||||
|
window.addEventListener("beforeunload", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.emit("close", {
|
||||||
|
userId: this.user ? this.user.id : null,
|
||||||
|
roomId: this.room ? this.room.id : null,
|
||||||
|
clientType: this.clientType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket = io("http://localhost:8000", {});
|
||||||
|
|
||||||
|
this.socket.on("connect", () => {
|
||||||
|
console.log("connect");
|
||||||
|
this.status = "login";
|
||||||
|
});
|
||||||
|
// 登录反馈
|
||||||
|
this.socket.on("login", (res) => {
|
||||||
|
console.log("login", res);
|
||||||
|
if (res.status === "success") {
|
||||||
|
this.user = res.data;
|
||||||
|
this.client = this.user.clients.find((p) => p.type === this.clientType);
|
||||||
|
sessionStorage.setItem("user", JSON.stringify(this.user));
|
||||||
|
this.status = "join";
|
||||||
|
|
||||||
|
if (sessionStorage.getItem("room")) {
|
||||||
|
let _room = JSON.parse(sessionStorage.getItem("room"));
|
||||||
|
this.joinParam = _room.id;
|
||||||
|
this.join();
|
||||||
|
}
|
||||||
|
} else if (res.status === "error") {
|
||||||
|
this.user = null;
|
||||||
|
this.status = "login";
|
||||||
|
}
|
||||||
|
this.loginParams = { name: null, unique: null };
|
||||||
|
});
|
||||||
|
// 登出反馈
|
||||||
|
this.socket.on("logout", (res) => {
|
||||||
|
console.log("logout", res);
|
||||||
|
sessionStorage.clear();
|
||||||
|
this.status = "login";
|
||||||
|
this.user = null;
|
||||||
|
this.client = null;
|
||||||
|
this.room = null;
|
||||||
|
this.users = [];
|
||||||
|
this.messages = [];
|
||||||
|
this.chats = [];
|
||||||
|
});
|
||||||
|
// 新建房间反馈
|
||||||
|
this.socket.on("create", (res) => {
|
||||||
|
console.log("create", res);
|
||||||
|
if (res.status === "success") {
|
||||||
|
this.room = res.data;
|
||||||
|
sessionStorage.setItem("room", JSON.stringify(this.room));
|
||||||
|
this.status = "room";
|
||||||
|
this.messages = this.room.messages.map((item) => {
|
||||||
|
let _user = this.users.find((p) => p.id === item.userId);
|
||||||
|
return {
|
||||||
|
user: _user,
|
||||||
|
message: item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.users = this.room.member;
|
||||||
|
console.log(this.room.chats);
|
||||||
|
this.room.chats.forEach((item) => {
|
||||||
|
if (item.user.id === this.user.id) return;
|
||||||
|
let _chat = this.chats.find((p) => p.user.id === item.user.id);
|
||||||
|
if (_chat && _chat.user && _chat.client) {
|
||||||
|
if (_chat.client.socketId !== item.client.socketId) {
|
||||||
|
// change peerConnection
|
||||||
|
if (_chat.connection) {
|
||||||
|
_chat.connection.close();
|
||||||
|
_chat.connection = null;
|
||||||
|
}
|
||||||
|
_chat.stream = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// create peerConnection
|
||||||
|
_chat = {
|
||||||
|
user: item.user,
|
||||||
|
client: item.client,
|
||||||
|
connection: null,
|
||||||
|
options: item.options,
|
||||||
|
stream: null,
|
||||||
|
};
|
||||||
|
this.chats.push(_chat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.createParam = null;
|
||||||
|
});
|
||||||
|
// 加入房间反馈
|
||||||
|
this.socket.on("join", (res) => {
|
||||||
|
console.log("join", res);
|
||||||
|
if (res.status === "success") {
|
||||||
|
this.joinParam = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 离开房间反馈
|
||||||
|
this.socket.on("leave", (res) => {
|
||||||
|
console.log("leave", res);
|
||||||
|
if (res.status === "success") {
|
||||||
|
this.status = "join";
|
||||||
|
this.room = null;
|
||||||
|
this.users = [];
|
||||||
|
this.messages = [];
|
||||||
|
this.chats = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 更新房间数据(users,messages,chats)
|
||||||
|
this.socket.on("room", (res) => {
|
||||||
|
console.log("room", res);
|
||||||
|
if (res.status === "success") {
|
||||||
|
this.room = res.data;
|
||||||
|
|
||||||
|
sessionStorage.setItem("room", JSON.stringify(this.room));
|
||||||
|
this.users = this.room.member;
|
||||||
|
this.messages = this.room.messages.map((item) => {
|
||||||
|
let _user = this.users.find((p) => p.id === item.userId);
|
||||||
|
return {
|
||||||
|
user: _user,
|
||||||
|
message: item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chats.forEach((item) => {
|
||||||
|
if (!this.room.chats.find((q) => q.user.id === item.user.id)) {
|
||||||
|
if (item.connection) {
|
||||||
|
item.connection.close();
|
||||||
|
item.connection = null;
|
||||||
|
item.stream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chats = this.chats.filter((p) =>
|
||||||
|
this.room.chats.find((q) => q.user.id === p.user.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.room.chats.forEach((item) => {
|
||||||
|
if (item.user.id === this.user.id) return;
|
||||||
|
let _chat = this.chats.find((p) => p.user.id === item.user.id);
|
||||||
|
|
||||||
|
if (!_chat) {
|
||||||
|
_chat = {
|
||||||
|
user: item.user,
|
||||||
|
client: item.client,
|
||||||
|
connection: null,
|
||||||
|
options: item.options,
|
||||||
|
stream: null,
|
||||||
|
};
|
||||||
|
this.chats.push(_chat);
|
||||||
|
} else if (_chat.client.socketId !== item.client.socketId) {
|
||||||
|
// change peerConnection
|
||||||
|
if (_chat.connection) {
|
||||||
|
_chat.connection.close();
|
||||||
|
_chat.connection = null;
|
||||||
|
}
|
||||||
|
_chat.client = item.client;
|
||||||
|
_chat.user = item.user;
|
||||||
|
_chat.stream = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.localStream) {
|
||||||
|
this.chats.forEach((item) => {
|
||||||
|
if (this.localStream) {
|
||||||
|
if (!item.connection) {
|
||||||
|
this.register(item);
|
||||||
|
}
|
||||||
|
if (this.status === "chat") {
|
||||||
|
this.connect(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = "room";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 发送消息反馈
|
||||||
|
this.socket.on("message", (res) => {
|
||||||
|
console.log("message", res);
|
||||||
|
if (res.status === "success") {
|
||||||
|
this.message = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 加入聊天(media)反馈
|
||||||
|
this.socket.on("join_chat", (res) => {
|
||||||
|
console.log("join_chat", res);
|
||||||
|
this.status = "chat";
|
||||||
|
});
|
||||||
|
// 离开聊天(media)反馈
|
||||||
|
this.socket.on("leave_chat", (res) => {
|
||||||
|
console.log("leave_chat", res);
|
||||||
|
this.status = "room";
|
||||||
|
this.chats = [];
|
||||||
|
});
|
||||||
|
// webrtc
|
||||||
|
this.socket.on("chat", (res) => {
|
||||||
|
console.log("chat", res);
|
||||||
|
if (res.status === "success") {
|
||||||
|
let _chat = this.chats.find((p) => p.client.socketId === res.data.from);
|
||||||
|
if (!_chat) return;
|
||||||
|
if (res.data.sdp) {
|
||||||
|
if (res.data.sdp.type === "offer") {
|
||||||
|
_chat.connection
|
||||||
|
.setRemoteDescription(res.data.sdp)
|
||||||
|
.then(() => {
|
||||||
|
return _chat.connection.createAnswer();
|
||||||
|
})
|
||||||
|
.then((answer) => {
|
||||||
|
return _chat.connection.setLocalDescription(answer);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.p2p(
|
||||||
|
res.data.from,
|
||||||
|
_chat.connection.localDescription,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_chat.connection.setRemoteDescription(res.data.sdp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
res.data.candidate &&
|
||||||
|
_chat.connection.remoteDescription &&
|
||||||
|
_chat.connection.remoteDescription.type
|
||||||
|
) {
|
||||||
|
_chat.connection.addIceCandidate(res.data.candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 断开连接
|
||||||
|
this.socket.on("disconnect", () => {
|
||||||
|
console.log("disconnect");
|
||||||
|
this.user = null;
|
||||||
|
this.client = null;
|
||||||
|
this.room = null;
|
||||||
|
this.users = [];
|
||||||
|
this.messages = [];
|
||||||
|
this.chats = [];
|
||||||
|
this.status = null;
|
||||||
|
this.socket = null;
|
||||||
|
this.localStream = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessionStorage.getItem("user")) {
|
||||||
|
let _user = JSON.parse(sessionStorage.getItem("user"));
|
||||||
|
this.loginParams = { name: _user.name, unique: _user.unique };
|
||||||
|
this.login();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
*/
|
||||||
|
login() {
|
||||||
|
this.socket.emit("login", {
|
||||||
|
...this.loginParams,
|
||||||
|
clientType: this.clientType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 登出
|
||||||
|
*/
|
||||||
|
logout() {
|
||||||
|
this.socket.emit("logout", {
|
||||||
|
userId: this.user.id,
|
||||||
|
roomId: this.room ? this.room.id : null,
|
||||||
|
clientType: this.clientType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 新建房间
|
||||||
|
*/
|
||||||
|
create() {
|
||||||
|
this.socket.emit("create", {
|
||||||
|
userId: this.user.id,
|
||||||
|
roomName: this.createParam,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 加入房间
|
||||||
|
*/
|
||||||
|
join() {
|
||||||
|
this.socket.emit("join", {
|
||||||
|
userId: this.user.id,
|
||||||
|
roomId: this.joinParam,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 离开房间
|
||||||
|
*/
|
||||||
|
leave() {
|
||||||
|
this.socket.emit("leave", { userId: this.user.id, roomId: this.room.id });
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*/
|
||||||
|
send() {
|
||||||
|
this.socket.emit("message", {
|
||||||
|
userId: this.user.id,
|
||||||
|
roomId: this.room.id,
|
||||||
|
content: this.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 加入聊天(media)
|
||||||
|
*/
|
||||||
|
joinChat() {
|
||||||
|
navigator.mediaDevices
|
||||||
|
.getUserMedia({
|
||||||
|
audio: true,
|
||||||
|
video: {
|
||||||
|
width: { min: 1024, ideal: 1280, max: 1920 },
|
||||||
|
height: { min: 576, ideal: 720, max: 1080 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((stream) => {
|
||||||
|
this.localStream = stream;
|
||||||
|
})
|
||||||
|
// .then(() => {
|
||||||
|
// this.$refs.local.srcObject = this.localStream;
|
||||||
|
// })
|
||||||
|
.then(() => {
|
||||||
|
this.socket.emit("join_chat", {
|
||||||
|
userId: this.user.id,
|
||||||
|
roomId: this.room.id,
|
||||||
|
clientType: this.clientType,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 离开聊天(media)
|
||||||
|
*/
|
||||||
|
leaveChat() {
|
||||||
|
this.localStream.getTracks().forEach(function (track) {
|
||||||
|
if (track.readyState === "live") {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.localStream = null;
|
||||||
|
this.socket.emit("leave_chat", {
|
||||||
|
userId: this.user.id,
|
||||||
|
roomId: this.room.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* webrtc
|
||||||
|
*/
|
||||||
|
p2p(to, sdp, candidate) {
|
||||||
|
this.socket.emit("chat", {
|
||||||
|
from: this.socket.id,
|
||||||
|
to: to,
|
||||||
|
sdp: sdp,
|
||||||
|
candidate: candidate,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 注册 peerConnection
|
||||||
|
*/
|
||||||
|
register(param) {
|
||||||
|
param.connection = new RTCPeerConnection();
|
||||||
|
this.localStream.getTracks().forEach((track) => {
|
||||||
|
param.connection.addTrack(track, this.localStream);
|
||||||
|
});
|
||||||
|
param.connection.ontrack = (event) => {
|
||||||
|
if (event.streams.length > 0) {
|
||||||
|
if (param.stream !== event.streams[0]) {
|
||||||
|
param.stream = event.streams[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
param.connection.onicecandidate = (event) => {
|
||||||
|
if (event.candidate) {
|
||||||
|
this.p2p(param.client.socketId, null, event.candidate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 发起 p2p 连接
|
||||||
|
*/
|
||||||
|
connect(param) {
|
||||||
|
param.connection
|
||||||
|
.createOffer()
|
||||||
|
.then((offer) => {
|
||||||
|
if (param.connection.signalingState !== "stable")
|
||||||
|
return Promise.reject(
|
||||||
|
`connection state is ${param.connection.signalingState}`
|
||||||
|
);
|
||||||
|
return param.connection.setLocalDescription(offer);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.p2p(
|
||||||
|
param.client.socketId,
|
||||||
|
param.connection.localDescription,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
moment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.home {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
> input {
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&-user,
|
||||||
|
&-message,
|
||||||
|
&-chat {
|
||||||
|
border: 1px solid #ebedf0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-user,
|
||||||
|
&-chat {
|
||||||
|
overflow: hidden scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&-list {
|
||||||
|
flex: 4 1 auto;
|
||||||
|
background: #f3f3f3;
|
||||||
|
border-bottom: 1px solid #ebedf0;
|
||||||
|
overflow: hidden scroll;
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-right {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-user {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 6px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-size: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-message {
|
||||||
|
border: 1px solid #b7eb8f;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #f6ffed;
|
||||||
|
padding: 6px 12px;
|
||||||
|
min-height: 33px;
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> textarea {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-btn {
|
||||||
|
flex: 0 1 48px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-chat {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
//width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&-main {
|
||||||
|
width: 100%;
|
||||||
|
//height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-card {
|
||||||
|
width: 200px;
|
||||||
|
height: 160px;
|
||||||
|
background: #f3f3f3;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 6px;
|
||||||
|
|
||||||
|
> video {
|
||||||
|
width: 200px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports = {
|
||||||
|
devServer: {
|
||||||
|
https: false,
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
loaderOptions: {
|
||||||
|
less: {
|
||||||
|
javascriptEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
配合实现 webrtc 的 nodejs server demo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
采用http,若采用https,需要自行修改
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
|
export class Client {
|
||||||
|
id: string
|
||||||
|
socketId: string
|
||||||
|
type: string
|
||||||
|
status: string
|
||||||
|
|
||||||
|
constructor(socketId: string, type: string, status: string) {
|
||||||
|
this.id = v4()
|
||||||
|
this.socketId = socketId
|
||||||
|
this.type = type;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
login(socketId: string) {
|
||||||
|
this.socketId = socketId
|
||||||
|
this.status = 'online'
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.socketId = ""
|
||||||
|
this.status = 'offline'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {v4} from "uuid";
|
||||||
|
|
||||||
|
export class Message {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
content: string
|
||||||
|
timestamp: number
|
||||||
|
|
||||||
|
|
||||||
|
constructor(userId: string, content: string) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.content = content;
|
||||||
|
this.id = v4()
|
||||||
|
this.timestamp = new Date().valueOf()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import {User} from "./User";
|
||||||
|
import {v4} from "uuid";
|
||||||
|
import {Message} from "./Message";
|
||||||
|
import {Client} from "./Client";
|
||||||
|
|
||||||
|
export class Room {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
admin: User
|
||||||
|
member: Array<User>
|
||||||
|
messages: Array<Message>
|
||||||
|
chats: Array<any>
|
||||||
|
|
||||||
|
constructor(name: string, admin: User) {
|
||||||
|
this.id = v4()
|
||||||
|
this.name = name
|
||||||
|
this.admin = admin
|
||||||
|
this.member = []
|
||||||
|
this.messages = []
|
||||||
|
this.chats = []
|
||||||
|
this.join(this.admin)
|
||||||
|
}
|
||||||
|
|
||||||
|
isIn(user: User): boolean {
|
||||||
|
let _index = this.member.findIndex(p => p.id === user.id)
|
||||||
|
return _index >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
join(user: User) {
|
||||||
|
let _index = this.member.findIndex(p => p.id === user.id)
|
||||||
|
if (_index < 0) {
|
||||||
|
this.member.push(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(user: User) {
|
||||||
|
let _index = this.member.findIndex(p => p.id === user.id)
|
||||||
|
if (_index >= 0) {
|
||||||
|
this.member.splice(_index, 1)
|
||||||
|
}
|
||||||
|
this.close(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
chown(admin: User, user: User) {
|
||||||
|
if (admin.id !== this.admin.id) throw new Error('no permission to change owner')
|
||||||
|
let _index = this.member.findIndex(p => p.id === user.id)
|
||||||
|
if (_index <= 0) throw new Error('not in the room')
|
||||||
|
this.admin = user
|
||||||
|
}
|
||||||
|
|
||||||
|
open(user: User, client: Client, options: any) {
|
||||||
|
let _index = this.chats.findIndex(p => p.user.id === user.id)
|
||||||
|
if (_index < 0) {
|
||||||
|
this.chats.push({user: user, client: client, options: options})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(user: User) {
|
||||||
|
let _index = this.chats.findIndex(p => p.user.id === user.id)
|
||||||
|
if (_index >= 0) {
|
||||||
|
this.chats.splice(_index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
import { Client } from './Client'
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
unique: string
|
||||||
|
status: string
|
||||||
|
clients: Array<Client>
|
||||||
|
|
||||||
|
constructor(name: string, unique: string, socketId: string, clientType: string) {
|
||||||
|
this.id = v4()
|
||||||
|
this.name = name
|
||||||
|
this.unique = unique
|
||||||
|
this.status = ''
|
||||||
|
this.clients = []
|
||||||
|
this.login(socketId, clientType)
|
||||||
|
}
|
||||||
|
|
||||||
|
login(socketId: string, clientType: string) {
|
||||||
|
this.status = 'online'
|
||||||
|
let _client = this.clients.find(p => p.type === clientType)
|
||||||
|
if (_client) {
|
||||||
|
_client.login(socketId)
|
||||||
|
} else {
|
||||||
|
_client = new Client(socketId, clientType, 'online')
|
||||||
|
this.clients.push(_client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(clientType: string) {
|
||||||
|
// this.status = 'offline'
|
||||||
|
let _client = this.clients.find(p => p.type === clientType)
|
||||||
|
if (_client) {
|
||||||
|
_client.logout()
|
||||||
|
// setTimeout(() => {
|
||||||
|
// this.clients.splice(this.clients.findIndex(p => p.type === clientType), 1)
|
||||||
|
// }, 1000 * 60 * 30)
|
||||||
|
}
|
||||||
|
this.status = this.clients.some(p => p.status === 'online') ? 'online' : 'offline'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,246 @@
|
||||||
|
import express from 'express'
|
||||||
|
import {createServer} from 'http'
|
||||||
|
import {Server, Socket} from 'socket.io'
|
||||||
|
import { Client } from './entity/Client';
|
||||||
|
import {User} from "./entity/User";
|
||||||
|
import {Room} from "./entity/Room";
|
||||||
|
import {Message} from "./entity/Message";
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
const httpServer = createServer(app)
|
||||||
|
const io = new Server(httpServer,{
|
||||||
|
cors: {
|
||||||
|
origin: '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const rooms: Array<Room> = []
|
||||||
|
|
||||||
|
function getRoom(id: string): Room | null {
|
||||||
|
return rooms.find(p => p.id === id) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const users: Array<User> = []
|
||||||
|
|
||||||
|
function checkUser(unique: string, name: string): boolean {
|
||||||
|
return users.some(p => p.unique === unique && p.name != name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function findUser(name: string, unique: string): User | null {
|
||||||
|
return users.find(p => p.name === name && p.unique === unique) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUser(id: string): User | null {
|
||||||
|
return users.find(p => p.id === id) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClient(user: User, clientType: string): Client | null {
|
||||||
|
return user.clients.find(p => p.type === clientType) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
io.on("connection", (socket: Socket) => {
|
||||||
|
console.log('connected', socket.id)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登陆操作
|
||||||
|
*/
|
||||||
|
socket.on('login', req => {
|
||||||
|
console.log('login', req)
|
||||||
|
if (checkUser(req.unique, req.name)) return socket.emit('login',{status: 'error', message: 'unique is already existed'})
|
||||||
|
let _user = findUser(req.name, req.unique)
|
||||||
|
if (_user) {
|
||||||
|
let _client = getClient(_user, req.clientType)
|
||||||
|
if (_client) {
|
||||||
|
socket.to(_client.socketId).emit('login', {status: 'error', message: 'login at other place'})
|
||||||
|
_user.logout(req.clientType)
|
||||||
|
}
|
||||||
|
_user.login(socket.id, req.clientType)
|
||||||
|
socket.emit('login', {status: 'success', message: 'login success', data: _user})
|
||||||
|
} else {
|
||||||
|
_user = new User(req.name, req.unique, socket.id, req.clientType)
|
||||||
|
users.push(_user)
|
||||||
|
socket.emit('login', {status: 'success', message: 'login success', data: _user})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出操作
|
||||||
|
*/
|
||||||
|
socket.on('logout', req => {
|
||||||
|
console.log('logout', req)
|
||||||
|
let _user = getUser(req.userId)
|
||||||
|
if (_user) {
|
||||||
|
_user.logout(req.clientType)
|
||||||
|
|
||||||
|
let _room = getRoom(req.roomId)
|
||||||
|
if (_room) {
|
||||||
|
_room.leave(_user)
|
||||||
|
socket.leave(_room.id)
|
||||||
|
|
||||||
|
if (_room.member.length === 0) {
|
||||||
|
rooms.splice(rooms.findIndex(p => p.id === req.roomId), 1)
|
||||||
|
} else {
|
||||||
|
io.in(req.roomId).emit('room', {status: 'success', data:_room})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('logout', {status: 'success', message: 'logout success'})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增房间
|
||||||
|
*/
|
||||||
|
socket.on('create', req => {
|
||||||
|
console.log('create', req)
|
||||||
|
let _user = getUser(req.userId)
|
||||||
|
if (!_user) return socket.emit('create', {status: 'failure', message: 'user not existed'})
|
||||||
|
|
||||||
|
let _room = new Room(req.roomName, _user)
|
||||||
|
rooms.push(_room)
|
||||||
|
socket.join(_room.id)
|
||||||
|
socket.emit('create', {status: 'success', message: 'create room success', data: _room})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加入房间
|
||||||
|
*/
|
||||||
|
socket.on('join', req => {
|
||||||
|
console.log('join', req)
|
||||||
|
let _user = getUser(req.userId)
|
||||||
|
if (!_user) return socket.emit('join', {status: 'failure', message: 'user not existed'})
|
||||||
|
let _room = getRoom(req.roomId)
|
||||||
|
if (!_room) return socket.emit('join', {status: 'failure', message: 'room not existed'})
|
||||||
|
|
||||||
|
_room.join(_user)
|
||||||
|
socket.join(_room.id)
|
||||||
|
|
||||||
|
socket.emit('join', {status: 'success', message: 'join success'})
|
||||||
|
io.in(req.roomId).emit('room', {status: 'success', data:_room})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取房间信息
|
||||||
|
*/
|
||||||
|
socket.on('room', req => {
|
||||||
|
let _room = getRoom(req.roomId)
|
||||||
|
if (!_room) return socket.emit('join', {status: 'failure', message: 'room not existed'})
|
||||||
|
socket.emit('room', {status: 'success',data:_room})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 离开房间
|
||||||
|
*/
|
||||||
|
socket.on('leave', req => {
|
||||||
|
console.log('leave', req)
|
||||||
|
let _user = getUser(req.userId)
|
||||||
|
if (!_user) return socket.emit('leave', {status: 'failure', message: 'user not existed'})
|
||||||
|
let _room = getRoom(req.roomId)
|
||||||
|
if (!_room) return socket.emit('leave', {status: 'failure', message: 'room not existed'})
|
||||||
|
|
||||||
|
_room.leave(_user)
|
||||||
|
socket.leave(_room.id)
|
||||||
|
|
||||||
|
if (_room.member.length === 0) {
|
||||||
|
rooms.splice(rooms.findIndex(p => p.id === req.roomId), 1)
|
||||||
|
} else {
|
||||||
|
io.in(req.roomId).emit('room', {status: 'success', data:_room})
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('leave', {status: 'success',message: 'leave success'})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*/
|
||||||
|
socket.on('message', req => {
|
||||||
|
console.log('message', req)
|
||||||
|
let _room = getRoom(req.roomId)
|
||||||
|
if (!_room) return socket.emit('message', {status: 'failure', message: 'room not existed'})
|
||||||
|
let _message = new Message(req.userId, req.content)
|
||||||
|
_room.messages.push(_message)
|
||||||
|
|
||||||
|
// socket.to(req.roomId).emit('message', {status: 'success', data: _message})
|
||||||
|
socket.emit('message', {status: 'success', message: 'send message success'})
|
||||||
|
io.in(req.roomId).emit('room', {status: 'success', data:_room})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加入聊天
|
||||||
|
*/
|
||||||
|
socket.on('join_chat', req => {
|
||||||
|
console.log('join_chat', req)
|
||||||
|
let _user = getUser(req.userId)
|
||||||
|
if (!_user) return socket.emit('join_chat', {status: 'failure', message: 'user not existed'})
|
||||||
|
let _room = getRoom(req.roomId)
|
||||||
|
if (!_room) return socket.emit('join_chat', {status: 'failure', message: 'room not existed'})
|
||||||
|
let _client = getClient(_user, req.clientType)
|
||||||
|
if (!_client) return socket.emit('join_chat', {status: 'failure', message: 'client not existed'})
|
||||||
|
|
||||||
|
_room.open(_user, _client, {video: req.video, audio: req.audio})
|
||||||
|
|
||||||
|
socket.emit('join_chat', {status: 'success',message: 'join chat success'})
|
||||||
|
|
||||||
|
io.in(req.roomId).emit('room', {status: 'success', data:_room})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 离开聊天
|
||||||
|
*/
|
||||||
|
socket.on('leave_chat', req => {
|
||||||
|
console.log('leave_chat', req)
|
||||||
|
let _user = getUser(req.userId)
|
||||||
|
if (!_user) return socket.emit('leave_chat', {status: 'failure', message: 'user not existed'})
|
||||||
|
let _room = getRoom(req.roomId)
|
||||||
|
if (!_room) return socket.emit('leave_chat', {status: 'failure', message: 'room not existed'})
|
||||||
|
|
||||||
|
_room.close(_user)
|
||||||
|
|
||||||
|
socket.emit('leave_chat', {status: 'success',message: 'leave chat success'})
|
||||||
|
io.in(req.roomId).emit('room', {status: 'success', data:_room})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* webrtc
|
||||||
|
*/
|
||||||
|
socket.on('chat', req => {
|
||||||
|
console.log('chat', req)
|
||||||
|
socket.to(req.to).emit('chat', {status: 'success', data: {from: req.from, to: req.to, sdp: req.sdp, candidate: req.candidate}})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开
|
||||||
|
*/
|
||||||
|
socket.on('close', req => {
|
||||||
|
console.log('close',req)
|
||||||
|
if (req.userId) {
|
||||||
|
let _user = users.find(p => p.id === req.userId)
|
||||||
|
if (_user) {
|
||||||
|
_user.logout(req.clientType)
|
||||||
|
|
||||||
|
let _room = rooms.find(p => p.id === req.roomId)
|
||||||
|
if (_room) {
|
||||||
|
_room.leave(_user)
|
||||||
|
|
||||||
|
if (_room.member.length === 0) {
|
||||||
|
rooms.splice(rooms.findIndex(p => p.id === req.roomId), 1)
|
||||||
|
} else {
|
||||||
|
io.in(req.roomId).emit('room', {status: 'success', data:_room})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('disconnect', socket.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
httpServer.listen(8000, () => {
|
||||||
|
console.log('application listening 8000')
|
||||||
|
})
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "server-nodejs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "webrtc demo server by nodejs",
|
||||||
|
"main": "main.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "null",
|
||||||
|
"dev": "nodemon --ext js,ts --exec 'ts-node main.ts'"
|
||||||
|
},
|
||||||
|
"author": "zhouxhere",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"socket.io": "^4.1.3",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/uuid": "^8.3.1",
|
||||||
|
"nodemon": "^2.0.12",
|
||||||
|
"ts-node": "^10.1.0",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es6",
|
||||||
|
"sourceMap": true,
|
||||||
|
"rootDir": "./",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue