This commit is contained in:
EleanorMao
2022-07-04 23:55:05 +08:00
commit 83089af7c7
24 changed files with 8904 additions and 0 deletions

View File

@ -0,0 +1,23 @@
// http://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
globals: {
Vue: true,
VIDEO_VIEW: true,
},
env: {
browser: true
},
extends: ['plugin:vue/essential', 'standard'],
plugins: ['vue'],
rules: {
eqeqeq: 0,
'comma-dangle': 0,
'generator-star-spacing': 0,
semi: [2, 'always'],
'prefer-promise-reject-errors': 0
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

View File

@ -0,0 +1,54 @@
{
"name": "webrtc-demo-client",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "vue-cli-service serve",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^2.6.5",
"element-ui": "^2.13.2",
"socket.io-client": "^2.3.0",
"vue": "^2.6.10"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.12.0",
"@vue/cli-plugin-eslint": "^3.12.0",
"@vue/cli-service": "^3.12.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^5.0.0",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<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 %>webrtc-icon-192x192.png">
<title>webrtc-demo</title>
</head>
<body>
<div id="app"></div>
<script src="<%= BASE_URL %>video-view.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

25
webrtc-static/src/App.vue Normal file
View File

@ -0,0 +1,25 @@
<template>
<div id="app">
<Room/>
</div>
</template>
<script>
import Room from './pages/Room.vue';
export default {
name: 'app',
components: {
Room
}
};
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
}
</style>

10
webrtc-static/src/main.js Normal file
View File

@ -0,0 +1,10 @@
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
Vue.use(ElementUI);
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');

View File

@ -0,0 +1,394 @@
<template>
<div class="m-room-wrapper">
<div class="can-support-rtc" v-if="canSupportVideo">
<div class="form-area" v-if="showFormArea">
<el-form
:model="roomForm"
:rules="rules"
ref="roomForm"
label-width="100px"
class="room-form"
>
<el-form-item label="房间ID" prop="roomId">
<el-input v-model.trim="roomForm.roomId" :disabled="!canClickBtn"></el-input>
</el-form-item>
<el-form-item label="姓名" prop="userName">
<el-input v-model.trim="roomForm.userName" :disabled="!canClickBtn"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :disabled="!canClickBtn">加入房间</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="list-area" v-if="!showFormArea">
<h2>当前房间id: {{ roomForm.roomId }} </h2>
<h2>在线人数: {{ roomUsers.length }} </h2>
<el-card class="box-card">
<div v-for="item in roomUsers" :key="item.sockId" class="item">
{{ item.userName }}
</div>
</el-card>
<el-button type="primary" v-if="roomUsers.length > 1 && sockId" @click="toSendVideo">
发起视频
</el-button>
</div>
</div>
<div v-else>
<h1>当前域名的浏览器不支持WebRTC</h1>
</div>
</div>
</template>
<script>
import socket from '../utils/socket.js';
export default {
name: 'Room',
created () {
if (this.canSupportWebRTC()) {
this.initSocketEvents();
this.initVIDEO_VIEWSdk();
}
},
data () {
const validateRoomId = (rule, value, callback) => {
const reg = /^\d{1,4}$/;
if (!reg.test(value)) {
return callback(new Error('房间ID只能为1-4位的数字'));
}
callback();
};
const validateName = (rule, value, callback) => {
const reg = /^[\u4e00-\u9fa5a-zA-Z-z]{1,10}$/;
if (!reg.test(value)) {
return callback(new Error('请输入合法的姓名'));
}
callback();
};
return {
showFormArea: true,
roomForm: {
roomId: '',
userName: ''
},
rules: {
roomId: [
{ required: true, message: '请输入房间ID', trigger: ['blur', 'change'] },
{ validator: validateRoomId, trigger: ['blur', 'change'] }
],
userName: [
{ required: true, message: '请输入姓名', trigger: ['blur', 'change'] },
{ validator: validateName, trigger: ['blur', 'change'] }
],
},
canClickBtn: true,
sockId: '',
roomUsers: [],
canSupportVideo: false,
localStream: null,
peer: null,
peerConfigs: {
// 本地测试无需打洞 如部署到公网 需填写coturn的配置
// iceServers: [{
// urls: 'turn:xxx:3478',
// credential: 'xxx',
// username: 'xxx'
// }],
},
offerOption: {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
},
};
},
computed: {
user () {
return Object.assign({}, { sockId: this.sockId }, this.roomForm);
},
receiveUser () {
return this.roomUsers.find(item => item.sockId !== this.sockId);
},
},
methods: {
canSupportWebRTC () {
// mediaDevices 只能在https下面才有
// 该对象可提供对相机和麦克风等媒体输入设备的连接访问,也包括屏幕共享。
if (typeof navigator.mediaDevices !== 'object') {
this.$message.error('No navigator.mediaDevices');
return false;
}
// 请求一个可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备等
if (typeof navigator.mediaDevices.enumerateDevices !== 'function') {
this.$message.error('No navigator.mediaDevices.enumerateDevices');
return false;
}
// 会提示用户给予使用媒体输入的许可媒体输入会产生一个MediaStream里面包含了请求的媒体类型的轨道。
// 此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、
// 一个音频轨道同样来自硬件或虚拟音频源比如麦克风、A/D 转换器等等),也可能是其它轨道类型。
if (typeof navigator.mediaDevices.getUserMedia !== 'function') {
this.$message.error('No navigator.mediaDevices.getUserMedia');
return false;
}
this.canSupportVideo = true;
this.getDevices();
return true;
},
async getDevices () {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
VIDEO_VIEW.showDevicesNameByDevices(devices);
} catch (error) {
console.error(error);
const msg = `getDevices error: ${error.name} : ${error.message}`;
this.$message.error(msg);
}
},
initSocketEvents () {
// 溜溜球 -0-
window.onbeforeunload = () => {
socket.emit('userLeave', {
userName: this.roomForm.userName,
sockId: this.sockId,
roomId: this.roomForm.roomId,
});
};
socket.on('connectionSuccess', (sockId) => {
this.sockId = sockId;
console.log('connectionSuccess client sockId:', sockId);
});
socket.on('checkRoomSuccess', (existRoomUsers) => {
this.canClickBtn = true;
if (existRoomUsers && existRoomUsers.length > 1) {
this.$message.info('当前房间人数已满~请换个房间id');
} else {
this.showFormArea = false;
this.roomUsers = [
{
userName: this.roomForm.userName + '(我)',
sockId: this.sockId,
roomId: this.roomForm.roomId,
}
];
}
});
socket.on('joinRoomSuccess', (roomUsers) => {
console.log('joinRoomSuccess client user:', roomUsers);
const otherUser = roomUsers.find(item => item.sockId !== this.sockId);
if (!otherUser) return false;
this.$message.success(`${otherUser.userName}加入了房间`);
this.roomUsers = [otherUser, {
userName: this.roomForm.userName + '(我)',
sockId: this.sockId,
roomId: this.roomForm.roomId,
}];
});
socket.on('userLeave', (roomUsers) => {
console.log('userLeave client user:', roomUsers);
if (!roomUsers.length) {
this.showFormArea = true;
this.sockId = '';
}
const serverSockIdArr = roomUsers.map(item => item.sockId);
this.roomUsers.forEach(item => {
if (serverSockIdArr.indexOf(item.sockId) === -1) {
this.$message.info(`${item.userName}离开了房间`);
if (item.sockId === this.sockId) {
this.showFormArea = true;
this.sockId = '';
}
}
});
this.roomUsers = roomUsers;
this.roomUsers.forEach((item) => {
if (item.sockId === this.sockId) {
item.userName = item.userName + '(我)';
}
});
// TODO: 挂断视频 0-0
VIDEO_VIEW.hideAllVideoModal();
});
socket.on('disconnect', (message) => {
this.showFormArea = true;
this.sockId = '';
console.log('client sock disconnect:', message);
socket.emit('userLeave', this.user);
// TODO: 挂断视频 0-0
VIDEO_VIEW.hideAllVideoModal();
});
// ================== 视频相关 =====================
// 取消发送视频
socket.on('cancelSendVideo', (user) => {
const infoTips = user.sockId === this.sockId ? '您取消了发送视频' : '对方取消了发送视频';
this.$message.info(infoTips);
VIDEO_VIEW.hideAllVideoModal();
});
// 接收视频邀请
socket.on('receiveVideo', (sender) => {
if (this.user.sockId === sender.sockId) return false;
VIDEO_VIEW.showReceiveVideoModalBySender(sender);
});
// 拒绝接收视频
socket.on('rejectReceiveVideo', (user) => {
const infoTips = user.sockId === this.sockId ? '您拒绝了接收视频' : '对方拒绝了接收视频';
this.$message.info(infoTips);
VIDEO_VIEW.hideAllVideoModal();
});
// 接听视频
socket.on('answerVideo', async (user) => {
VIDEO_VIEW.showInvideoModal();
// 创建本地视频流信息
this.localStream = await this.createLocalVideoStream();
document.querySelector('#echat-local').srcObject = this.localStream;
// Link: https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection
// RTCPeerConnection 接口代表一个由本地计算机到远端的 WebRTC 连接
// 呼叫方发送一个 offer(请求),被呼叫方发出一个 answer应答来回答请求
this.peer = new RTCPeerConnection();
console.log(this.peer);
this.initPeerListen();
// Adding a local stream won't trigger the onaddstream callback
this.peer.addStream(this.localStream);
if (user.sockId === this.sockId) {
// 接收方
} else {
// 发送方 创建offer
const offer = await this.peer.createOffer(this.offerOption);
console.log('我的offer', offer);
// send the offer to a server to be forwarded to the friend you're calling.
await this.peer.setLocalDescription(offer);
socket.emit('receiveOffer', { user: this.user, offer });
}
});
// 挂断视频
socket.on('hangupVideo', (user) => {
const infoTips = user.sockId === this.sockId ? '您挂断了视频' : '对方挂断了视频';
this.$message.info(infoTips);
this.peer.close();
this.peer = null;
VIDEO_VIEW.hideAllVideoModal();
document.querySelector('#echat-remote-1').srcObject = null;
document.querySelector('#echat-local').srcObject = null;
});
//
socket.on('addIceCandidate', async (candidate) => {
await this.peer.addIceCandidate(candidate);
});
socket.on('receiveOffer', async (offer) => {
// send the answer to a server to be forwarded back to the caller
await this.peer.setRemoteDescription(offer);
const answer = await this.peer.createAnswer();
await this.peer.setLocalDescription(answer);
socket.emit('receiveAnswer', { answer, user: this.user });
});
socket.on('receiveAnswer', (answer) => {
// 处理应答, 同时在呼叫发起方,你会收到这个应答(前面被呼叫方发出的 answer你需要将它设置为你的远端连接。
this.peer.setRemoteDescription(answer);
});
},
submitForm () {
if (!this.sockId) {
this.$message.error('socket未连接成功,请刷新再尝试!');
window.location.reload();
return false;
}
this.$refs.roomForm.validate((valid) => {
if (valid) {
// 检查该房间人数
this.canClickBtn = false;
socket.emit('checkRoom', {
roomId: this.roomForm.roomId,
sockId: this.sockId,
userName: this.roomForm.userName
});
} else {
console.log('error submit!!');
}
});
},
resetForm () {
this.$refs.roomForm.resetFields();
this.roomForm.roomId = '';
this.roomForm.userName = '';
},
// 发送视频
toSendVideo () {
socket.emit('toSendVideo', this.user);
VIDEO_VIEW.showStartVideoModalByReceiver(this.receiveUser);
},
initVIDEO_VIEWSdk () {
const configOptios = {
startVideoCancelCb: this.startVideoCancelCb,
receiveVideoCancelCb: this.receiveVideoCancelCb,
receiveVideoAnswerCb: this.receiveVideoAnswerCb,
hangUpVideoCb: this.hangUpVideoCb,
openMikeCb: this.openMikeCb,
closeMikeCb: this.closeMikeCb,
openCammerCb: this.openCammerCb,
closeCammerCb: this.closeCammerCb,
toScreenCb: this.toScreenCb,
};
VIDEO_VIEW.configCallBack(configOptios);
},
startVideoCancelCb () {
socket.emit('cancelSendVideo', this.user);
VIDEO_VIEW.hideAllVideoModal();
},
receiveVideoCancelCb () {
socket.emit('rejectReceiveVideo', this.user);
VIDEO_VIEW.hideAllVideoModal();
},
receiveVideoAnswerCb () {
socket.emit('answerVideo', this.user);
},
hangUpVideoCb () {
socket.emit('hangupVideo', this.user);
},
openMikeCb () {
},
closeMikeCb () {
},
openCammerCb () {
},
closeCammerCb () {
},
toScreenCb () {
},
async createLocalVideoStream () {
const constraints = { audio: true, video: true };
// Link: https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
const localStream = await navigator.mediaDevices.getUserMedia(constraints);
console.log('localStream:', localStream);
return localStream;
},
initPeerListen () {
this.peer.onicecandidate = (event) => {
if (event.candidate) { socket.emit('addIceCandidate', { candidate: event.candidate, user: this.user }); }
};
this.peer.onaddstream = (event) => {
// 拿到对方的视频流
document.querySelector('#echat-remote-1').srcObject = event.stream;
};
this.peer.onclose = () => {
};
},
}
};
</script>
<style>
.m-room-wrapper{
margin-top: 20px;
}
.m-room-wrapper .box-card {
width: 480px;
}
.m-room-wrapper .box-card .item{
padding: 18px 0;
}
</style>

View File

@ -0,0 +1,4 @@
import io from 'socket.io-client';
const host = 'localhost:3000';
const socket = io.connect(host);
export default socket;

View File

@ -0,0 +1,3 @@
module.exports = {
outputDir: '../webrtc-server/static'
};

7578
webrtc-static/yarn.lock Normal file

File diff suppressed because it is too large Load Diff