uniapp开发WebRTC语音直播间支持app(android+IOS)和H5,并记录了全部踩得 ...

打印 上一主题 下一主题

主题 1031|帖子 1031|积分 3093

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
一、效果图



二、主要功能

1. 创建自己的语音直播间
2. 查询全部直播间列表
3.加入房间
4.申请上位
5.麦克风控制
6.声音控制
7.赠送礼品(特效 + 批量移动动画)
8.退出房间
三、原理

1.uniapp 实现客户端H5、安卓、苹果
2.webRTC实现语音直播间(具体原理网上有很多文章我就不讲了,贴个图)

3.使用node.js搭建信令服务器(我用的是socket)
4.礼品及特效使用svga
四、踩坑及解决方案

1. 客户端(这里重点在于app端)一定要在视图层创建webRTC!!!不要在逻辑层创建!!!因为会要求使用安全毗连,也就是说要用到SSL证书,这个很多人都没有,有的话当我没说。怎样在视图层创建RTC呢?在uniapp中使用renderjs!
  1. <script module="webRTC" lang="renderjs">
  2. new RTCPeerConnection(iceServers)
  3. </script>
复制代码
2. (这里重点也在于app)客户端创建和信令服务器进行通讯的socket时app端在页面跳转后socket状态消失无法响应信令服务器消息。解决方案是:一定不要在客户端视图层创建socket!!!也就是说socket不要创建在renderjs里,要在逻辑层用uniapp提供的api进行创建,然后使用uniapp文档中说明的逻辑层和视图层的通讯方式进行通讯,这样虽然在开发中有些繁琐,但是能解决问题。
  1. onShow(){
  2. // socketTask是使用uniapp提供的uni.connectSocket创建出来的socket实例
  3. // watchSocketMessage代理了socket实例的onMessage方法
  4. socketTask.watchSocketMessage = (data) => {
  5.                                 this.watchSocketMessage(data)
  6.                         }
  7.    
  8. }
  9. methed:{
  10.     watchSocketMessage(){
  11.         // 这里是收到信令服务器socket后的逻辑
  12.     }
  13. }
复制代码
  1. // 这里是逻辑层和renderjs通信的方式,通过监听状态的改变从而触发renderjs的对应的方法
  2. // 注意在页面刚加载完成后这些方法会被默认触发一边,所以要在这些放方法做好判断return出去
  3. <view :rid="rid" :change:rid="webRTC.initRid" :userId="userId" :change:userId="webRTC.initUserId"
  4.                         :giftnum="giftnum" :change:giftnum="webRTC.initgiftnum" :micPosition="micPosition"
  5.                         :change:micPosition="webRTC.initMicPositions" :giftPosition="giftPosition"
  6.                         :change:giftPosition="webRTC.initGiftPosition" :RTCJoin="RTCJoin" :change:RTCJoin="webRTC.changeRTCjoin"
  7.                         :RTCOffier="RTCOffier" :change:RTCOffier="webRTC.changeRTCoffier" :RTCAnswer="RTCAnswer" :isAudio="isAudio"
  8.                         :change:isAudio="webRTC.changeIsAudio" :change:RTCAnswer="webRTC.changeRTCAnswer"
  9.                         :RTCCandidate="RTCCandidate" :change:RTCCandidate="webRTC.changeRTCCandidate" :isTrue="isTrue"
  10.                         :change:isTrue="webRTC.changeIsTrue" :newMess="newMess" :change:newMess="webRTC.changeNewMessage"
  11.                         :isMedia="isMedia" :name="name" :change:name="webRTC.changeName" :change:isMedia="webRTC.changeIsMedia"
  12.                         :animos="animos" :change:animos="changeAnimos" class="chat">
  13. </view>
