基于你的 SRS 配置(通过 http_hooks 回调实现鉴权),结合 TP6 框架,以下是完整的 PHP SDK 封装方案,包括回调验证服务和推流 / 拉流工具类:
一、整体流程说明
- SRS 回调触发 :当有推流、拉流等操作时,SRS 会向配置的
http://你的服务器IP:端口/api/verify发送 HTTP 请求。 - TP6 验证服务:接收 SRS 回调请求,验证权限(如 token 有效性),返回 200 允许操作,返回 403 拒绝。
- SDK 工具类:封装推流 / 拉流地址生成、token 签名等功能,方便业务调用。
二、TP6 后端实现
1. 配置回调路由(route/api.php)
php
<?php
use think\facade\Route;
//直播接口
// SRS回调接口分离路由
//SRS连接回调
Route::rule('verify/connect', 'SrsAuth/connect','post|get');
//SRS推流验证回调
Route::rule('verify/publish', 'SrsAuth/publish','post|get');
//SRS播放验证回调
Route::rule('verify/play', 'SrsAuth/play','post|get');
//SRS断开连接记录回调
Route::rule('verify/close', 'SrsAuth/close','post|get');
//SRS停止推流记录回调
Route::rule('verify/unpublish', 'SrsAuth/unpublish','post|get');
//SRS停止播放记录回调
Route::rule('verify/stop', 'SrsAuth/stop','post|get');
//获取推流信息
Route::any('getPublishUrl', 'Stream/getPublishUrl');
//获取拉流信息
Route::any('getPlayUrl', 'Stream/getPlayUrl');
2. 回调验证控制器(app/api/controller/SrsAuth.php)
php
<?php
namespace app\api\controller;
use app\BaseController;
use think\facade\Log;
use think\facade\Request;
use think\Response;
use think\App;
class SrsAuth extends BaseController
{
private $secret;
private $expire;
public function __construct(App $app)
{
try {
parent::__construct($app);
$this->secret = config('srs.secret', 'srs2025123456');
$this->expire = config('srs.token_expire', 1800);
$this->initLogDir();
} catch (\Exception $e) {
$this->logError('SrsAuth控制器初始化失败', [
'error_msg' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* 处理 on_connect 事件(对应路由:verify/connect)
*/
public function connect()
{
$rawData = Request::getContent();
$params = json_decode($rawData, true);
$this->logDebug('收到 on_connect 事件', ['params' => $params]);
// 非验证类事件,直接通过
return $this->jsonResponse(0, 'success');
}
/**
* 处理 on_publish 事件(对应路由:verify/publish)- 核心推流验证
*/
public function publish()
{
try {
$rawData = Request::getContent();
$params = json_decode($rawData, true);
if (empty($params)) {
$this->logError('on_publish 参数解析失败', ['raw_data' => $rawData]);
return $this->jsonResponse(403, '参数格式错误');
}
$this->logDebug('收到 on_publish 事件', ['params' => $params]);
$result = $this->validatePublish($params);
return $result
? $this->jsonResponse(0, 'success')
: $this->jsonResponse(403, '推流验证失败');
} catch (\Exception $e) {
$this->logError('on_publish 事件处理异常', [
'error_msg' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return $this->jsonResponse(500, '服务器内部错误');
}
}
/**
* 处理 on_play 事件(对应路由:verify/play)- 拉流验证
*/
public function play()
{
try {
$rawData = Request::getContent();
$params = json_decode($rawData, true);
if (empty($params)) {
$this->logError('on_play 参数解析失败', ['raw_data' => $rawData]);
return $this->jsonResponse(403, '参数格式错误');
}
$this->logDebug('收到 on_play 事件', ['params' => $params]);
$result = $this->validatePlay($params);
return $result
? $this->jsonResponse(0, 'success')
: $this->jsonResponse(403, '拉流验证失败');
} catch (\Exception $e) {
$this->logError('on_play 事件处理异常', [
'error_msg' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return $this->jsonResponse(500, '服务器内部错误');
}
}
/**
* 处理 on_close 事件(对应路由:verify/close)
*/
public function close()
{
$rawData = Request::getContent();
$params = json_decode($rawData, true);
$this->logDebug('收到 on_close 事件', ['params' => $params]);
// 非验证类事件,直接通过
return $this->jsonResponse(0, 'success');
}
/**
* 处理 on_unpublish 事件(对应路由:verify/unpublish)
*/
public function unpublish()
{
$rawData = Request::getContent();
$params = json_decode($rawData, true);
$this->logDebug('收到 on_unpublish 事件', ['params' => $params]);
// 非验证类事件,直接通过
return $this->jsonResponse(0, 'success');
}
/**
* 处理 on_stop 事件(对应路由:verify/stop)
*/
public function stop()
{
$rawData = Request::getContent();
$params = json_decode($rawData, true);
$this->logDebug('收到 on_stop 事件', ['params' => $params]);
// 非验证类事件,直接通过
return $this->jsonResponse(0, 'success');
}
// ---------------------- 以下方法复用原有逻辑,无需修改 ----------------------
private function validatePublish($params)
{
$requiredFields = ['app', 'stream', 'param'];
$missingFields = [];
foreach ($requiredFields as $field) {
if (empty($params[$field])) {
$missingFields[] = $field;
}
}
if (!empty($missingFields)) {
$this->logError('推流参数不完整', [
'missing_fields' => $missingFields,
'current_params' => $params
]);
return false;
}
$token = $this->extractTokenFromParam($params['param']);
if (empty($token)) {
$this->logError('推流请求未携带token', ['param_str' => $params['param']]);
return false;
}
return $this->checkToken($token, $params['app'], $params['stream'], 'publish');
}
private function validatePlay($params)
{
$requiredFields = ['app', 'stream', 'param'];
$missingFields = [];
foreach ($requiredFields as $field) {
if (empty($params[$field])) {
$missingFields[] = $field;
}
}
if (!empty($missingFields)) {
$this->logError('拉流参数不完整', [
'missing_fields' => $missingFields,
'current_params' => $params
]);
return false;
}
$token = $this->extractTokenFromParam($params['param']);
if (empty($token)) {
$this->logError('拉流请求未携带token', ['param_str' => $params['param']]);
return false;
}
return $this->checkToken($token, $params['app'], $params['stream'], 'play');
}
private function extractTokenFromParam($paramStr)
{
if (empty($paramStr)) {
return '';
}
$paramStr = ltrim($paramStr, '?');
parse_str($paramStr, $paramArr);
return isset($paramArr['token']) ? urldecode($paramArr['token']) : '';
}
private function checkToken($token, $app, $stream, $type)
{
$tokenParts = explode(':', $token, 2);
if (count($tokenParts) !== 2) {
$this->logError('token格式错误', [
'error' => '需满足"签名:时间戳"格式',
'token' => $token,
'app' => $app,
'stream' => $stream,
'type' => $type
]);
return false;
}
list($sign, $timestamp) = $tokenParts;
$currentTime = time();
if (!is_numeric($timestamp)) {
$this->logError('token时间戳无效', [
'error' => '时间戳必须为数字',
'timestamp' => $timestamp,
'token' => $token
]);
return false;
}
$timestamp = (int)$timestamp;
$timeDiff = $currentTime - $timestamp;
if ($timeDiff > $this->expire) {
$this->logError('token已过期', [
'current_time' => $currentTime,
'token_time' => $timestamp,
'time_diff' => $timeDiff . '秒',
'expire' => $this->expire . '秒',
'token' => $token
]);
return false;
}
$expectedSign = $this->generateSign($app, $stream, $type, $timestamp);
if (!hash_equals($sign, $expectedSign)) {
$this->logError('签名验证失败', [
'expected_sign' => $expectedSign,
'actual_sign' => $sign,
'sign_source' => "{$app}{$stream}{$type}{$timestamp}{$this->secret}",
'token' => $token
]);
return false;
}
$this->logDebug('token验证成功', [
'app' => $app,
'stream' => $stream,
'type' => $type,
'token_expire_remaining' => $this->expire - $timeDiff . '秒'
]);
return true;
}
private function generateSign($app, $stream, $type, $timestamp)
{
$source = "{$app}{$stream}{$type}{$timestamp}{$this->secret}";
return md5($source);
}
private function jsonResponse($code, $message, $data = [])
{
$responseData = [
'code' => $code,
'message' => $message,
'data' => $data,
'request_id' => uniqid()
];
$headers = [
'Content-Type' => 'application/json; charset=utf-8',
'Access-Control-Allow-Origin' => '*'
];
return Response::create($responseData, 'json', $code)
->header($headers);
}
private function initLogDir()
{
$debugLogDir = dirname(runtime_path() . 'api/srs_debug.log');
$errorLogDir = dirname(runtime_path() . 'api/srs_error.log');
if (!is_dir($debugLogDir)) {
mkdir($debugLogDir, 0755, true);
}
if (!is_dir($errorLogDir)) {
mkdir($errorLogDir, 0755, true);
}
}
private function logDebug($message, $data = [])
{
$logFile = runtime_path() . 'api/srs_debug.log';
$logContent = date('Y-m-d H:i:s')
. " [DEBUG] "
. $message
. " | 详情:" . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
. "\n\n";
file_put_contents($logFile, $logContent, FILE_APPEND);
}
private function logError($message, $data = [])
{
$logFile = runtime_path() . 'api/srs_error.log';
$logContent = date('Y-m-d H:i:s')
. " [ERROR] "
. $message
. " | 详情:" . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
. "\n\n";
file_put_contents($logFile, $logContent, FILE_APPEND);
}
}
3. SDK 工具类(app/common/SrsSdk.php)
封装推流 / 拉流地址生成、token 签名等功能:
php
<?php
namespace app\common;
class SrsSdk
{
// SRS 服务器配置
private $server = [
'ip' => '175.27.158.198', // 服务器公网 IP
'rtmp_port' => 19350, // RTMP 端口
'http_port' => 8081, // HTTP-FLV/HLS 端口
'rtc_port' => 8000, // WebRTC 端口
];
private $secret; // 与验证控制器中的密钥一致
public function __construct()
{
$this->secret = config('srs.secret'); // 建议在 config/srs.php 中配置密钥
}
/**
* 生成推流地址(RTMP)
* @param string $app 应用名(如 live)
* @param string $stream 流名(如 test)
* @return string 带 token 的推流地址
*/
public function getPublishUrl($app = 'live', $stream)
{
$timestamp = time();
$sign = $this->generateSign($app, $stream, 'publish', $timestamp);
$token = "{$sign}:{$timestamp}"; // token 格式:签名:时间戳
return "rtmp://{$this->server['ip']}:{$this->server['rtmp_port']}/{$app}/{$stream}?token={$token}";
}
/**
* 生成拉流地址(支持多种协议)
* @param string $app 应用名
* @param string $stream 流名
* @param string $protocol 协议(rtmp/flv/hls/webrtc)
* @return string 带 token 的拉流地址
*/
public function getPlayUrl($app = 'live', $stream, $protocol = 'flv')
{
$timestamp = time();
$sign = $this->generateSign($app, $stream, 'play', $timestamp);
$token = "{$sign}:{$timestamp}";
switch ($protocol) {
case 'rtmp':
return "rtmp://{$this->server['ip']}:{$this->server['rtmp_port']}/{$app}/{$stream}?token={$token}";
case 'flv':
return "http://{$this->server['ip']}:{$this->server['http_port']}/{$app}/{$stream}.flv?token={$token}";
case 'hls':
return "http://{$this->server['ip']}:{$this->server['http_port']}/{$app}/{$stream}.m3u8?token={$token}";
case 'webrtc':
return "webrtc://{$this->server['ip']}:{$this->server['rtc_port']}/{$app}/{$stream}?token={$token}";
default:
throw new \Exception("不支持的协议:{$protocol}");
}
}
/**
* 生成签名(与验证控制器中的方法一致)
*/
private function generateSign($app, $stream, $type, $timestamp)
{
$str = "{$app}{$stream}{$type}{$timestamp}{$this->secret}";
return md5($str);
}
}
4. 配置文件(config/srs.php)
php
<?php
namespace app\common;
use think\facade\Config;
class SrsSdk
{
// SRS服务器配置(从配置文件读取,统一管理)
private $config;
// 密钥(与SrsAuth保持一致)
private $secret;
// token有效期(与配置文件同步)
private $expire;
/**
* 初始化SDK(从配置文件读取参数,减少硬编码)
*/
public function __construct()
{
// 1. 读取SRS配置(优先使用配置文件,默认值兜底)
$this->config = Config::get('srs.server', [
'ip' => '175.27.158.198',
'rtmp_port' => 19350,
'http_port' => 8081,
'rtc_port' => 8000
]);
// 2. 读取密钥和有效期(与SrsAuth统一来源)
$this->secret = Config::get('srs.secret', 'srs2025123456');
$this->expire = Config::get('srs.token_expire', 1800);
}
/**
* 生成推流地址(RTMP协议,优化参数校验)
* @param string $app 应用名(默认live)
* @param string $stream 流名(必填)
* @return string 标准化推流地址
* @throws \InvalidArgumentException 流名为空时抛出异常
*/
public function getPublishUrl($app = 'live', $stream)
{
// 校验流名(避免生成无效地址)
if (empty($stream) || !is_string($stream)) {
throw new \InvalidArgumentException('流名不能为空且必须为字符串');
}
// 生成token(与SrsAuth签名规则完全一致)
$timestamp = time();
$sign = $this->generateSign($app, $stream, 'publish', $timestamp);
$token = "{$sign}:{$timestamp}";
// 拼接地址(使用http_build_query优化参数拼接,避免编码问题)
$query = http_build_query(['token' => $token]);
return sprintf(
'rtmp://%s:%d/%s/%s?%s',
$this->config['ip'],
$this->config['rtmp_port'],
$app,
$stream,
$query
);
}
/**
* 生成拉流地址(支持多协议,优化参数校验)
* @param string $app 应用名(默认live)
* @param string $stream 流名(必填)
* @param string $protocol 协议(rtmp/flv/hls/webrtc,默认flv)
* @return string 标准化拉流地址
* @throws \InvalidArgumentException 非法参数时抛出异常
*/
public function getPlayUrl($app = 'live', $stream, $protocol = 'flv')
{
// 1. 校验流名
if (empty($stream) || !is_string($stream)) {
throw new \InvalidArgumentException('流名不能为空且必须为字符串');
}
// 2. 校验协议
$supportedProtocols = ['rtmp', 'flv', 'hls', 'webrtc'];
if (!in_array($protocol, $supportedProtocols)) {
throw new \InvalidArgumentException(
"不支持的拉流协议:{$protocol},支持的协议:" . implode(',', $supportedProtocols)
);
}
// 3. 生成token
$timestamp = time();
$sign = $this->generateSign($app, $stream, 'play', $timestamp);
$token = "{$sign}:{$timestamp}";
$query = http_build_query(['token' => $token]);
// 4. 按协议拼接地址
switch ($protocol) {
case 'rtmp':
return sprintf(
'rtmp://%s:%d/%s/%s?%s',
$this->config['ip'],
$this->config['rtmp_port'],
$app,
$stream,
$query
);
case 'flv':
return sprintf(
'http://%s:%d/%s/%s.flv?%s',
$this->config['ip'],
$this->config['http_port'],
$app,
$stream,
$query
);
case 'hls':
return sprintf(
'http://%s:%d/%s/%s.m3u8?%s',
$this->config['ip'],
$this->config['http_port'],
$app,
$stream,
$query
);
case 'webrtc':
return sprintf(
'webrtc://%s:%d/%s/%s?%s',
$this->config['ip'],
$this->config['rtc_port'],
$app,
$stream,
$query
);
default:
throw new \InvalidArgumentException("未知协议:{$protocol}");
}
}
/**
* 生成签名(与SrsAuth完全一致,确保验证通过)
*/
private function generateSign($app, $stream, $type, $timestamp)
{
$source = "{$app}{$stream}{$type}{$timestamp}{$this->secret}";
return md5($source);
}
/**
* 获取服务器状态信息(增加协议说明,便于使用)
*/
/**
* 获取服务器状态信息(修复token_expire格式化问题)
*/
public function getServerInfo()
{
// 提前计算分钟数,并用number_format保留1位小数(避免浮点数过长)
$expireMinutes = number_format($this->expire / 60, 1);
return [
'server_ip' => $this->config['ip'],
'protocols' => [
'rtmp' => [
'url' => "rtmp://{$this->config['ip']}:{$this->config['rtmp_port']}",
'usage' => '推流/拉流'
],
'http_flv' => [
'url' => "http://{$this->config['ip']}:{$this->config['http_port']}",
'usage' => '拉流(低延迟)'
],
'hls' => [
'url' => "http://{$this->config['ip']}:{$this->config['http_port']}",
'usage' => '拉流(高兼容性,延迟较高)'
],
'webrtc' => [
'url' => "webrtc://{$this->config['ip']}:{$this->config['rtc_port']}",
'usage' => '拉流(极低延迟,需HTTPS环境)'
]
],
// 使用提前计算好的分钟数,避免字符串内直接运算
'token_expire' => "{$this->expire}秒({$expireMinutes}分钟)"
];
}
/**
* 快速生成测试推流命令(便于调试,减少手动拼接错误)
* @param string $app 应用名
* @param string $stream 流名
* @param string $localVideoPath 本地视频文件路径
* @return string FFmpeg推流命令
*/
public function getTestPublishCmd($app = 'live', $stream, $localVideoPath)
{
$publishUrl = $this->getPublishUrl($app, $stream);
// FFmpeg基础命令(兼容多数场景,可根据需求调整参数)
return sprintf(
'ffmpeg -re -i "%s" -c:v libx264 -c:a aac -f flv "%s"',
$localVideoPath,
$publishUrl
);
}
}
三、使用示例
1. 生成推流地址(在控制器中调用)
php
<?php
namespace app\api\controller;
use app\common\SrsSdk;
use think\Controller;
class Stream extends Controller
{
// 获取推流地址
public function getPublishUrl()
{
$srs = new SrsSdk();
$publishUrl = $srs->getPublishUrl('live', 'test_stream'); // 应用名 live,流名 test_stream
return json(['code' => 200, 'data' => ['url' => $publishUrl]]);
}
// 获取拉流地址
public function getPlayUrl()
{
$srs = new SrsSdk();
$playUrl = $srs->getPlayUrl('live', 'test_stream', 'flv'); // 获取 HTTP-FLV 拉流地址
return json(['code' => 200, 'data' => ['url' => $playUrl]]);
}
}
2. 推流 / 拉流验证
- 推流 :使用 FFmpeg 或 OBS 推流到生成的 URL(如
rtmp://175.27.158.198:19350/live/test_stream?token=xxx:xxx),SRS 会调用on_publish回调,TP6 验证通过后允许推流。 - 拉流 :前端通过生成的拉流地址(如
http://175.27.158.198:8081/live/test_stream.flv?token=xxx:xxx)播放,SRS 调用on_play回调验证通过后允许播放。
结果如下
获取推流信息
{
"code": 200,
"data": {
"url": "rtmp://175.27.158.198:19350/live/test_stream?token=29d1a8696c05d30e22c31b817d3ec007%3A1762431784"
}
}
获取拉流信息掊输出
{
"code": 200,
"data": {
"url": "http://175.27.158.198:8081/live/test_stream.flv?token=4dd41ecffb93ebbc09825e6ca579c185%3A1762431781"
}
}
四、关键说明
- 安全性:token 包含时间戳和签名,防止伪造和过期使用(默认有效期 30 分钟,可调整)。
- 日志调试 :回调日志保存在
runtime/srs_callback.log,方便排查验证失败原因。 - 扩展性 :可在
checkToken方法中添加更复杂的逻辑(如数据库查询用户权限)。 - 配置一致性 :确保
SrsAuth控制器和SrsSdk中的secret完全一致,否则签名验证会失败。
按照此方案部署后,你的 SRS 配置即可通过 TP6 后端实现完整的鉴权功能。