自建直播系统(nginx+rtmp+hls+php)

注:这里用的是hls 延迟有10秒左右,所以慎重

一、安装RTMP

给nginx安装第三方模块nginx-rtmp-module

https://github.com/arut/nginx-rtmp-module.git 

1、进入nginx的目录

cd /nginx目录

2、加入第三方包nginx-rtmp-module

./configure --add-module=nginx-rtmp-module的绝对路径

3、重新编译nginx

make && make install

4、查看是否安装nginx-rtmp-module

nginx -v

看看configure里面有没有nginx-rtmp-module的路径 有代表安装成功,没有代表失败

5、启动nginx

二、配置rtmp->hls

rtmp {
  server {
    listen 1935;                        //rtmp监听的断开
    chunk_size 8129;                //块的大小
    notify_method post; #接口校验的请求方式

application hls {
      live on;                                //开启直播
      hls on;                                //开启hls模式
      max_connections 1024;                    # 最大连接数    
      hls_path /www/wwwroot/ModStartCMS-master/public/data/live; #ls片段存放的位置
      hls_fragment 3s; #设置 HLS 分段长度。默认为 5 秒钟。
      hls_playlist_length 30s; #设置 HLS 播放列表长度。默认为 30 秒钟。
      hls_sync 100ms; #设置 HLS 时间戳同步阈值。默认为 2 ms。这一功能可以防止由低分辨率 RTMP (1KHz) 转换到高分辨率 MPEG-TS (90KHz) 之后出现噪音。
      hls_continuous on; #切换 HLS 连续模式。这一模式下 HLS 序列号由其上次停止的最后时间开始。老的分段保留下来。默认为 off。
      hls_nested on; #切换 HLS 嵌套模式。这一模式下为每个流创建了一个 hls_path 的子目录。播放列表和分段在那个子目录中创建。默认为 off。
      hls_cleanup off; #切换 HLS 清理。这一功能默认为开启的。在这一模式下 nginx 缓存管理进程将老的 HLS 片段和播放列表由 HLS 清理掉。
      publish_notify on;#验证开启
      on_publish http://80.vaiwan.cn/api/live/auth;                #鉴权的地址
    }
  }

}

三、鉴权 (根据自己规则或者业务需求修改)

public function liveAuth(Request $request){
    //rtmp推流时有个串密钥,他会作为name参数发送给你
    $key = $request->get('name','');
    if (empty($key)) {
        header('HTTP/1.1 401 Unauthorized');
        header('Status: 401 Unauthorized');
        exit();
    }
    //此处我是解密这个密钥串的信息,看看是否正确,是否失效(建议给每个密钥串一个过期时间)
    if (empty(LiveUtil::orderSecury($key,'D'))) {
        header('HTTP/1.1 401 Unauthorized');
        header('Status: 401 Unauthorized');
        exit();
    }
    //校验密钥串对应直播间是否正常,如果是封号则不允许推流
    $live  = LiveUtil::getByKey($key);
    if (empty($live['status'])){
        header('HTTP/1.1 403 Unauthorized');
        header('Status: 403 Unauthorized');
        exit();
    }
    //更新直播间状态
    LiveUtil::update(['push_key' => $key],['open'=>1]);
    //socket通知客户端   这边我是通过socket通知客户端因为假设如果主播是晚上九点开播,那么有些用户就会在8.50分开始就等直播间开播,那么我们就可以通过socket通知到对应的客户端,使客户端开始加载直播内容,更新直播间信息
    $ws = new WebSocketClient("ws://127.0.0.1:1234/room?id={$live['id']}");
    $content = [
        'type'  =>'open',             //开播
        'url'   => $live['clone_addr']
    ];
    $ws->send(json_encode($content));
    $ws->close();
    //鉴权成功
    header("HTTP/1.1 200 OK");
    header("Status: 200 OK");
    exit();
}

四、后台怎么生成直播密钥串

我这边是将一些直播间的信息拼接成串加密等到密钥串

比如直播间ID:1,主播ID:10,开播时间:20220429160000

然后拼接还曾1_10_20220429160000,然后使用下面的的方法进行加密,然后到时推流的时候我就会解密出来这些数据,校验数据的正确性,并且根据开播时间判断当前是否是可以推流,是否过期

/**
 * @param $string  要加密的字符串
 * @param $operation D:解密,E: 加密
 * @param string $key 加密键值(自定义)
 * @return array|false|string|string[]
 */
