feat: 添加Hdmi视频串流后端

This commit is contained in:
SikongJueluo 2025-08-04 13:26:20 +08:00
parent 51b39cee07
commit 35647d21bb
No known key found for this signature in database
5 changed files with 467 additions and 1 deletions

View File

@ -145,6 +145,8 @@ try
// 添加 HTTP 视频流服务 // 添加 HTTP 视频流服务
builder.Services.AddSingleton<HttpVideoStreamService>(); builder.Services.AddSingleton<HttpVideoStreamService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpVideoStreamService>()); builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpVideoStreamService>());
builder.Services.AddSingleton<HttpHdmiVideoStreamService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpHdmiVideoStreamService>());
// Application Settings // Application Settings
var app = builder.Build(); var app = builder.Build();

View File

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

View File

@ -32,14 +32,16 @@ class HdmiIn
/// </summary> /// </summary>
/// <param name="address">HDMI输入设备IP地址</param> /// <param name="address">HDMI输入设备IP地址</param>
/// <param name="port">HDMI输入设备端口</param> /// <param name="port">HDMI输入设备端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="timeout">超时时间(毫秒)</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) if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout)); throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address; this.address = address;
this.port = port; this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port); this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.taskID = taskID;
this.timeout = timeout; this.timeout = timeout;
} }

View File

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

View File

@ -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 { export class JtagClient {
protected instance: AxiosInstance; protected instance: AxiosInstance;
protected baseUrl: string; protected baseUrl: string;
@ -8019,6 +8136,54 @@ export interface ICreateExamRequest {
isVisibleToUsers: boolean; 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 { export enum CaptureStatus {
None = 0, None = 0,