getMediaList(); return json(['code' => 0, 'msg' => 'ok', 'data' => $streams]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => $e->getMessage()]); } } /** * API: 获取检测状态 */ public function status(Request $request) { $status = [ 'detection_enabled' => true, 'check_interval' => 500, 'detect_threshold' => 3, 'cameras' => [] ]; return json(['code' => 0, 'msg' => 'ok', 'data' => $status]); } /** * API: 保存ROI配置 */ public function saveRoi(Request $request) { $streamKey = $request->post('stream_key'); $roi = $request->post('roi'); if (empty($streamKey) || empty($roi)) { return json(['code' => 400, 'msg' => '参数错误']); } $configFile = runtime_path() . '/roi_config/' . $streamKey . '.json'; if (!is_dir(dirname($configFile))) { mkdir(dirname($configFile), 0777, true); } file_put_contents($configFile, json_encode($roi)); return json(['code' => 0, 'msg' => '保存成功']); } /** * API: 获取ROI配置 */ public function getRoi(Request $request) { $streamKey = $request->get('stream_key'); if (empty($streamKey)) { return json(['code' => 400, 'msg' => '参数错误']); } $configFile = runtime_path() . '/roi_config/' . $streamKey . '.json'; if (file_exists($configFile)) { $roi = json_decode(file_get_contents($configFile), true); return json(['code' => 0, 'msg' => 'ok', 'data' => $roi]); } // 默认ROI return json(['code' => 0, 'msg' => 'ok', 'data' => [ ['x' => 0, 'y' => 0], ['x' => 640, 'y' => 0], ['x' => 640, 'y' => 480], ['x' => 0, 'y' => 480] ]]); } /** * API: 代理ZLM截图请求 - 解决跨域问题 */ public function proxyZlmSnap(Request $request) { $app = $request->get('app'); $stream = $request->get('stream'); $processed = $request->get('processed'); // 是否获取处理后的帧 if (empty($app) || empty($stream)) { return json(['code' => 400, 'msg' => '参数错误']); } try { $zlmConfig = Config::getInstance()->zlm; $secret = $zlmConfig->secret; // 构建ZLM截图URL $snapUrl = "{$zlmConfig->apiBaseUrl}/index/api/getSnap"; $params = [ 'secret' => $secret, 'app' => $app, 'stream' => $stream, 'timeout_sec' => 5 ]; $url = $snapUrl . '?' . http_build_query($params); // 获取截图数据 $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $imageData = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); $error = curl_error($ch); curl_close($ch); if ($httpCode !== 200 || empty($imageData)) { // 记录错误日志 \plugin\admin\app\common\Logger::error("ZLM截图失败: HTTP {$httpCode}, 错误: {$error}, URL: {$url}"); // 返回错误提示图 return $this->generateErrorImage("截图失败: HTTP {$httpCode}"); } // 如果是处理后的帧,添加标记(后续可替换为实际处理逻辑) if ($processed) { // TODO: 调用PHP处理流程处理图像 // 目前先返回原图,但添加水印表示是"处理后"的 $imageData = $this->addProcessedWatermark($imageData); } // 返回图片 return response($imageData, 200, [ 'Content-Type' => $contentType ?: 'image/jpeg', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0' ]); } catch (\Throwable $e) { \plugin\admin\app\common\Logger::error("ZLM截图异常: " . $e->getMessage()); return $this->generateErrorImage("截图异常: " . $e->getMessage()); } } /** * 添加"已处理"水印 */ private function addProcessedWatermark(string $imageData): string { // 如果安装了GD库,添加水印 if (extension_loaded('gd')) { try { $image = imagecreatefromstring($imageData); if ($image) { $color = imagecolorallocate($image, 233, 69, 96); $font = 5; // 内置字体 $text = "PROCESSED"; imagestring($image, $font, 10, 10, $text, $color); ob_start(); imagejpeg($image, null, 85); $watermarked = ob_get_clean(); imagedestroy($image); return $watermarked ?: $imageData; } } catch (\Throwable $e) { // 失败返回原图 } } return $imageData; } /** * 生成错误提示图 */ private function generateErrorImage(string $message): Response { // 创建带错误信息的图片 if (extension_loaded('gd')) { try { $width = 640; $height = 480; $image = imagecreatetruecolor($width, $height); $bgColor = imagecolorallocate($image, 10, 10, 10); $textColor = imagecolorallocate($image, 233, 69, 96); $white = imagecolorallocate($image, 255, 255, 255); imagefill($image, 0, 0, $bgColor); // 绘制边框 imagerectangle($image, 0, 0, $width - 1, $height - 1, $textColor); // 绘制文字 $lines = [ "视频加载失败", "", $message, "", "请检查:", "1. ZLM服务是否运行", "2. 视频流是否正常", "3. 配置是否正确" ]; $y = $height / 2 - count($lines) * 10; foreach ($lines as $line) { $x = ($width - strlen($line) * 8) / 2; imagestring($image, 3, (int)$x, (int)$y, $line, $white); $y += 20; } ob_start(); imagejpeg($image, null, 85); $imageData = ob_get_clean(); imagedestroy($image); return response($imageData, 200, ['Content-Type' => 'image/jpeg']); } catch (\Throwable $e) { // 失败返回简单占位图 } } // 返回1x1透明像素 $pixel = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='); return response($pixel, 200, ['Content-Type' => 'image/png']); } public function view(Request $request) { return view('index/view', ['name' => 'webman']); } }