ソースを参照

视频聊天功能-开发中

xie.bx 3 年 前
コミット
c427d3c63d

+ 21 - 0
im-ui/src/api/enums.js

@@ -0,0 +1,21 @@
+
+const MESSAGE_TYPE = {
+	RTC_CALL: 101,
+	RTC_ACCEPT: 102,
+	RTC_REJECT: 103,
+	RTC_CANCEL: 104,
+	RTC_FAILED: 105,
+	RTC_HANDUP: 106,
+	RTC_CANDIDATE: 107
+}
+
+const USER_STATE = {
+	OFFLINE: 0,
+	FREE: 1,
+	BUSY: 2
+}
+
+export {
+	MESSAGE_TYPE,
+	USER_STATE
+}

+ 1 - 1
im-ui/src/components/chat/ChatBox.vue

@@ -36,7 +36,7 @@
 							</div>
 							<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
 							</div>
-							<div title="发起视频" class="el-icon-phone-outline" @click="showVideoBox()">
+							<div title="视频聊天" v-show="chat.type=='PRIVATE'" class="el-icon-phone-outline" @click="showVideoBox()">
 							</div>
 							<div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
 						</div>

+ 322 - 0
im-ui/src/components/chat/ChatPrivateVideo.vue

@@ -0,0 +1,322 @@
+<template>
+	<el-dialog :title="title" :visible.sync="visible" width="800px" :before-close="handleClose">
+		<div class="chat-video">
+			<div class="chat-video-box">
+				<div class="chat-video-friend" 
+					v-loading="loading" 
+					element-loading-text="等待对方接听..." 
+					element-loading-spinner="el-icon-loading"
+				 element-loading-background="rgba(0, 0, 0, 0.9)">
+					<video ref="friendVideo" autoplay=""></video>
+				</div>
+				<div class="chat-video-mine">
+					<video ref="mineVideo" autoplay=""></video>
+				</div>
+			</div>
+			<div class="chat-video-controllbar">
+				
+				<div v-show="state=='CONNECTING'" title="取消呼叫" class="icon iconfont icon-phone-reject reject" style="color: red;" @click="cancel()"></div>
+				<div v-show="state=='CONNECTED'" title="挂断" class="icon iconfont icon-phone-reject reject" style="color: red;" @click="handup()"></div>
+				
+			</div>
+		</div>
+	</el-dialog>
+
+</template>
+
+<script>
+	export default {
+		name: 'chatVideo',
+		props: {
+			visible: {
+				type: Boolean
+			},
+			friend: {
+				type: Object
+			},
+			master: {
+				type: Boolean
+			},
+			offer: {
+				type: Object
+			}
+		},
+		data() {
+			return {
+				stream: null,
+				loading: false,
+				peerConnection: null,
+				state: 'NOT_CONNECTED',
+				candidates: [],
+				configuration: {
+					iceServers: [{
+							"urls": navigator.mozGetUserMedia ? "stun:stun.services.mozilla.com" : navigator.webkitGetUserMedia ?
+								"stun:stun.l.google.com:19302" : "stun:23.21.150.121"
+						},
+						{
+							urls: "stun:stun.l.google.com:19302"
+						}
+					]
+				}
+			}
+		},
+		methods: {
+			init() {
+				if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) {
+					this.$message.error("您的浏览器不支持WebRTC");
+					if (!this.master) {
+						this.sendFailed("对方浏览器不支持WebRTC")
+					}
+					return;
+				}
+
+				// 打开摄像头
+				this.openCamera((stream) => {
+					// 初始化webrtc连接
+					this.setupPeerConnection(stream);
+					if (this.master) {
+						// 发起呼叫
+						this.call();
+					} else {
+						// 接受呼叫
+						this.accept(this.offer);
+					}
+				});
+
+			},
+			openCamera(callback) {
+				navigator.getUserMedia({
+						video: true,
+						audio: true
+					},
+					(stream) => {
+						this.stream = stream;
+						this.$refs.mineVideo.srcObject = stream;
+						this.$refs.mineVideo.muted = true;
+						callback(stream)
+					},
+					(error) => {
+						this.$message.error("打开摄像头失败:" + error);
+						callback()
+					});
+			},
+			closeCamera(){
+				if(this.stream){
+					this.stream.getVideoTracks().forEach((track) =>{
+						track.stop();
+						this.$refs.mineVideo.srcObject = null;
+					 });
+					 this.stream = null;
+				}
+				
+			},
+			setupPeerConnection(stream) {
+				this.peerConnection = new RTCPeerConnection(this.configuration);
+				this.peerConnection.onaddstream = (e) => {
+					console.log("onaddstream")
+					this.$refs.friendVideo.srcObject = e.stream;
+				};
+				this.peerConnection.onicecandidate = (event) => {
+					if (event.candidate) {
+						if(this.state == 'CONNECTED'){
+							// 已连接,直接发送
+							this.sendCandidate(event.candidate);
+						}else{
+							// 未连接,缓存起来,连接后再发送
+							this.candidates.push(event.candidate)
+						}
+					}
+				}
+				if (stream) {
+					this.peerConnection.addStream(stream);
+				}
+			},
+			handleMessage(msg) {
+				if (msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) {
+					this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content)));
+					// 关闭等待提示
+					this.loading = false;
+					// 状态为连接中
+					this.state = 'CONNECTED';
+					// 发送candidate
+					this.candidates.forEach((candidate) => {
+						this.sendCandidate(candidate);
+					})
+				}
+				if (msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) {
+					this.$message.error("对方拒绝了您的视频请求");
+					this.peerConnection.close();
+					// 关闭等待提示
+					this.loading = false;
+					// 状态为未连接
+					this.state = 'NOT_CONNECTED';
+				}
+				if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) {
+					this.$message.error(msg.content)
+					// 关闭等待提示
+					this.loading = false;
+					// 状态为未连接
+					this.state = 'NOT_CONNECTED';
+				}
+				if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
+					this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content)));
+				}
+				if (msg.type == this.$enums.MESSAGE_TYPE.RTC_HANDUP) {
+					this.$message.success("对方已挂断");
+					this.close();
+				}
+			},
+			call() {
+				this.peerConnection.createOffer((offer) => {
+						this.peerConnection.setLocalDescription(offer);
+						this.$http({
+							url: `/webrtc/private/call?uid=${this.friend.id}`,
+							method: 'post',
+							data: offer
+						}).then(()=>{
+							this.loading = true;
+							this.state = 'CONNECTING';
+						});
+					},
+					(error) => {
+						this.$message.error(error);
+					});
+
+			},
+			accept(offer) {
+				this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
+				this.peerConnection.createAnswer((answer) => {
+						this.peerConnection.setLocalDescription(answer);
+						this.$http({
+							url: `/webrtc/private/accept?uid=${this.friend.id}`,
+							method: 'post',
+							data: answer
+						})
+						this.state='CONNECTED';
+					},
+					(error) => {
+						this.$message.error(error);
+					});
+
+			},
+			handup() {
+				this.$http({
+					url: `/webrtc/private/handup?uid=${this.friend.id}`,
+					method: 'post'
+				})
+				this.close();
+			},
+			cancel(){
+				this.$http({
+					url: `/webrtc/private/cancel?uid=${this.friend.id}`,
+					method: 'post'
+				})
+				this.close();
+			},
+			sendFailed(reason) {
+				this.$http({
+					url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`,
+					method: 'post'
+				})
+			},
+			sendCandidate(candidate) {
+				this.$http({
+					url: `/webrtc/private/candidate?uid=${this.friend.id}`,
+					method: 'post',
+					data: candidate
+				})
+			},
+			close() {
+				this.$emit("close");
+				this.closeCamera();
+				this.loading = false;
+				this.state = 'NOT_CONNECTED';
+				this.candidates = [];
+				this.$store.commit("setUserState",this.$enums.USER_STATE.FREE);
+				this.$refs.friendVideo.srcObject = null;
+				this.peerConnection.close();
+				this.peerConnection.onicecandidate = null;
+				this.peerConnection.onaddstream = null;
+				
+			},
+			handleClose(){
+				if(this.state=='CONNECTED'){
+					this.handup()
+				}else if(this.state == 'CONNECTING'){
+					this.cancel();
+				}else{
+					this.close();
+				}
+			},
+			hasUserMedia() {
+				navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ||
+					navigator.msGetUserMedia;
+				return !!navigator.getUserMedia;
+			},
+			hasRTCPeerConnection() {
+				window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
+				window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;
+				window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
+				return !!window.RTCPeerConnection;
+			}
+		},
+		watch: {
+			visible: {
+				handler(newValue, oldValue) {
+					if (newValue) {
+						this.init();
+						// 用户忙状态
+						this.$store.commit("setUserState",this.$enums.USER_STATE.BUSY);
+						console.log(this.$store.state.userStore.state)
+					}
+				}
+			}
+		},
+		computed: {
+			title() {
+				return `视频聊天-${this.friend.nickName}`;
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.chat-video {
+
+		.chat-video-box {
+			position: relative;
+			border: #2C3E50 solid 1px;
+			background-color: #eeeeee;
+
+			.chat-video-friend {
+				height: 600px;
+				video {
+					width: 100%;
+					height: 100%;
+				}
+			}
+
+			.chat-video-mine {
+				position: absolute;
+				z-index: 99999;
+				width: 200px;
+				right: 0;
+				bottom: 0;
+
+				video {
+					width: 100%;
+				}
+			}
+		}
+			
+		.chat-video-controllbar {
+			display: flex;
+			justify-content: space-around;
+			padding: 10px;
+			.icon {
+				font-size: 50px;
+				cursor: pointer;
+			}
+		}
+	}
+</style>

+ 124 - 0
im-ui/src/components/chat/VideoAcceptor.vue

@@ -0,0 +1,124 @@
+<template>
+	<div class="video-acceptor">
+		<div>
+			<head-image :size="120" :url="this.friend.headImage" :id="this.friend.id"></head-image>
+		</div>
+		<div>
+			{{friend.nickName}} 请求和您进行视频通话...
+		</div>
+		<div class="video-acceptor-btn-group">
+		  <div  class="icon iconfont icon-phone-accept accept" @click="accpet()"></div>
+		  <div  class="icon iconfont icon-phone-reject reject" @click="reject()"></div>
+		</div>
+	</div>
+</template>
+
+<script>
+	import HeadImage from '../common/HeadImage.vue';
+	
+	export default {
+		name: "videoAcceptor",
+		components:{HeadImage},
+		props: {
+			friend:{
+				type: Object
+			}
+		},
+		data(){
+			return {
+				offer:{}
+			}
+		},
+		methods:{
+			accpet(){
+				let info ={
+					friend: this.friend,
+					master: false,
+					offer: this.offer
+				}
+				this.$store.commit("showChatPrivateVideoBox",info);
+				this.close();
+			},
+			reject(){
+				this.$http({
+					url: `/webrtc/private/reject?uid=${this.friend.id}`,
+					method: 'post'
+				})
+				this.close();
+			},
+			failed(reason){
+				this.$http({
+					url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`,
+					method: 'post'
+				})
+				this.close();
+			},
+			onCall(msgInfo){
+				console.log("onCall")
+				this.offer = JSON.parse(msgInfo.content);
+				if(this.$store.state.userStore.state == this.$enums.USER_STATE.BUSY){
+					this.failed("对方正忙,暂时无法接听");
+					return;
+				}
+				// 超时未接听
+				this.timer && clearTimeout(this.timer);
+				this.timer = setTimeout(()=>{
+					this.failed("对方未接听");
+				},30000)
+			},
+			onCancel(){
+				this.$message.success("对方取消了呼叫");
+				this.close();
+			},
+			handleMessage(msgInfo){
+				if(msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL){
+					this.onCall(msgInfo);
+				}else if(msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL){
+					this.onCancel();
+				}
+			},
+			close(){
+				this.timer && clearTimeout(this.timer);
+				this.$emit("close");
+			}
+		}
+	}
+	
+</script>
+
+<style scoped lang="scss">
+	.video-acceptor {
+		position: absolute;
+		right: 1px;
+		bottom: 1px;
+		width: 250px;
+		height: 250px;
+		padding: 10px;
+		text-align: center;
+		background-color: #eeeeee;
+		border: #dddddd solid 1px;
+		
+		.video-acceptor-btn-group {
+			display: flex;
+			justify-content: space-around;
+			margin-top: 20px;
+			
+			.icon {
+				font-size: 50px;
+				cursor: pointer;
+				&.accept {
+					color: green;
+				}
+				&.reject {
+					color: red;
+				}
+			}
+			
+			
+			
+			
+			
+			
+		}
+	}
+</style>