注:这里用的是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;