Files
Ayay/SHH.MjpegPlayer/Server/MjpegSession.cs

342 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}