当前位置:   article > 正文

uniapp开发WebRTC语音直播间支持app(android+IOS)和H5,并记录了所有踩得坑_webrtc uniapp

webrtc uniapp

一、效果图

二、主要功能

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. methed:{
  9. watchSocketMessage(){
  10. // 这里是收到信令服务器socket后的逻辑
  11. }
  12. }
  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. if (peerr[0].peer == {}) {
  17. return
  18. } else {
  19. console.log("candidatecandidate", data.candidate)
  20. await peerr[0].peer.addIceCandidate(new RTCIceCandidate(data.candidate))
  21. }
  22. },
  23. // 监听answer,找到对应peer设置answer
  24. async changeRTCAnswer(data) {
  25. if (!data) {
  26. return
  27. }
  28. let peers = this.myPeerConnections.filter(item => {
  29. return item.otherId == data.userId
  30. })
  31. console.log("peers[0]", peers[0])
  32. await peers[0].peer.setRemoteDescription(new RTCSessionDescription(data.answer))
  33. },
  34. // 监听offier,RTCAnswer的创建
  35. async changeRTCoffier(data) {
  36. if (!data) {
  37. return
  38. }
  39. let pear = null;
  40. try {
  41. pear = new RTCPeerConnection(iceServers);
  42. } catch (e) {
  43. console.log("实例化RTC-pear失败", e);
  44. }
  45. // 将音频流加入到Peer中
  46. this.localStream.getAudioTracks()[0].enabled = this.isTrue;
  47. this.localStream.getTracks().forEach(
  48. (track) => pear.addTrack(track, this.localStream)
  49. );
  50. this.otherPeerConnections.push({
  51. peer: pear,
  52. otherId: data.userId
  53. })
  54. //当远程用户向对等连接添加流时,我们将显示它
  55. pear.ontrack = (event) => {
  56. // 为该用户创建audio
  57. const track = event.track || event.streams[0]?.getTracks()[0];
  58. if (track && track.kind === 'audio') {
  59. console.log("存在音轨", event.streams[0]);
  60. this.renderAudio(data.userId, event.streams[0]);
  61. } else {
  62. console.warn("No audio track found in the received stream.");
  63. }
  64. };
  65. // 通过监听onicecandidate事件获取candidate信息
  66. pear.onicecandidate = async (event) => {
  67. if (event.candidate) {
  68. // 通过信令服务器发送candidate信息给用户B
  69. await this.$ownerInstance.callMethod("sendCandidate", {
  70. type: "candidate",
  71. userId: this.userId,
  72. rid: this.rid,
  73. msg: event.candidate,
  74. formUserId: data.userId,
  75. })
  76. }
  77. }
  78. pear.setRemoteDescription(new RTCSessionDescription(data.offer))
  79. // 接收端创建answer并发送给发起端
  80. pear.createAnswer().then(answer => {
  81. pear.setLocalDescription(answer);
  82. // 通知serve层给房间用户发送answer
  83. this.$ownerInstance.callMethod("sendAnswer", {
  84. type: "answer",
  85. userId: this.userId,
  86. rid: this.rid,
  87. msg: answer,
  88. formUserId: data.userId,
  89. })
  90. })
  91. },
  92. // 发起连接申请,offier的创建
  93. changeRTCjoin(RTCjoin) {
  94. if (!RTCjoin) {
  95. return
  96. }
  97. RTCjoin.forEach((item, index) => {
  98. let peer = null;
  99. try {
  100. peer = new RTCPeerConnection(iceServers);
  101. } catch (e) {
  102. console.log("实例化RTC失败", e);
  103. }
  104. this.localStream.getAudioTracks()[0].enabled = this.isTrue;
  105. this.localStream.getTracks().forEach(
  106. (track) => peer.addTrack(track, this.localStream)
  107. );
  108. peer.ontrack = (event) => {
  109. console.log("发起连接申请,offier的创建:peer.ontrack");
  110. const track = event.track || event.streams[0]?.getTracks()[0];
  111. if (track && track.kind === 'audio') {
  112. console.log("存在音轨2", event.streams[0]);
  113. this.renderAudio(item.userId, event.streams[0]);
  114. } else {
  115. console.warn("No audio track found in the received stream.");
  116. }
  117. };
  118. // 通过监听onicecandidate事件获取candidate信息
  119. peer.onicecandidate = (event) => {
  120. if (event.candidate) {
  121. // 通过信令服务器发送candidate信息给用户B
  122. this.$ownerInstance.callMethod("sendCandidate", {
  123. type: "candidate",
  124. userId: this.userId,
  125. rid: this.rid,
  126. msg: event.candidate,
  127. formUserId: item.userId,
  128. })
  129. }
  130. }
  131. this.myPeerConnections.push({
  132. peer: peer,
  133. otherId: item.userId
  134. })
  135. peer.createOffer(this.offerOptions).then(offer => {
  136. peer.setLocalDescription(offer);
  137. // 通知serve层给房间用户发送offier
  138. this.$ownerInstance.callMethod("sendOffier", {
  139. type: "offer",
  140. userId: this.userId,
  141. rid: this.rid,
  142. msg: offer,
  143. formUserId: item.userId,
  144. })
  145. })
  146. })
  147. },
  148. renderAudio(uid, stream) {
  149. let audio2 = document.getElementById(`audio_${uid}`);
  150. console.log("audio_name", `audio_${uid}`);
  151. if (!audio2) {
  152. audio2 = document.createElement('audio');
  153. audio2.id = `audio_${uid}`;
  154. audio2.setAttribute("webkit-playsinline", "");
  155. audio2.setAttribute("autoplay", true);
  156. audio2.setAttribute("playsinline", "");
  157. audio2.onloadedmetadata = () => {
  158. if (this.isAudio == 1) {
  159. console.log("不自动播放");
  160. audio2.pause();
  161. } else {
  162. audio2.play();
  163. }
  164. };
  165. this.audioList.push(audio2)
  166. }
  167. if ("srcObject" in audio2) {
  168. console.log("使用了srcObject赋值");
  169. audio2.srcObject = stream;
  170. } else {
  171. console.log("找不到srcObject赋值");
  172. audio2.src = window.URL.createObjectURL(stream);
  173. }
  174. },
  175. async initMedia() {
  176. const that = this;
  177. console.log("##########", this.isMedia);
  178. // #ifdef APP-PLUS
  179. if (this.isMedia == 'android') {
  180. console.log("androidandroidandroidandroid");
  181. await plus.android.requestPermissions(
  182. ['android.permission.RECORD_AUDIO'],
  183. async (resultObj) => {
  184. var result = 0;
  185. for (var i = 0; i < resultObj.granted.length; i++) {
  186. var grantedPermission = resultObj.granted[i];
  187. result = 1
  188. }
  189. for (var i = 0; i < resultObj.deniedPresent.length; i++) {
  190. var deniedPresentPermission = resultObj.deniedPresent[i];
  191. result = 0
  192. }
  193. for (var i = 0; i < resultObj.deniedAlways.length; i++) {
  194. var deniedAlwaysPermission = resultObj.deniedAlways[i];
  195. result = -1
  196. }
  197. that.localStream = await that.getUserMedia();
  198. that.$ownerInstance.callMethod("sendJoin", {
  199. type: "join",
  200. userId: that.userId,
  201. rid: that.rid,
  202. name: that.name
  203. })
  204. },
  205. function(error) {
  206. console.log("导入android出现错误", error);
  207. }
  208. );
  209. } else {
  210. console.log("iosiosiosiosiosios");
  211. that.localStream = await that.getUserMedia().catch(err => {
  212. console.log("出错了", err);
  213. })
  214. that.$ownerInstance.callMethod("sendJoin", {
  215. type: "join",
  216. userId: that.userId,
  217. rid: that.rid,
  218. name: that.name
  219. })
  220. }
  221. // #endif
  222. // #ifdef H5
  223. that.localStream = await that.getUserMedia();
  224. // 通知serve层加入成功
  225. this.$ownerInstance.callMethod("sendJoin", {
  226. type: "join",
  227. userId: this.userId,
  228. rid: this.rid,
  229. name: this.name
  230. })
  231. // #endif
  232. },
  233. getUserMedia(then) {
  234. return new Promise((resolve, reject) => {
  235. navigator.mediaDevices.getUserMedia(this.mediaConstraints).then((stream) => {
  236. return resolve(stream);
  237. }).catch(err => {
  238. if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
  239. // 用户拒绝了授权
  240. reject(new Error('用户拒绝了访问摄像头和麦克风的请求'));
  241. } else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
  242. // 没有找到摄像头或麦克风
  243. reject(new Error('没有找到摄像头或麦克风'));
  244. } else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
  245. // 摄像头或麦克风不可读
  246. reject(new Error('摄像头或麦克风不可读'));
  247. } else if (err.name === 'OverconstrainedError' || err.name ===
  248. 'ConstraintNotSatisfiedError') {
  249. // 由于媒体流的约束条件无法满足,请求被拒绝
  250. reject(new Error('请求被拒绝,由于媒体流的约束条件无法满足'));
  251. } else if (err.name === 'TypeError' || err.name === 'TypeError') {
  252. // 发生了类型错误
  253. reject(new Error('发生了类型错误'));
  254. } else {
  255. // 其他未知错误
  256. reject(new Error('发生了未知错误'));
  257. }
  258. })
  259. });
  260. },
  261. </script>

4.信令服务器

略(就是socket,里面写swich,不会私信,小额收费)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/278259
推荐阅读
相关标签
  

闽ICP备14008679号