436 lines
15 KiB
C#
436 lines
15 KiB
C#
using System.Net;
|
||
using System.Collections.Concurrent;
|
||
using Peripherals.HdmiInClient;
|
||
using Peripherals.JpegClient;
|
||
|
||
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 HdmiVideoStreamClient
|
||
{
|
||
public required HdmiIn HdmiInClient { get; set; }
|
||
|
||
public required Jpeg JpegClient { get; set; }
|
||
|
||
public required CancellationTokenSource CTS { get; set; }
|
||
|
||
public required int Offset { get; set; }
|
||
|
||
public int Width { get; set; }
|
||
public int Height { 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, HdmiVideoStreamClient> _clientDict = new();
|
||
|
||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||
{
|
||
_httpListener = new HttpListener();
|
||
_httpListener.Prefixes.Add($"http://{Global.LocalHost}:{_serverPort}/");
|
||
_httpListener.Start();
|
||
logger.Info($"HDMI Video Stream Service started on port {_serverPort}");
|
||
|
||
await base.StartAsync(cancellationToken);
|
||
}
|
||
|
||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||
{
|
||
while (!stoppingToken.IsCancellationRequested)
|
||
{
|
||
if (_httpListener == null) continue;
|
||
try
|
||
{
|
||
logger.Debug("Waiting for HTTP request...");
|
||
var contextTask = _httpListener.GetContextAsync();
|
||
var completedTask = await Task.WhenAny(contextTask, Task.Delay(-1, stoppingToken));
|
||
if (completedTask == contextTask)
|
||
{
|
||
var context = contextTask.Result;
|
||
logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}");
|
||
if (context != null)
|
||
_ = HandleRequestAsync(context, stoppingToken);
|
||
}
|
||
else
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, "Error in GetContextAsync");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||
{
|
||
logger.Info("Stopping HDMI Video Stream Service...");
|
||
|
||
// 禁用所有活跃的HDMI传输
|
||
var disableTasks = new List<Task>();
|
||
foreach (var hdmiKey in _clientDict.Keys)
|
||
{
|
||
disableTasks.Add(DisableHdmiTransmissionAsync(hdmiKey));
|
||
}
|
||
|
||
// 等待所有禁用操作完成
|
||
await Task.WhenAll(disableTasks);
|
||
|
||
// 清空字典
|
||
_clientDict.Clear();
|
||
|
||
await base.StopAsync(cancellationToken);
|
||
}
|
||
|
||
public async Task DisableHdmiTransmissionAsync(string key)
|
||
{
|
||
try
|
||
{
|
||
var client = _clientDict[key];
|
||
client.CTS.Cancel();
|
||
|
||
var disableResult = await client.JpegClient.SetEnable(false);
|
||
if (disableResult)
|
||
{
|
||
logger.Info("Successfully disabled HDMI transmission");
|
||
}
|
||
else
|
||
{
|
||
logger.Error($"Failed to disable HDMI transmission");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, "Exception occurred while disabling HDMI transmission");
|
||
}
|
||
}
|
||
|
||
private async Task<HdmiVideoStreamClient?> GetOrCreateClientAsync(string boardId)
|
||
{
|
||
if (_clientDict.TryGetValue(boardId, out var client))
|
||
{
|
||
client.Width = client.JpegClient.Width;
|
||
client.Height = client.JpegClient.Height;
|
||
return client;
|
||
}
|
||
|
||
var userManager = new Database.UserManager();
|
||
|
||
var boardRet = userManager.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;
|
||
|
||
client = new HdmiVideoStreamClient()
|
||
{
|
||
HdmiInClient = new HdmiIn(board.IpAddr, board.Port, 1),
|
||
JpegClient = new Jpeg(board.IpAddr, board.Port, 1),
|
||
CTS = new CancellationTokenSource(),
|
||
Offset = 0
|
||
};
|
||
|
||
// 启用HDMI传输
|
||
try
|
||
{
|
||
// var hdmiEnableRet = await client.JpegClient.EnableTrans(true);
|
||
// if (!hdmiEnableRet.IsSuccessful)
|
||
// {
|
||
// logger.Error($"Failed to enable HDMI transmission for board {boardId}: {hdmiEnableRet.Error}");
|
||
// return null;
|
||
// }
|
||
// logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
|
||
|
||
var jpegEnableRet = await client.JpegClient.Init(true);
|
||
if (!jpegEnableRet.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to enable JPEG transmission for board {boardId}: {jpegEnableRet.Error}");
|
||
return null;
|
||
}
|
||
logger.Info($"Successfully enabled JPEG transmission for board {boardId}");
|
||
|
||
client.Width = client.JpegClient.Width;
|
||
client.Height = client.JpegClient.Height;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, $"Exception occurred while enabling HDMI transmission for board {boardId}");
|
||
return null;
|
||
}
|
||
|
||
_clientDict[boardId] = client;
|
||
return client;
|
||
}
|
||
|
||
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 client = await GetOrCreateClientAsync(boardId);
|
||
if (client == null)
|
||
{
|
||
await SendErrorAsync(context.Response, "Invalid boardId or board not available");
|
||
return;
|
||
}
|
||
|
||
var hdmiInToken = _clientDict[boardId].CTS.Token;
|
||
|
||
if (path == "/snapshot")
|
||
{
|
||
await HandleSnapshotRequestAsync(context.Response, client, hdmiInToken);
|
||
}
|
||
else if (path == "/mjpeg")
|
||
{
|
||
await HandleMjpegStreamAsync(context.Response, client, hdmiInToken);
|
||
}
|
||
else if (path == "/video")
|
||
{
|
||
await SendVideoHtmlPageAsync(context.Response, boardId);
|
||
}
|
||
else
|
||
{
|
||
await SendIndexHtmlPageAsync(context.Response, boardId);
|
||
}
|
||
}
|
||
|
||
private async Task HandleSnapshotRequestAsync(
|
||
HttpListenerResponse response, HdmiVideoStreamClient client, CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
logger.Debug("处理HDMI快照请求");
|
||
|
||
// 从HDMI读取RGB565数据
|
||
var frameResult = await client.JpegClient.GetMultiFrames((uint)client.Offset);
|
||
if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0)
|
||
{
|
||
logger.Error("HDMI快照获取失败");
|
||
response.StatusCode = 500;
|
||
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get HDMI snapshot");
|
||
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
||
response.Close();
|
||
return;
|
||
}
|
||
|
||
var jpegData = frameResult.Value[0];
|
||
var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height);
|
||
if (!jpegImage.IsSuccessful)
|
||
{
|
||
logger.Error("JPEG数据补全失败");
|
||
response.StatusCode = 500;
|
||
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to complete JPEG data");
|
||
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
||
response.Close();
|
||
return;
|
||
}
|
||
|
||
// 设置响应头(参考Camera版本)
|
||
response.ContentType = "image/jpeg";
|
||
response.ContentLength64 = jpegImage.Value.Length;
|
||
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||
|
||
await response.OutputStream.WriteAsync(jpegImage.Value, 0, jpegImage.Value.Length, cancellationToken);
|
||
await response.OutputStream.FlushAsync(cancellationToken);
|
||
|
||
logger.Debug("已发送HDMI快照图像,大小:{Size} 字节", jpegImage.Value.Length);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, "处理HDMI快照请求时出错");
|
||
response.StatusCode = 500;
|
||
}
|
||
finally
|
||
{
|
||
response.Close();
|
||
}
|
||
}
|
||
|
||
private async Task HandleMjpegStreamAsync(
|
||
HttpListenerResponse response, HdmiVideoStreamClient client, CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
// 设置MJPEG流的响应头(参考Camera版本)
|
||
response.ContentType = "multipart/x-mixed-replace; boundary=--boundary";
|
||
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||
response.Headers.Add("Pragma", "no-cache");
|
||
response.Headers.Add("Expires", "0");
|
||
|
||
logger.Debug("开始HDMI MJPEG流传输");
|
||
|
||
int frameCounter = 0;
|
||
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
var frameStartTime = DateTime.UtcNow;
|
||
|
||
var frameResult =
|
||
await client.JpegClient.GetMultiFrames((uint)client.Offset);
|
||
if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0)
|
||
{
|
||
logger.Error("获取HDMI帧失败");
|
||
await Task.Delay(100, cancellationToken);
|
||
continue;
|
||
}
|
||
|
||
foreach (var framebytes in frameResult.Value)
|
||
{
|
||
var jpegImage = Common.Image.CompleteJpegData(framebytes, client.Width, client.Height);
|
||
if (!jpegImage.IsSuccessful)
|
||
{
|
||
logger.Error("JPEG数据不完整");
|
||
await Task.Delay(100, cancellationToken);
|
||
continue;
|
||
}
|
||
|
||
var frameRet = Common.Image.CreateMjpegFrameFromJpeg(jpegImage.Value);
|
||
if (!frameRet.IsSuccessful)
|
||
{
|
||
logger.Error("创建MJPEG帧失败");
|
||
await Task.Delay(100, cancellationToken);
|
||
continue;
|
||
}
|
||
var frame = frameRet.Value;
|
||
|
||
await response.OutputStream.WriteAsync(frame.header, 0, frame.header.Length, cancellationToken);
|
||
await response.OutputStream.WriteAsync(frame.data, 0, frame.data.Length, cancellationToken);
|
||
await response.OutputStream.WriteAsync(frame.footer, 0, frame.footer.Length, cancellationToken);
|
||
await response.OutputStream.FlushAsync(cancellationToken);
|
||
|
||
frameCounter++;
|
||
|
||
var totalTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds;
|
||
|
||
// 性能统计日志(每30帧记录一次)
|
||
if (frameCounter % 30 == 0)
|
||
{
|
||
logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节",
|
||
frameCounter, totalTime, frame.data.Length);
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, "HDMI MJPEG流处理异常");
|
||
}
|
||
finally
|
||
{
|
||
try
|
||
{
|
||
// 停止传输时禁用HDMI传输
|
||
await client.HdmiInClient.EnableTrans(false);
|
||
logger.Info("已禁用HDMI传输");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, "禁用HDMI传输时出错");
|
||
}
|
||
|
||
try
|
||
{
|
||
response.Close();
|
||
}
|
||
catch
|
||
{
|
||
// 忽略关闭时的错误
|
||
}
|
||
logger.Debug("HDMI MJPEG流连接已关闭");
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取所有可用的HDMI视频流终端点
|
||
/// </summary>
|
||
/// <returns>返回所有可用的HDMI视频流终端点列表</returns>
|
||
public List<HdmiVideoStreamEndpoint>? GetAllVideoEndpoints()
|
||
{
|
||
var userManager = new Database.UserManager();
|
||
|
||
var boards = userManager.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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定板卡ID的HDMI视频流终端点
|
||
/// </summary>
|
||
/// <param name="boardId">板卡ID</param>
|
||
/// <returns>返回指定板卡的HDMI视频流终端点</returns>
|
||
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}"
|
||
};
|
||
}
|
||
}
|