Browse Source

feat: 多人音视频功能

master
xsx 2 years ago
parent
commit
9ac5c36796
  1. 76
      im-ui/src/api/camera.js
  2. 3
      im-ui/src/api/eventBus.js
  3. 156
      im-ui/src/api/rtcGroupApi.js
  4. 117
      im-ui/src/api/webrtc.js
  5. 116
      im-ui/src/components/rtc/RtcGroupJoin.vue
  6. 32
      im-ui/src/store/configStore.js
  7. 1
      im-uniapp/hybrid/html/index.html
  8. 13
      im-uniapp/hybrid/html/rtc-group/index.html
  9. 13
      im-uniapp/hybrid/html/rtc-private/index.html

76
im-ui/src/api/camera.js

@ -0,0 +1,76 @@
class ImCamera {
constructor() {
this.stream = null;
}
}
ImCamera.prototype.isEnable = function() {
return !!navigator && !!navigator.mediaDevices && !!navigator.mediaDevices.getUserMedia;
}
ImCamera.prototype.openVideo = function(isFacing) {
return new Promise((resolve, reject) => {
if(this.stream){
this.close()
}
let facingMode = isFacing ? "user" : "environment";
let constraints = {
video: {
facingMode: facingMode
},
audio: {
echoCancellation: true, //音频开启回音消除
noiseSuppression: true // 开启降噪
}
}
console.log("getUserMedia")
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
console.log("摄像头打开")
this.stream = stream;
resolve(stream);
}).catch((e) => {
console.log(e)
console.log("摄像头未能正常打开")
reject({
code: 0,
message: "摄像头未能正常打开"
})
})
})
}
ImCamera.prototype.openAudio = function() {
return new Promise((resolve, reject) => {
let constraints = {
video: false,
audio: {
echoCancellation: true, //音频开启回音消除
noiseSuppression: true // 开启降噪
}
}
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
this.stream = stream;
resolve(stream);
}).catch(() => {
console.log("麦克风未能正常打开")
reject({
code: 0,
message: "麦克风未能正常打开"
})
})
})
}
ImCamera.prototype.close = function() {
// 停止流
if (this.stream) {
this.stream.getTracks().forEach((track) => {
track.stop();
});
}
}
export default ImCamera;

3
im-ui/src/api/eventBus.js

@ -0,0 +1,3 @@
import Vue from 'vue';
export default new Vue();

156
im-ui/src/api/rtcGroupApi.js

@ -0,0 +1,156 @@
import http from './httpRequest.js'
class RtcGroupApi {
constructor() {
this.http = http;
}
}
RtcGroupApi.prototype.setup = function(groupId, userInfos) {
let formData = {
groupId,
userInfos
}
return this.http({
url: '/webrtc/group/setup',
method: 'post',
data: formData
})
}
RtcGroupApi.prototype.accept = function(groupId) {
return this.http({
url: '/webrtc/group/accept?groupId='+groupId,
method: 'post'
})
}
RtcGroupApi.prototype.reject = function(groupId) {
return this.http({
url: '/webrtc/group/reject?groupId='+groupId,
method: 'post'
})
}
RtcGroupApi.prototype.failed = function(groupId,reason) {
let formData = {
groupId,
reason
}
return this.http({
url: '/webrtc/group/failed',
method: 'post',
data: formData
})
}
RtcGroupApi.prototype.join = function(groupId) {
return this.http({
url: '/webrtc/group/join?groupId='+groupId,
method: 'post'
})
}
RtcGroupApi.prototype.invite = function(groupId, userInfos) {
let formData = {
groupId,
userInfos
}
return this.http({
url: '/webrtc/group/invite',
method: 'post',
data: formData
})
}
RtcGroupApi.prototype.offer = function(groupId, userId, offer) {
let formData = {
groupId,
userId,
offer
}
return this.http({
url: '/webrtc/group/offer',
method: 'post',
data: formData
})
}
RtcGroupApi.prototype.answer = function(groupId, userId, answer) {
let formData = {
groupId,
userId,
answer
}
return this.http({
url: '/webrtc/group/answer',
method: 'post',
data: formData
})
}
RtcGroupApi.prototype.quit = function(groupId) {
return this.http({
url: '/webrtc/group/quit?groupId=' + groupId,
method: 'post'
})
}
RtcGroupApi.prototype.cancel = function(groupId) {
return this.http({
url: '/webrtc/group/cancel?groupId=' + groupId,
method: 'post'
})
}
RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
let formData = {
groupId,
userId,
candidate
}
return this.http({
url: '/webrtc/group/candidate',
method: 'post',
data: formData
})
}
RtcGroupApi.prototype.device = function(groupId, isCamera, isMicroPhone) {
let formData = {
groupId,
isCamera,
isMicroPhone
}
return this.http({
url: '/webrtc/group/device',
method: 'post',
data: formData
})
}
RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
let formData = {
groupId,
userId,
candidate
}
return this.http({
url: '/webrtc/group/candidate',
method: 'post',
data: formData
})
}
RtcGroupApi.prototype.heartbeat = function(groupId) {
return this.http({
url: '/webrtc/group/heartbeat?groupId=' + groupId,
method: 'post'
})
}
export default RtcGroupApi;

