当前位置:首页 > PHP > 正文内容

利用workerman实现webrtc实时音视频通话

陈杰2个月前 (01-29)PHP600


实现原理利用workerman的websocket实现实时消息传递。


webrtc自带p2p功能,利用STUN中继服务器实现webrtc实时音视频

image.png



image.png


看看我们的前端文件,只是一个单页面

<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);
    }
}



扫描二维码至手机访问

扫描二维码推送至手机访问。

版权声明:本文由何烦过虎溪发布,如需转载请注明出处。

转载请注明出处:http://95shouyou.com/?id=36

分享给朋友:

相关文章

laravel的Observer观察者模式模型事件实战

laravel的orm特别好用,特别是observer观察者模式,可以在不更改原有业务代码的情况下做到切入编程,有点类似于我们之前常用的钩子函数。下面就是我们深入observer观察者模式的实战代码现...

Laravel带条件查询多个count

Laravel带条件查询多个count

在一个数据表中,我们需要用到聚合函数count来查询符合条件的总数。举例一个场景:我们有一个帖子,这个帖子可以分别被好中差评,好中差评记录用一个单独的记录表来存储。如果用户点击好评,那么该帖子作者的经...

通过代码创建多个同样的mysql表

在分库分表中可能要同时创建多个结构相同但后缀不同的表,通过代码实现for ($i = 3; $i <= 20; $i++) {    DB::connection('...

mysql查找附近的人,经纬度查询

经纬度排序mysql函数CREATE DEFINER=`root`@`localhost` FUNCTION `get_distance`(`lon1` float,`lat1` float,`lon...

laravel分表model映射的思路

预先估计会出现大数据量并且访问频繁的表,将其分为若干个表这种预估大差不差的,论坛里面发表帖子的表,时间长了这张表肯定很大,几十万,几百万都有可能。 聊天室里面信息表,几十个人在一起一聊一个晚上,时间长...

Lumen框架报错Cannot declare class Event, because the name is already in use

Lumen框架报错Cannot declare class Event, because the name is already in use

Lumen框架报错信息Cannot declare class Event, because the name is already in use(1/1) ErrorExceptionCa...

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。