using System; using System.Collections.Generic; using System.Text; namespace ivf_tl_Operate.Debug { /// /// MJPEG multipart 流切帧状态机(纯逻辑)。喂入任意大小的字节块,吐出完整 JPEG 帧。 /// 处理:一帧跨多个块、一个块含多帧、半个头跨块。只认 Content-Length 截帧(不靠扫 boundary 找帧尾,稳)。 /// 流格式:--frame\r\nContent-Type: image/jpeg\r\nContent-Length: N\r\n\r\n[N 字节 JPEG]\r\n /// public sealed class MjpegFrameParser { private readonly List _buf = new List(); private int _expectedLen = -1; // -1 = 还没解析到帧头;>=0 = 正在收帧体 private int _bodyStart = -1; /// 喂入一段字节,返回这次能切出的完整 JPEG 帧(可能 0~多帧)。 public IEnumerable Feed(byte[] chunk, int count) { if (chunk == null || count <= 0) return Array.Empty(); if (count == chunk.Length) _buf.AddRange(chunk); else for (int i = 0; i < count; i++) _buf.Add(chunk[i]); var frames = new List(); while (true) { if (_expectedLen < 0) { // 找头部结束标志 \r\n\r\n int sep = IndexOfDoubleCrlf(_buf); if (sep < 0) break; // 头还没收全,等下一块 string header = Encoding.ASCII.GetString(_buf.ToArray(), 0, sep); int len = ParseContentLength(header); if (len < 0) { // 头里没 Content-Length(异常/坏帧)→ 丢弃到分隔符后,继续 _buf.RemoveRange(0, sep + 4); continue; } _expectedLen = len; _bodyStart = sep + 4; // \r\n\r\n 之后是帧体 } // 收帧体:需要 _bodyStart + _expectedLen 字节 if (_buf.Count < _bodyStart + _expectedLen) break; // 帧体没收全,等下一块 var jpeg = new byte[_expectedLen]; _buf.CopyTo(_bodyStart, jpeg, 0, _expectedLen); frames.Add(jpeg); // 移除已消费的帧(头 + 体 + 可能的尾 \r\n)。下一帧从 --frame 开始,统一靠下轮找 \r\n\r\n。 int consumed = _bodyStart + _expectedLen; // 跳过帧体后的 \r\n(若存在) if (_buf.Count >= consumed + 2 && _buf[consumed] == (byte)'\r' && _buf[consumed + 1] == (byte)'\n') consumed += 2; _buf.RemoveRange(0, consumed); _expectedLen = -1; _bodyStart = -1; } return frames; } private static int IndexOfDoubleCrlf(List buf) { for (int i = 0; i + 3 < buf.Count; i++) if (buf[i] == (byte)'\r' && buf[i + 1] == (byte)'\n' && buf[i + 2] == (byte)'\r' && buf[i + 3] == (byte)'\n') return i; return -1; } private static int ParseContentLength(string header) { foreach (var line in header.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries)) { int colon = line.IndexOf(':'); if (colon < 0) continue; if (line.Substring(0, colon).Trim().Equals("Content-Length", StringComparison.OrdinalIgnoreCase)) if (int.TryParse(line.Substring(colon + 1).Trim(), out int n)) return n; } return -1; } } }