413 lines
14 KiB
Vue
413 lines
14 KiB
Vue
<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>
|
||
<Video
|
||
:cameras="cameras"
|
||
:currentCamera="currentCamera"
|
||
:showStartVideoByReceiver="showStartVideoByReceiver"
|
||
:showStartVideoBySender="showStartVideoBySender"
|
||
:showVideo="showVideo"
|
||
@cancelSendVideo="cancelSendVideo"
|
||
@cancelReceiveVideo="cancelReceiveVideo"
|
||
@hangupVideo="hangUpVideo"
|
||
@answerVideo="answerVideo"
|
||
@cameraChange="cameraChange"
|
||
/>
|
||
</div>
|
||
<div v-else>
|
||
<h1>当前域名的浏览器不支持WebRTC!</h1>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import socket from '../utils/socket.js';
|
||
import Video, { setRemoteSteam, setLocalStream } from '@/pages/Video';
|
||
|
||
export default {
|
||
name: 'Room',
|
||
components: { Video },
|
||
created () {
|
||
if (this.canSupportWebRTC()) {
|
||
this.initSocketEvents();
|
||
}
|
||
},
|
||
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,
|
||
showVideo: false,
|
||
devices: [],
|
||
showStartVideoByReceiver: null,
|
||
showStartVideoBySender: null,
|
||
remoteStream: null,
|
||
currentCamera: 'default',
|
||
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);
|
||
},
|
||
cameras () {
|
||
return this.devices.filter(i => i.kind === 'videoinput');
|
||
}
|
||
},
|
||
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();
|
||
this.devices = devices;
|
||
console.log('devices', 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
|
||
this.hideAllVideoModal();
|
||
});
|
||
socket.on('disconnect', (message) => {
|
||
this.showFormArea = true;
|
||
this.sockId = '';
|
||
console.log('client sock disconnect:', message);
|
||
socket.emit('userLeave', this.user);
|
||
// TODO: 挂断视频 0-0
|
||
this.hideAllVideoModal();
|
||
});
|
||
// ================== 视频相关 =====================
|
||
|
||
// 取消发送视频
|
||
socket.on('cancelSendVideo', (user) => {
|
||
const infoTips = user.sockId === this.sockId ? '您取消了发送视频' : '对方取消了发送视频';
|
||
this.$message.info(infoTips);
|
||
this.hideAllVideoModal();
|
||
});
|
||
// 接收视频邀请
|
||
socket.on('receiveVideo', (sender) => {
|
||
if (this.user.sockId === sender.sockId) return false;
|
||
this.showStartVideoBySender = sender;
|
||
});
|
||
// 拒绝接收视频
|
||
socket.on('rejectReceiveVideo', (user) => {
|
||
const infoTips = user.sockId === this.sockId ? '您拒绝了接收视频' : '对方拒绝了接收视频';
|
||
this.$message.info(infoTips);
|
||
this.hideAllVideoModal();
|
||
});
|
||
// 接听视频
|
||
socket.on('answerVideo', async (user) => {
|
||
this.showVideo = true;
|
||
// 创建本地视频流信息
|
||
this.localStream = await this.createLocalVideoStream();
|
||
setLocalStream(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.peer.onicecandidate = (event) => {
|
||
if (event.candidate) {
|
||
socket.emit('addIceCandidate', { candidate: event.candidate, user: this.user });
|
||
}
|
||
};
|
||
this.peer.onaddstream = (event) => {
|
||
// 拿到对方的视频流
|
||
setRemoteSteam(event.stream);
|
||
};
|
||
this.peer.onclose = () => {
|
||
};
|
||
// 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;
|
||
this.hideAllVideoModal();
|
||
setRemoteSteam(null);
|
||
setLocalStream(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);
|
||
this.showStartVideoByReceiver = this.receiveUser;
|
||
},
|
||
hideAllVideoModal () {
|
||
this.showVideo = false;
|
||
this.showStartVideoByReceiver = null;
|
||
this.showStartVideoBySender = null;
|
||
},
|
||
cancelSendVideo () {
|
||
socket.emit('cancelSendVideo', this.user);
|
||
this.hideAllVideoModal();
|
||
},
|
||
cancelReceiveVideo () {
|
||
socket.emit('rejectReceiveVideo', this.user);
|
||
this.hideAllVideoModal();
|
||
},
|
||
answerVideo () {
|
||
socket.emit('answerVideo', this.user);
|
||
},
|
||
hangUpVideo () {
|
||
socket.emit('hangupVideo', this.user);
|
||
},
|
||
async cameraChange (deviceId) {
|
||
const localStream = await navigator.mediaDevices.getUserMedia({
|
||
audio: true,
|
||
video: {
|
||
deviceId
|
||
}
|
||
});
|
||
const videoTrack = localStream.getVideoTracks()[0];
|
||
const sender = this.peer.getSenders().find(function (s) {
|
||
return s.track.kind == videoTrack.kind;
|
||
});
|
||
console.log('found sender:', sender);
|
||
sender.replaceTrack(videoTrack);
|
||
this.localStream = localStream;
|
||
setLocalStream(localStream);
|
||
},
|
||
async createLocalVideoStream () {
|
||
// Link: https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
|
||
const localStream = await navigator.mediaDevices.getUserMedia({
|
||
audio: true,
|
||
video: true
|
||
});
|
||
console.log('localStream:', localStream);
|
||
return localStream;
|
||
},
|
||
}
|
||
};
|
||
</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>
|