复制代码
3.毗连顺序的问题,一定是:新进入的用户通过信令服务器给房间已有用户发送Offer,用户接收到Offer回应Answer,记着这个逻辑!
4.因为webRTC是运行在视图层的(也就是欣赏器),而苹果默认欣赏器是Safari,Safari欣赏器默认机制是在用户主动和页面进行交互后,自动播放声音才会生效(也就是才有声音),所以在IOS端全部用户进入直播房间后默认都是静音的,用户主动开启音频才会受到直播间的声音(这是目前我发现的最好的解决办法)
五、核心代码(只有关键步调)

1. 客户端socket
  1. const socketTask = {
  2.         socket: null,
  3.         connect: () => {
  4.                 getApp().globalData.socket = uni.connectSocket({
  5.                         url:'ws://180.76.158.110:9000/socket/websocketv',
  6.                         // url: 'ws://192.168.3.254:9000/socket/websocketv',
  7.                         complete: (e) => {
  8.                                 console.log(e);
  9.                         },
  10.                 });
  11.                 getApp().globalData.socket.onOpen((data) => {
  12.                         console.log("111111111");
  13.                         getApp().globalData.socket.send({
  14.                                 data: JSON.stringify({
  15.                                         type: "newConnect",
  16.                                         userId: uni.getStorageSync('user').id,
  17.                                 })
  18.                         })
  19.                 })
  20.                 getApp().globalData.socket.onClose((res) => {
  21.                         console.log("连接关闭", res);
  22.                         getApp().globalData.socket = null;
  23.                         setTimeout(() => {
  24.                                 socketTask.connect()
  25.                         }, 3000)
  26.                 })
  27.                 getApp().globalData.socket.onError((err) => {
  28.                         console.log("连接异常", err);
  29.                         getApp().globalData.socket = null;
  30.                         setTimeout(() => {
  31.                                 socketTask.connect()
  32.                         }, 1)
  33.                 })
  34.                 getApp().globalData.socket.onMessage((data) => {
  35.                         socketTask.watchSocketMessage(data)
  36.                 })
  37.         },
  38.         start: function() {
  39.                 this.connect()
  40.         },
  41.         watchSocketMessage: function() {
  42.                 // 这里实现自己的业务逻辑
  43.         }
  44. }
  45. export default socketTask
复制代码
2.客户端房间列表页
  1. async onShow() {
  2.                         if (!getApp().globalData.socket) {
  3.                                 await socketTask.start();
  4.                         }
  5.                         socketTask.watchSocketMessage = (data) => {
  6.                                 console.log("===========收到新消息==========",data);
  7.                                 this.watchSocketMessages(data)
  8.                         }
  9.                 },
  10. methed:{
  11. // 监听socket消息
  12.                         watchSocketMessages(res) {
  13.                                 try {
  14.                                         const socket_msg = JSON.parse(res.data);
  15.                                         console.log("收到新消息", socket_msg);
  16.                                         switch (socket_msg.type) {
  17.                                                 case "homeList":
  18.                                                         if (socket_msg.data.length == 0) {
  19.                                                                 this.homeList = [];
  20.                                                                 uni.showToast({
  21.                                                                         title: "暂无房间,快去创建一个吧",
  22.                                                                         icon: "none"
  23.                                                                 })
  24.                                                         } else {
  25.                                                                 this.homeList = socket_msg.data;
  26.                                                         }
  27.                                                         break
  28.                                                 case "leave":
  29.                                                         getApp().globalData.socket.send({
  30.                                                                 data: JSON.stringify({
  31.                                                                         type: "homeList",
  32.                                                                         userId: this.userInfo.userId,
  33.                                                                 })
  34.                                                         })
  35.                                                         break
  36.                                                 case "createSuccess":
  37.                                                         uni.redirectTo({
  38.                                                                 url: `broadRoom?rid=${socket_msg.data.groupId}&&userId=${this.userInfo.id}&&groupInfo=${JSON.stringify(socket_msg.data)}`
  39.                                                         })
  40.                                                         break
  41.                                         }
  42.                                 } catch (e) {
  43.                                 }
  44.                         },
  45. }
