基于你的 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 后端实现完整的鉴权功能。

相关推荐
REDcker9 小时前
Linux信号机制详解 POSIX语义与内核要点 sigaction与备用栈实践
linux·运维·php
REDcker11 小时前
浏览器端Web程序性能分析与优化实战 DevTools指标与工程清单
开发语言·前端·javascript·vue·ecmascript·php·js
汤愈韬13 小时前
三种常用 NAT 的经典案例
网络协议·网络安全·security
云云只是个程序马喽13 小时前
AI漫剧创作系统开发定制指南
人工智能·小程序·php
汤愈韬14 小时前
NAT Server 与目的Nat
网络·网络协议·网络安全·security
7ACE16 小时前
Wireshark TS | TLP 超时时间
网络·网络协议·tcp/ip·wireshark·tcpdump
凯瑟琳.奥古斯特21 小时前
NAT原理及作用详解
网络·网络协议
W.A委员会1 天前
DNS详解
http
niucloud-admin1 天前
PHP V6 单商户常见问题——云编译报错处理
php
xxjj998a1 天前
Laravel 1.x:PHP框架的原始魅力
android·php·laravel