356 lines
12 KiB
C#
356 lines
12 KiB
C#
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
|
||
{
|
||
#region Defines
|
||
|
||
private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||
|
||
private CancellationTokenSource? _sessionCts;
|
||
|
||
private SumByTime _sumBySecond = new SumByTime();
|
||
|
||
/// <summary>计数器</summary>
|
||
public SumByTime Counter => _sumBySecond;
|
||
|
||
// 引入 Disposed 标志位
|
||
private volatile bool _isDisposed = false;
|
||
|
||
#endregion
|
||
|
||
#region Info
|
||
|
||
/// <summary>
|
||
/// 基础信息
|
||
/// </summary>
|
||
public SessionInfo Info { get; private set; }
|
||
|
||
#endregion
|
||
|
||
#region Cmd
|
||
|
||
/// <summary>
|
||
/// 命令
|
||
/// </summary>
|
||
public string? Cmd { get; set; }
|
||
|
||
#endregion
|
||
|
||
#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;
|
||
|
||
// 绑定全局取消令牌,确保系统停止时能立即强制中断所有会话
|
||
_sessionCts = new CancellationTokenSource();
|
||
|
||
Task.Run(() => { DoWorkTask(client, _sessionCts.Token); }, _sessionCts.Token);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_sysLog.Error($"MjpegSession Create Exception, 异常信息:{ex.Message}, {ex.StackTrace}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region DoWorkTask
|
||
|
||
private void DoWorkTask(TcpClient client, CancellationToken token)
|
||
{
|
||
try
|
||
{
|
||
using (var stream = client.GetStream())
|
||
{
|
||
// 设置写入超时 3秒
|
||
stream.WriteTimeout = 3000;
|
||
|
||
#region 等待连接, 处理地址栏参数
|
||
|
||
int iLoc = 0;
|
||
while (!client.Connected && !token.IsCancellationRequested)
|
||
{
|
||
Thread.Sleep(50);
|
||
if (++iLoc > 60) return;
|
||
}
|
||
if (token.IsCancellationRequested) 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 && !token.IsCancellationRequested)
|
||
{
|
||
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)
|
||
Task.Delay(needSleep, token).Wait(token);
|
||
}
|
||
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;
|
||
|
||
// 释放令牌资源
|
||
try
|
||
{
|
||
_sessionCts?.Cancel();
|
||
_sessionCts?.Dispose();
|
||
}
|
||
catch { }
|
||
|
||
MjpegStatics.Sessions.RemoveSession(this);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
} |