265 lines
8.4 KiB
PHP
265 lines
8.4 KiB
PHP
<?php
|
|
|
|
namespace app\zlm;
|
|
|
|
use app\config\Config;
|
|
use plugin\admin\app\common\Logger;
|
|
|
|
/**
|
|
* ZLMediaKit API 客户端
|
|
* 封装 ZLM HTTP API 调用
|
|
*/
|
|
class ZLMClient
|
|
{
|
|
/**
|
|
* @var string API基础URL
|
|
*/
|
|
private string $apiBaseUrl;
|
|
|
|
/**
|
|
* @var string API密钥
|
|
*/
|
|
private string $secret;
|
|
|
|
/**
|
|
* @var int 请求超时时间(秒)
|
|
*/
|
|
private int $timeout = 10;
|
|
|
|
/**
|
|
* @var int 连接超时时间(秒)
|
|
*/
|
|
private int $connectTimeout = 5;
|
|
|
|
public function __construct()
|
|
{
|
|
$zlmConfig = Config::getInstance()->zlm;
|
|
$this->apiBaseUrl = $zlmConfig->apiBaseUrl;
|
|
$this->secret = $zlmConfig->secret;
|
|
}
|
|
|
|
/**
|
|
* 获取媒体流列表
|
|
* @return array 流列表
|
|
*/
|
|
public function getMediaList(): array
|
|
{
|
|
$result = $this->request('/index/api/getMediaList', ['secret' => $this->secret]);
|
|
|
|
if ($result === null || !isset($result['code']) || $result['code'] !== 0) {
|
|
Logger::error("获取媒体流列表失败: " . ($result['msg'] ?? '未知错误'));
|
|
return [];
|
|
}
|
|
|
|
return $this->parseMediaList($result['data'] ?? []);
|
|
}
|
|
|
|
/**
|
|
* 解析媒体流列表数据
|
|
* @param array $data 原始数据
|
|
* @return array 解析后的流列表
|
|
*/
|
|
private function parseMediaList(array $data): array
|
|
{
|
|
if (empty($data)) {
|
|
return [];
|
|
}
|
|
|
|
$streams = [];
|
|
$seen = [];
|
|
|
|
foreach ($data as $media) {
|
|
$app = $media['app'] ?? '';
|
|
$stream = $media['stream'] ?? '';
|
|
$vhost = $media['vhost'] ?? '__defaultVhost__';
|
|
|
|
if (empty($app) || empty($stream)) {
|
|
continue;
|
|
}
|
|
|
|
// 去重
|
|
$key = "{$app}_{$stream}";
|
|
if (in_array($key, $seen)) {
|
|
continue;
|
|
}
|
|
$seen[] = $key;
|
|
|
|
// 解析视频轨道信息
|
|
$tracks = $media['tracks'] ?? [];
|
|
$videoTrack = null;
|
|
$audioTrack = null;
|
|
|
|
foreach ($tracks as $track) {
|
|
$codecType = $track['codec_type'] ?? -1;
|
|
if ($codecType === 0) {
|
|
$videoTrack = $track;
|
|
} elseif ($codecType === 1) {
|
|
$audioTrack = $track;
|
|
}
|
|
}
|
|
|
|
$resolution = $videoTrack ? "{$videoTrack['width']}x{$videoTrack['height']}" : 'unknown';
|
|
$fps = $videoTrack ? round(floatval($videoTrack['fps'])) : 'unknown';
|
|
$hasAudio = $audioTrack !== null;
|
|
|
|
// 生成播放URL
|
|
$playUrls = $this->generatePlayUrls($app, $stream, $vhost);
|
|
|
|
$streams[] = [
|
|
'app' => $app,
|
|
'stream' => $stream,
|
|
'vhost' => $vhost,
|
|
'key' => $key,
|
|
'resolution' => $resolution,
|
|
'fps' => $fps,
|
|
'hasAudio' => $hasAudio,
|
|
'bytesSpeed' => $media['bytesSpeed'] ?? 0,
|
|
'originUrl' => $media['originUrl'] ?? '',
|
|
'originType' => $media['originType'] ?? 0,
|
|
'createTime' => $media['createTime'] ?? '',
|
|
'playUrls' => $playUrls
|
|
];
|
|
}
|
|
|
|
return $streams;
|
|
}
|
|
|
|
/**
|
|
* 生成播放URL
|
|
* @param string $app 应用名
|
|
* @param string $stream 流名
|
|
* @param string $vhost 虚拟主机
|
|
* @return array 各种协议的播放URL
|
|
*/
|
|
private function generatePlayUrls(string $app, string $stream, string $vhost): array
|
|
{
|
|
$zlmConfig = Config::getInstance()->zlm;
|
|
$address = $zlmConfig->address;
|
|
$schema = $zlmConfig->schema;
|
|
$query = "vhost={$vhost}";
|
|
|
|
$baseHttp = "{$schema}://{$address}";
|
|
$baseWs = str_replace(['http://', 'https://'], ['ws://', 'wss://'], $baseHttp);
|
|
$baseHttps = str_replace('http://', 'https://', $baseHttp);
|
|
$baseWss = str_replace('ws://', 'wss://', $baseWs);
|
|
|
|
return [
|
|
// WebRTC
|
|
'webrtc' => "{$baseHttp}/index/api/webrtc?app={$app}&stream={$stream}&type=play&{$query}",
|
|
'webrtcSecure' => "{$baseHttps}/index/api/webrtc?app={$app}&stream={$stream}&type=play&{$query}",
|
|
|
|
// WHEP/WHIP
|
|
'whep' => "{$baseHttp}/index/api/whep?app={$app}&stream={$stream}&{$query}",
|
|
'whepSecure' => "{$baseHttps}/index/api/whep?app={$app}&stream={$stream}&{$query}",
|
|
'whip' => "{$baseHttp}/index/api/whip?app={$app}&stream={$stream}&{$query}",
|
|
'whipSecure' => "{$baseHttps}/index/api/whip?app={$app}&stream={$stream}&{$query}",
|
|
|
|
// FLV
|
|
'flv' => "{$baseHttp}/{$app}/{$stream}.live.flv?{$query}",
|
|
'flvSecure' => "{$baseHttps}/{$app}/{$stream}.live.flv?{$query}",
|
|
'wsFlv' => "{$baseWs}/{$app}/{$stream}.live.flv?{$query}",
|
|
'wssFlv' => "{$baseWss}/{$app}/{$stream}.live.flv?{$query}",
|
|
|
|
// HLS
|
|
'hls' => "{$baseHttp}/{$app}/{$stream}/hls.m3u8?{$query}",
|
|
'hlsSecure' => "{$baseHttps}/{$app}/{$stream}/hls.m3u8?{$query}",
|
|
'hlsFmp4' => "{$baseHttp}/{$app}/{$stream}/hls.fmp4.m3u8?{$query}",
|
|
'hlsFmp4Secure' => "{$baseHttps}/{$app}/{$stream}/hls.fmp4.m3u8?{$query}",
|
|
|
|
// TS
|
|
'ts' => "{$baseHttp}/{$app}/{$stream}.live.ts?{$query}",
|
|
'tsSecure' => "{$baseHttps}/{$app}/{$stream}.live.ts?{$query}",
|
|
'wsTs' => "{$baseWs}/{$app}/{$stream}.live.ts?{$query}",
|
|
'wssTs' => "{$baseWss}/{$app}/{$stream}.live.ts?{$query}",
|
|
|
|
// FMP4
|
|
'fmp4' => "{$baseHttp}/{$app}/{$stream}.live.mp4?{$query}",
|
|
'fmp4Secure' => "{$baseHttps}/{$app}/{$stream}.live.mp4?{$query}",
|
|
'wsFmp4' => "{$baseWs}/{$app}/{$stream}.live.mp4?{$query}",
|
|
'wssFmp4' => "{$baseWss}/{$app}/{$stream}.live.mp4?{$query}",
|
|
|
|
// RTMP/RTSP
|
|
'rtmp' => "rtmp://{$address}/{$app}/{$stream}?{$query}",
|
|
'rtsp' => "rtsp://{$address}/{$app}/{$stream}?{$query}",
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 发送HTTP请求
|
|
* @param string $path API路径
|
|
* @param array $params 请求参数
|
|
* @return array|null 响应数据
|
|
*/
|
|
private function request(string $path, array $params = []): ?array
|
|
{
|
|
$url = $this->apiBaseUrl . $path;
|
|
|
|
if (!empty($params)) {
|
|
$url .= '?' . http_build_query($params);
|
|
}
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connectTimeout);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($response === false || $httpCode !== 200) {
|
|
Logger::error("ZLM API 请求失败: HTTP {$httpCode}, 错误: {$error}");
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
Logger::error("ZLM API 响应解析失败: " . json_last_error_msg());
|
|
return null;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* 关闭指定流
|
|
* @param string $app 应用名
|
|
* @param string $stream 流名
|
|
* @return bool 是否成功
|
|
*/
|
|
public function closeStream(string $app, string $stream): bool
|
|
{
|
|
$result = $this->request('/index/api/close_stream', [
|
|
'secret' => $this->secret,
|
|
'app' => $app,
|
|
'stream' => $app,
|
|
'force' => 1
|
|
]);
|
|
|
|
return $result !== null && isset($result['code']) && $result['code'] === 0;
|
|
}
|
|
|
|
/**
|
|
* 获取流信息
|
|
* @param string $app 应用名
|
|
* @param string $stream 流名
|
|
* @return array|null 流信息
|
|
*/
|
|
public function getStreamInfo(string $app, string $stream): ?array
|
|
{
|
|
$result = $this->request('/index/api/getStreamInfo', [
|
|
'secret' => $this->secret,
|
|
'app' => $app,
|
|
'stream' => $stream
|
|
]);
|
|
|
|
if ($result === null || !isset($result['code']) || $result['code'] !== 0) {
|
|
return null;
|
|
}
|
|
|
|
return $result['data'] ?? null;
|
|
}
|
|
}
|