利用Swoft实现PHP+websocket直播,即时通讯代码

PHP.swoft 框架部分

用swoft框架做webSocket服务器 linux 环境

一、推荐用 composer 安装 swoft 框架。 这里只做概述不讲详细安装步骤
composer create-project swoft/swoft swoft安装完后进入目录
Alt
二、安装好后需要引入 WebSocket 服务
composer require swoft/websocket-server
三、 配置ws端口文件 .env
一般就在 swoft/.env 这里找到 #HTTP 部分
HTTP_PORT这个是项目运行后,监听socket端口
# HTTP
HTTP_HOST=0.0.0.0
HTTP_PORT=8188 
HTTP_MODE=SWOOLE_PROCESS
HTTP_TYPE=SWOOLE_SOCK_TCP
四、创建控制器 websocket 控制器
php bin/swoft gen:websocket Camera --prefix /Camera
生成的代码一般在 swoft/app/WebSocket/xxx.php
/**
 * Class CameraController
 * @package App\WebSocket
 * @WebSocket("/con_camera/camera")
 */
class CameraController implements HandlerInterface
注意 @WebSocket("/con_camera/camera") 这里是访问的路由需要自己改
/**
 * @param Server $server
 * @param Request $request
 * @param int $fd
 */
public function onOpen(Server $server, Request $request, int $fd)
{
     $server->push($fd, '{"type":"id", "id":"'.$fd.'"}');
}

/**
 * @param Server $server
 * @param Frame $frame
 * @return mixed
 */
public function onMessage(Server $server, Frame $frame)
{
	// 这里是广播。 自己发出的内容,不广播给自己
    \Swoft::$server->sendToSome($frame->data, [], [$frame->fd]);
     // $server->push($frame->fd, $frame->data);
}

/**
 * @param Server $server
 * @param int $fd
 * @return mixed
 */
public function onClose(Server $server, int $fd)
{
    $server->close($fd);
    // do something. eg. record log
}
后台运行swoft项目 php bin/swoft ws:restart -d
结束用 php bin/swoft ws:stop
五、测试代码
大家可以在 function onOpen函数内 做打印
    /**
     * @param Server $server
     * @param Request $request
     * @param int $fd
     */
    public function onOpen(Server $server, Request $request, int $fd)
    {
        var_dump($fd,'这里是测试部分');
        $server->push($fd, '{"type":"id", "id":"'.$fd.'"}');
    }
测试方法很多,我只用 websocket 测试 谷歌浏览器

<html>
<head>
    <meta charset="utf-8">
	head>
<body>
<script type="text/javascript">
function websocketOpen () {
    if ("WebSocket" in window) {
        alert("您的浏览器支持 WebSocket!")
        return false;
    } else {
        // 浏览器不支持 WebSocket123
        alert("您的浏览器不支持 WebSocket!");
        return true;
    }
}

if(websocketOpen()){
    console.log('链接失败')
}

let ws = new WebSocket("wss://localhost.com/con_camera/camera");

ws.onopen = function (evt) {
    alert('连接成功')
}

ws.onmessage = function (evt) {
    let received_msg = evt.data
    console.log(received_msg);
}

ws.onclose = function (){
    alert('关闭链接')
}

