Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab
This commit is contained in:
commit
04b136117d
|
@ -145,6 +145,8 @@ try
|
|||
// 添加 HTTP 视频流服务
|
||||
builder.Services.AddSingleton<HttpVideoStreamService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpVideoStreamService>());
|
||||
builder.Services.AddSingleton<HttpHdmiVideoStreamService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpHdmiVideoStreamService>());
|
||||
|
||||
// Application Settings
|
||||
var app = builder.Build();
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Security.Claims;
|
||||
using server.Services;
|
||||
using Database;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[EnableCors("Users")]
|
||||
public class HdmiVideoStreamController : ControllerBase
|
||||
{
|
||||
private readonly HttpHdmiVideoStreamService _videoStreamService;
|
||||
private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
public HdmiVideoStreamController(HttpHdmiVideoStreamService videoStreamService)
|
||||
{
|
||||
_videoStreamService = videoStreamService;
|
||||
}
|
||||
|
||||
// 管理员获取所有板子的 endpoints
|
||||
[HttpGet("AllEndpoints")]
|
||||
[Authorize("Admin")]
|
||||
public ActionResult<List<HdmiVideoStreamEndpoint>> GetAllEndpoints()
|
||||
{
|
||||
var endpoints = _videoStreamService.GetAllVideoEndpoints();
|
||||
if (endpoints == null)
|
||||
return NotFound("No boards found.");
|
||||
return Ok(endpoints);
|
||||
}
|
||||
|
||||
// 用户获取自己板子的 endpoint
|
||||
[HttpGet("MyEndpoint")]
|
||||
[Authorize]
|
||||
public ActionResult<HdmiVideoStreamEndpoint> GetMyEndpoint()
|
||||
{
|
||||
var userName = User.FindFirstValue(ClaimTypes.Name);
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
return Unauthorized("User name not found in claims.");
|
||||
|
||||
var db = new AppDataConnection();
|
||||
if (db == null)
|
||||
return NotFound("Database connection failed.");
|
||||
|
||||
var userRet = db.GetUserByName(userName);
|
||||
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
|
||||
return NotFound("User not found.");
|
||||
|
||||
var user = userRet.Value.Value;
|
||||
var boardId = user.BoardID;
|
||||
if (boardId == Guid.Empty)
|
||||
return NotFound("No board bound to this user.");
|
||||
|
||||
var boardRet = db.GetBoardByID(boardId);
|
||||
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
|
||||
return NotFound("Board not found.");
|
||||
|
||||
var endpoint = _videoStreamService.GetVideoEndpoint(boardId.ToString());
|
||||
return Ok(endpoint);
|
||||
}
|
||||
}
|
|
@ -32,14 +32,16 @@ class HdmiIn
|
|||
/// </summary>
|
||||
/// <param name="address">HDMI输入设备IP地址</param>
|
||||
/// <param name="port">HDMI输入设备端口</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
public HdmiIn(string address, int port, int timeout = 500)
|
||||
public HdmiIn(string address, int port, int taskID, int timeout = 500)
|
||||
{
|
||||
if (timeout < 0)
|
||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.taskID = taskID;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
using System.Net;
|
||||
using System.Collections.Concurrent;
|
||||
using Peripherals.HdmiInClient;
|
||||
|
||||
namespace server.Services;
|
||||
|
||||
public class HdmiVideoStreamEndpoint
|
||||
{
|
||||
public string BoardId { get; set; } = "";
|
||||
public string MjpegUrl { get; set; } = "";
|
||||
public string VideoUrl { get; set; } = "";
|
||||
public string SnapshotUrl { get; set; } = "";
|
||||
}
|
||||
|
||||
public class HttpHdmiVideoStreamService : BackgroundService
|
||||
{
|
||||
private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
private HttpListener? _httpListener;
|
||||
private readonly int _serverPort = 4322;
|
||||
private readonly ConcurrentDictionary<string, HdmiIn> _hdmiInDict = new();
|
||||
private bool _isEnabled = true;
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_httpListener = new HttpListener();
|
||||
_httpListener.Prefixes.Add($"http://*:{_serverPort}/");
|
||||
_httpListener.Start();
|
||||
logger.Info($"HDMI Video Stream Service started on port {_serverPort}");
|
||||
|
||||
await base.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
HttpListenerContext? context = null;
|
||||
try
|
||||
{
|
||||
context = await _httpListener.GetContextAsync();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Listener closed, exit loop
|
||||
break;
|
||||
}
|
||||
catch (HttpListenerException)
|
||||
{
|
||||
// Listener closed, exit loop
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Error in GetContextAsync");
|
||||
break;
|
||||
}
|
||||
if (context != null)
|
||||
_ = HandleRequestAsync(context, stoppingToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_httpListener?.Close();
|
||||
logger.Info("HDMI Video Stream Service stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
logger.Info("Stopping HDMI Video Stream Service...");
|
||||
_isEnabled = false;
|
||||
_httpListener?.Close(); // 立即关闭监听器,唤醒阻塞
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// 获取/创建 HdmiIn 实例
|
||||
private HdmiIn? GetOrCreateHdmiIn(string boardId)
|
||||
{
|
||||
if (_hdmiInDict.TryGetValue(boardId, out var hdmiIn))
|
||||
return hdmiIn;
|
||||
|
||||
var db = new Database.AppDataConnection();
|
||||
if (db == null)
|
||||
{
|
||||
logger.Error("Failed to create HdmiIn instance");
|
||||
return null;
|
||||
}
|
||||
|
||||
var boardRet = db.GetBoardByID(Guid.Parse(boardId));
|
||||
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
|
||||
{
|
||||
logger.Error($"Failed to get board with ID {boardId}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var board = boardRet.Value.Value;
|
||||
|
||||
hdmiIn = new HdmiIn(board.IpAddr, board.Port, 0); // taskID 可根据实际需求调整
|
||||
_hdmiInDict[boardId] = hdmiIn;
|
||||
return hdmiIn;
|
||||
}
|
||||
|
||||
private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = context.Request.Url?.AbsolutePath ?? "/";
|
||||
var boardId = context.Request.QueryString["boardId"];
|
||||
if (string.IsNullOrEmpty(boardId))
|
||||
{
|
||||
await SendErrorAsync(context.Response, "Missing boardId");
|
||||
return;
|
||||
}
|
||||
|
||||
var hdmiIn = GetOrCreateHdmiIn(boardId);
|
||||
if (hdmiIn == null)
|
||||
{
|
||||
await SendErrorAsync(context.Response, "Invalid boardId or board not available");
|
||||
return;
|
||||
}
|
||||
|
||||
if (path == "/snapshot")
|
||||
{
|
||||
await HandleSnapshotRequestAsync(context.Response, hdmiIn, cancellationToken);
|
||||
}
|
||||
else if (path == "/mjpeg")
|
||||
{
|
||||
await HandleMjpegStreamAsync(context.Response, hdmiIn, cancellationToken);
|
||||
}
|
||||
else if (path == "/video")
|
||||
{
|
||||
await SendVideoHtmlPageAsync(context.Response, boardId);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendIndexHtmlPageAsync(context.Response, boardId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken)
|
||||
{
|
||||
var frameResult = await hdmiIn.ReadFrame();
|
||||
if (frameResult.IsSuccessful)
|
||||
{
|
||||
response.ContentType = "image/jpeg";
|
||||
await response.OutputStream.WriteAsync(frameResult.Value, 0, frameResult.Value.Length, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
response.StatusCode = 500;
|
||||
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes("Failed to get snapshot"));
|
||||
}
|
||||
response.Close();
|
||||
}
|
||||
|
||||
private async Task HandleMjpegStreamAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken)
|
||||
{
|
||||
response.ContentType = "multipart/x-mixed-replace; boundary=--frame";
|
||||
while (!cancellationToken.IsCancellationRequested && _isEnabled)
|
||||
{
|
||||
var frameResult = await hdmiIn.ReadFrame();
|
||||
if (frameResult.IsSuccessful)
|
||||
{
|
||||
var header = $"--frame\r\nContent-Type: image/jpeg\r\nContent-Length: {frameResult.Value.Length}\r\n\r\n";
|
||||
await response.OutputStream.WriteAsync(System.Text.Encoding.ASCII.GetBytes(header));
|
||||
await response.OutputStream.WriteAsync(frameResult.Value, 0, frameResult.Value.Length, cancellationToken);
|
||||
await response.OutputStream.WriteAsync(System.Text.Encoding.ASCII.GetBytes("\r\n"));
|
||||
}
|
||||
await Task.Delay(33, cancellationToken); // ~30fps
|
||||
}
|
||||
response.Close();
|
||||
}
|
||||
|
||||
private async Task SendVideoHtmlPageAsync(HttpListenerResponse response, string boardId)
|
||||
{
|
||||
string html = $@"<html><body>
|
||||
<h1>HDMI Video Stream for Board {boardId}</h1>
|
||||
<img src='/mjpeg?boardId={boardId}' />
|
||||
</body></html>";
|
||||
response.ContentType = "text/html";
|
||||
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(html));
|
||||
response.Close();
|
||||
}
|
||||
|
||||
private async Task SendIndexHtmlPageAsync(HttpListenerResponse response, string boardId)
|
||||
{
|
||||
string html = $@"<html><body>
|
||||
<h1>Welcome to HDMI Video Stream Service</h1>
|
||||
<a href='/video?boardId={boardId}'>View Video Stream for Board {boardId}</a>
|
||||
</body></html>";
|
||||
response.ContentType = "text/html";
|
||||
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(html));
|
||||
response.Close();
|
||||
}
|
||||
|
||||
private async Task SendErrorAsync(HttpListenerResponse response, string message)
|
||||
{
|
||||
response.StatusCode = 400;
|
||||
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
|
||||
response.Close();
|
||||
}
|
||||
|
||||
public List<HdmiVideoStreamEndpoint>? GetAllVideoEndpoints()
|
||||
{
|
||||
var db = new Database.AppDataConnection();
|
||||
var boards = db?.GetAllBoard();
|
||||
if (boards == null)
|
||||
return null;
|
||||
|
||||
var endpoints = new List<HdmiVideoStreamEndpoint>();
|
||||
foreach (var board in boards)
|
||||
{
|
||||
endpoints.Add(new HdmiVideoStreamEndpoint
|
||||
{
|
||||
BoardId = board.ID.ToString(),
|
||||
MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={board.ID}",
|
||||
VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={board.ID}",
|
||||
SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={board.ID}"
|
||||
});
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
public HdmiVideoStreamEndpoint GetVideoEndpoint(string boardId)
|
||||
{
|
||||
return new HdmiVideoStreamEndpoint
|
||||
{
|
||||
BoardId = boardId,
|
||||
MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={boardId}",
|
||||
VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={boardId}",
|
||||
SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={boardId}"
|
||||
};
|
||||
}
|
||||
}
|
165
src/APIClient.ts
165
src/APIClient.ts
|
@ -2944,6 +2944,123 @@ export class ExamClient {
|
|||
}
|
||||
}
|
||||
|
||||
export class HdmiVideoStreamClient {
|
||||
protected instance: AxiosInstance;
|
||||
protected baseUrl: string;
|
||||
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||
|
||||
constructor(baseUrl?: string, instance?: AxiosInstance) {
|
||||
|
||||
this.instance = instance || axios.create();
|
||||
|
||||
this.baseUrl = baseUrl ?? "http://127.0.0.1:5000";
|
||||
|
||||
}
|
||||
|
||||
getAllEndpoints( cancelToken?: CancelToken): Promise<HdmiVideoStreamEndpoint[]> {
|
||||
let url_ = this.baseUrl + "/api/HdmiVideoStream/AllEndpoints";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: AxiosRequestConfig = {
|
||||
method: "GET",
|
||||
url: url_,
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
},
|
||||
cancelToken
|
||||
};
|
||||
|
||||
return this.instance.request(options_).catch((_error: any) => {
|
||||
if (isAxiosError(_error) && _error.response) {
|
||||
return _error.response;
|
||||
} else {
|
||||
throw _error;
|
||||
}
|
||||
}).then((_response: AxiosResponse) => {
|
||||
return this.processGetAllEndpoints(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processGetAllEndpoints(response: AxiosResponse): Promise<HdmiVideoStreamEndpoint[]> {
|
||||
const status = response.status;
|
||||
let _headers: any = {};
|
||||
if (response.headers && typeof response.headers === "object") {
|
||||
for (const k in response.headers) {
|
||||
if (response.headers.hasOwnProperty(k)) {
|
||||
_headers[k] = response.headers[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (status === 200) {
|
||||
const _responseText = response.data;
|
||||
let result200: any = null;
|
||||
let resultData200 = _responseText;
|
||||
if (Array.isArray(resultData200)) {
|
||||
result200 = [] as any;
|
||||
for (let item of resultData200)
|
||||
result200!.push(HdmiVideoStreamEndpoint.fromJS(item));
|
||||
}
|
||||
else {
|
||||
result200 = <any>null;
|
||||
}
|
||||
return Promise.resolve<HdmiVideoStreamEndpoint[]>(result200);
|
||||
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
const _responseText = response.data;
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
}
|
||||
return Promise.resolve<HdmiVideoStreamEndpoint[]>(null as any);
|
||||
}
|
||||
|
||||
getMyEndpoint( cancelToken?: CancelToken): Promise<HdmiVideoStreamEndpoint> {
|
||||
let url_ = this.baseUrl + "/api/HdmiVideoStream/MyEndpoint";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: AxiosRequestConfig = {
|
||||
method: "GET",
|
||||
url: url_,
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
},
|
||||
cancelToken
|
||||
};
|
||||
|
||||
return this.instance.request(options_).catch((_error: any) => {
|
||||
if (isAxiosError(_error) && _error.response) {
|
||||
return _error.response;
|
||||
} else {
|
||||
throw _error;
|
||||
}
|
||||
}).then((_response: AxiosResponse) => {
|
||||
return this.processGetMyEndpoint(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processGetMyEndpoint(response: AxiosResponse): Promise<HdmiVideoStreamEndpoint> {
|
||||
const status = response.status;
|
||||
let _headers: any = {};
|
||||
if (response.headers && typeof response.headers === "object") {
|
||||
for (const k in response.headers) {
|
||||
if (response.headers.hasOwnProperty(k)) {
|
||||
_headers[k] = response.headers[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (status === 200) {
|
||||
const _responseText = response.data;
|
||||
let result200: any = null;
|
||||
let resultData200 = _responseText;
|
||||
result200 = HdmiVideoStreamEndpoint.fromJS(resultData200);
|
||||
return Promise.resolve<HdmiVideoStreamEndpoint>(result200);
|
||||
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
const _responseText = response.data;
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
}
|
||||
return Promise.resolve<HdmiVideoStreamEndpoint>(null as any);
|
||||
}
|
||||
}
|
||||
|
||||
export class JtagClient {
|
||||
protected instance: AxiosInstance;
|
||||
protected baseUrl: string;
|
||||
|
@ -8019,6 +8136,54 @@ export interface ICreateExamRequest {
|
|||
isVisibleToUsers: boolean;
|
||||
}
|
||||
|
||||
export class HdmiVideoStreamEndpoint implements IHdmiVideoStreamEndpoint {
|
||||
boardId!: string;
|
||||
mjpegUrl!: string;
|
||||
videoUrl!: string;
|
||||
snapshotUrl!: string;
|
||||
|
||||
constructor(data?: IHdmiVideoStreamEndpoint) {
|
||||
if (data) {
|
||||
for (var property in data) {
|
||||
if (data.hasOwnProperty(property))
|
||||
(<any>this)[property] = (<any>data)[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(_data?: any) {
|
||||
if (_data) {
|
||||
this.boardId = _data["boardId"];
|
||||
this.mjpegUrl = _data["mjpegUrl"];
|
||||
this.videoUrl = _data["videoUrl"];
|
||||
this.snapshotUrl = _data["snapshotUrl"];
|
||||
}
|
||||
}
|
||||
|
||||
static fromJS(data: any): HdmiVideoStreamEndpoint {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
let result = new HdmiVideoStreamEndpoint();
|
||||
result.init(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
toJSON(data?: any) {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
data["boardId"] = this.boardId;
|
||||
data["mjpegUrl"] = this.mjpegUrl;
|
||||
data["videoUrl"] = this.videoUrl;
|
||||
data["snapshotUrl"] = this.snapshotUrl;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IHdmiVideoStreamEndpoint {
|
||||
boardId: string;
|
||||
mjpegUrl: string;
|
||||
videoUrl: string;
|
||||
snapshotUrl: string;
|
||||
}
|
||||
|
||||
/** 逻辑分析仪运行状态枚举 */
|
||||
export enum CaptureStatus {
|
||||
None = 0,
|
||||
|
|
Loading…
Reference in New Issue