342 lines
11 KiB
C#
342 lines
11 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
|
|||
|
|
{
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|