9 changed files with 526 additions and 1 deletions
@ -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; |
||||
@ -0,0 +1,3 @@ |
|||||
|
import Vue from 'vue'; |
||||
|
|
||||
|
export default new Vue(); |
||||
@ -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; |
||||
@ -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; |
||||
@ -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> |
||||
@ -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 +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> |
|
||||
@ -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> |
||||
@ -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…
Reference in new issue