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