复制代码
3.客户端直播间
逻辑层:
  1. async onShow() {
  2.                         const that = this;
  3.                         if (!getApp().globalData.socket) {
  4.                                 console.log("socket不存在,重新连接");
  5.                                 await socketTask.start();
  6.                         }
  7.                         socketTask.watchSocketMessage = (data) => {
  8.                                 this.watchSocketMessage(data)
  9.                         }
  10.                         // 编译平台信息
  11.                         uni.getSystemInfo({
  12.                                 success(res) {
  13.                                         console.log("当前平台是", res);
  14.                                         if (res.osName == 'ios') {
  15.                                                 console.log("我是ios", res)
  16.                                                 that.isMedia = 'ios';
  17.                                         } else {
  18.                                                 console.log("我是安卓", res)
  19.                                                 that.isMedia = 'android';
  20.                                         }
  21.                                 }
  22.                         })
  23.                 }
  24. methed:{
  25. async watchSocketMessage(date) {
  26.                                 const data = JSON.parse(date.data);
  27.                                 switch (data.type) {
  28.                                         case "join":
  29.                                                 console.log("join成功", data);
  30.                                                 this.newMessaGes(data);
  31.                                                 this.setUserList(data.admin);
  32.                                                 this.updataNewMic(data)
  33.                                                 // 找出自己以外的其他用户
  34.                                                 const arr = this.userList.filter((item, index) => {
  35.                                                         return item.userId !== this.userId
  36.                                                 })
  37.                                                 console.log("找出自己以外的其他用户", arr)
  38.                                                 // 通知renderjs层创建RTC
  39.                                                 this.RTCJoin = arr;
  40.                                                 this.updataIsShow()
  41.                                                 break
  42.                                         case "newjoin":
  43.                                                 this.newMessaGes(data);
  44.                                                 this.setUserList(data.admin);
  45.                                                 break
  46.                                         case "offer":
  47.                                                 //通知renderjs层有新人进入创建answer
  48.                                                 console.log("收到offer", data)
  49.                                                 this.RTCOffier = data;
  50.                                                 break
  51.                                         case "answer":
  52.                                                 // 找到对应peer,设置answer
  53.                                                 console.log("收到offer", data)
  54.                                                 this.RTCAnswer = data;
  55.                                                 break
  56.                                         case "candidate":
  57.                                                 // 找到对应的peer,将candidate添加进去
  58.                                                 this.RTCCandidate = data;
  59.                                                 break
  60.                                         case "leave":
  61.                                                 if (data.data == "房主已解散房间") {
  62.                                                         this.closesAdmin()
  63.                                                 } else {
  64.                                                         const datas = {
  65.                                                                 data,
  66.                                                         }
  67.                                                         this.newMessaGes(datas)
  68.                                                         this.setUserList(data.admin);
  69.                                                         this.updataNewMic(data);
  70.                                                 }
  71.                                                 break
  72.                                         case "apply-admin":
  73.                                                 this.updataIsApply(data.data)
  74.                                                 break
  75.                                         case "newMic":
  76.                                                 this.updataNewMic(data)
  77.                                                 break
  78.                                         case "uplMicro":
  79.                                                 this.updataNewMic(data)
  80.                                                 break
  81.                                         case "newMessage":
  82.                                                 this.newMess = data;
  83.                                                 break
  84.                                 }
  85.                         },
  86. }
