im/public/h5/hybrid/html/index.html

614 lines
18 KiB
HTML
Raw Normal View History

2023-09-26 18:09:46 +08:00
<html lang="zh">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<script type="text/javascript" src="./rtc/adapter-latest.js"></script>
<script type="text/javascript" src='./js/uni.webview.js'></script>
<script type="text/javascript" src='./js/utils.js'></script>
<script type="text/javascript" src='./js/jsonly.js'></script>
<style>
body{
padding:0;
margin:0;
background-image: url('image/wallpaper.png');
background-size: contain;
}
.webrtc-box{
background: #666;
border-radius: 6px;
width:100%;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.localvideo{
width:100vw;
height:100vh;
object-fit: cover;
}
.remotevideo{
min-height: 160px;
width: 100px;
position: fixed;
top: 40px;
right: 15px;
z-index:10;
object-fit: cover;
}
.call-user-box{
position:fixed;
bottom: 20px;
width:100%;
}
.call-user{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom:50px;
}
.call-user .avatar{
width:60px;
height:60px;
object-fit: contain;
border-radius: 50%;
overflow: hidden;
}
.call-user .text{
font-size:16px;
margin-top:15px;
color:#f6f6f6
}
.call-time{
color:#f6f6f6;
font-size: 24px;
text-align: center;
}
.calling-button{
display: flex;
justify-content: space-around;
padding: 20px;
}
.calling-button .button{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.calling-button .button .image{
width:60px;
height:60px;
margin-bottom: 10px;
}
.calling-button .button .text{
color:#f6f6f6;
}
.calling-button .switch-btn .text{
font-size:12px !important;
}
.calling-button .button .image-icon{
width:40px;
height:40px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div id="app">
<div class="webrtc-box">
<audio id="music1">
<source src="https://im.file.raingad.com/static/voice/calling.mp3">
</audio>
<video v-show="localStream && is_video" class="localvideo" ref="localvideo" x5-video-player-fullscreen="true" autoplay x5-playsinline playsinline webkit-playsinline @click="displayBtn = !displayBtn" poster="./image/wallpaper.png"></video>
<video v-show="remoteStream && is_video" class="remotevideo" ref="remotevideo" x5-video-player-fullscreen="true" autoplay x5-playsinline playsinline webkit-playsinline @click="changeVideo()" poster="./image/wallpaper.png"></video>
<div class="call-user-box" v-if="displayBtn">
<div class="call-user" v-if="contact">
<img class="avatar" v-if="status!=2 || !is_video" :src="contact.avatar" alt="">
<div class="text">
<b v-if="!is_video && status==2">{{contact.displayName}}</b>
<span v-if="status!=2">
<span v-if="status==3"> {{contact.displayName}} 正在请求与您{{is_video ? '视频' : '语音'}}通话</span>
<span v-else>您正对 <b>{{contact.displayName}}</b> 发起{{is_video ? '视频' : '语音'}}通话</span>
</span>
</div>
</div>
<div class="call-time" v-if="callTime && status==2">
{{setCallTime()}}
</div>
<div class="calling-button">
<div class="button" v-if="status==3" >
<img class="image" src="./image/jieting.png" @click="answer()"/>
<div class="text">接听</div>
</div>
<div class="button switch-btn" v-if="status<3" >
<img class="image-icon" :src="'./image/voice'+(voiceStatus ? '' : '-off')+'.png'" @click="switchVoice()"/>
<div class="text">{{ voiceStatus ? '关闭' : '开启'}}麦克风</div>
</div>
<div class="button" v-if="status!=0" >
<img class="image" src="./image/guaduan.png" @click="hangup(true)"/>
<div class="text">挂断</div>
</div>
<div class="button switch-btn" v-if="status<3" >
<template v-if="is_video">
<img class="image-icon" :src="'./image/video.png'" @click="exchangeVideo()"/>
<div class="text">切换摄像头</div>
</template>
<template v-else>
<img class="image-icon" :src="'./image/speaker'+(speaker ? '' : '-off')+'.png'" @click="speakBtn()"/>
<div class="text">{{ speaker ? '关闭' : '开启'}}扬声器</div>
</template>
</div>
</div>
</div>
</div>
</div>
</body>
<script type="text/javascript" src='./js/vue.js'></script>
<script>
const params=parseUrl(window.location.href);
const opt=JSON.parse(decodeURIComponent(params.stun));
const config = {
'iceServers': [{
'urls': ['stun:stun.xten.com', 'stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302',
'stun:stun2.l.google.com:19302', 'stun:stun3.l.google.com:19302', 'stun:stun4.l.google.com:19302'
]
},{
'urls': opt.stun ? opt.stun : ['stun:stun.callwithus.com'], // 自己搭建服务器地址
"username":opt.stunUser ?? '',
"credential":opt.stunPass ?? ''
}
],
};
const Counter = {
data() {
return {
displayBtn:true,
platform:params.platform,
status: 0, //状态0默认1拨号中2通话中3来电中4忙线
pc: null, //pc实力化
localVideo: "", //本地视频的DOM
remoteVideo: "", //远程视频的DOM
remoteStream: null, // 远端视频流
localStream: null, // 本地视频流
is_video: 0, //是否为视频通话
videoStatus: true, //视频开启状态
voiceStatus: true, //语音开启状态
cutdown: 40, //拨号超时
timer: null, //计时器
offerParams:{},
plus:null,
streamType:1, //视频通话展示方式
facingMode:'user',//前置摄像头还是后置摄像头 user-前置 environment-后置
headset : true, //麦克风 打开true 关闭false,
senders: null, // 数据流
speaker:true, // 听筒 false 扬声器true
callTime:0, //通话时间
callTimeDis:'', //通话时间展示
timerIntervalId:null, //通话计时器
contact:{
id:params.target_id,
displayName:params.name,
avatar:params.avatar
}
};
},
mounted() {
this.pc = new RTCPeerConnection(config);
this.pc.ontrack = (event) => {
console.log(event,'接收视频流');
if(this.localVideo){
this.remoteStream = event.streams[0];
setTimeout(()=>{
this.streamType=2;
},50)
}
};
if (this.platform === 'app') {
document.addEventListener('plusready', () => {
console.log('设置扬声器')
this.plus = plus.audio.createPlayer();
this.plus.setRoute(plus.audio.ROUTE_SPEAKER);
});
}
this.localVideo = this.$refs.localvideo;
this.remoteVideo = this.$refs.remotevideo;
window.addEventListener('message', (e) => {
this.callMessagecallback(e)
}, false);
window.getUniAppMessage = (arg) => {
const data = {
data: jsonly(arg)
}
this.callMessagecallback(data)
}
this.is_video = params.type==1 ? true : false;
this.offerParams = this.is_video ? {
offerToRecieveAudio: 1,
offerToRecieveVideo: 1
} : {
offerToRecieveAudio: 1,
offerToRecieveVideo: 0
}
this.status=params.status
// 如果状态为1,表示拨打电话并且calling状态为1的时候才是直接拨打
if(this.status==1){
if(params.calling==1){
this.called(this.is_video)
}
}else{
this.playMusicCall('state');
}
},
watch:{
streamType(val){
// 切换镜头位置
if(val==1){
this.localVideo.srcObject = this.localStream;
this.remoteVideo.srcObject = this.remoteStream;
this.localVideo.muted=true;
this.remoteVideo.muted=false;
}else{
this.localVideo.srcObject = this.remoteStream;
this.remoteVideo.srcObject = this.localStream;
this.localVideo.muted=false;
this.remoteVideo.muted=true;
}
}
},
methods: {
// 开始通话计时
startTime() {
this.timerIntervalId=setInterval(()=>{
this.callTime++
},1000)
},
// 设置通话时间
setCallTime(){
let time=this.callTime;
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time - (hours * 3600)) / 60);
const seconds = time - (hours * 3600) - (minutes * 60);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
},
// 视频电话初始化本地视频
initLocalStream(call_id, is_video) {
let video=is_video;
if(is_video){
video = {
width: window.screen.height,
height: window.screen.width
}
}
navigator.mediaDevices.getUserMedia({
video: video,
audio: {echoCancellation: true}
}).then((stream) => {
this.localStream = stream;
// 同步音频
stream.getTracks().forEach((track) => {
this.pc.addTrack(track, stream);
});
this.localVideo.srcObject = this.localStream;
// 把自己的视频静音
this.localVideo.muted = true;
if(call_id){
this.postMsg({
event:'calling',
status:3,
code:901
});
// 计时器,如果一段时间没有接听则自动挂断
this.timer = setInterval(() => {
this.cutdown--;
if (this.cutdown == 0) {
this.hangup(true);
}
}, 1000)
}else{
// 告诉对方已经接听电话
this.postMsg({ event: 'acceptRtc',code:904});
}
// 监听远程媒体流
}).catch((e) => {
this.postMsg({
event: 'mediaDevices',
})
});
},
// 拨打电话
called(is_video) {
this.is_video = is_video;
this.initLocalStream(true, is_video);
this.playMusicCall('state');
},
// 接听电话
answer() {
this.status = 2;
this.initLocalStream(false, this.is_video);
this.playMusicCall('close');
this.startTime();
},
// 挂断电话
hangup(btn) {
clearInterval(this.timer);
clearInterval(this.timerIntervalId);
if(this.status!=2){
this.playMusicCall('close');
}
if (this.status) {
this.closeLocalMedia(); //关闭本地媒体
this.remoteStream=null; //关闭远程媒体
}
// 通话取消
let code=902;
// 通话中挂断
if(this.status==2 ){
code=906
// 拒绝挂断
}else if(this.status==3 ){
code=903
//对方忙线中
}else if(this.status==4 ){
code=907
}
this.postMsg({
event:'hangup',
isbtn:btn,
callTime:this.callTime,
code:code
})
},
// 关闭本地媒体
closeLocalMedia() {
if (this.localStream && this.localStream.getTracks()) {
this.localStream.getTracks().forEach((track) => {
track.stop();
});
}
this.localStream = null;
},
// 打开或关闭声音
switchVoice() {
if (this.localStream == null) {
alert('请打开音视频');
return false;
}
const tracks = this.localStream.getTracks();
if (this.voiceStatus) {
tracks.forEach(track => {
if (track.kind === 'audio') {
track.enabled = false
}
});
this.voiceStatus = false;
} else {
tracks.forEach(track => {
if (track.kind === 'audio') {
track.enabled = true
}
});
this.voiceStatus = true;
}
},
// 临时开、关视频
switchVideo() {
if (this.localStream == null) {
alert('请打开音视频');
return false;
}
const tracks = this.localStream.getTracks();
if (this.videoStatus) {
tracks.forEach(track => {
if (track.kind === 'video') {
track.enabled = false
}
});
this.videoStatus = false;
} else {
tracks.forEach(track => {
if (track.kind === 'video') {
track.enabled = true
}
});
this.videoStatus = true;
}
},
// 切换前后摄像头
exchangeVideo() {
this.localStream.getTracks().forEach(track => track.stop());
if (this.facingMode == 'user') this.facingMode = 'environment'
else this.facingMode = 'user'
navigator.mediaDevices.getUserMedia({
video: {
width: window.screen.height,
height: window.screen.width,
facingMode: {
exact: this.facingMode
}
},
audio: {
echoCancellation: true,
}
}).then((mediastream) => {
this.senders = this.pc.getSenders()
let videoTrack = mediastream.getVideoTracks()[0];
let audioTrack = mediastream.getAudioTracks()[0];
var sender = this.senders.find((s) => {
return s.track.kind == 'video';
});
var sender2 = this.senders.find((s) => {
return s.track.kind == 'audio';
});
sender.replaceTrack(videoTrack);
sender2.replaceTrack(audioTrack);
if (this.streamType === 2) this.remoteVideo.srcObject = mediastream;
else this.localVideo.srcObject = mediastream
this.localStream = mediastream
if(this.voiceStatus==false){
this.voiceStatus=true;
this.switchVoice();
}
if(this.speaker){
this.speaker = !this.speaker
}else{
this.speaker = !this.speaker
}
})
},
// 播放响铃
playMusicCall(type) {
var audio = document.getElementById("music1");
if(type=='close' && !audio.paused){
audio.pause(); // 暂停
return;
}
if (type === "state") {
audio.loop = true;
} else {
audio.loop = false;
}
if (audio.paused) {
audio.play(); // 播放
} else {
audio.pause(); // 暂停
}
},
// 向uniapp发送消息页面通讯
postMsg(data) {
if (this.platform === 'app') {
uni.postMessage({
data: data
})
} else {
window.parent.postMessage(data)
}
},
// 接收websocket发送过来的消息,由uniapp接收后传输到当前页面
callMessagecallback(msg){
let e=msg.data;
switch (e.event) {
case "calling":
console.log('发起通话...');
this.called(this.is_video);
break;
case "hangup":
this.hangup(false);
break;
case "busy":
this.status=4;
this.hangup(false);
break;
case "acceptRtc": //已经接听创建offer并发送
this.status = 2;
clearInterval(this.timer);
this.startTime();
this.playMusicCall();
this.createOffer()
break;
case "turndown":
break;
case "answer":
//同步answer信息...
this.pc.setRemoteDescription(new RTCSessionDescription({
type: 'answer',
sdp: e.sdp
}));
break;
case "iceCandidate":
setTimeout(()=>{
// 添加ice完成通话连接
if (typeof(e.iceCandidate) === 'object') {
this.pc.addIceCandidate(new RTCIceCandidate(e.iceCandidate));
} else {
this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(e.iceCandidate)));
}
},100)
break;
case "offer":
this.pc.setRemoteDescription(new RTCSessionDescription({
type: 'offer',
sdp: e.sdp
}));
this.createAnswer();
break;
}
},
// 创建offer-sdp
createOffer() {
this.pc.createOffer(this.offerParams).then((offer) => {
this.pc.setLocalDescription(offer);
this.postMsg({
event: 'offer',
sdp: offer.sdp
}, '*');
});
// 创建offer需要监听ice流
this.onicecandidate();
},
// 创建应答sdp
createAnswer() {
this.pc.createAnswer(this.offerParams).then((answer) => {
this.pc.setLocalDescription(answer);
this.postMsg({
event: 'answer',
sdp: answer.sdp
}, '*');
this.onicecandidate();
});
},
onicecandidate(){
this.pc.onicecandidate = (event) => {
var iceCandidate = event.candidate;
if (iceCandidate) {
this.postMsg({
event: 'iceCandidate',
iceCandidate: JSON.parse(JSON.stringify(iceCandidate))
}, '*');
}
};
},
//切换视频显示位置
changeVideo(){
this.streamType==1 ? this.streamType=2 : this.streamType=1;
},
//打开关闭扬声器 h5端就是静音 ROUTE_EARPIECE 听筒 ROUTE_SPEAKER 扬声器
speakBtn() {
if (this.speaker) { //扬声器 => 听筒
this.speaker = !this.speaker
if (this.platform === 'h5') {
this.localVideo.muted = true
}
if (this.platform === 'app') {
this.plus.setRoute(plus.audio.ROUTE_EARPIECE);
}
} else { //听筒 => 扬声器
this.speaker = !this.speaker
if (this.platform === 'h5') {
this.localVideo.muted = false
}
if (this.platform === 'app') {
this.plus.setRoute(plus.audio.ROUTE_SPEAKER);
}
}
}
}
}
const app = Vue.createApp(Counter);
app.mount('#app');
</script>
</html>