feat: 更新api,并更新了串流页面

This commit is contained in:
SikongJueluo 2025-07-09 13:39:03 +08:00
parent 67bdec8570
commit 443aea5e3e
No known key found for this signature in database
7 changed files with 311 additions and 164 deletions

View File

@ -14,6 +14,11 @@ public class TutorialController : ControllerBase
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IWebHostEnvironment _environment; private readonly IWebHostEnvironment _environment;
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="environment">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public TutorialController(IWebHostEnvironment environment) public TutorialController(IWebHostEnvironment environment)
{ {
_environment = environment; _environment = environment;

View File

@ -109,6 +109,7 @@ public class UDPController : ControllerBase
/// 获取指定IP地址接收的数据列表 /// 获取指定IP地址接收的数据列表
/// </summary> /// </summary>
/// <param name="address">IP地址</param> /// <param name="address">IP地址</param>
/// <param name="taskID">任务ID</param>
[HttpGet("GetRecvDataArray")] [HttpGet("GetRecvDataArray")]
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
/// <summary> /// <summary>
/// 视频流控制器,支持动态配置摄像头连接 /// 视频流控制器,支持动态配置摄像头连接
@ -17,10 +17,16 @@ public class VideoStreamController : ControllerBase
/// </summary> /// </summary>
public class CameraConfigRequest public class CameraConfigRequest
{ {
/// <summary>
/// 摄像头地址
/// </summary>
[Required] [Required]
[RegularExpression(@"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", ErrorMessage = "IP地址")] [RegularExpression(@"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", ErrorMessage = "IP地址")]
public string Address { get; set; } = ""; public string Address { get; set; } = "";
/// <summary>
/// 摄像头端口
/// </summary>
[Required] [Required]
[Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")] [Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")]
public int Port { get; set; } public int Port { get; set; }
@ -54,17 +60,7 @@ public class VideoStreamController : ControllerBase
var status = _videoStreamService.GetServiceStatus(); var status = _videoStreamService.GetServiceStatus();
// 转换为小写首字母的JSON属性符合前端惯例 // 转换为小写首字母的JSON属性符合前端惯例
return TypedResults.Ok(new return TypedResults.Ok(status);
{
isRunning = true, // HTTP视频流服务作为后台服务始终运行
serverPort = _videoStreamService.ServerPort,
streamUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html",
mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
connectedClients = _videoStreamService.ConnectedClientsCount,
clientEndpoints = _videoStreamService.GetConnectedClientEndpoints(),
cameraStatus = _videoStreamService.GetCameraStatus()
});
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -95,8 +91,6 @@ public class VideoStreamController : ControllerBase
htmlUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html", htmlUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html",
mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream", mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot", snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
cameraAddress = _videoStreamService.CameraAddress,
cameraPort = _videoStreamService.CameraPort
}); });
} }
catch (Exception ex) catch (Exception ex)
@ -183,35 +177,23 @@ public class VideoStreamController : ControllerBase
} }
/// <summary> /// <summary>
/// 测试摄像头连接 /// 控制 HTTP 视频流服务开关
/// </summary> /// </summary>
/// <returns>连接测试结果</returns> /// <param name="enabled">是否启用服务</param>
[HttpPost("TestCameraConnection")] /// <returns>操作结果</returns>
[HttpPost("SetEnabled")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)] [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async Task<IResult> TestCameraConnection() public IResult SetEnabled([FromQuery] bool enabled)
{ {
try logger.Info("设置视频流服务开关: {Enabled}", enabled);
_videoStreamService.Enabled = enabled;
return TypedResults.Ok(new
{ {
logger.Info("测试摄像头连接"); success = true,
enabled = _videoStreamService.Enabled
var (isSuccess, message) = await _videoStreamService.TestCameraConnectionAsync(); });
return TypedResults.Ok(new
{
success = isSuccess,
message = message,
cameraAddress = _videoStreamService.CameraAddress,
cameraPort = _videoStreamService.CameraPort,
timestamp = DateTime.Now
});
}
catch (Exception ex)
{
logger.Error(ex, "测试摄像头连接失败");
return TypedResults.InternalServerError(ex.Message);
}
} }
/// <summary> /// <summary>
@ -229,16 +211,29 @@ public class VideoStreamController : ControllerBase
logger.Info("测试 HTTP 视频流连接"); logger.Info("测试 HTTP 视频流连接");
// 尝试通过HTTP请求检查视频流服务是否可访问 // 尝试通过HTTP请求检查视频流服务是否可访问
bool isConnected = false;
using (var httpClient = new HttpClient()) using (var httpClient = new HttpClient())
{ {
httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间 httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间
var response = await httpClient.GetAsync($"http://localhost:{_videoStreamService.ServerPort}/"); var response = await httpClient.GetAsync($"http://localhost:{_videoStreamService.ServerPort}/");
// 只要能连接上就认为成功,不管返回状态 // 只要能连接上就认为成功,不管返回状态
bool isConnected = response.IsSuccessStatusCode; isConnected = response.IsSuccessStatusCode;
return TypedResults.Ok(isConnected);
} }
logger.Info("测试摄像头连接");
var (isSuccess, message) = await _videoStreamService.TestCameraConnectionAsync();
return TypedResults.Ok(new
{
isConnected = isConnected,
success = isSuccess,
message = message,
cameraAddress = _videoStreamService.CameraAddress,
cameraPort = _videoStreamService.CameraPort,
timestamp = DateTime.Now
});
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -38,7 +38,7 @@ class Camera
public async ValueTask<Result<bool>> Init() public async ValueTask<Result<bool>> Init()
{ {
var i2c = new Peripherals.I2cClient.I2c(this.address, this.port, this.timeout); var i2c = new Peripherals.I2cClient.I2c(this.address, this.port, this.timeout);
var ret = await i2c.WriteData(0x78, new byte[] { 0x08, 0x30, 0x02 }, Peripherals.I2cClient.I2cProtocol.I2c); var ret = await i2c.WriteData(0x78, new byte[] { 0x30, 0x08, 0x02 }, Peripherals.I2cClient.I2cProtocol.I2c);
if (!ret.IsSuccessful) if (!ret.IsSuccessful)
{ {
logger.Error($"I2C write failed during camera initialization for {this.address}:{this.port}, error: {ret.Error}"); logger.Error($"I2C write failed during camera initialization for {this.address}:{this.port}, error: {ret.Error}");

View File

@ -4,6 +4,73 @@ using Peripherals.CameraClient; // 添加摄像头客户端引用
namespace server.Services; namespace server.Services;
/// <summary>
/// 表示摄像头连接状态信息
/// </summary>
public class CameraStatus
{
/// <summary>
/// 摄像头的IP地址
/// </summary>
public string Address { get; set; } = string.Empty;
/// <summary>
/// 摄像头的端口号
/// </summary>
public int Port { get; set; }
/// <summary>
/// 是否已配置摄像头
/// </summary>
public bool IsConfigured { get; set; }
/// <summary>
/// 摄像头连接字符串IP:端口)
/// </summary>
public string ConnectionString { get; set; } = string.Empty;
}
/// <summary>
/// 表示视频流服务的运行状态
/// </summary>
public class ServiceStatus
{
/// <summary>
/// 服务是否正在运行
/// </summary>
public bool IsRunning { get; set; }
/// <summary>
/// 服务监听的端口号
/// </summary>
public int ServerPort { get; set; }
/// <summary>
/// 视频流的帧率FPS
/// </summary>
public int FrameRate { get; set; }
/// <summary>
/// 视频分辨率(如 640x480
/// </summary>
public string Resolution { get; set; } = string.Empty;
/// <summary>
/// 当前连接的客户端数量
/// </summary>
public int ConnectedClients { get; set; }
/// <summary>
/// 当前连接的客户端端点列表
/// </summary>
public List<string> ClientEndpoints { get; set; } = new();
/// <summary>
/// 摄像头连接状态信息
/// </summary>
public CameraStatus CameraStatus { get; set; } = new();
}
/// <summary> /// <summary>
/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页 /// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页
/// 支持动态配置摄像头地址和端口 /// 支持动态配置摄像头地址和端口
@ -28,6 +95,11 @@ public class HttpVideoStreamService : BackgroundService
private readonly List<HttpListenerResponse> _activeClients = new List<HttpListenerResponse>(); private readonly List<HttpListenerResponse> _activeClients = new List<HttpListenerResponse>();
private readonly object _clientsLock = new object(); private readonly object _clientsLock = new object();
/// <summary>
/// 获取 / 设置视频流服务是否启用
/// </summary>
public bool Enabled { get; set; } = false;
/// <summary> /// <summary>
/// 获取当前连接的客户端数量 /// 获取当前连接的客户端数量
/// </summary> /// </summary>
@ -94,7 +166,7 @@ public class HttpVideoStreamService : BackgroundService
try try
{ {
await Task.Run(() => await Task.Run(async () =>
{ {
lock (_cameraLock) lock (_cameraLock)
{ {
@ -122,6 +194,22 @@ public class HttpVideoStreamService : BackgroundService
logger.Info("摄像头配置已更新: {Address}:{Port}", _cameraAddress, _cameraPort); logger.Info("摄像头配置已更新: {Address}:{Port}", _cameraAddress, _cameraPort);
} }
// Init Camera
{
var ret = await _camera.Init();
if (!ret.IsSuccessful)
{
logger.Error(ret.Error);
throw ret.Error;
}
if (!ret.Value)
{
logger.Error($"Camera Init Failed!");
throw new Exception($"Camera Init Failed!");
}
}
}); });
return true; return true;
} }
@ -176,18 +264,15 @@ public class HttpVideoStreamService : BackgroundService
/// 获取摄像头连接状态 /// 获取摄像头连接状态
/// </summary> /// </summary>
/// <returns>连接状态信息</returns> /// <returns>连接状态信息</returns>
public object GetCameraStatus() public CameraStatus GetCameraStatus()
{ {
lock (_cameraLock) return new CameraStatus
{ {
return new Address = _cameraAddress,
{ Port = _cameraPort,
Address = _cameraAddress, IsConfigured = _camera != null,
Port = _cameraPort, ConnectionString = $"{_cameraAddress}:{_cameraPort}"
IsConfigured = _camera != null, };
ConnectionString = $"{_cameraAddress}:{_cameraPort}"
};
}
} }
/// <summary> /// <summary>
@ -215,7 +300,17 @@ public class HttpVideoStreamService : BackgroundService
_ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken); _ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken);
// 开始生成视频帧 // 开始生成视频帧
await GenerateVideoFrames(stoppingToken); while (!stoppingToken.IsCancellationRequested)
{
if (Enabled)
{
await GenerateVideoFrames(stoppingToken);
}
else
{
await Task.Delay(500, stoppingToken);
}
}
} }
catch (HttpListenerException ex) catch (HttpListenerException ex)
{ {
@ -660,13 +755,13 @@ public class HttpVideoStreamService : BackgroundService
/// <summary> /// <summary>
/// 获取服务状态信息 /// 获取服务状态信息
/// </summary> /// </summary>
public object GetServiceStatus() public ServiceStatus GetServiceStatus()
{ {
var cameraStatus = GetCameraStatus(); var cameraStatus = GetCameraStatus();
return new return new ServiceStatus
{ {
IsRunning = _httpListener?.IsListening ?? false, IsRunning = (_httpListener?.IsListening ?? false) && Enabled,
ServerPort = _serverPort, ServerPort = _serverPort,
FrameRate = _frameRate, FrameRate = _frameRate,
Resolution = $"{_frameWidth}x{_frameHeight}", Resolution = $"{_frameWidth}x{_frameHeight}",
@ -683,6 +778,8 @@ public class HttpVideoStreamService : BackgroundService
{ {
logger.Info("正在停止 HTTP 视频流服务..."); logger.Info("正在停止 HTTP 视频流服务...");
Enabled = false;
if (_httpListener != null && _httpListener.IsListening) if (_httpListener != null && _httpListener.IsListening)
{ {
_httpListener.Stop(); _httpListener.Stop();

View File

@ -216,11 +216,16 @@ export class VideoStreamClient {
} }
/** /**
* * HTTP
* @return * @param enabled (optional)
* @return
*/ */
testCameraConnection(): Promise<any> { setEnabled(enabled: boolean | undefined): Promise<any> {
let url_ = this.baseUrl + "/api/VideoStream/TestCameraConnection"; let url_ = this.baseUrl + "/api/VideoStream/SetEnabled?";
if (enabled === null)
throw new Error("The parameter 'enabled' cannot be null.");
else if (enabled !== undefined)
url_ += "enabled=" + encodeURIComponent("" + enabled) + "&";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = { let options_: RequestInit = {
@ -231,11 +236,11 @@ export class VideoStreamClient {
}; };
return this.http.fetch(url_, options_).then((_response: Response) => { return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processTestCameraConnection(_response); return this.processSetEnabled(_response);
}); });
} }
protected processTestCameraConnection(response: Response): Promise<any> { protected processSetEnabled(response: Response): Promise<any> {
const status = response.status; const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) { if (status === 200) {

View File

@ -149,69 +149,40 @@
</svg> </svg>
摄像头配置 摄像头配置
</h3> </h3>
<div class="flex flex-row justify-between items-center gap-4">
<div class="form-control"> <div class="flex flex-row justify-around gap-4">
<label class="label"> <div class="grow">
<span class="label-text">IP地址</span> <IpInputField
</label> v-model="tempCameraConfig.address"
<input required
type="text"
v-model="cameraConfig.address"
placeholder="例如: 192.168.1.100"
class="input input-bordered input-sm"
/> />
</div> </div>
<div class="form-control">
<label class="label"> <div class="grow">
<span class="label-text">端口号</span> <PortInputField
</label> v-model="tempCameraConfig.port"
<input required
type="number"
v-model="cameraConfig.port"
placeholder="例如: 8080"
class="input input-bordered input-sm"
/> />
</div> </div>
</div>
<div class="card-actions justify-end mt-4">
<button <button
class="btn btn-primary btn-sm" class="btn btn-ghost"
@click="confirmCameraConfig" @click="resetCameraConfig"
:disabled="configuring" :disabled="isDefaultCamera"
> >
<svg <RotateCcw class="w-4 h-4" />
v-if="configuring" 重置
class="animate-spin h-4 w-4 mr-1" </button>
fill="none" <button
viewBox="0 0 24 24" class="btn btn-primary"
> @click="confirmCameraConfig"
<circle :disabled="!isValidCameraConfig || !hasChangesCamera"
class="opacity-25" :class="{ loading: configuring }"
cx="12" >
cy="12" <Save class="w-4 h-4" v-if="!configuring" />
r="10" {{ configuring ? "配置中..." : "保存配置" }}
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<svg
v-else
class="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
{{ configuring ? "配置中..." : "确认" }}
</button> </button>
</div> </div>
</div> </div>
@ -540,8 +511,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue"; import { ref, computed, reactive, watch, onMounted, onUnmounted } from "vue";
import { useStorage } from "@vueuse/core";
import { z } from "zod";
import {
Save,
RotateCcw,
} from "lucide-vue-next";
import { VideoStreamClient, CameraConfigRequest } from "@/APIClient"; import { VideoStreamClient, CameraConfigRequest } from "@/APIClient";
import { IpInputField, PortInputField } from "@/components/InputField";
// //
const loading = ref(false); const loading = ref(false);
@ -573,12 +551,92 @@ const streamInfo = ref({
snapshotUrl: "", snapshotUrl: "",
}); });
// //
const cameraConfig = ref({ const cameraConfigSchema = z.object({
address: z
.string()
.ip({ version: "v4", message: "请输入有效的IPv4地址" })
.min(1, "请输入IP地址"),
port: z
.number()
.int("端口必须是整数")
.min(1, "端口必须大于0")
.max(65535, "端口必须小于等于65535"),
});
type CameraConfig = z.infer<typeof cameraConfigSchema>;
//
const defaultCameraConfig: CameraConfig = {
address: "192.168.1.100", address: "192.168.1.100",
port: 8080, port: 8080,
};
// 使 VueUse
const cameraConfig = useStorage<CameraConfig>(
"camera-config",
defaultCameraConfig,
localStorage,
{
serializer: {
read: (value: string) => {
try {
const parsed = JSON.parse(value);
const result = cameraConfigSchema.safeParse(parsed);
return result.success ? result.data : defaultCameraConfig;
} catch {
return defaultCameraConfig;
}
},
write: (value: CameraConfig) => JSON.stringify(value),
},
},
);
//
const tempCameraConfig = reactive<CameraConfig>({
address: cameraConfig.value.address,
port: cameraConfig.value.port,
}); });
//
const isValidCameraConfig = computed(() => {
return tempCameraConfig.address && tempCameraConfig.port &&
tempCameraConfig.port >= 1 && tempCameraConfig.port <= 65535 &&
/^(\d{1,3}\.){3}\d{1,3}$/.test(tempCameraConfig.address);
});
//
const hasChangesCamera = computed(() => {
return (
tempCameraConfig.address !== cameraConfig.value.address ||
tempCameraConfig.port !== cameraConfig.value.port
);
});
const isDefaultCamera = computed(() => {
return (
defaultCameraConfig.address === tempCameraConfig.address &&
defaultCameraConfig.port === tempCameraConfig.port
);
});
//
const resetCameraConfig = () => {
tempCameraConfig.address = defaultCameraConfig.address;
tempCameraConfig.port = defaultCameraConfig.port;
};
//
watch(
cameraConfig,
(newConfig) => {
tempCameraConfig.address = newConfig.address;
tempCameraConfig.port = newConfig.port;
},
{ deep: true },
);
const currentVideoSource = ref(""); const currentVideoSource = ref("");
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]); const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
@ -605,8 +663,11 @@ const loadCameraConfig = async () => {
const config = await videoClient.getCameraConfig(); const config = await videoClient.getCameraConfig();
if (config && config.address && config.port) { if (config && config.address && config.port) {
cameraConfig.value.address = config.address; //
cameraConfig.value.port = config.port; cameraConfig.value = {
address: config.address,
port: config.port,
};
addLog("success", `摄像头配置加载成功: ${config.address}:${config.port}`); addLog("success", `摄像头配置加载成功: ${config.address}:${config.port}`);
} else { } else {
addLog("warning", "未找到保存的摄像头配置,使用默认值"); addLog("warning", "未找到保存的摄像头配置,使用默认值");
@ -619,20 +680,30 @@ const loadCameraConfig = async () => {
// //
const confirmCameraConfig = async () => { const confirmCameraConfig = async () => {
if (!cameraConfig.value.address || !cameraConfig.value.port) { if (!isValidCameraConfig.value) return;
addLog("error", "请填写完整的摄像头IP地址和端口号");
return;
}
configuring.value = true; configuring.value = true;
try { try {
addLog( addLog(
"info", "info",
`正在配置摄像头: ${cameraConfig.value.address}:${cameraConfig.value.port}`, `正在配置摄像头: ${tempCameraConfig.address}:${tempCameraConfig.port}`,
); );
//
await new Promise((resolve) => setTimeout(resolve, 500));
//
cameraConfig.value = {
address: tempCameraConfig.address,
port: tempCameraConfig.port,
};
// 使API
const result = await videoClient.configureCamera( const result = await videoClient.configureCamera(
CameraConfigRequest.fromJS(cameraConfig.value), CameraConfigRequest.fromJS({
address: tempCameraConfig.address,
port: tempCameraConfig.port,
}),
); );
if (result) { if (result) {
@ -650,35 +721,6 @@ const confirmCameraConfig = async () => {
} }
}; };
//
const testCameraConnection = async () => {
if (!cameraConfig.value.address || !cameraConfig.value.port) {
addLog("error", "请先配置摄像头IP地址和端口号");
return;
}
testingCamera.value = true;
try {
addLog(
"info",
`正在测试摄像头连接: ${cameraConfig.value.address}:${cameraConfig.value.port}`,
);
const result = await videoClient.testCameraConnection();
if (result && result.success) {
addLog("success", "摄像头连接测试成功");
} else {
addLog("error", `摄像头连接测试失败: ${result?.message || "未知错误"}`);
}
} catch (error) {
addLog("error", `摄像头连接测试失败: ${error}`);
console.error("摄像头连接测试失败:", error);
} finally {
testingCamera.value = false;
}
};
// //
const formatTime = (time: Date) => { const formatTime = (time: Date) => {
return time.toLocaleTimeString(); return time.toLocaleTimeString();
@ -823,6 +865,7 @@ const startStream = async () => {
try { try {
addLog("info", "正在启动视频流..."); addLog("info", "正在启动视频流...");
videoStatus.value = "正在连接视频流..."; videoStatus.value = "正在连接视频流...";
videoClient.setEnabled(true);
// //
await refreshStatus(); await refreshStatus();
@ -846,6 +889,7 @@ const startStream = async () => {
const stopStream = () => { const stopStream = () => {
try { try {
addLog("info", "正在停止视频流..."); addLog("info", "正在停止视频流...");
videoClient.setEnabled(false);
// //
currentVideoSource.value = ""; currentVideoSource.value = "";