public static function orderSecury($string,$operation,$key='live'){
    $key=md5($key);
    $key_length=strlen($key);
    $string=$operation=='D'?base64_decode($string):substr(md5($string.$key),0,8).$string;
    $string_length=strlen($string);
    $rndkey=$box=array();
    $result='';
    for($i=0;$i<=255;$i++){
        $rndkey[$i]=ord($key[$i%$key_length]);
        $box[$i]=$i;
    }
    for($j=$i=0;$i<256;$i++){
        $j=($j+$box[$i]+$rndkey[$i])%256;
        $tmp=$box[$i];
        $box[$i]=$box[$j];
        $box[$j]=$tmp;
    }
    for($a=$j=$i=0;$i<$string_length;$i++){
        $a=($a+1)%256;
        $j=($j+$box[$a])%256;
        $tmp=$box[$a];
        $box[$a]=$box[$j];
        $box[$j]=$tmp;
        $result.=chr(ord($string[$i])^($box[($box[$a]+$box[$j])%256]));
    }
    if($operation=='D'){
        if(substr($result,0,8)==substr(md5(substr($result,8).$key),0,8)){
            return substr($result,8);
        }else{
            return'';
        }
    }else{
        return str_replace('=','',base64_encode($result));
    }
}

五、客户端

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>{{$room['title']}}</title>

    <script src="http://80.vaiwan.cn:8081/asset/live/video.min.js"></script>
    <script type="text/javascript" src="http://80.vaiwan.cn:8081/asset/vendor/jquery.js"></script>
    <link href="http://80.vaiwan.cn:8081/asset/live/bootstrap.min.css" rel="stylesheet">
    <link href="http://80.vaiwan.cn:8081/asset/live/hls.js" rel="stylesheet">
    <link href="http://80.vaiwan.cn:8081/asset/live/video-js.css" rel="stylesheet">
</head>
<style>
    .vjs-error-display-before:before{
        color: #fff;
        content: "主播已下播";
        font-family: Arial, Helvetica, sans-serif;
        font-size: 4em;
        left: 0;
        line-height: 1;
        margin-top: -0.5em;
        position: absolute;
        text-shadow: 0.05em 0.05em 0.1em #000;
        text-align: center;
        top: 50%;
        vertical-align: middle;
        width: 100%;
    }
</style>
<body>
<center>
    <video id=example-video width=600 height=300 class="video-js vjs-default-skin vjs-big-play-centered" controls>

    </video>
    <div class="input-group" style="margin-top: 10px; width: 740px;">
        <input type="text" name="stream_address" id="stream_address" required autofocus
               placeholder="input HLS Stream Address 留神不要含有空格" class="form-control">
        <span class="input-group-btn input-btn">
                <button class="btn btn-default" id="form_button" type="button">提交</button>
            </span>
    </div>
</center>
<script type="text/javascript">
    var default_hls_address = "{{$room['clone_addr']}}";
    var player ;
    var options = {
        width: 1280,
        height: 720,
        poster: "{{\ModStart\Core\Assets\AssetsUtil::fix($room['cover'])}}",
        autoplay: true,
        controls: true,
        loop: true,
        preload: 'auto',
        sourceOrder: true,
        sources: [{
            src: default_hls_address,
            type: 'application/x-mpegURL'
        }, {
            src: '//path/to/video.webm',
            type: 'video/webm'
        }],
        techOrder: ['html5', 'flash'],
        flash: {
            swf: 'videojs/video-js.swf'
        }
    }
    player    = videojs('example-video', options);
    player.addClass('vjs-matrix');

    player.on(['loadstart', 'play', 'playing', 'firstplay', 'pause', 'ended', 'adplay', 'adplaying', 'adfirstplay', 'adpause', 'adended', 'contentplay', 'contentplaying', 'contentfirstplay', 'contentpause', 'contentended', 'contentupdate','error'], function (e) {
        //        console.warn('VIDEOJS player event: ', e.type);
        if (e.type == "play") {
            console.log('开始播放');
        } else if (e.type == "playing") {
            console.log('正在播放...');
        } else if (e.type == "pause") {
            console.log('暂停视频播放');
        } else if (e.type == "firstplay") {
            console.log('firstplay播放');
        } else if (e.type == 'ended'){
   //当主播停止推流时,停止视频播放,弹出遮罩
            $('.vjs-error-display').removeClass('vjs-hidden')
            $('.vjs-error-display').addClass('vjs-error-display-before')
            $(".vjs-error-display").append("<style>.vjs-error-display-before::before{}</style>");
            player.pause()
        }else if (e.type == 'error'){
            console.log(e)
        }else {
            console.log('1111111111111');
        }
    });

    $(function () {
        $("#form_button").click(function () {
            var msg = $("#msg");
            stream_address = $('input[name="stream_address"] ').val();
            console.log(stream_address);
            if (stream_address == "") {
                $('#stream_address ').css("border", "1px #ff0000 solid");
                msg.text("请输出媒体流地址");
                msg.addClass("warning");
                return false;
            } else {
                $('#stream_address').css("border", "1px #ff00ff solid");
                msg.text("error");
                msg.removeClass("warning");
            }
            $('#stream_address_code ').html("\"" + stream_address + "\"");
            player.src({
                src: stream_address,
                type: "application/x-mpegURL"
            });
        });
    });

    var ws = null;
    connect();
    function connect() {
        // 创建一个 websocket 连接
        ws = new WebSocket("ws:/80.vaiwan.cn:1234//room?id={{$room['id']}}");
        // websocket 创建成功事件
        ws.onopen = onopen;
        // websocket 接收到消息事件
        ws.onmessage = onmessage;
        ws.onclose = onclose;
        ws.onerror = onerror;
    }

    function onopen()
    {
        console.log('欢迎进入直播间')
    }

    // function onclose()
    // {
    //     connect();
    // }
    function onmessage(e)
    {
        var data = e.data
        data = JSON.parse(data);
        console.log(data)
        switch (data.type) {
            case 'handShake':
                console.log('欢迎进入直播间')
                break;
            case 'open':
                //当收到服务端开播的小心,刷新视频播放器
                setTimeout(function(){
                    player.src({
                        src: data.url,
                        type: "application/x-mpegURL"
                    });
                },5000);
                break;
        }
    }
