feat:提交代码

This commit is contained in:
zhouxhere 2021-12-24 17:58:05 +08:00 committed by 徐洲
commit edc7fb6507
27 changed files with 35641 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# webstorm
.idea
# mac
.DS_Store
# nodejs
node_modules

21
LICENSE Normal file
View File

@ -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.

View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

14
client-vue/.eslintrc.js Normal file
View File

@ -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",
},
};

32
client-vue/README.md Normal file
View File

@ -0,0 +1,32 @@
# client-vue
webrtc 客户端 vue 实现 client demo
问题navigator.mediaDevices 需要在https下 localhost 除外)
1. 调试时可修改 vue.config.js 中 devServer https但是后端也需要修改为 https
2. chrome 浏览器 设置 chrome://flagsInsecure 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/).

View File

@ -0,0 +1,13 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: [
[
"import",
{
libraryName: "ant-design-vue",
libraryDirectory: "es",
style: true,
},
],
],
};

30315
client-vue/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

44
client-vue/package.json Normal file
View File

@ -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

View File

@ -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>

17
client-vue/src/App.vue Normal file
View File

@ -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

View File

@ -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>

24
client-vue/src/core/bootstrap.js vendored Normal file
View File

@ -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);

12
client-vue/src/main.js Normal file
View File

@ -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");

View File

@ -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;

View File

@ -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 = [];
}
});
// usersmessageschats
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>

12
client-vue/vue.config.js Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
devServer: {
https: false,
},
css: {
loaderOptions: {
less: {
javascriptEnabled: true,
},
},
},
};

5
server-nodejs/README.md Normal file
View File

@ -0,0 +1,5 @@
配合实现 webrtc 的 nodejs server demo
采用http若采用https需要自行修改

View File

@ -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'
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

@ -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'
}
}

246
server-nodejs/main.ts Normal file
View File

@ -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')
})

3858
server-nodejs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"sourceMap": true,
"rootDir": "./",
"outDir": "./dist",
"esModuleInterop": true,
"strict": true
},
"exclude": [
"node_modules"
]
}