feat: 使用DDR读取Hdmi视频流
This commit is contained in:
parent
7e53b805ae
commit
3c73aa344a
|
@ -7,8 +7,16 @@ namespace Peripherals.HdmiInClient;
|
||||||
static class HdmiInAddr
|
static class HdmiInAddr
|
||||||
{
|
{
|
||||||
public const UInt32 BASE = 0xA000_0000;
|
public const UInt32 BASE = 0xA000_0000;
|
||||||
public const UInt32 HdmiIn_CTRL = BASE + 0x0; //[0]: rstn, 0 is reset.
|
|
||||||
public const UInt32 HdmiIn_READFIFO = BASE + 0x1;
|
public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
|
||||||
|
|
||||||
|
public const UInt32 START_WR_ADDR0 = BASE + 0x2;
|
||||||
|
public const UInt32 END_WR_ADDR0 = BASE + 0x3;
|
||||||
|
|
||||||
|
public const UInt32 HDMI_NOT_READY = BASE + 0x8;
|
||||||
|
public const UInt32 HDMI_HEIGHT_WIDTH = BASE + 0x9;
|
||||||
|
|
||||||
|
public const UInt32 ADDR_HDMI_WD_START = 0x0400_0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HdmiIn
|
public class HdmiIn
|
||||||
|
@ -21,10 +29,9 @@ public class HdmiIn
|
||||||
readonly string address;
|
readonly string address;
|
||||||
private IPEndPoint ep;
|
private IPEndPoint ep;
|
||||||
|
|
||||||
// 动态分辨率参数
|
public int Width { get; private set; }
|
||||||
private UInt16 _currentWidth = 960;
|
public int Height { get; private set; }
|
||||||
private UInt16 _currentHeight = 540;
|
public int FrameLength => Width * Height * 3 / 4;
|
||||||
private UInt32 _currentFrameLength = 960 * 540 * 2 / 4; // RGB565格式,2字节/像素,按4字节对齐
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化HDMI输入客户端
|
/// 初始化HDMI输入客户端
|
||||||
|
@ -44,9 +51,54 @@ public class HdmiIn
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result<bool>> EnableTrans(bool isEnable)
|
public async ValueTask<Result<bool>> Init(bool enable = true)
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.HdmiIn_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
|
{
|
||||||
|
var ret = await CheckHdmiIsReady();
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to check HDMI ready: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
if (!ret.Value)
|
||||||
|
{
|
||||||
|
logger.Error("HDMI not ready");
|
||||||
|
return new(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = -1, height = -1;
|
||||||
|
{
|
||||||
|
var ret = await GetHdmiResolution();
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
(width, height) = ret.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var ret = await ConnectJpeg2Hdmi(width, height);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to connect JPEG to HDMI: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
if (!ret.Value)
|
||||||
|
{
|
||||||
|
logger.Error("Failed to connect JPEG to HDMI");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enable) return await SetTransEnable(true);
|
||||||
|
else return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<bool>> SetTransEnable(bool isEnable)
|
||||||
|
{
|
||||||
|
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.CAPTURE_RD_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
|
||||||
if (!ret.IsSuccessful)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to write HdmiIn_CTRL to HdmiIn at {this.address}:{this.port}, error: {ret.Error}");
|
logger.Error($"Failed to write HdmiIn_CTRL to HdmiIn at {this.address}:{this.port}, error: {ret.Error}");
|
||||||
|
@ -75,8 +127,8 @@ public class HdmiIn
|
||||||
var result = await UDPClientPool.ReadAddr4BytesAsync(
|
var result = await UDPClientPool.ReadAddr4BytesAsync(
|
||||||
this.ep,
|
this.ep,
|
||||||
this.taskID, // taskID
|
this.taskID, // taskID
|
||||||
HdmiInAddr.HdmiIn_READFIFO,
|
HdmiInAddr.ADDR_HDMI_WD_START,
|
||||||
(int)_currentFrameLength, // 使用当前分辨率的动态大小
|
FrameLength, // 使用当前分辨率的动态大小
|
||||||
BurstType.FixedBurst,
|
BurstType.FixedBurst,
|
||||||
this.timeout);
|
this.timeout);
|
||||||
|
|
||||||
|
@ -99,7 +151,7 @@ public class HdmiIn
|
||||||
return result.Value;
|
return result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<(byte[] header, byte[] data, byte[] footer)?> GetMJpegFrame()
|
public async ValueTask<Optional<(byte[] header, byte[] data, byte[] footer)>> GetMJpegFrame()
|
||||||
{
|
{
|
||||||
// 从HDMI读取RGB24数据
|
// 从HDMI读取RGB24数据
|
||||||
var readStartTime = DateTime.UtcNow;
|
var readStartTime = DateTime.UtcNow;
|
||||||
|
@ -110,13 +162,13 @@ public class HdmiIn
|
||||||
if (!frameResult.IsSuccessful || frameResult.Value == null)
|
if (!frameResult.IsSuccessful || frameResult.Value == null)
|
||||||
{
|
{
|
||||||
logger.Warn("HDMI帧读取失败或为空");
|
logger.Warn("HDMI帧读取失败或为空");
|
||||||
return null;
|
return Optional<(byte[] header, byte[] data, byte[] footer)>.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rgb24Data = frameResult.Value;
|
var rgb24Data = frameResult.Value;
|
||||||
|
|
||||||
// 验证数据长度是否正确 (RGB24为每像素2字节)
|
// 验证数据长度是否正确 (RGB24为每像素2字节)
|
||||||
var expectedLength = _currentWidth * _currentHeight * 2;
|
var expectedLength = Width * Height * 2;
|
||||||
if (rgb24Data.Length != expectedLength)
|
if (rgb24Data.Length != expectedLength)
|
||||||
{
|
{
|
||||||
logger.Warn("HDMI数据长度不匹配,期望: {Expected}, 实际: {Actual}",
|
logger.Warn("HDMI数据长度不匹配,期望: {Expected}, 实际: {Actual}",
|
||||||
|
@ -125,40 +177,105 @@ public class HdmiIn
|
||||||
|
|
||||||
// 将RGB24转换为JPEG(参考Camera版本的处理)
|
// 将RGB24转换为JPEG(参考Camera版本的处理)
|
||||||
var jpegStartTime = DateTime.UtcNow;
|
var jpegStartTime = DateTime.UtcNow;
|
||||||
var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Data, _currentWidth, _currentHeight, 80);
|
var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Data, Width, Height, 80);
|
||||||
var jpegEndTime = DateTime.UtcNow;
|
var jpegEndTime = DateTime.UtcNow;
|
||||||
var jpegTime = (jpegEndTime - jpegStartTime).TotalMilliseconds;
|
var jpegTime = (jpegEndTime - jpegStartTime).TotalMilliseconds;
|
||||||
|
|
||||||
if (!jpegResult.IsSuccessful)
|
if (!jpegResult.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error("HDMI RGB24转JPEG失败: {Error}", jpegResult.Error);
|
logger.Error("HDMI RGB24转JPEG失败: {Error}", jpegResult.Error);
|
||||||
return null;
|
return Optional<(byte[] header, byte[] data, byte[] footer)>.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
var jpegData = jpegResult.Value;
|
var jpegData = jpegResult.Value;
|
||||||
|
|
||||||
// 发送MJPEG帧(使用Camera版本的格式)
|
|
||||||
var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length);
|
var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length);
|
||||||
var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
|
var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
|
||||||
|
|
||||||
return (mjpegFrameHeader, jpegData, mjpegFrameFooter);
|
return (mjpegFrameHeader, jpegData, mjpegFrameFooter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async ValueTask<Result<bool>> CheckHdmiIsReady()
|
||||||
/// 获取当前分辨率
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>当前分辨率(宽度, 高度)</returns>
|
|
||||||
public (int Width, int Height) GetCurrentResolution()
|
|
||||||
{
|
{
|
||||||
return (_currentWidth, _currentHeight);
|
var ret = await UDPClientPool.ReadAddrWithWait(
|
||||||
|
this.ep, this.taskID, HdmiInAddr.HDMI_NOT_READY, 0b00, 0b01, 100, this.timeout);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to check HDMI status: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
return ret.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async ValueTask<Result<(int, int)>> GetHdmiResolution()
|
||||||
/// 获取当前帧长度
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>当前帧长度</returns>
|
|
||||||
public UInt32 GetCurrentFrameLength()
|
|
||||||
{
|
{
|
||||||
return _currentFrameLength;
|
var ret = await UDPClientPool.ReadAddrByte(
|
||||||
|
this.ep, this.taskID, HdmiInAddr.HDMI_HEIGHT_WIDTH, this.timeout);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = ret.Value.Options.Data;
|
||||||
|
if (data == null || data.Length != 4)
|
||||||
|
{
|
||||||
|
logger.Error($"Invalid HDMI resolution data length: {data?.Length ?? 0}");
|
||||||
|
return new(new Exception("Invalid HDMI resolution data length"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var width = data[3] | (data[2] << 8);
|
||||||
|
var height = data[1] | (data[0] << 8);
|
||||||
|
this.Width = width;
|
||||||
|
this.Height = height;
|
||||||
|
|
||||||
|
logger.Info($"HDMI resolution: {width}x{height}");
|
||||||
|
|
||||||
|
return new((width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<bool>> ConnectJpeg2Hdmi(int width, int height)
|
||||||
|
{
|
||||||
|
if (width <= 0 || height <= 0)
|
||||||
|
{
|
||||||
|
logger.Error($"Invalid HDMI resolution: {width}x{height}");
|
||||||
|
return new(new ArgumentException("Invalid HDMI resolution"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var frameSize = (UInt32)(width * height);
|
||||||
|
|
||||||
|
{
|
||||||
|
var ret = await UDPClientPool.WriteAddr(
|
||||||
|
this.ep, this.taskID, HdmiInAddr.START_WR_ADDR0, HdmiInAddr.ADDR_HDMI_WD_START, this.timeout);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to set HDMI output start address: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
if (!ret.Value)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to set HDMI output start address");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var ret = await UDPClientPool.WriteAddr(
|
||||||
|
this.ep, this.taskID, HdmiInAddr.END_WR_ADDR0,
|
||||||
|
HdmiInAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to set HDMI output end address: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
if (!ret.Value)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to set HDMI output address");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ public class HdmiVideoStreamClient
|
||||||
{
|
{
|
||||||
public required HdmiIn HdmiInClient { get; set; }
|
public required HdmiIn HdmiInClient { get; set; }
|
||||||
|
|
||||||
public required Jpeg JpegClient { get; set; }
|
// public required Jpeg JpegClient { get; set; }
|
||||||
|
|
||||||
public required CancellationTokenSource CTS { get; set; }
|
public required CancellationTokenSource CTS { get; set; }
|
||||||
|
|
||||||
|
@ -102,7 +102,8 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
var client = _clientDict[key];
|
var client = _clientDict[key];
|
||||||
client.CTS.Cancel();
|
client.CTS.Cancel();
|
||||||
|
|
||||||
var disableResult = await client.JpegClient.SetEnable(false);
|
// var disableResult = await client.JpegClient.SetEnable(false);
|
||||||
|
var disableResult = await client.HdmiInClient.SetTransEnable(false);
|
||||||
if (disableResult)
|
if (disableResult)
|
||||||
{
|
{
|
||||||
logger.Info("Successfully disabled HDMI transmission");
|
logger.Info("Successfully disabled HDMI transmission");
|
||||||
|
@ -111,6 +112,8 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to disable HDMI transmission");
|
logger.Error($"Failed to disable HDMI transmission");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.CTS = new CancellationTokenSource();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -120,13 +123,8 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
|
|
||||||
private async Task<HdmiVideoStreamClient?> GetOrCreateClientAsync(string boardId)
|
private async Task<HdmiVideoStreamClient?> GetOrCreateClientAsync(string boardId)
|
||||||
{
|
{
|
||||||
if (_clientDict.TryGetValue(boardId, out var client))
|
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 userManager = new Database.UserManager();
|
||||||
|
|
||||||
var boardRet = userManager.GetBoardByID(Guid.Parse(boardId));
|
var boardRet = userManager.GetBoardByID(Guid.Parse(boardId));
|
||||||
|
@ -141,32 +139,35 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
client = new HdmiVideoStreamClient()
|
client = new HdmiVideoStreamClient()
|
||||||
{
|
{
|
||||||
HdmiInClient = new HdmiIn(board.IpAddr, board.Port, 1),
|
HdmiInClient = new HdmiIn(board.IpAddr, board.Port, 1),
|
||||||
JpegClient = new Jpeg(board.IpAddr, board.Port, 1),
|
// JpegClient = new Jpeg(board.IpAddr, board.Port, 1),
|
||||||
CTS = new CancellationTokenSource(),
|
CTS = new CancellationTokenSource(),
|
||||||
Offset = 0
|
Offset = 0
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 启用HDMI传输
|
// 启用HDMI传输
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// var hdmiEnableRet = await client.JpegClient.EnableTrans(true);
|
var hdmiEnableRet = await client.HdmiInClient.Init(true);
|
||||||
// if (!hdmiEnableRet.IsSuccessful)
|
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}");
|
logger.Error($"Failed to enable HDMI transmission for board {boardId}: {hdmiEnableRet.Error}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
logger.Info($"Successfully enabled JPEG transmission for board {boardId}");
|
logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
|
||||||
|
|
||||||
client.Width = client.JpegClient.Width;
|
// var jpegEnableRet = await client.JpegClient.Init(true);
|
||||||
client.Height = client.JpegClient.Height;
|
// 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.HdmiInClient.Width;
|
||||||
|
client.Height = client.HdmiInClient.Height;
|
||||||
|
// client.Width = client.JpegClient.Width;
|
||||||
|
// client.Height = client.JpegClient.Height;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -195,15 +196,16 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hdmiInToken = _clientDict[boardId].CTS.Token;
|
var token = CancellationTokenSource.CreateLinkedTokenSource(
|
||||||
|
cancellationToken, client.CTS.Token).Token;
|
||||||
|
|
||||||
if (path == "/snapshot")
|
if (path == "/snapshot")
|
||||||
{
|
{
|
||||||
await HandleSnapshotRequestAsync(context.Response, client, hdmiInToken);
|
await HandleSnapshotRequestAsync(context.Response, client, token);
|
||||||
}
|
}
|
||||||
else if (path == "/mjpeg")
|
else if (path == "/mjpeg")
|
||||||
{
|
{
|
||||||
await HandleMjpegStreamAsync(context.Response, client, hdmiInToken);
|
await HandleMjpegStreamAsync(context.Response, client, token);
|
||||||
}
|
}
|
||||||
else if (path == "/video")
|
else if (path == "/video")
|
||||||
{
|
{
|
||||||
|
@ -223,36 +225,47 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
logger.Debug("处理HDMI快照请求");
|
logger.Debug("处理HDMI快照请求");
|
||||||
|
|
||||||
// 从HDMI读取RGB565数据
|
// 从HDMI读取RGB565数据
|
||||||
var frameResult = await client.JpegClient.GetMultiFrames((uint)client.Offset);
|
// var frameResult = await client.JpegClient.GetMultiFrames((uint)client.Offset);
|
||||||
if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0)
|
// if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0)
|
||||||
{
|
// {
|
||||||
logger.Error("HDMI快照获取失败");
|
// logger.Error("HDMI快照获取失败");
|
||||||
response.StatusCode = 500;
|
// response.StatusCode = 500;
|
||||||
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get HDMI snapshot");
|
// var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get HDMI snapshot");
|
||||||
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
// await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
||||||
response.Close();
|
// response.Close();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
var jpegData = frameResult.Value[0];
|
// var jpegData = frameResult.Value[0];
|
||||||
|
|
||||||
var quantTableResult = await client.JpegClient.GetQuantizationTable();
|
// var quantTableResult = await client.JpegClient.GetQuantizationTable();
|
||||||
if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
|
// if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
|
||||||
{
|
// {
|
||||||
logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
|
// logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
|
||||||
response.StatusCode = 500;
|
// response.StatusCode = 500;
|
||||||
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table");
|
// var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table");
|
||||||
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
// await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
||||||
response.Close();
|
// response.Close();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height, quantTableResult.Value);
|
// var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height, quantTableResult.Value);
|
||||||
if (!jpegImage.IsSuccessful)
|
// 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
var jpegImage = await client.HdmiInClient.GetMJpegFrame();
|
||||||
|
if (!jpegImage.HasValue)
|
||||||
{
|
{
|
||||||
logger.Error("JPEG数据补全失败");
|
logger.Error("获取HDMI MJPEG帧失败");
|
||||||
response.StatusCode = 500;
|
response.StatusCode = 500;
|
||||||
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to complete JPEG data");
|
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get HDMI MJPEG frame");
|
||||||
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
||||||
response.Close();
|
response.Close();
|
||||||
return;
|
return;
|
||||||
|
@ -260,13 +273,13 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
|
|
||||||
// 设置响应头(参考Camera版本)
|
// 设置响应头(参考Camera版本)
|
||||||
response.ContentType = "image/jpeg";
|
response.ContentType = "image/jpeg";
|
||||||
response.ContentLength64 = jpegImage.Value.Length;
|
response.ContentLength64 = jpegImage.Value.data.Length;
|
||||||
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
|
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.WriteAsync(jpegImage.Value.data, 0, jpegImage.Value.data.Length, cancellationToken);
|
||||||
await response.OutputStream.FlushAsync(cancellationToken);
|
await response.OutputStream.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
logger.Debug("已发送HDMI快照图像,大小:{Size} 字节", jpegImage.Value.Length);
|
logger.Debug("已发送HDMI快照图像,大小:{Size} 字节", jpegImage.Value.data.Length);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -275,6 +288,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
response.StatusCode = 200;
|
||||||
response.Close();
|
response.Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -292,17 +306,17 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
|
|
||||||
logger.Debug("开始HDMI MJPEG流传输");
|
logger.Debug("开始HDMI MJPEG流传输");
|
||||||
|
|
||||||
var quantTableResult = await client.JpegClient.GetQuantizationTable();
|
// var quantTableResult = await client.JpegClient.GetQuantizationTable();
|
||||||
if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
|
// if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
|
||||||
{
|
// {
|
||||||
logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
|
// logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
|
||||||
response.StatusCode = 500;
|
// response.StatusCode = 500;
|
||||||
await response.OutputStream.WriteAsync(
|
// await response.OutputStream.WriteAsync(
|
||||||
System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table"), 0, 0, cancellationToken);
|
// System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table"), 0, 0, cancellationToken);
|
||||||
response.Close();
|
// response.Close();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
var quantTable = quantTableResult.Value;
|
// var quantTable = quantTableResult.Value;
|
||||||
|
|
||||||
int frameCounter = 0;
|
int frameCounter = 0;
|
||||||
|
|
||||||
|
@ -310,30 +324,10 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
{
|
{
|
||||||
var frameStartTime = DateTime.UtcNow;
|
var frameStartTime = DateTime.UtcNow;
|
||||||
|
|
||||||
var frameResult =
|
var frameRet = await client.HdmiInClient.GetMJpegFrame();
|
||||||
await client.JpegClient.GetMultiFrames((uint)client.Offset);
|
if (!frameRet.HasValue)
|
||||||
if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0)
|
|
||||||
{
|
{
|
||||||
logger.Error("获取HDMI帧失败");
|
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, quantTable);
|
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
var frame = frameRet.Value;
|
var frame = frameRet.Value;
|
||||||
|
@ -354,7 +348,50 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
frameCounter, totalTime, frame.data.Length);
|
frameCounter, totalTime, frame.data.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// 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, quantTable);
|
||||||
|
// 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)
|
catch (Exception ex)
|
||||||
|
@ -366,7 +403,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 停止传输时禁用HDMI传输
|
// 停止传输时禁用HDMI传输
|
||||||
await client.HdmiInClient.EnableTrans(false);
|
await client.HdmiInClient.SetTransEnable(false);
|
||||||
logger.Info("已禁用HDMI传输");
|
logger.Info("已禁用HDMI传输");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
@ -81,7 +81,12 @@
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { ExamClient, ResourceClient, type ExamInfo } from "@/APIClient";
|
import {
|
||||||
|
ExamClient,
|
||||||
|
ResourceClient,
|
||||||
|
ResourcePurpose,
|
||||||
|
type ExamInfo,
|
||||||
|
} from "@/APIClient";
|
||||||
|
|
||||||
// 接口定义
|
// 接口定义
|
||||||
interface Tutorial {
|
interface Tutorial {
|
||||||
|
@ -146,7 +151,7 @@ onMounted(async () => {
|
||||||
const resourceList = await resourceClient.getResourceList(
|
const resourceList = await resourceClient.getResourceList(
|
||||||
exam.id,
|
exam.id,
|
||||||
"cover",
|
"cover",
|
||||||
"template",
|
ResourcePurpose.Template,
|
||||||
);
|
);
|
||||||
if (resourceList && resourceList.length > 0) {
|
if (resourceList && resourceList.length > 0) {
|
||||||
// 使用第一个封面资源
|
// 使用第一个封面资源
|
||||||
|
|
|
@ -430,13 +430,13 @@ function startStream() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止播放视频流
|
// 停止播放视频流
|
||||||
function stopStream() {
|
async function stopStream() {
|
||||||
isPlaying.value = false;
|
isPlaying.value = false;
|
||||||
currentVideoSource.value = "";
|
currentVideoSource.value = "";
|
||||||
videoStatus.value = "已停止播放";
|
videoStatus.value = "已停止播放";
|
||||||
|
|
||||||
const client = AuthManager.createClient(HdmiVideoStreamClient);
|
const client = AuthManager.createClient(HdmiVideoStreamClient);
|
||||||
client.disableHdmiTransmission();
|
await client.disableHdmiTransmission();
|
||||||
|
|
||||||
addLog("info", "停止播放HDMI视频流");
|
addLog("info", "停止播放HDMI视频流");
|
||||||
alert?.info("已停止播放HDMI视频流");
|
alert?.info("已停止播放HDMI视频流");
|
||||||
|
@ -467,8 +467,9 @@ function handleVideoClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重试连接
|
// 重试连接
|
||||||
function tryReconnect() {
|
async function tryReconnect() {
|
||||||
hasVideoError.value = false;
|
hasVideoError.value = false;
|
||||||
|
await stopStream();
|
||||||
if (endpoint.value) {
|
if (endpoint.value) {
|
||||||
startStream();
|
startStream();
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,7 +171,12 @@ import { useProvideComponentManager } from "@/components/LabCanvas";
|
||||||
import { useAlertStore } from "@/components/Alert";
|
import { useAlertStore } from "@/components/Alert";
|
||||||
import { AuthManager } from "@/utils/AuthManager";
|
import { AuthManager } from "@/utils/AuthManager";
|
||||||
import { useEquipments } from "@/stores/equipments";
|
import { useEquipments } from "@/stores/equipments";
|
||||||
import { DataClient, ResourceClient, type Board } from "@/APIClient";
|
import {
|
||||||
|
DataClient,
|
||||||
|
ResourceClient,
|
||||||
|
ResourcePurpose,
|
||||||
|
type Board,
|
||||||
|
} from "@/APIClient";
|
||||||
|
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
@ -257,7 +262,11 @@ async function loadDocumentContent() {
|
||||||
const client = AuthManager.createClient(ResourceClient);
|
const client = AuthManager.createClient(ResourceClient);
|
||||||
|
|
||||||
// 获取markdown类型的模板资源列表
|
// 获取markdown类型的模板资源列表
|
||||||
const resources = await client.getResourceList(examId, "doc", "template");
|
const resources = await client.getResourceList(
|
||||||
|
examId,
|
||||||
|
"doc",
|
||||||
|
ResourcePurpose.Template,
|
||||||
|
);
|
||||||
|
|
||||||
if (resources && resources.length > 0) {
|
if (resources && resources.length > 0) {
|
||||||
// 获取第一个markdown资源
|
// 获取第一个markdown资源
|
||||||
|
|
Loading…
Reference in New Issue