复制代码
视图层:
  1. <script module="webRTC" lang="renderjs">
  2. // 以下方法都在methed:{}中
  3. // 监听changeRTCCandidate
  4.                         async changeRTCCandidate(data) {
  5.                                 if (!data) {
  6.                                         return
  7.                                 }
  8.                                 console.log("this.otherPeerConnections", this.otherPeerConnections);
  9.                                 let arrs = this.otherPeerConnections.concat(this.myPeerConnections);
  10.                                 if (arrs.length == 0) {
  11.                                         return
  12.                                 }
  13.                                 let peerr = arrs.filter(item => {
  14.                                         return item.otherId == data.userId
  15.                                 })
  16.                                
  17.                                 if (peerr[0].peer == {}) {
  18.                                         return
  19.                                 } else {
  20.                                         console.log("candidatecandidate", data.candidate)
  21.                                         await peerr[0].peer.addIceCandidate(new RTCIceCandidate(data.candidate))
  22.                                 }
  23.                         },
  24.                         // 监听answer,找到对应peer设置answer
  25.                         async changeRTCAnswer(data) {
  26.                                 if (!data) {
  27.                                         return
  28.                                 }
  29.                                 let peers = this.myPeerConnections.filter(item => {
  30.                                         return item.otherId == data.userId
  31.                                 })
  32.                                 console.log("peers[0]", peers[0])
  33.                                 await peers[0].peer.setRemoteDescription(new RTCSessionDescription(data.answer))
  34.                         },
  35.                         // 监听offier,RTCAnswer的创建
  36.                         async changeRTCoffier(data) {
  37.                                 if (!data) {
  38.                                         return
  39.                                 }
  40.                                 let pear = null;
  41.                                 try {
  42.                                         pear = new RTCPeerConnection(iceServers);
  43.                                 } catch (e) {
  44.                                         console.log("实例化RTC-pear失败", e);
  45.                                 }
  46.                                 // 将音频流加入到Peer中
  47.                                 this.localStream.getAudioTracks()[0].enabled = this.isTrue;
  48.                                 this.localStream.getTracks().forEach(
  49.                                         (track) => pear.addTrack(track, this.localStream)
  50.                                 );
  51.                                 this.otherPeerConnections.push({
  52.                                         peer: pear,
  53.                                         otherId: data.userId
  54.                                 })
  55.                                 //当远程用户向对等连接添加流时,我们将显示它
  56.                                 pear.ontrack = (event) => {
  57.                                         // 为该用户创建audio
  58.                                         const track = event.track || event.streams[0]?.getTracks()[0];
  59.                                         if (track && track.kind === 'audio') {
  60.                                                 console.log("存在音轨", event.streams[0]);
  61.                                                 this.renderAudio(data.userId, event.streams[0]);
  62.                                         } else {
  63.                                                 console.warn("No audio track found in the received stream.");
  64.                                         }
  65.                                 };
  66.                                 // 通过监听onicecandidate事件获取candidate信息
  67.                                 pear.onicecandidate = async (event) => {
  68.                                         if (event.candidate) {
  69.                                                 // 通过信令服务器发送candidate信息给用户B
  70.                                                 await this.$ownerInstance.callMethod("sendCandidate", {
  71.                                                         type: "candidate",
  72.                                                         userId: this.userId,
  73.                                                         rid: this.rid,
  74.                                                         msg: event.candidate,
  75.                                                         formUserId: data.userId,
  76.                                                 })
  77.                                         }
  78.                                 }
  79.                                 pear.setRemoteDescription(new RTCSessionDescription(data.offer))
  80.                                 // 接收端创建answer并发送给发起端
  81.                                 pear.createAnswer().then(answer => {
  82.                                         pear.setLocalDescription(answer);
  83.                                         // 通知serve层给房间用户发送answer
  84.                                         this.$ownerInstance.callMethod("sendAnswer", {
  85.                                                 type: "answer",
  86.                                                 userId: this.userId,
  87.                                                 rid: this.rid,
  88.                                                 msg: answer,
  89.                                                 formUserId: data.userId,
  90.                                         })
  91.                                 })
  92.                         },
  93.                         // 发起连接申请,offier的创建
  94.                         changeRTCjoin(RTCjoin) {
  95.                                 if (!RTCjoin) {
  96.                                         return
  97.                                 }
  98.                                 RTCjoin.forEach((item, index) => {
  99.                                         let peer = null;
  100.                                         try {
  101.                                                 peer = new RTCPeerConnection(iceServers);
  102.                                         } catch (e) {
  103.                                                 console.log("实例化RTC失败", e);
  104.                                         }
  105.                                         this.localStream.getAudioTracks()[0].enabled = this.isTrue;
  106.                                         this.localStream.getTracks().forEach(
  107.                                                 (track) => peer.addTrack(track, this.localStream)
  108.                                         );
  109.                                         peer.ontrack = (event) => {
  110.                                                 console.log("发起连接申请,offier的创建:peer.ontrack");
  111.                                                 const track = event.track || event.streams[0]?.getTracks()[0];
  112.                                                 if (track && track.kind === 'audio') {
  113.                                                         console.log("存在音轨2", event.streams[0]);
  114.                                                         this.renderAudio(item.userId, event.streams[0]);
  115.                                                 } else {
  116.                                                         console.warn("No audio track found in the received stream.");
  117.                                                 }
  118.                                         };
  119.                                         // 通过监听onicecandidate事件获取candidate信息
  120.                                         peer.onicecandidate = (event) => {
  121.                                                 if (event.candidate) {
  122.                                                         // 通过信令服务器发送candidate信息给用户B
  123.                                                         this.$ownerInstance.callMethod("sendCandidate", {
  124.                                                                 type: "candidate",
  125.                                                                 userId: this.userId,
  126.                                                                 rid: this.rid,
  127.                                                                 msg: event.candidate,
  128.                                                                 formUserId: item.userId,
  129.                                                         })
  130.                                                 }
  131.                                         }
  132.                                         this.myPeerConnections.push({
  133.                                                 peer: peer,
  134.                                                 otherId: item.userId
  135.                                         })
  136.                                         peer.createOffer(this.offerOptions).then(offer => {
  137.                                                 peer.setLocalDescription(offer);
  138.                                                 // 通知serve层给房间用户发送offier
  139.                                                 this.$ownerInstance.callMethod("sendOffier", {
  140.                                                         type: "offer",
  141.                                                         userId: this.userId,
  142.                                                         rid: this.rid,
  143.                                                         msg: offer,
  144.                                                         formUserId: item.userId,
  145.                                                 })
  146.                                         })
  147.                                 })
  148.                         },
  149.                         renderAudio(uid, stream) {
  150.                                 let audio2 = document.getElementById(`audio_${uid}`);
  151.                                 console.log("audio_name", `audio_${uid}`);
  152.                                 if (!audio2) {
  153.                                         audio2 = document.createElement('audio');
  154.                                         audio2.id = `audio_${uid}`;
  155.                                         audio2.setAttribute("webkit-playsinline", "");
  156.                                         audio2.setAttribute("autoplay", true);
  157.                                         audio2.setAttribute("playsinline", "");
  158.                                         audio2.onloadedmetadata = () => {
  159.                                                 if (this.isAudio == 1) {
  160.                                                         console.log("不自动播放");
  161.                                                         audio2.pause();
  162.                                                 } else {
  163.                                                         audio2.play();
  164.                                                 }
  165.                                         };
  166.                                         this.audioList.push(audio2)
  167.                                 }
  168.                                 if ("srcObject" in audio2) {
  169.                                         console.log("使用了srcObject赋值");
  170.                                         audio2.srcObject = stream;
  171.                                 } else {
  172.                                         console.log("找不到srcObject赋值");
  173.                                         audio2.src = window.URL.createObjectURL(stream);
  174.                                 }
  175.                         },
  176. async initMedia() {
  177.                                 const that = this;
  178.                                 console.log("##########", this.isMedia);
  179.                                 // #ifdef APP-PLUS
  180.                                 if (this.isMedia == 'android') {
  181.                                         console.log("androidandroidandroidandroid");
  182.                                         await plus.android.requestPermissions(
  183.                                                 ['android.permission.RECORD_AUDIO'],
  184.                                                 async (resultObj) => {
  185.                                                                 var result = 0;
  186.                                                                 for (var i = 0; i < resultObj.granted.length; i++) {
  187.                                                                         var grantedPermission = resultObj.granted[i];
  188.                                                                         result = 1
  189.                                                                 }
  190.                                                                 for (var i = 0; i < resultObj.deniedPresent.length; i++) {
  191.                                                                         var deniedPresentPermission = resultObj.deniedPresent[i];
  192.                                                                         result = 0
  193.                                                                 }
  194.                                                                 for (var i = 0; i < resultObj.deniedAlways.length; i++) {
  195.                                                                         var deniedAlwaysPermission = resultObj.deniedAlways[i];
  196.                                                                         result = -1
  197.                                                                 }
  198.                                                                 that.localStream = await that.getUserMedia();
  199.                                                                 that.$ownerInstance.callMethod("sendJoin", {
  200.                                                                         type: "join",
  201.                                                                         userId: that.userId,
  202.                                                                         rid: that.rid,
  203.                                                                         name: that.name
  204.                                                                 })
  205.                                                         },
  206.                                                         function(error) {
  207.                                                                 console.log("导入android出现错误", error);
  208.                                                         }
  209.                                         );
  210.                                 } else {
  211.                                         console.log("iosiosiosiosiosios");
  212.                                         that.localStream = await that.getUserMedia().catch(err => {
  213.                                                 console.log("出错了", err);
  214.                                         })
  215.                                         that.$ownerInstance.callMethod("sendJoin", {
  216.                                                 type: "join",
  217.                                                 userId: that.userId,
  218.                                                 rid: that.rid,
  219.                                                 name: that.name
  220.                                         })
  221.                                 }
  222.                                 // #endif
  223.                                 // #ifdef H5
  224.                                 that.localStream = await that.getUserMedia();
  225.                                 // 通知serve层加入成功
  226.                                 this.$ownerInstance.callMethod("sendJoin", {
  227.                                         type: "join",
  228.                                         userId: this.userId,
  229.                                         rid: this.rid,
  230.                                         name: this.name
  231.                                 })
  232.                                 // #endif
  233.                         },
  234.                         getUserMedia(then) {
  235.                                 return new Promise((resolve, reject) => {
  236.                                         navigator.mediaDevices.getUserMedia(this.mediaConstraints).then((stream) => {
  237.                                                 return resolve(stream);
  238.                                         }).catch(err => {
  239.                                                 if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
  240.                                                         // 用户拒绝了授权
  241.                                                         reject(new Error('用户拒绝了访问摄像头和麦克风的请求'));
  242.                                                 } else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
  243.                                                         // 没有找到摄像头或麦克风
  244.                                                         reject(new Error('没有找到摄像头或麦克风'));
  245.                                                 } else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
  246.                                                         // 摄像头或麦克风不可读
  247.                                                         reject(new Error('摄像头或麦克风不可读'));
  248.                                                 } else if (err.name === 'OverconstrainedError' || err.name ===
  249.                                                         'ConstraintNotSatisfiedError') {
  250.                                                         // 由于媒体流的约束条件无法满足,请求被拒绝
  251.                                                         reject(new Error('请求被拒绝,由于媒体流的约束条件无法满足'));
  252.                                                 } else if (err.name === 'TypeError' || err.name === 'TypeError') {
  253.                                                         // 发生了类型错误
  254.                                                         reject(new Error('发生了类型错误'));
  255.                                                 } else {
  256.                                                         // 其他未知错误
  257.                                                         reject(new Error('发生了未知错误'));
  258.                                                 }
  259.                                         })
  260.                                 });
  261.                         },
  262. </script>
复制代码
4.信令服务器
略(就是socket,内里写swich,不会私信,小额收费)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

王海鱼

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表