117
im-ui/src/api/webrtc.js

@ -0,0 +1,117 @@
class ImWebRtc {
constructor() {
this.configuration = {}
this.stream = null;
}
}
ImWebRtc.prototype.isEnable = function() {
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;
}
ImWebRtc.prototype.init = function(configuration) {
this.configuration = configuration;
}
ImWebRtc.prototype.setupPeerConnection = function(callback) {
this.peerConnection = new RTCPeerConnection(this.configuration);
this.peerConnection.ontrack = (e) => {
// 对方的视频流
callback(e.streams[0]);
};
}
ImWebRtc.prototype.setStream = function(stream) {
if(this.stream){
this.peerConnection.removeStream(this.stream)
}
stream.getTracks().forEach((track) => {
this.peerConnection.addTrack(track, stream);
});
this.stream = stream;
}
ImWebRtc.prototype.onIcecandidate = function(callback) {
this.peerConnection.onicecandidate = (event) => {
// 追踪到候选信息
if (event.candidate) {
callback(event.candidate)
}
}
}
ImWebRtc.prototype.onStateChange = function(callback) {
// 监听连接状态
this.peerConnection.oniceconnectionstatechange = (event) => {
let state = event.target.iceConnectionState;
console.log("ICE连接状态变化: : " + state)
callback(state)
};
}
ImWebRtc.prototype.createOffer = function() {
return new Promise((resolve, reject) => {
const offerParam = {};
offerParam.offerToRecieveAudio = 1;
offerParam.offerToRecieveVideo = 1;
// 创建本地sdp信息
this.peerConnection.createOffer(offerParam).then((offer) => {
// 设置本地sdp信息
this.peerConnection.setLocalDescription(offer);
// 发起呼叫请求
resolve(offer)
}).catch((e) => {
reject(e)
})
});
}
ImWebRtc.prototype.createAnswer = function(offer) {
return new Promise((resolve, reject) => {
// 设置远端的sdp
this.setRemoteDescription(offer);
// 创建本地dsp
const offerParam = {};
offerParam.offerToRecieveAudio = 1;
offerParam.offerToRecieveVideo = 1;
this.peerConnection.createAnswer(offerParam).then((answer) => {
// 设置本地sdp信息
this.peerConnection.setLocalDescription(answer);
// 接受呼叫请求
resolve(answer)
}).catch((e) => {
reject(e)
})
});
}
ImWebRtc.prototype.setRemoteDescription = function(offer) {
// 设置对方的sdp信息
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
}
ImWebRtc.prototype.addIceCandidate = function(candidate) {
// 添加对方的候选人信息
this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
ImWebRtc.prototype.close = function(uid) {
// 关闭RTC连接
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection.onicecandidate = null;
this.peerConnection.onaddstream = null;
}
}
export default ImWebRtc;

116
im-ui/src/components/rtc/RtcGroupJoin.vue