script>
body>
html>
new WebSocket(“wss://localhost.com/con_camera/camera”)
这里 wss代表服务器配置了https证书,如果服务器没有配置证书需要用 ws
利用Swoft实现PHP+websocket直播,即时通讯代码_第1张图片

以上内容是PHP部分,没有多少需要编辑的。php.swoft只做转发和广播


webRtc部分

首先需要了解WebSocket,MediaDevices,RTCPeerConnection,信令服务器作用

WebSocket

基本都知道吧,不懂的看 官方文档

WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。

基本理解成 它是一个双向管道

浏览页面 股票服务器 管理人员 我们搭建个管道吧 ok,我一有消息就通知你 操作服务器,A股涨了30元 服务器通知:A股涨了30元 操作服务器,B股涨了20元 服务器通知:B股涨了20元 我关闭了,不用推送了 服务器关闭浏览页面 的管道不在推送 浏览页面 股票服务器 管理人员

MediaDevices 媒体设备

简单的理解成。调用媒体设备,获取 摄像头媒体设备 输入的信息 官方文档


RTCPeerConnection

简单理解成本地到远端的连接 官方文档
官方推荐引入Adapter.js保持浏览器的兼容性


信令服务器

作为webRTC中极为重要的一部分,会话管理需要建立服务器端与客户端之间的连接。
有人就问了:webRTC建立的是点对点连接,流数据是从浏览器直接传输到另一个浏览器,不需要服务器周转,怎么还需要建立服务器端与客户端连接呢?
这是个很好的问题!尽管webRTC建立的是P2P连接,但由于流数据传输需要一条信道,而这个信道则是由信令服务器提供的。而在webRTC中并没有这一过程,所以需要我们手动建立信号的传递和交涉过程。

信令服务器的实现软件有很多,我们这里用coturn官方文档

参考原文:https://blog.csdn.net/vainfanfan/article/details/82632737


我们组合一下信息

利用Swoft实现PHP+websocket直播,即时通讯代码_第2张图片

  1. 主播与客户端建立websocket通道
  2. 通过web获取流媒体MediaDevices
  3. 主播生成流媒体通道插座信息,通过websocket传输给客户,客户受到信息后设置插销信息
    与主播建立流媒体通道RTCPeerConnection。并且把主播流媒体通过流媒体通道传给客户端
  4. 这中间通过coturn信令服务器把流媒体穿透给外网,让客户端可以在外网获取

步骤很简单,但是中间遇到很多坑,我会把遇到的问题,也列出来


实现MediaDevices获取流媒体

需要的文件
jquery-3.2.1.min.js
adapter.js这个最好用迅雷下载

<html>
<head>

    <meta charset="utf-8">
    <meta name="description" content="php web-rtc例子,一对一聊天-基于workerman">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
    <meta itemprop="description" content="Video chat using the reference WebRTC application">
    <meta itemprop="name" content="AppRTC">
    <meta name="mobile-web-app-capable" content="yes">
    <meta id="theme-color" name="theme-color" content="#1e1e1e">
    <title>webRtc视频通话title>
head>

<body>

<div class="videos">
    <video id="localVideo" autoplay>video>
    <video id="remoteVideo" autoplay class="hidden">video>
div>

<script src="jquery-3.2.1.min.js">script>
<script src="adapter.js">script>
<script type="text/javascript">

    var localVideo = document.getElementById('localVideo');
    var remoteVideo = document.getElementById('remoteVideo');

    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
    
    navigator.mediaDevices.getUserMedia({
        // audio: true,
        video: true
    }).then(function (stream) {
        localVideo.srcObject = stream;
        localStream = stream;
        localVideo.addEventListener('loadedmetadata', function () {
        	console.log('视频加载成功')
        });
    }).catch(function (e) {
        alert(e);
    });
script>
body>
html>

利用Swoft实现PHP+websocket直播,即时通讯代码_第3张图片
我的电脑摄像头坏了


实现链接媒体通道RTCPeerConnection 在局域网内可测试

用上面的代码修改下

<html>
<head>

    <meta charset="utf-8">
    <meta name="description" content="php web-rtc例子,一对一聊天-基于workerman">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
    <meta itemprop="description" content="Video chat using the reference WebRTC application">
    <meta itemprop="name" content="AppRTC">
    <meta name="mobile-web-app-capable" content="yes">
    <meta id="theme-color" name="theme-color" content="#1e1e1e">
    <title>webRtc视频通话title>
head>

<body>

<div class="videos">
    <video id="localVideo" autoplay>video>
    <video id="remoteVideo" autoplay class="hidden">video>
div>

<script src="jquery-3.2.1.min.js">script>
<script src="adapter.js">script>
<script type="text/javascript">

    var localVideo = document.getElementById('localVideo');
    var remoteVideo = document.getElementById('remoteVideo');
    // 流媒体对象
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
    // 流媒体通道配置
    var configuration = {
        iceTransportPolicy:"all",
        iceCandidatePoolSize:"0"
    };
    // 协商次数
    var answer = 1;

/*
    流媒体获取
*/

    function openMediaSetSend() {
        navigator.mediaDevices.getUserMedia({
            // audio: true,
            video: true
        }).then(function (stream) {
            localVideo.srcObject = stream;
            localStream = stream;
            localVideo.addEventListener('loadedmetadata', function () {
                // 广播客户端 准备连接 
                publish('client-call', null)
            });
        }).catch(function (e) {
            alert(e);
        });
    }


/*
    websocketOpen
*/
    function websocketOpen () {
        if ("WebSocket" in window) {
            alert("您的浏览器支持 WebSocket!")
            return false;
        } else {
            // 浏览器不支持 WebSocket123
            alert("您的浏览器不支持 WebSocket!");
            return true;
        }
    }

    if(websocketOpen()){
        console.log('链接失败')
    }

    let ws = new WebSocket("wss://www.cwtui.com/con_camera/camera");

    //发送消息
    function publish(event, data) {
        ws.send(JSON.stringify({
            cmd:'publish',
            // subject: subject, // 房间号 暂时不做这个
            event:event,
            data:data
        }));
    }

    ws.onopen = function (evt) {
        openMediaSetSend() // 广播客户端 准备连接
    }

    ws.onmessage = function(e){
        let package = JSON.parse(e.data)
        let data = package.data;

        switch (package.event) {
            case 'client-call':
            // 主播模块
                client_call();
                break;
            case 'client-answer':
            // 更改与连接关联的 远程描述
                pc.setRemoteDescription(data).then().catch(e=>{alert(e)})
                break;
            case 'client-offer':
            // 粉丝模块
                client_offer(data)
                break;
            case 'client-candidate':
            /*
                将新接收的候选服务器传递到浏览器的ICE代理服务器。
                值为空字符串(“”),则表示已传递所有远程候选对象(候选对象结束)
                在协商过程中,您的应用程序可能会收到许多候选项,您可以通过这种方式将这些候选项传递给ICE代理,
                从而允许它构建一个潜在连接方法的列表。
            */
                pc.addIceCandidate(new RTCIceCandidate(data)).then().catch(e=>{alert(e)})
                break;
        }
    };

    ws.onclose = function (){
        alert('关闭链接')
    }

 


/*
    流媒体通道创建
*/
    function icecandidate(localStream) {
        // 创建链接
        pc = new RTCPeerConnection(configuration);

        /**
            onicecandidate属性 是一个事件处理程序 
            当调用 RTCPeerConnection.setlocaldescription() 或将 RTCIceCandidate 添加到 RTCPeerConnection 时
            应将候选对象传输到 远程客户端 以便 ICE代理与远程客户端 协商 
        */
        pc.onicecandidate = event => {
            if (event.candidate) {
                publish('client-candidate', event.candidate);
            }
        }

        var tracks = localStream.getTracks();
        // 将一个媒体流 添加到一组流中,这些新流将被传输给 客户端
        for(const val of tracks){
            pc.addTrack(val, localStream);
        }

        pc.ontrack = event => {
            $('#remoteVideo').removeClass('hidden');
            $('#localVideo').remove();
            alert(event.streams)
            remoteVideo.srcObject = event.streams[0];
        };
    }

    // 主播 开始准备 插座 信息
    function client_call() {
        // 创建流媒体通道
        icecandidate(localStream);

        // 生成插座
        pc.createOffer({  // 创建一个 SDP 提供一个新的 webrtc 连接到远程节点 
            offerToReceiveAudio: false,
            offerToReceiveVideo: true
        }).then(function (desc) {
            // 更改与连接关联的本地描述 设置后 onicecandidate 协商 
            pc.setLocalDescription(desc).then(function () {
                publish('client-offer', pc.localDescription);
            }).catch(function (e) {
                alert(e);
            });

        }).catch(function (e) {
            alert(e);
        });
    }

    // 粉丝端 收到消息 开始链接
    function client_offer(data) {
        // 创建流媒体通道
        icecandidate(localStream);
        // 更改与连接关联的 远程描述
        pc.setRemoteDescription(data).then(function (){

            if (answer) {
                answer = 0;
                /*
                    方法在WebRTC 连接的要约/答复 协商期间,为从粉丝户端接收到的要约创建一个SDP信息。
                    答案包含关于已经附加到会话的任何媒体、浏览器支持的编解码器和选项以及已经收集到的ICE候选项的信息。
                    答案是交付给返回的承诺,然后应该发送到报价源继续谈判过程。
                */
                return pc.createAnswer();
                
            }
            return false;
            
        }).then( desc => {

            if(desc !== false){
                // 更改与连接关联的本地描述 设置后 onicecandidate 协商 createAnswer
                pc.setLocalDescription(desc).then(function (){
                    publish('client-answer', pc.localDescription);
                })
            }

        }).catch( e => {
            alert(e)
        })

    }

    
    



script>
body>
html>

主播页面和粉丝页面不同的地方

<html>
<head>

    <meta charset="utf-8">
    <meta name="description" content="php web-rtc例子,一对一聊天-基于workerman">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
    <meta itemprop="description" content="Video chat using the reference WebRTC application">
    <meta itemprop="name" content="AppRTC">
    <meta name="mobile-web-app-capable" content="yes">
    <meta id="theme-color" name="theme-color" content="#1e1e1e">
    <title>webRtc主播title>
head>

<body>

<div class="videos">
    <video id="localVideo" autoplay>video>
    <video id="remoteVideo" autoplay class="hidden">video>
div>

<script src="jquery-3.2.1.min.js">script>
<script src="adapter.js">script>
<script type="text/javascript">

    var localVideo = document.getElementById('localVideo');
    var remoteVideo = document.getElementById('remoteVideo');
    // 流媒体对象
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
    // 流媒体通道配置
    var configuration = {
        iceTransportPolicy:"all",
        iceCandidatePoolSize:"0"
    };
    // 协商次数
    var answer = 1;

/*
    流媒体获取
*/

    function openMediaSetSend() {
        navigator.mediaDevices.getUserMedia({
            // audio: true,
            video: true
        }).then(function (stream) {
            localVideo.srcObject = stream;
            localStream = stream;
            localVideo.addEventListener('loadedmetadata', function () {
                // 广播客户端 准备连接 
                publish('client-call', null)
            });
        }).catch(function (e) {
            alert(e);
        });
    }


/*
    websocketOpen
*/
    function websocketOpen () {
        if ("WebSocket" in window) {
            alert("您的浏览器支持 WebSocket!")
            return false;
        } else {
            // 浏览器不支持 WebSocket123
            alert("您的浏览器不支持 WebSocket!");
            return true;
        }
    }

    if(websocketOpen()){
        console.log('链接失败')
    }

    let ws = new WebSocket("wss://www.cwtui.com/con_camera/camera");

    //发送消息
    function publish(event, data) {
        ws.send(JSON.stringify({
            cmd:'publish',
            // subject: subject, // 房间号 暂时不做这个
            event:event,
            data:data
        }));
    }

    ws.onopen = function (evt) {
        openMediaSetSend() // 广播客户端 准备连接
    }

    ws.onmessage = function(e){
        let package = JSON.parse(e.data)
        let data = package.data;

        switch (package.event) {
            case 'client-call':
            // 主播模块
                client_call();
                break;
            case 'client-answer':
                // 更改与连接关联的 远程描述
                pc.setRemoteDescription(data).then().catch(e=>{alert(e)})
                break;

        }
    };

    ws.onclose = function (){
        alert('关闭链接')
    }

/*
    流媒体通道创建
*/
    function icecandidate(localStream) {
        // 创建链接
        pc = new RTCPeerConnection(configuration);

        /**
            onicecandidate属性 是一个事件处理程序 
            当调用 RTCPeerConnection.setlocaldescription() 或将 RTCIceCandidate 添加到 RTCPeerConnection 时
            应将候选对象传输到 远程客户端 以便 ICE代理与远程客户端 协商 
        */
        pc.onicecandidate = event => {
            if (event.candidate) {
                publish('client-candidate', event.candidate);
            }
        }

        var tracks = localStream.getTracks();
        // 将一个媒体流 添加到一组流中,这些新流将被传输给 客户端
        for(const val of tracks){
            pc.addTrack(val, localStream);
        }

        pc.ontrack = event => {
            $('#remoteVideo').removeClass('hidden');
            $('#localVideo').remove();
            // alert(event.streams)
            remoteVideo.srcObject = event.streams[0];
        };
    }

    // 主播 开始准备 插座 信息
    function client_call() {
        // 创建流媒体通道
        icecandidate(localStream);

        // 生成插座
        pc.createOffer({  // 创建一个 SDP 提供一个新的 webrtc 连接到远程节点 
            offerToReceiveAudio: false,
            offerToReceiveVideo: true
        }).then(function (desc) {
            // 更改与连接关联的本地描述 设置后 onicecandidate 协商 
            pc.setLocalDescription(desc).then(function () {
                publish('client-offer', pc.localDescription);
            }).catch(function (e) {
                alert(e);
            });

        }).catch(function (e) {
            alert(e);
        });
    }

script>
body>
html>

<html>
<head>

    <meta charset="utf-8">
    <meta name="description" content="php web-rtc例子,一对一聊天-基于workerman">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
    <meta itemprop="description" content="Video chat using the reference WebRTC application">
    <meta itemprop="name" content="AppRTC">
    <meta name="mobile-web-app-capable" content="yes">
    <meta id="theme-color" name="theme-color" content="#1e1e1e">
    <title>webRtc粉丝title>
head>

<body>

<div class="videos">
    <video id="localVideo" autoplay>video>
    <video id="remoteVideo" autoplay class="hidden">video>
div>

<script src="jquery-3.2.1.min.js">script>
<script src="adapter.js">script>
<script type="text/javascript">

    var localVideo = document.getElementById('localVideo');
    var remoteVideo = document.getElementById('remoteVideo');
    // 流媒体对象
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
    // 流媒体通道配置
    var configuration = {
        iceTransportPolicy:"all",
        iceCandidatePoolSize:"0"
    };
    // 协商次数
    var answer = 1;


/*
    websocketOpen
*/
    function websocketOpen () {
        if ("WebSocket" in window) {
            alert("您的浏览器支持 WebSocket!")
            return false;
        } else {
            // 浏览器不支持 WebSocket123
            alert("您的浏览器不支持 WebSocket!");
            return true;
        }
    }

    if(websocketOpen()){
        console.log('链接失败')
    }

    let ws = new WebSocket("wss://www.cwtui.com/con_camera/camera");

    //发送消息
    function publish(event, data) {
        ws.send(JSON.stringify({
            cmd:'publish',
            // subject: subject, // 房间号 暂时不做这个
            event:event,
            data:data
        }));
    }

    ws.onopen = function (evt) {
        publish('client-call', null) // 广播客户端 准备连接
    }

    ws.onmessage = function(e){
        let package = JSON.parse(e.data)
        let data = package.data;

        switch (package.event) {
            case 'client-offer':
            // 粉丝模块
                if(answer){
                    client_offer(data)
                }
                break;
            case 'client-candidate':
            /*
                将新接收的候选服务器传递到浏览器的ICE代理服务器。
                值为空字符串(“”),则表示已传递所有远程候选对象(候选对象结束)
                在协商过程中,您的应用程序可能会收到许多候选项,您可以通过这种方式将这些候选项传递给ICE代理,
                从而允许它构建一个潜在连接方法的列表。
            */
                pc.addIceCandidate(new RTCIceCandidate(data)).then().catch(e=>{alert(e)})
                break;
        }
    };

    ws.onclose = function (){
        alert('关闭链接')
    }

 


/*
    流媒体通道创建
*/
    function icecandidate() {
        // 创建链接
        pc = new RTCPeerConnection(configuration);

        /**
            onicecandidate属性 是一个事件处理程序 
            当调用 RTCPeerConnection.setlocaldescription() 或将 RTCIceCandidate 添加到 RTCPeerConnection 时
            应将候选对象传输到 远程客户端 以便 ICE代理与远程客户端 协商 
        */

        pc.ontrack = event => {
            $('#remoteVideo').removeClass('hidden');
            $('#localVideo').remove();
            // alert(event.streams)
            remoteVideo.srcObject = event.streams[0];
        };
    }


    // 粉丝端 收到消息 开始链接
    function client_offer(data) {
        // 创建流媒体通道
        

            icecandidate();
            // 更改与连接关联的 远程描述
            pc.setRemoteDescription(data).then(function (){
                answer = 0;
                /*
                    方法在WebRTC 连接的要约/答复 协商期间,为从粉丝户端接收到的要约创建一个SDP信息。
                    答案包含关于已经附加到会话的任何媒体、浏览器支持的编解码器和选项以及已经收集到的ICE候选项的信息。
                    答案是交付给返回的承诺,然后应该发送到报价源继续谈判过程。
                */
                return pc.createAnswer();
            }).then( desc => {
                if(desc !== false){
                    // // 更改与连接关联的 远程描述
                    pc.setLocalDescription(desc).then(function (){
                        publish('client-answer', pc.localDescription);
                    })
                }
            }).catch( e => {
                alert(e)
            })



    }

script>
body>
html>
区别
主播不需要 与粉丝端建立握手机制 client-candidate
不需要 client-candidate 添加 ICE代理服务器连接方法的列表
粉丝端需要只有一次创建 流媒体通道
粉丝端,不需要获取媒体流
粉丝端,不需要与远程端协商

接下来需要配置信令服务器,打通内外网的差异

配置一下页面的信令服务器

改下上面的代码
var configuration = {
    iceServers:[{
        urls:["turn:www.cwtui.com:3478"], //地址
        username:"hu", // 账号
        credential:"123456" // 密码
    }],
    iceTransportPolicy:"all",
    iceCandidatePoolSize:"0"
};

重点注意下 turn 协议和 stun 这两个协议 我们配置的协议是turn

安装信令服务器与配置

coturn 下载页面

cd coturn 
./configure 
make 
sudo make install

查看是否安装成功
使用命令:which turnserver

我的配置

listening-port=3478
tls-listening-port=5349
listening-ip=172.17.142.214 // 内网ip
relay-device=eth0
relay-ip=172.17.142.214 // 内网ip
external-ip=47.94.228.114/172.17.142.214 // 外网ip/内网ip
relay-threads=5
min-port=49152
max-port=65535
fingerprint
lt-cred-mech
user=hu:123456
realm=www.cwtui.com
cert=/etc/turn_server_cert.pem
pkey=/etc/turn_server_pkey.pem
pidfile="/var/run/turnserver.pid"
cli-password=qwerty

参考原文: https://www.pressc.cn/967.html
参考原文: https://blog.csdn.net/bvngh3247/article/details/80742396

启动命令
turnserver -a -f -v -r www.cwtui.com
turnserver -a -f --user=hu:123456 -v -r www.cwtui.com
启动参数解析
-a 长期验证机制
-o 如果指定 -o 则后台运行
-f 使用指纹
-v
-r realm组别
–user 指定账户运行

参考原文:https://www.cnblogs.com/mobilecard/p/6542294.html

希望朋友给点个赞~~~~ 谢谢大家看到这里

你可能感兴趣的:(详解文档)