MjpegFrameParser.cs 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. namespace ivf_tl_Operate.Debug
  5. {
  6. /// <summary>
  7. /// MJPEG multipart 流切帧状态机(纯逻辑)。喂入任意大小的字节块,吐出完整 JPEG 帧。
  8. /// 处理:一帧跨多个块、一个块含多帧、半个头跨块。只认 Content-Length 截帧(不靠扫 boundary 找帧尾,稳)。
  9. /// 流格式:--frame\r\nContent-Type: image/jpeg\r\nContent-Length: N\r\n\r\n[N 字节 JPEG]\r\n
  10. /// </summary>
  11. public sealed class MjpegFrameParser
  12. {
  13. private readonly List<byte> _buf = new List<byte>();
  14. private int _expectedLen = -1; // -1 = 还没解析到帧头;>=0 = 正在收帧体
  15. private int _bodyStart = -1;
  16. /// <summary>喂入一段字节,返回这次能切出的完整 JPEG 帧(可能 0~多帧)。</summary>
  17. public IEnumerable<byte[]> Feed(byte[] chunk, int count)
  18. {
  19. if (chunk == null || count <= 0) return Array.Empty<byte[]>();
  20. if (count == chunk.Length) _buf.AddRange(chunk);
  21. else for (int i = 0; i < count; i++) _buf.Add(chunk[i]);
  22. var frames = new List<byte[]>();
  23. while (true)
  24. {
  25. if (_expectedLen < 0)
  26. {
  27. // 找头部结束标志 \r\n\r\n
  28. int sep = IndexOfDoubleCrlf(_buf);
  29. if (sep < 0) break; // 头还没收全,等下一块
  30. string header = Encoding.ASCII.GetString(_buf.ToArray(), 0, sep);
  31. int len = ParseContentLength(header);
  32. if (len < 0)
  33. {
  34. // 头里没 Content-Length(异常/坏帧)→ 丢弃到分隔符后,继续
  35. _buf.RemoveRange(0, sep + 4);
  36. continue;
  37. }
  38. _expectedLen = len;
  39. _bodyStart = sep + 4; // \r\n\r\n 之后是帧体
  40. }
  41. // 收帧体:需要 _bodyStart + _expectedLen 字节
  42. if (_buf.Count < _bodyStart + _expectedLen) break; // 帧体没收全,等下一块
  43. var jpeg = new byte[_expectedLen];
  44. _buf.CopyTo(_bodyStart, jpeg, 0, _expectedLen);
  45. frames.Add(jpeg);
  46. // 移除已消费的帧(头 + 体 + 可能的尾 \r\n)。下一帧从 --frame 开始,统一靠下轮找 \r\n\r\n。
  47. int consumed = _bodyStart + _expectedLen;
  48. // 跳过帧体后的 \r\n(若存在)
  49. if (_buf.Count >= consumed + 2 && _buf[consumed] == (byte)'\r' && _buf[consumed + 1] == (byte)'\n')
  50. consumed += 2;
  51. _buf.RemoveRange(0, consumed);
  52. _expectedLen = -1; _bodyStart = -1;
  53. }
  54. return frames;
  55. }
  56. private static int IndexOfDoubleCrlf(List<byte> buf)
  57. {
  58. for (int i = 0; i + 3 < buf.Count; i++)
  59. if (buf[i] == (byte)'\r' && buf[i + 1] == (byte)'\n' && buf[i + 2] == (byte)'\r' && buf[i + 3] == (byte)'\n')
  60. return i;
  61. return -1;
  62. }
  63. private static int ParseContentLength(string header)
  64. {
  65. foreach (var line in header.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
  66. {
  67. int colon = line.IndexOf(':');
  68. if (colon < 0) continue;
  69. if (line.Substring(0, colon).Trim().Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
  70. if (int.TryParse(line.Substring(colon + 1).Trim(), out int n)) return n;
  71. }
  72. return -1;
  73. }
  74. }
  75. }