</script>
</body>
</html>

六、socket服务端

<?php
date_default_timezone_set("Asia/Shanghai");
class socketServer
{
    const LOG_PATH = "/www/wwwroot/websocket-master-master/log/";
    private $_ip = "0.0.0.0";
    private $_port = 1234;
    private $_socketPool = array();     //所有socket链接索引
    private $_master = null;
    private $_roomid = null;
    private $_socketPoolGroup = array(); //socket链接分组
    public function __construct()
    {
        $this->initSocket();
    }

    private function initSocket(){

        // 创建webSocket服务对象 监听 0.0.0.0:9001 端口 这里参数二和参数三很重要
         $this->_master = new swoole_websocket_server($this->_ip, $this->_port, SWOOLE_BASE, SWOOLE_SOCK_TCP );

        // 监听WebSocket连接打开事件
        $this->_master->on('open', function($ws, $request){
            echo "client-{$request->fd} is open\n";
            $fd = $request->fd;
            $header = $request->header;
            $server = $request->server;
//            if ($header['origin'] != 'http://80.vaiwan.cn:8081' && $server['remote_addr'] != '127.0.0.1'){
            if ($header['origin'] != 'http://live.laravel.com' && $server['remote_addr'] != '127.0.0.1'){
                $this->_master->push($fd, json_encode(array('type' => 'refuse', 'msg' => '拒绝接入')));
                $this->_master->close($fd);
                return ;
            }
            if (!isset($server['query_string'])){
                $this->_master->push($fd, json_encode(array('type' => 'refuse', 'msg' => '拒绝接入')));
                $this->_master->close($fd);
                return ;
            }
            $param = $this->analytical_parameters($server['query_string']);
            if (!isset($param['id'])){
                $this->_master->push($fd, json_encode(array('type' => 'refuse', 'msg' => '拒绝接入')));
                $this->_master->close($fd);
                return ;
            }
            $this->_socketPool[(int)$fd] = $param['id'];
            $this->_socketPoolGroup[$param['id']][(int)$fd]= $server['remote_addr'];
            $msg = array('type' => 'handShake', 'msg' => '欢迎进入直播间');
            $this->_master->push($fd, json_encode($msg));

        });

        // 监听WebSocket消息事件
        $this->_master->on('message', function($ws, $frame){
            $data = json_decode($frame->data,true);
            if(isset($this->_socketPool[(int)$frame->fd] ) && isset($this->_socketPoolGroup[$this->_socketPool[(int)$frame->fd]][(int)$frame->fd]) && $this->_socketPoolGroup[$this->_socketPool[(int)$frame->fd]][(int)$frame->fd] == '127.0.0.1'){
                $this->_roomid = $this->_socketPool[(int)$frame->fd];
                $this->broadcast(json_encode($data));
            }
        });

        // 监听WebSocket连接关闭事件
        $this->_master->on('close', function ($ws, $fd){
            unset($this->_socketPoolGroup[$this->_socketPool[$fd]][$fd]);
            unset($this->_socketPool[$fd]);
            echo "client-{$fd} is close\n";
        });

        $this->_master->start();
    }
    //广播
    private function broadcast($data){
        foreach($this->_socketPoolGroup[$this->_roomid] as $fd => $addr){
            $this->_master->push($fd, $data);
        }
    }

    //解析参数
    private function analytical_parameters($data){
        $data = explode('&',$data);
        $array = [];
        foreach ($data as $item){
            $value = explode('=',$item);
            $array[$value[0]] = $value[1];
        }
        return $array;
    }

}
new socketServer;
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PHP小渣渣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值