init
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace app\zlm;
|
||||
|
||||
use app\config\Config;
|
||||
use app\utils\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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user