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