learn-webrtc/webrtc-static/src/pages/Room.vue
2022-07-05 15:58:51 +08:00

413 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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