Przeglądaj źródła

feat(d2-02): MjpegStreamWriter 纯逻辑(RGB→JPEG 编码 + multipart 帧封装)+2 单测

huangjie 1 dzień temu
rodzic
commit
85be9de164

+ 52 - 0
ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/MjpegStreamWriterTests.cs

@@ -0,0 +1,52 @@
+using System.IO;
+using System.Windows.Media.Imaging;
+using IvfTl.ControlHost.Debug;
+using Xunit;
+
+namespace IvfTl.ControlHost.Tests
+{
+    public class MjpegStreamWriterTests
+    {
+        // 造一张 4x4 纯色 24bpp BGR 像素(每像素 3 字节)。
+        private static byte[] SolidBgr(int w, int h, byte b, byte g, byte r)
+        {
+            var buf = new byte[w * h * 3];
+            for (int i = 0; i < w * h; i++) { buf[i * 3] = b; buf[i * 3 + 1] = g; buf[i * 3 + 2] = r; }
+            return buf;
+        }
+
+        [Fact]
+        public void EncodeJpeg_ProducesDecodableJpeg_WithRightSize()
+        {
+            var rgb = SolidBgr(4, 4, 10, 20, 30);
+            byte[] jpeg = MjpegStreamWriter.EncodeJpeg(rgb, 4, 4);
+
+            Assert.NotNull(jpeg);
+            Assert.True(jpeg.Length > 2);
+            // JPEG 魔数 FF D8 开头
+            Assert.Equal(0xFF, jpeg[0]);
+            Assert.Equal(0xD8, jpeg[1]);
+            // 能被解码器读回、尺寸对
+            var dec = new JpegBitmapDecoder(new MemoryStream(jpeg),
+                BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
+            Assert.Equal(4, dec.Frames[0].PixelWidth);
+            Assert.Equal(4, dec.Frames[0].PixelHeight);
+        }
+
+        [Fact]
+        public void FrameBytes_WrapsJpegInMultipartBoundary()
+        {
+            var jpeg = new byte[] { 0xFF, 0xD8, 1, 2, 0xFF, 0xD9 };
+            byte[] frame = MjpegStreamWriter.FrameBytes(jpeg);
+
+            string head = System.Text.Encoding.ASCII.GetString(frame, 0, 60);
+            Assert.Contains("--frame", head);
+            Assert.Contains("Content-Type: image/jpeg", head);
+            Assert.Contains("Content-Length: 6", head);
+            // 帧尾部含 jpeg 原始字节 + 末尾 \r\n
+            Assert.Equal(0xFF, frame[frame.Length - 8]); // jpeg 起点附近(粗校验帧体存在)
+            Assert.Equal((byte)'\r', frame[frame.Length - 2]);
+            Assert.Equal((byte)'\n', frame[frame.Length - 1]);
+        }
+    }
+}

+ 50 - 0
ivf_tl_operate_2.0/control/ivf_tl_ControlHost/Debug/MjpegStreamWriter.cs

@@ -0,0 +1,50 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace IvfTl.ControlHost.Debug
+{
+    /// <summary>
+    /// MJPEG 推流的纯逻辑:RGB 像素 → JPEG 字节,JPEG → multipart 帧字节。
+    /// 无 IO、无相机依赖,可纯单测。真正的抓帧/写流由 ControlHttpServer 推流分支驱动。
+    /// 相机抓帧返回 24bpp BGR(GrabStable),WPF JpegBitmapEncoder 需 Bgr24 像素格式。
+    /// </summary>
+    public static class MjpegStreamWriter
+    {
+        public const string Boundary = "frame";
+
+        /// <summary>把 24bpp BGR 像素(width*height*3)编码成 JPEG 字节(质量 85)。</summary>
+        public static byte[] EncodeJpeg(byte[] bgr, int width, int height, int quality = 85)
+        {
+            if (bgr == null || bgr.Length < width * height * 3) return null;
+            int stride = width * 3;
+            var bmp = BitmapSource.Create(width, height, 96, 96,
+                PixelFormats.Bgr24, null, bgr, stride);
+            var encoder = new JpegBitmapEncoder { QualityLevel = quality };
+            encoder.Frames.Add(BitmapFrame.Create(bmp));
+            using (var ms = new MemoryStream())
+            {
+                encoder.Save(ms);
+                return ms.ToArray();
+            }
+        }
+
+        /// <summary>把 JPEG 字节封成一个 multipart/x-mixed-replace 帧(含 boundary 头 + 帧体 + \r\n)。</summary>
+        public static byte[] FrameBytes(byte[] jpeg)
+        {
+            string header = $"--{Boundary}\r\nContent-Type: image/jpeg\r\nContent-Length: {jpeg.Length}\r\n\r\n";
+            byte[] head = Encoding.ASCII.GetBytes(header);
+            byte[] tail = Encoding.ASCII.GetBytes("\r\n");
+            var frame = new byte[head.Length + jpeg.Length + tail.Length];
+            Buffer.BlockCopy(head, 0, frame, 0, head.Length);
+            Buffer.BlockCopy(jpeg, 0, frame, head.Length, jpeg.Length);
+            Buffer.BlockCopy(tail, 0, frame, head.Length + jpeg.Length, tail.Length);
+            return frame;
+        }
+
+        /// <summary>响应头里的 Content-Type 值(供 ControlHttpServer 写头用)。</summary>
+        public static string ContentType => $"multipart/x-mixed-replace; boundary={Boundary}";
+    }
+}