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