利用workerman实现webrtc实时音视频通话
实现原理利用workerman的websocket实现实时消息传递。
webrtc自带p2p功能,利用STUN中继服务器实现webrtc实时音视频
看看我们的前端文件,只是一个单页面
<html> <head> <meta name="referrer" content="always"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"> <meta name="format-detection" content="telephone=no"> <title>新申言自研rtc实时音视频通话</title> <meta name="keywords" content="申研社,申研社官方网站"> <meta name="description" itemprop="description" content="申研社是一款专注于公考训练的专业APP"> </head> <!-- 引入样式文件 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vant@2.5.9/lib/index.css"> <!-- 引入 Vue 和 Vant 的 JS 文件 --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vant@2.5.9/lib/vant.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js"></script> <style type="text/css"> [v-cloak] { display: none; } body { /* IOS禁止微信调整字体大小 */ -webkit-text-size-adjust: 100% !important; text-size-adjust: 100% !important; -moz-text-size-adjust: 100% !important; } </style> <body> <div id="app" v-cloak> <div v-for="(v,k) in list">{{v.cid}}</div> <div @click="play" style="background-color: black;color: #ffffff;width: 200px;text-align: center">播放</div> <h2>My Stream</h2> <video id="localStream" autoplay src=""></video> <h2>Remote Stream</h2> <video id="remoteStream" autoplay src=""></video> </div> </body> <script> // 在 #app 标签下渲染一个按钮组件 new Vue({ el: '#app', data: { cid: {}, list: "", ws: '', ws_url: "ws://127.0.0.1:8282", localStream: '', p2p: '', stunConfig: { iceServers: [{ urls: "stun:stun.xten.com" }] }, remoteStream: '', answer: false }, created() { this.openSocket() }, methods: { play() { console.log(this.remoteStream) console.log(this.localStream) document.getElementById('localStream').srcObject = this.localStream document.getElementById('remoteStream').srcObject = this.remoteStream // this.remoteStream.play() // this.localStream.play() }, openSocket() { this.ws = new WebSocket(this.ws_url) this.ws.onopen = (res) => { } this.ws.onmessage = (res) => { res = JSON.parse(res.data) if (res.type === 'init') { this.cid.cid = res.data.cid this.createStream() } if (res.type === "list") { this.list = res.data } if (res.type === "call") { console.log("call") this.icecandidate() this.p2p.createOffer({ offerToReceiveAudio: 1 }).then(ret => { this.p2p.setLocalDescription(ret).then(rey => { this.send('offer', this.p2p.localDescription) }) }) } if (res.type === "answer") { this.p2p.setRemoteDescription(new RTCSessionDescription(res.data)) } if (res.type === "offer") { this.icecandidate() this.p2p.setRemoteDescription(new RTCSessionDescription(res.data)) console.log("offer") if (!this.answer) { console.log("aaa") this.p2p.createAnswer((ret) => { this.p2p.setLocalDescription(ret).then(() => { this.send("answer", this.p2p.localDescription) }, err => { alert(err) }) } , (e) => { alert(e); }); this.answer = true; } } if (res.type === "candidate") { console.log("candidate") this.p2p.addIceCandidate(new RTCIceCandidate(res.data)) } } }, //p2p打洞 icecandidate() { this.p2p = new RTCPeerConnection(this.stunConfig) console.log(this.p2p) this.p2p.onicecandidate = (res => { if (res.candidate) { this.send("candidate", res.candidate) } }) try { this.p2p.addStream(this.localStream) } catch (e) { console.log(111111111) alert(e) } this.p2p.onaddstream = (res) => { this.remoteStream = res.stream } }, createStream() { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { alert("当前浏览器不支持webrtc!") return false } navigator.mediaDevices.getUserMedia({ audio: true }).then(res => { console.log(res) this.localStream = res this.send("call", null) }, err => { alert(err) }) }, send(type, data) { let msg = { type: type, data: data } msg = JSON.stringify(msg) this.ws.send(msg) } } }) </script> </html>
前端文件框架用的vue,主要是为了简单,只要实现webrtc就行了。
简单分析一下,加载完成后首先开启websocket链接并绑定到一个频道号上面,在该频道上得客户端均实现webrtc链接。
webrtc基本传输频道是流 stream,由本地摄像头和麦克风采集的就是本地流 localStream,由远端客服端采集发送给我们的就是远端流remoteStream。
我们要做的就是把本地localSteam推送给其他用户,把接收到其他的用户的remoteStream播放出来,这样就实现了实时音视频。
在这个过程中有一个p2p打洞的过程,现代浏览器差不多都自带了webrtc功能
this.p2p = new RTCPeerConnection(this.stunConfig)
RTCPeerConnnection可以自行百度该类的用法,我们的webrtc基本上都是基于该浏览器自带功能实现。
后台workerman Gateway Events.php代码内容
<?php //declare(ticks=1); use \GatewayWorker\Lib\Gateway; $list = array(); class Events { public static function onConnect($client_id) { $data = self::success('init', ['cid' => $client_id]); Gateway::sendToCurrentClient($data); $list = Gateway::getAllClientIdList(); $arr = []; foreach ($list as $v) { $arr[$v] = ["cid" => $v]; } $data = self::success('list', $arr); Gateway::sendToAll($data); } public static function onMessage($client_id, $message) { // 向所有人发送 $message = json_decode($message, true); $data = self::success($message['type'], $message['data']); $list = Gateway::getAllClientIdList(); foreach ($list as $v) { if ($v !== $client_id) Gateway::sendToClient($v, $data); } } public static function onClose($client_id) { $data = self::success('leave', ['cid' => $client_id]); GateWay::sendToAll($data); } public static function success($type = 'success', $data = [], $msg = '成功', $code = 200) { $error = [ 'type' => $type, 'msg' => $msg, 'code' => $code, 'data' => $data ]; return json_encode($error, 256); } public static function error($type = 'error', $msg = '错误', $code = 400, $data = []) { $error = [ 'type' => $type, 'msg' => $msg, 'code' => $code, 'data' => $data ]; return json_encode($error, 256); } }
