• 1V1音视频实时互动直播系统


    李超老师的项目

    先肯定分为两个两个端,一个是服务器端一个是客户端。客户端用于UI界面的显示,服务器端用于处理客户端发来的消息。

    我们先搭建stun和turn服务器

    首先介绍一下什么是stun协议,

    它是用来干什么的?

    stun协议存在的目的就是进行NAT穿越。stun是典型的客户端、服务器模式。客户端发送请求,服务器进行响应。

    那么是什么NAT穿越呢?

    首先我们先了解一下为什么要进行NAT穿越。下面举个例子,在两个浏览器之间进行实时的音视频互动,对于底层来说,这就是两个端点之间进行高效的网络传输。

    为了解决音视频网络传输的问题,webrtc引入了一些网络传输协议。

    1.NAT:那么此时我们介绍NAT,简单理解为将内网的地址转换为公网的地址,内网地址无法通讯,通过NAT转换为公网之后,才有通信的可能。

    2.说到这里那么顺便介绍一下stun,这个stun充当的是中介的作用,在NAT的基础上,交换两个公网的信息,使得;两个公网之间可以建立连接。

    3.turn,stun是有一定几率是不成功的,因此turn会在云端架设一个服务器,在p2p连接不成功的情况下,保证音视频的互通,它就相当于一个中转站。

    4.ICE, 罗列所有通信可能性,选择最优解。

    NAT又分为四种类型:

    完全锥型

    地址限制锥型

    端口限制锥型

    对称型

    根据图中所示很好理解,caller与信令服务器连接,同时callee也跟信令服务器连接,第二个callee也跟信令服务器连接;caller向信令服务器发出join请求,信令服务器响应返回joined信息,第一个callee同理,但是第二个callee发送完join之后服务器发现成员已满,因此返回一个full信息,该callee不能join。然后caller和callee进行媒体协商,协商成功之后进行媒体流的数据传输。之后callee主动发出leave请求,服务器响应跟caller发出bye信息并返回callee leaved信息。

    我们来看一下服务器端的代码。主要就是处理客户端发来的信令消息。也就是以上的流程转换为代码。

    1. io.sockets.on('connection', (socket)=> {
    2. socket.on('message', (room, data)=>{
    3. socket.to(room).emit('message',room, data);
    4. });
    5. socket.on('join', (room)=>{
    6. socket.join(room);
    7. var myRoom = io.sockets.adapter.rooms[room];
    8. var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
    9. logger.debug('the user number of room is: ' + users);
    10. if(users < USERCOUNT){
    11. socket.emit('joined', room, socket.id); //发给自己
    12. if(users > 1){
    13. socket.to(room).emit('otherjoin', room, socket.id);//发给除自己之外的房间内的所有人
    14. }
    15. }else{
    16. socket.leave(room);
    17. socket.emit('full', room, socket.id);
    18. }
    19. //socket.emit('joined', room, socket.id); //发给自己
    20. //socket.broadcast.emit('joined', room, socket.id); //发给除自己之外的这个节点上的所有人
    21. //io.in(room).emit('joined', room, socket.id); //发给房间内的所有人
    22. });
    23. socket.on('leave', (room)=>{
    24. var myRoom = io.sockets.adapter.rooms[room];
    25. var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
    26. logger.debug('the user number of room is: ' + (users-1));
    27. //socket.emit('leaved', room, socket.id);
    28. //socket.broadcast.emit('leaved', room, socket.id);
    29. socket.to(room).emit('bye', room, socket.id);
    30. socket.emit('leaved', room, socket.id);
    31. //io.in(room).emit('leaved', room, socket.id);
    32. });
    33. });

    iceRestart是一个很好的方案,能够帮助我们重新选择数据传输的线路

    下面了解一下状态机是什么这点蛮重要

    下面介绍一下客户端代码编写的流程图

    首先写函数start

    start函数用于采集音视频数据

    采集成功则与信令服务器连接,并注册信令函数

    注册以上这些消息的处理函数

    如果是joined则设置状态为joined,创建PC并绑定媒体流。

    如果是otherjoin,则判断自身的状态是否为joined_unbind,如果是则需要重新创建PC并绑定媒体流并将状态设置为joined_conn,如果一开始状态为joined则直接将状态转换为joined_conn,接着开始媒体协商。

    如果是full则状态设置为full并关闭PC,并关闭本地媒体流

    客户端的实现需要注意的几点是

    1.网络连接要在音视频数据获取到之后,否则可能导致绑定音视频流失败

    2.当一端退出房间之后,另一端的PeerConnection要关闭重建,否则与新用户互通时媒体协商会失败。

    3.所有的处理流程为异步处理

    这里要了解一下什么是异步处理:

    异步事件处理:要等待收到一个消息或事件后,才能做下一步的操作

    同步处理:做完一步,直接做下一步

    接下来我们介绍一下客户端的代码

    首先先了解一个api

    其中较为关键的是iceServers

    第二个api

    根据流程写以下代码:

    首先start函数获取音视频数据

    1. function start(){
    2. if(!navigator.mediaDevices ||
    3. !navigator.mediaDevices.getUserMedia){
    4. console.error('the getUserMedia is not supported!');
    5. return;
    6. }else {
    7. var constraints;
    8. if( shareDeskBox.checked && shareDesk()){
    9. constraints = {
    10. video: false,
    11. audio: {
    12. echoCancellation: true,
    13. noiseSuppression: true,
    14. autoGainControl: true
    15. }
    16. }
    17. }else{
    18. constraints = {
    19. video: true,
    20. audio: {
    21. echoCancellation: true,
    22. noiseSuppression: true,
    23. autoGainControl: true
    24. }
    25. }
    26. }
    27. navigator.mediaDevices.getUserMedia(constraints)
    28. .then(getMediaStream)
    29. .catch(handleError);
    30. }
    31. }

    与信令服务器连接,注册处理函数

    1. function conn(){
    2. socket = io.connect();
    3. socket.on('joined', (roomid, id) => {
    4. console.log('receive joined message!', roomid, id);
    5. state = 'joined'
    6. //如果是多人的话,第一个人不该在这里创建peerConnection
    7. //都等到收到一个otherjoin时再创建
    8. //所以,在这个消息里应该带当前房间的用户数
    9. //
    10. //create conn and bind media track
    11. createPeerConnection();
    12. bindTracks();
    13. btnConn.disabled = true;
    14. btnLeave.disabled = false;
    15. console.log('receive joined message, state=', state);
    16. });
    17. socket.on('otherjoin', (roomid) => {
    18. console.log('receive joined message:', roomid, state);
    19. //如果是多人的话,每上来一个人都要创建一个新的 peerConnection
    20. //
    21. if(state === 'joined_unbind'){
    22. createPeerConnection();
    23. bindTracks();
    24. }
    25. state = 'joined_conn';
    26. call();
    27. console.log('receive other_join message, state=', state);
    28. });
    29. socket.on('full', (roomid, id) => {
    30. console.log('receive full message', roomid, id);
    31. hangup();
    32. closeLocalMedia();
    33. state = 'leaved';
    34. console.log('receive full message, state=', state);
    35. alert('the room is full!');
    36. });
    37. socket.on('leaved', (roomid, id) => {
    38. console.log('receive leaved message', roomid, id);
    39. state='leaved'
    40. socket.disconnect();
    41. console.log('receive leaved message, state=', state);
    42. btnConn.disabled = false;
    43. btnLeave.disabled = true;
    44. });
    45. socket.on('bye', (room, id) => {
    46. console.log('receive bye message', roomid, id);
    47. //state = 'created';
    48. //当是多人通话时,应该带上当前房间的用户数
    49. //如果当前房间用户不小于 2, 则不用修改状态
    50. //并且,关闭的应该是对应用户的peerconnection
    51. //在客户端应该维护一张peerconnection表,它是
    52. //一个key:value的格式,key=userid, value=peerconnection
    53. state = 'joined_unbind';
    54. hangup();
    55. offer.value = '';
    56. answer.value = '';
    57. console.log('receive bye message, state=', state);
    58. });
    59. socket.on('disconnect', (socket) => {
    60. console.log('receive disconnect message!', roomid);
    61. if(!(state === 'leaved')){
    62. hangup();
    63. closeLocalMedia();
    64. }
    65. state = 'leaved';
    66. });
    67. socket.on('message', (roomid, data) => {
    68. console.log('receive message!', roomid, data);
    69. if(data === null || data === undefined){
    70. console.error('the message is invalid!');
    71. return;
    72. }
    73. if(data.hasOwnProperty('type') && data.type === 'offer') {
    74. offer.value = data.sdp;
    75. pc.setRemoteDescription(new RTCSessionDescription(data));
    76. //create answer
    77. pc.createAnswer()
    78. .then(getAnswer)
    79. .catch(handleAnswerError);
    80. }else if(data.hasOwnProperty('type') && data.type == 'answer'){
    81. answer.value = data.sdp;
    82. pc.setRemoteDescription(new RTCSessionDescription(data));
    83. }else if (data.hasOwnProperty('type') && data.type === 'candidate'){
    84. var candidate = new RTCIceCandidate({
    85. sdpMLineIndex: data.label,
    86. candidate: data.candidate
    87. });
    88. pc.addIceCandidate(candidate);
    89. }else{
    90. console.log('the message is invalid!', data);
    91. }
    92. });
    93. roomid = getQueryVariable('room');
    94. socket.emit('join', roomid);
    95. return true;
    96. }

    媒体协商call函数

    1. function call(){
    2. if(state === 'joined_conn'){
    3. var offerOptions = {
    4. offerToRecieveAudio: 1,
    5. offerToRecieveVideo: 1
    6. }
    7. pc.createOffer(offerOptions)
    8. .then(getOffer)
    9. .catch(handleOfferError);
    10. }
    11. }
    12. function getOffer(desc){
    13. pc.setLocalDescription(desc);
    14. offer.value = desc.sdp;
    15. offerdesc = desc;
    16. //send offer sdp
    17. sendMessage(roomid, offerdesc);
    18. }
    19. function getAnswer(desc){
    20. pc.setLocalDescription(desc);
    21. answer.value = desc.sdp;
    22. //send answer sdp
    23. sendMessage(roomid, desc);
    24. }

    每个端都维护一个自己的peerconnection

    1. var pcConfig = {
    2. 'iceServers': [{
    3. 'urls': 'turn:stun.al.learningrtc.cn:3478',
    4. 'credential': "mypasswd",
    5. 'username': "garrylea"
    6. }]
    7. };
    8. function createPeerConnection(){
    9. //如果是多人的话,在这里要创建一个新的连接.
    10. //新创建好的要放到一个map表中。
    11. //key=userid, value=peerconnection
    12. console.log('create RTCPeerConnection!');
    13. if(!pc){
    14. pc = new RTCPeerConnection(pcConfig);
    15. pc.onicecandidate = (e)=>{
    16. if(e.candidate) {
    17. sendMessage(roomid, {
    18. type: 'candidate',
    19. label:event.candidate.sdpMLineIndex,
    20. id:event.candidate.sdpMid,
    21. candidate: event.candidate.candidate
    22. });
    23. }else{
    24. console.log('this is the end candidate');
    25. }
    26. }
    27. pc.ontrack = getRemoteStream;
    28. }else {
    29. console.warning('the pc have be created!');
    30. }
    31. return;
    32. }

  • 相关阅读:
    【MySQL】内置函数——字符串函数
    第52篇-某驾校登录参数sign分析【2022-08-19】
    软件层面缓存基本概念与分类
    go-kit中如何开启websocket服务
    天地图全国幼儿园数据下载与处理分析
    第四章 网络层
    ctfshow XSS web316~web333
    Rocky Linux 9 安装 Gitelab-ee(15.6.0-ee)
    基于SNN脉冲神经网络的Hebbian学习训练过程matlab仿真
    深度神经网络的主要模型,神经网络预测模型优点
  • 原文地址:https://blog.csdn.net/weixin_44593575/article/details/139423621