499 lines
18 KiB
C#
499 lines
18 KiB
C#
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 readonly ConcurrentDictionary<string, CancellationTokenSource> _hdmiInCtsDict = new();
|
||
|
||
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
|
||
{
|
||
logger.Debug("Waiting for HTTP request...");
|
||
context = await _httpListener.GetContextAsync();
|
||
logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}");
|
||
}
|
||
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...");
|
||
|
||
// 禁用所有活跃的HDMI传输
|
||
var disableTasks = new List<Task>();
|
||
foreach (var hdmiKey in _hdmiInDict.Keys)
|
||
{
|
||
disableTasks.Add(DisableHdmiTransmissionAsync(hdmiKey));
|
||
}
|
||
|
||
// 等待所有禁用操作完成
|
||
await Task.WhenAll(disableTasks);
|
||
|
||
// 清空字典
|
||
_hdmiInDict.Clear();
|
||
_hdmiInCtsDict.Clear();
|
||
|
||
_httpListener?.Close(); // 立即关闭监听器,唤醒阻塞
|
||
await base.StopAsync(cancellationToken);
|
||
}
|
||
|
||
public async Task DisableHdmiTransmissionAsync(string key)
|
||
{
|
||
try
|
||
{
|
||
var cts = _hdmiInCtsDict[key];
|
||
cts.Cancel();
|
||
|
||
var hdmiIn = _hdmiInDict[key];
|
||
var disableResult = await hdmiIn.EnableTrans(false);
|
||
if (disableResult.IsSuccessful)
|
||
{
|
||
logger.Info("Successfully disabled HDMI transmission");
|
||
}
|
||
else
|
||
{
|
||
logger.Error($"Failed to disable HDMI transmission: {disableResult.Error}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, "Exception occurred while disabling HDMI transmission");
|
||
}
|
||
}
|
||
|
||
// 获取/创建 HdmiIn 实例
|
||
private async Task<HdmiIn?> GetOrCreateHdmiInAsync(string boardId)
|
||
{
|
||
if (_hdmiInDict.TryGetValue(boardId, out var hdmiIn))
|
||
{
|
||
try
|
||
{
|
||
var enableResult = await hdmiIn.EnableTrans(true);
|
||
if (!enableResult.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to enable HDMI transmission for board {boardId}: {enableResult.Error}");
|
||
return null;
|
||
}
|
||
logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, $"Exception occurred while enabling HDMI transmission for board {boardId}");
|
||
return null;
|
||
}
|
||
|
||
_hdmiInDict[boardId] = hdmiIn;
|
||
_hdmiInCtsDict[boardId] = new CancellationTokenSource();
|
||
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 可根据实际需求调整
|
||
|
||
// 启用HDMI传输
|
||
try
|
||
{
|
||
var enableResult = await hdmiIn.EnableTrans(true);
|
||
if (!enableResult.IsSuccessful)
|
||
{
|
||
logger.Error($"Failed to enable HDMI transmission for board {boardId}: {enableResult.Error}");
|
||
return null;
|
||
}
|
||
logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, $"Exception occurred while enabling HDMI transmission for board {boardId}");
|
||
return null;
|
||
}
|
||
|
||
_hdmiInDict[boardId] = hdmiIn;
|
||
_hdmiInCtsDict[boardId] = new CancellationTokenSource();
|
||
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 = await GetOrCreateHdmiInAsync(boardId);
|
||
if (hdmiIn == null)
|
||
{
|
||
await SendErrorAsync(context.Response, "Invalid boardId or board not available");
|
||
return;
|
||
}
|
||
|
||
var hdmiInToken = _hdmiInCtsDict[boardId].Token;
|
||
if (hdmiInToken == null)
|
||
{
|
||
await SendErrorAsync(context.Response, "HDMI input is not available");
|
||
return;
|
||
}
|
||
|
||
if (path == "/snapshot")
|
||
{
|
||
await HandleSnapshotRequestAsync(context.Response, hdmiIn, hdmiInToken);
|
||
}
|
||
else if (path == "/mjpeg")
|
||
{
|
||
await HandleMjpegStreamAsync(context.Response, hdmiIn, hdmiInToken);
|
||
}
|
||
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)
|
||
{
|
||
try
|
||
{
|
||
logger.Debug("处理HDMI快照请求");
|
||
|
||
const int frameWidth = 960; // HDMI输入分辨率
|
||
const int frameHeight = 540;
|
||
|
||
// 从HDMI读取RGB565数据
|
||
var frameResult = await hdmiIn.ReadFrame();
|
||
if (!frameResult.IsSuccessful || frameResult.Value == null)
|
||
{
|
||
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 rgb565Data = frameResult.Value;
|
||
|
||
// 验证数据长度
|
||
var expectedLength = frameWidth * frameHeight * 2;
|
||
if (rgb565Data.Length != expectedLength)
|
||
{
|
||
logger.Warn("HDMI快照数据长度不匹配,期望: {Expected}, 实际: {Actual}",
|
||
expectedLength, rgb565Data.Length);
|
||
}
|
||
|
||
// 将RGB565转换为RGB24
|
||
var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, frameWidth, frameHeight, isLittleEndian: false);
|
||
if (!rgb24Result.IsSuccessful)
|
||
{
|
||
logger.Error("HDMI快照RGB565转RGB24失败: {Error}", rgb24Result.Error);
|
||
response.StatusCode = 500;
|
||
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to process HDMI snapshot");
|
||
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
||
response.Close();
|
||
return;
|
||
}
|
||
|
||
// 将RGB24转换为JPEG
|
||
var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Result.Value, frameWidth, frameHeight, 80);
|
||
if (!jpegResult.IsSuccessful)
|
||
{
|
||
logger.Error("HDMI快照RGB24转JPEG失败: {Error}", jpegResult.Error);
|
||
response.StatusCode = 500;
|
||
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to encode HDMI snapshot");
|
||
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
||
response.Close();
|
||
return;
|
||
}
|
||
|
||
var jpegData = jpegResult.Value;
|
||
|
||
// 设置响应头(参考Camera版本)
|
||
response.ContentType = "image/jpeg";
|
||
response.ContentLength64 = jpegData.Length;
|
||
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||
|
||
await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
|
||
await response.OutputStream.FlushAsync(cancellationToken);
|
||
|
||
logger.Debug("已发送HDMI快照图像,大小:{Size} 字节", jpegData.Length);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, "处理HDMI快照请求时出错");
|
||
response.StatusCode = 500;
|
||
}
|
||
finally
|
||
{
|
||
response.Close();
|
||
}
|
||
}
|
||
|
||
private async Task HandleMjpegStreamAsync(HttpListenerResponse response, HdmiIn hdmiIn, 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;
|
||
const int frameWidth = 960; // HDMI输入分辨率
|
||
const int frameHeight = 540;
|
||
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
try
|
||
{
|
||
var frameStartTime = DateTime.UtcNow;
|
||
|
||
// 从HDMI读取RGB565数据
|
||
var readStartTime = DateTime.UtcNow;
|
||
var frameResult = await hdmiIn.ReadFrame();
|
||
var readEndTime = DateTime.UtcNow;
|
||
var readTime = (readEndTime - readStartTime).TotalMilliseconds;
|
||
|
||
if (!frameResult.IsSuccessful || frameResult.Value == null)
|
||
{
|
||
logger.Warn("HDMI帧读取失败或为空");
|
||
continue;
|
||
}
|
||
|
||
var rgb565Data = frameResult.Value;
|
||
|
||
// 验证数据长度是否正确 (RGB565为每像素2字节)
|
||
var expectedLength = frameWidth * frameHeight * 2;
|
||
if (rgb565Data.Length != expectedLength)
|
||
{
|
||
logger.Warn("HDMI数据长度不匹配,期望: {Expected}, 实际: {Actual}",
|
||
expectedLength, rgb565Data.Length);
|
||
}
|
||
|
||
// 将RGB565转换为RGB24(参考Camera版本的处理)
|
||
var convertStartTime = DateTime.UtcNow;
|
||
var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, frameWidth, frameHeight, isLittleEndian: false);
|
||
var convertEndTime = DateTime.UtcNow;
|
||
var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds;
|
||
|
||
if (!rgb24Result.IsSuccessful)
|
||
{
|
||
logger.Error("HDMI RGB565转RGB24失败: {Error}", rgb24Result.Error);
|
||
continue;
|
||
}
|
||
|
||
// 将RGB24转换为JPEG(参考Camera版本的处理)
|
||
var jpegStartTime = DateTime.UtcNow;
|
||
var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Result.Value, frameWidth, frameHeight, 80);
|
||
var jpegEndTime = DateTime.UtcNow;
|
||
var jpegTime = (jpegEndTime - jpegStartTime).TotalMilliseconds;
|
||
|
||
if (!jpegResult.IsSuccessful)
|
||
{
|
||
logger.Error("HDMI RGB24转JPEG失败: {Error}", jpegResult.Error);
|
||
continue;
|
||
}
|
||
|
||
var jpegData = jpegResult.Value;
|
||
|
||
// 发送MJPEG帧(使用Camera版本的格式)
|
||
var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length);
|
||
var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
|
||
|
||
await response.OutputStream.WriteAsync(mjpegFrameHeader, 0, mjpegFrameHeader.Length, cancellationToken);
|
||
await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
|
||
await response.OutputStream.WriteAsync(mjpegFrameFooter, 0, mjpegFrameFooter.Length, cancellationToken);
|
||
await response.OutputStream.FlushAsync(cancellationToken);
|
||
|
||
frameCounter++;
|
||
|
||
var totalTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds;
|
||
|
||
// 性能统计日志(每30帧记录一次)
|
||
if (frameCounter % 30 == 0)
|
||
{
|
||
logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 读取: {ReadTime:F1}ms, RGB转换: {ConvertTime:F1}ms, JPEG转换: {JpegTime:F1}ms, 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节",
|
||
frameCounter, readTime, convertTime, jpegTime, totalTime, jpegData.Length);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, "处理HDMI帧时发生错误");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.Error(ex, "HDMI MJPEG流处理异常");
|
||
}
|
||
finally
|
||
{
|
||
try
|
||
{
|
||
// 停止传输时禁用HDMI传输
|
||
await hdmiIn.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 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;
|
||
}
|
||
|
||
/// <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}"
|
||
};
|
||
}
|
||
}
|