@ -0,0 +1,116 @@
<template>
<el-dialog title="是否加入通话?" :visible.sync="isShow" width="400px">
<div class="rtc-group-join">
<div class="host-info">
<head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image>
<div class="host-text">{{'发起人:'+rtcInfo.host.nickName}}</div>
</div>
<div class="users-info">
<div>{{rtcInfo.userInfos.length+'人正在通话中'}}</div>
<div class="user-list">
<div class="user-item" v-for="user in rtcInfo.userInfos" :key="user.id">
<head-image :url="user.headImage" :name="user.nickName" :size="40">
</head-image>
</div>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="onCancel()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button>
</span>
</el-dialog>
</template>
<script>
import HeadImage from '@/components/common/HeadImage'
export default{
name: "rtcGroupJoin",
components:{
HeadImage
},
data() {
return {
isShow: false,
rtcInfo: {
host:{},
userInfos:[]
}
}
},
props: {
groupId: {
type: Number
}
},
methods: {
open(rtcInfo) {
this.rtcInfo = rtcInfo;
this.isShow = true;
},
onOk() {
this.isShow = false;
let userInfos = this.rtcInfo.userInfos;
let mine = this.$store.state.userStore.userInfo;
if(!userInfos.find((user)=>user.id==mine.id)){
//
userInfos.push({
id: mine.id,
nickName: mine.nickName,
headImage: mine.headImageThumb,
isCamera: false,
isMicroPhone: true
})
}
let rtcInfo = {
isHost: false,
groupId: this.groupId,
inviterId: mine.id,
userInfos: userInfos
}
// home.vue
this.$eventBus.$emit("openGroupVideo", rtcInfo);
},
onCancel(){
this.isShow = false;
}
}
}
</script>
<style lang="scss" scoped>
.rtc-group-join {
height: 260px;
padding: 10px;
.host-info {
display: flex;
flex-direction: column;
font-size: 16px;
padding: 10px;
height: 100px;
align-items: center;
.host-text{
margin-top: 5px;
}
}
.users-info {
font-size: 16px;
margin-top: 20px;
.user-list {
display: flex;
padding: 5px 5px;
height: 90px;
flex-wrap: wrap;
justify-content: center;
.user-item{
padding: 2px;
}
}
}
}
</style>

32
im-ui/src/store/configStore.js

@ -0,0 +1,32 @@
import http from '../api/httpRequest.js'
export default {
state: {
webrtc: {}
},
mutations: {
setConfig(state, config) {
state.webrtc = config.webrtc;
},
clear(state){
state.webrtc = {};
}
},
actions:{
loadConfig(context){
return new Promise((resolve, reject) => {
http({
url: '/system/config',
method: 'GET'
}).then((config) => {
console.log("系统配置",config)
context.commit("setConfig",config);
resolve();
}).catch((res)=>{
reject(res);
});
})
}
}
}

1
im-uniapp/hybrid/html/index.html

@ -1 +0,0 @@
<!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"><title>web</title><link href="css/app.51459a25.css" rel="preload" as="style"><link href="js/app.8c902b8a.js" rel="preload" as="script"><link href="js/chunk-vendors.f7e74caf.js" rel="preload" as="script"><link href="css/app.51459a25.css" rel="stylesheet"></head><body style="margin: 0;"><div id="app"></div><script src="static/uni/uni.webview.1.5.5.js"></script><script src="js/chunk-vendors.f7e74caf.js"></script><script src="js/app.8c902b8a.js"></script></body></html>

13
im-uniapp/hybrid/html/rtc-group/index.html

@ -0,0 +1,13 @@
<!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">
<link rel="icon" href="favicon.ico">
<title>语音通话</title>
</head>
<body>
<div style="padding-top:10px; text-align: center;font-size: 16px;">音视频通话为付费功能,有需要请联系作者...</div>
</body>
</html>

13
im-uniapp/hybrid/html/rtc-private/index.html

@ -0,0 +1,13 @@
<!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">
<link rel="icon" href="favicon.ico">
<title>视频通话</title>
</head>
<body>
<div style="padding-top:10px; text-align: center;font-size: 16px;">音视频通话为付费功能,有需要请联系作者...</div>
</body>
</html>
Loading…
Cancel
Save