123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614 |
- <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 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" 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" >
- <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 ? opt.stunUser : '',
- "credential":opt.stunPass ? 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>
|