新增 Mjpegplayer 用来播放 Web 流

This commit is contained in:
2026-01-21 19:03:59 +08:00
parent f79cb6e74d
commit c438edfa0d
71 changed files with 4538 additions and 452 deletions

View File

@@ -0,0 +1,341 @@
using Ayay.SerilogLogs;
using Core.WcfProtocol;
using Serilog;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace SHH.MjpegPlayer
{
/// <summary>
/// Mjpeg 会话工作单元
/// </summary>
public class MjpegSession : IDisposable
{
private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
#region Counter
private SumByTime _sumBySecond = new SumByTime();
/// <summary>
/// 计数器
/// </summary>
public SumByTime Counter => _sumBySecond;
#endregion
#region Info
/// <summary>
/// 基础信息
/// </summary>
public SessionInfo Info { get; private set; }
#endregion
#region Cmd
/// <summary>
/// 命令
/// </summary>
public string? Cmd { get; set; }
#endregion
// [修复] 引入 Disposed 标志位
private volatile bool _isDisposed = false;
#region Constructor
/// <summary>
/// 构造函数
/// </summary>
public MjpegSession()
{
Info = new SessionInfo
{
Counter = _sumBySecond
};
}
#endregion
#region DoHttpHeader
private bool DoHttpHeader(NetworkStream stream)
{
try
{
byte[] buffer = new byte[4096];
int totalBytesRead = 0;
string httpRequest = string.Empty;
while (totalBytesRead < buffer.Length)
{
int bytesRead = stream.Read(buffer, totalBytesRead, buffer.Length - totalBytesRead);
if (bytesRead == 0) return false;
totalBytesRead += bytesRead;
httpRequest = Encoding.ASCII.GetString(buffer, 0, totalBytesRead);
if (httpRequest.Contains("\r\n\r\n")) break;
}
if (!httpRequest.Contains("\r\n\r\n")) return false;
if (!string.IsNullOrEmpty(httpRequest))
{
int queryStartIndex = httpRequest.IndexOf('?');
if (queryStartIndex > -1)
{
int spaceIndex = httpRequest.IndexOf(' ', queryStartIndex);
if (spaceIndex == -1) spaceIndex = httpRequest.Length;
string queryString = httpRequest.Substring(queryStartIndex + 1, spaceIndex - queryStartIndex - 1);
var queryParams = System.Web.HttpUtility.ParseQueryString(queryString);
if (queryParams != null)
{
Info.DeviceId = queryParams["id"];
Info.TypeCode = queryParams["typeCode"];
Cmd = queryParams["cmd"];
}
}
}
if (string.IsNullOrEmpty(Cmd))
{
if (string.IsNullOrEmpty(Info.DeviceId) || string.IsNullOrEmpty(Info.TypeCode))
{
SendErrorResponse(stream, "错误缺少必要参数id 或 typeCode");
return false;
}
}
else
{
if (MjpegHttpCmd.DoHttpCmd(stream, Info, Cmd)) return false;
}
return true;
}
catch (Exception ex)
{
SendErrorResponse(stream, $"解析异常: {ex.Message}");
return false;
}
}
private void SendErrorResponse(NetworkStream stream, string msg)
{
try
{
byte[] response = Encoding.UTF8.GetBytes(
$"HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{msg}");
stream.Write(response, 0, response.Length);
stream.Flush();
}
catch { }
}
#endregion
#region Create
/// <summary>
/// 创建会话
/// </summary>
public void Create(TcpClient client)
{
try
{
if (Info == null) return;
Info.AcceptTime = DateTime.Now;
// 初始化最近接收时间,避免刚连接就被判定为超时
LastRecImgTime = DateTime.Now;
Task.Run(() => { DoWorkTask(client); });
}
catch (Exception ex)
{
_sysLog.Error($"MjpegSession Create Exception, 异常信息:{ex.Message}, {ex.StackTrace}");
}
}
#endregion
#region DoWorkTask
private void DoWorkTask(TcpClient client)
{
try
{
using (var stream = client.GetStream())
{
// 设置写入超时 3秒
stream.WriteTimeout = 3000;
#region ,
int iLoc = 0;
while (!client.Connected)
{
Thread.Sleep(50);
if (++iLoc > 60) return;
}
try
{
if (client.Client?.RemoteEndPoint is IPEndPoint endpoint)
{
Info.ClientIp = endpoint.Address.ToString();
Info.ClientPort = endpoint.Port;
}
}
catch { }
if (!DoHttpHeader(stream)) return;
#endregion
MjpegStatics.Sessions.AddSession(this);
byte[] header = Encoding.ASCII.GetBytes(
"HTTP/1.1 200 OK\r\n" +
"Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n");
stream.Write(header, 0, header.Length);
var frameInterval = MjpegStatics.Cfg.FrameInterval;
if (frameInterval < 1 || frameInterval > 500) frameInterval = 125;
Stopwatch stopwatch = Stopwatch.StartNew();
UploadImageRequest? lastProcItem = null;
byte[] boundaryBytes = Encoding.ASCII.GetBytes("\r\n--frame\r\nContent-Type: image/jpeg\r\nContent-Length: ");
byte[] doubleNewLine = Encoding.ASCII.GetBytes("\r\n\r\n");
while (client.Connected && !_isDisposed)
{
try
{
// [新增] 僵尸连接熔断机制:如果源头超过 60 秒没有新图片,主动断开连接
if ((DateTime.Now - LastRecImgTime).TotalSeconds > 60)
{
_sysLog.Warning($"会话超时断开 (源头无数据 > 60s): {Info.Key} - Client: {Info.ClientIp}");
break;
}
stopwatch.Restart();
if (_lastRecObj == null || _lastRecObj.ImageBytes == null)
{
Info.Message = "等待图片数据抵达";
Thread.Sleep(40);
continue;
}
Info.Message = "视频流播放中";
if (lastProcItem != null && lastProcItem != _lastRecObj)
{
_sumBySecond.Refresh("有效帧数");
}
lastProcItem = _lastRecObj;
// 如果图片太旧超过3秒认为是滞后数据暂时不发
if ((DateTime.Now - _lastRecObj.Time).TotalSeconds > 3)
{
Thread.Sleep(40);
continue;
}
byte[] imageData = _lastRecObj.ImageBytes;
stream.Write(boundaryBytes, 0, boundaryBytes.Length);
byte[] lengthBytes = Encoding.ASCII.GetBytes(imageData.Length.ToString());
stream.Write(lengthBytes, 0, lengthBytes.Length);
stream.Write(doubleNewLine, 0, doubleNewLine.Length);
stream.Write(imageData, 0, imageData.Length);
_sumBySecond.Refresh("播放帧数");
stopwatch.Stop();
var needSleep = frameInterval - (int)stopwatch.ElapsedMilliseconds;
if (needSleep > 0) Thread.Sleep(needSleep);
}
catch (Exception ex)
{
_sysLog.Warning($"播放连接断开, : {ex.Message}");
break;
}
}
}
}
catch (Exception ex)
{
_sysLog.Error($"异常信息:{ex.Message}, {ex.StackTrace}");
}
finally
{
Dispose();
if (client != null)
{
try { client.Close(); client.Dispose(); } catch { }
}
}
}
#endregion
#region DoImageProc
private UInt64 _lastPlayImageOrder = 0;
private UploadImageRequest? _lastRecObj = null;
/// <summary>
/// 处置图片
/// </summary>
public void DoImageProc(UploadImageRequest req)
{
try
{
LastRecImgTime = DateTime.Now;
_sumBySecond.Refresh("接收帧数");
if (req == null || req.ImageBytes == null || req.ImageBytes.Length < 100) return;
// 防止死锁:序号重置检测
if (req.Order < _lastPlayImageOrder)
{
if (_lastPlayImageOrder > 100 && req.Order < 100)
{
_lastPlayImageOrder = req.Order;
}
else
{
return;
}
}
_lastPlayImageOrder = req.Order;
_lastRecObj = req;
}
catch (Exception ex)
{
_sysLog.Error($"异常信息:{ex.Message}, {ex.StackTrace}");
}
}
/// <summary>
/// 最近收到图片时间
/// </summary>
public DateTime LastRecImgTime { get; set; } = DateTime.MinValue;
#endregion
#region Dispose
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
MjpegStatics.Sessions.RemoveSession(this);
}
#endregion
}
}