Files
zimoyin 1a84e92384 init
2026-04-03 11:32:19 +08:00

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;
}
}