php swoole实现webrtc支持内网外网p2p视频通话的html代码
webrt内网不需要turn及stun服务器就能实现p2p链接,但是如果是通讯设备在各种的内网中的话就需要stun及turn来进行打通或中继了,首先
1、交换sdp信息
SDP(会话描述协议)信息包含了一些关键的元数据,用于建立和维护音视频通信会话。SDP通常包括以下主要信息:
会话名称(Session Name): 会话的名称或标识符,用于标识这个会话。
会话信息(Session Information): 会话的一般信息,通常是一段描述性的文本。
媒体类型(Media Types): 描述会话中使用的媒体类型,通常包括音频和视频。媒体类型通常由"M="行指定,例如"M=audio 12345 RTP/AVP 0"表示音频。
媒体描述(Media Description): 描述媒体流的详细信息,包括媒体格式、端口号、传输协议等。媒体描述通常由"a=rtpmap"和"a=fmtp"等行指定。
媒体连接信息(Connection Information): 描述媒体的连接信息,包括IP地址和端口号。这对于NAT穿越和防火墙遍历非常重要。
会话带宽(Session Bandwidth): 描述会话的带宽要求,以确保适当的网络资源分配。
时间描述(Timing Description): 描述会话的时间信息,通常包括会话的开始时间和结束时间。
加密和安全性信息(Encryption and Security Information): 描述媒体流的加密和安全性设置,用于确保通信的安全性和隐私。
ICE(Interactive Connectivity Establishment)候选者(ICE Candidates): 描述网络候选者,用于建立P2P连接,包括IP地址和端口号。
传输层协议(Transport Layer Protocol): 描述用于传输媒体的协议,通常是RTP(实时传输协议)。
媒体方向(Media Direction): 描述媒体流的方向,包括发送方向、接收方向或双向。
其他定制属性(Custom Attributes): 可能包括其他自定义的SDP属性,用于特定的应用或需求。
通过offer与answer来交换,这时候需要用到信令服务器进行sdp的交换
sdp交换完成后就尝试p2p直连,首先尝试局域网,连不上就打洞进行互联网直连,还是连不上就直接走中继服务器中转数据。
代码如下:
首先搭建一个信令服务器,我们采用swoole来搭建一个websocket服务器
<?php $userlist = []; $server = new Swoole\Websocket\Server("0.0.0.0", 9502, SWOOLE_BASE, SWOOLE_SOCK_TCP | SWOOLE_SSL); $server->set([ 'ssl_cert_file' => '/data/cert/6284283_web.debug.only.bfw.wiki.pem', 'ssl_key_file' => '/data/cert/6284283_web.debug.only.bfw.wiki.key', ]); $server->on('open', function($server, $req) { $_get = $req->get; $_username = $_get['username']; global $userlist; $userlist[$_username] = $req->fd; echo "connection open: {$req->fd}{$_username}\n"; }); $server->on('message', function($server, $frame) { $data = json_decode($frame->data, true); //消息类型 $type = $data["type"]; $_ret = ["type" => $type]; //to user $toUser = $data["toUser"]; $fromUser = $data["fromUser"]; $msg = isset($data["msg"])?$data["msg"]:""; //msg $sdp = isset($data["sdp"])?$data["sdp"]:""; //sdp $iceCandidate = isset($data["iceCandidate"])?$data["iceCandidate"]:""; //ice //对方挂断 if ("hangup" == $type) { $_ret['fromUser'] = $fromUser; $_ret['msg'] = "对方挂断"; } //视频通话请求 if ("call_start" == $type) { $_ret['fromUser'] = $fromUser; $_ret['msg'] = "1"; } //视频通话请求回应 if ("call_back" == $type) { $_ret['fromUser'] = $fromUser; $_ret['msg'] = $msg; } //offer if ("offer" == $type) { $_ret['fromUser'] = $toUser; $_ret['sdp'] = $sdp; } //answer if ("answer" == $type) { $_ret['fromUser'] = $toUser; $_ret['sdp'] = $sdp; } //ice if ("_ice" == $type) { $_ret['fromUser'] = $toUser; $_ret['iceCandidate'] = $iceCandidate; } if ($toUser != "") { global $userlist; if (!isset($userlist[$toUser])) { $_ret['msg'] = "Sorry,呼叫的用户不在线!"; $_ret['type'] = "call_back"; $_ret['fromUser'] = "系统消息"; $_senddata = json_encode($_ret); $server->push($frame->fd, $_senddata); } else { foreach ($userlist as $key => $val) { if ($key == $toUser) { $_senddata = json_encode($_ret); echo "send message: {$_senddata}\n"; $server->push($val, $_senddata); } } } } // echo "received message: {$frame->data}\n"; }); $server->on('close', function($server, $fd) { echo "connection close: {$fd}\n"; }); $server->start();实现sdp及ice的信息交换
编写html代码实现p2p视频通话
<!DOCTYPE html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"> <style> html,body{ margin: 0; padding: 0; } #main{ position: absolute; width: 370px; height: 550px; } #localVideo{ position: absolute; background: #757474; top: 10px; right: 10px; width: 100px; height: 150px; z-index: 2; } #remoteVideo{ position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; background: #222; } #buttons{ z-index: 3; bottom: 20px; left: 90px; position: absolute; } #toUser{ border: 1px solid #ccc; padding: 7px 0px; border-radius: 5px; padding-left: 5px; margin-bottom: 5px; } #toUser:focus{ border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6); box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6) } #call{ width: 70px; height: 35px; background-color: #00BB00; border: none; margin-right: 25px; color: white; border-radius: 5px; } #yourno{ color: WHITE; } #hangup{ width:70px; height:35px; background-color:#FF5151; border:none; color:white; border-radius: 5px; } </style> </head> <body> <div id="main"> <video id="remoteVideo" playsinline autoplay></video> <video id="localVideo" playsinline autoplay muted></video> <div id="buttons"> <span id="yourno"></span> <input id="toUser" placeholder="输入在线好友账号" /><br/> <button id="call">视频通话</button> <button id="hangup">挂断</button> </div> </div> <script type="text/javascript" > var icecanarr=[]; let username = randomString(6); document.getElementById('yourno').innerHTML="您的账户名是:"+username; let localVideo = document.getElementById('localVideo'); let remoteVideo = document.getElementById('remoteVideo'); let websocket = null; let peer = null; WebSocketInit(); ButtonFunInit(); function randomString(len) { len = len || 32; var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; var maxPos = $chars.length; var pwd = ''; for (i = 0; i < len; i++) { pwd += $chars.charAt(Math.floor(Math.random() * maxPos)); } return pwd; } /* WebSocket */ function WebSocketInit(){ //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { websocket = new WebSocket("wss:/web.debug.only.bfw.wiki:9502/?username="+username); } else { alert("当前浏览器不支持WebSocket!"); } //连接发生错误的回调方法 websocket.onerror = function (e) { alert("WebSocket连接发生错误!"); }; //连接关闭的回调方法 websocket.onclose = function () { console.error("WebSocket连接关闭"); }; //连接成功建立的回调方法 websocket.onopen = function () { console.log("WebSocket连接成功"); }; //接收到消息的回调方法 websocket.onmessage = async function (event) { let { type, fromUser, msg, sdp, iceCandidate } = JSON.parse(event.data.replace(/\n/g,"\\n").replace(/\r/g,"\\r")); console.log(type); if (type === 'hangup') { console.log(msg); document.getElementById('hangup').click(); return; } if (type === 'call_start') { let msg = "0" if(confirm(fromUser + "发起视频通话,确定接听吗")==true){ document.getElementById('toUser').value = fromUser; WebRTCInit(); msg = "1" } websocket.send(JSON.stringify({ type:"call_back", toUser:fromUser, fromUser:username, msg:msg })); return; } if (type === 'call_back') { if(msg === "1"){ console.log(document.getElementById('toUser').value + "同意视频通话"); //创建本地视频并发送offer let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) localVideo.srcObject = stream; stream.getTracks().forEach(track => { peer.addTrack(track, stream); }); let offer = await peer.createOffer(); await peer.setLocalDescription(offer); let newOffer = offer.toJSON(); newOffer["fromUser"] = username; newOffer["toUser"] = document.getElementById('toUser').value; websocket.send(JSON.stringify(newOffer)); }else if(msg === "0"){ alert(document.getElementById('toUser').value + "拒绝视频通话"); document.getElementById('hangup').click(); }else{ alert(msg); document.getElementById('hangup').click(); } return; } if (type === 'offer') { let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = stream; stream.getTracks().forEach(track => { peer.addTrack(track, stream); }); await peer.setRemoteDescription(new RTCSessionDescription({ type, sdp })); for (let i = 0; i < icecanarr.length; i++) { peer.addIceCandidate(icecanarr[i]); } console.log(peer.remoteDescription) let answer = await peer.createAnswer(); let newAnswer = answer.toJSON(); newAnswer["fromUser"] = username; newAnswer["toUser"] = document.getElementById('toUser').value; websocket.send(JSON.stringify(newAnswer)); await peer.setLocalDescription(answer); //ice return; } if (type === 'answer') { console.log(type) console.log(sdp) peer.setRemoteDescription(new RTCSessionDescription({ type, sdp })); for (let i = 0; i < icecanarr.length; i++) { peer.addIceCandidate(icecanarr[i]); } return; } if (type === '_ice') { if(peer.remoteDescription!=null){ peer.addIceCandidate(iceCandidate); }else{ icecanarr.push(iceCandidate); } // console.log(iceCandidate); // peer.addIceCandidate(iceCandidate); return; } } } /* WebRTC */ function WebRTCInit(){ const config = { 'iceServers': [ { 'url': 'stun:web.debug.only.bfw.wiki:3478', }, { 'url': 'turn:web.debug.only.bfw.wiki:3478', 'username': 'bfw', 'credential': 'bfw' } ] }; peer = new RTCPeerConnection(config); peer.onicecandidate = function (e) { if (e.candidate) { websocket.send(JSON.stringify({ type: '_ice', toUser:document.getElementById('toUser').value, fromUser:username, iceCandidate: e.candidate })); } }; //track peer.ontrack = function (e) { if (e && e.streams) { remoteVideo.srcObject = e.streams[0]; } }; } /* 按钮事件 */ function ButtonFunInit(){ //视频通话 document.getElementById('call').onclick = function (e){ document.getElementById('toUser').style.visibility = 'hidden'; let toUser = document.getElementById('toUser').value; if(!toUser){ alert("请先指定好友账号,再发起视频通话!"); return; } if(peer == null){ WebRTCInit(); } websocket.send(JSON.stringify({ type:"call_start", fromUser:username, toUser:toUser, })); } //挂断 document.getElementById('hangup').onclick = function (e){ document.getElementById('toUser').style.visibility = 'unset'; if(localVideo.srcObject){ const videoTracks = localVideo.srcObject.getVideoTracks(); videoTracks.forEach(videoTrack => { videoTrack.stop(); localVideo.srcObject.removeTrack(videoTrack); }); } if(remoteVideo.srcObject){ const videoTracks = remoteVideo.srcObject.getVideoTracks(); videoTracks.forEach(videoTrack => { videoTrack.stop(); remoteVideo.srcObject.removeTrack(videoTrack); }); //挂断同时,通知对方 websocket.send(JSON.stringify({ type:"hangup", fromUser:username, toUser:document.getElementById('toUser').value, })); } if(peer){ peer.ontrack = null; peer.onremovetrack = null; peer.onremovestream = null; peer.onicecandidate = null; peer.oniceconnectionstatechange = null; peer.onsignalingstatechange = null; peer.onicegatheringstatechange = null; peer.onnegotiationneeded = null; peer.close(); peer = null; } localVideo.srcObject = null; remoteVideo.srcObject = null; } } </script> </body> </html>
网友评论