基于你的 SRS 直播流煤体配置(通过 http_hooks 回调实现鉴权),结合 TP6 框架,以下是完整的 PHP SDK 封装方案

基于你的 SRS 配置(通过 http_hooks 回调实现鉴权),结合 TP6 框架,以下是完整的 PHP SDK 封装方案,包括回调验证服务和推流 / 拉流工具类:

一、整体流程说明

  1. SRS 回调触发 :当有推流、拉流等操作时,SRS 会向配置的 http://你的服务器IP:端口/api/verify 发送 HTTP 请求。
  2. TP6 验证服务:接收 SRS 回调请求,验证权限(如 token 有效性),返回 200 允许操作,返回 403 拒绝。
  3. 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"

}

}

四、关键说明

  1. 安全性:token 包含时间戳和签名,防止伪造和过期使用(默认有效期 30 分钟,可调整)。
  2. 日志调试 :回调日志保存在 runtime/srs_callback.log,方便排查验证失败原因。
  3. 扩展性 :可在 checkToken 方法中添加更复杂的逻辑(如数据库查询用户权限)。
  4. 配置一致性 :确保 SrsAuth 控制器和 SrsSdk 中的 secret 完全一致,否则签名验证会失败。

按照此方案部署后,你的 SRS 配置即可通过 TP6 后端实现完整的鉴权功能。

相关推荐
技术小丁2 小时前
使用 PHP 和 PhpSpreadsheet 在 Excel 中插入图片(附完整代码)
后端·php
七七七七073 小时前
【计算机网络】UDP协议深度解析:从报文结构到可靠性设计
服务器·网络·网络协议·计算机网络·算法·udp
CDwenhuohuo3 小时前
WebSocket 前端node启用ws调试
前端·websocket·网络协议
半桔3 小时前
【IO多路转接】epoll 高性能网络编程:从底层机制到服务器实战
linux·运维·服务器·网络·php
一种乐趣3 小时前
PHP推荐权重算法以及分页
算法·php·推荐算法
爱编程的鱼3 小时前
301 是什么意思?——HTTP 状态码详解与应用
网络·网络协议·http
刘恒1234567894 小时前
Windows 10 docker 配置(PHP+Nginx+Mysql)(thinkphp5项目)环境
windows·docker·php
JaguarJack4 小时前
PHP 开发中 你可能不知道的非常好用 PhpStorm 插件
后端·php
-孤存-7 小时前
深入浅出:TCP/UDP协议核心原理
网络·网络协议·tcp/ip·1024程序员节