[C#][WinForm] IPCam MJPEG解碼(MJPEG Decode)

[C#][WinForm] IPCam/NVR/CGI MJPEG解碼(MJPEG Decode)

目前有個CGI會處理和傳送影像

在Chrome上看到是這樣

那要寫一隻WinForm來做到一樣的事情該怎麼做呢

先開新專案 命名就隨意吧

在裡面加入一個PictureBox 且Dock = DockStyle.Fill 如下圖

然後就開始寫Code吧

新增一個Class命名為 MJPEGDecoder

主要的做法就是用HttpWebRequest 附帶帳號密碼認證後去撈取串流

在分析串流內是否有JPEG的開頭{FF,D8}

而在wiki上的 M-JPEG over HTTP 有寫著一些簡介

A special mime-type content type multipart/x-mixed-replace;boundary=<boundary-name> informs the client to expect several parts (frames) as an answer delimited by <boundary-name>.

所以只要找出 <boundary-name> 就可以知道每個frame的界線在哪

[PS: 當然 JPEG也有結尾的位元 {FF,D8} 也可以用此來判斷是否為結尾]

並利用抓到完整圖檔時觸發事件來更改pictureBox1的圖片

這樣放進無窮迴圈中就可以當作影片來看了

    public class MJPEGDecoder
    {
        public Bitmap bitmap { get; set; }

        // magic 2 byte header for JPEG images
        private byte[] JpegHeader = new byte[] { 0xff, 0xd8 };
        // you can replace boundaryBytes with JpegEnd
        //private byte[] JpegEnd = new byte[] { 0xff, 0xd9 }; 

        // pull down 1024 bytes at a time
        private const int ChunkSize = 1024;

        // used to cancel reading the stream
        private bool _streamActive;

        // current encoded JPEG image
        public byte[] CurrentFrame { get; private set; }

        // used to marshal back to UI thread
        private SynchronizationContext _context;

        // event to get the buffer above handed to you
        public event EventHandler<FrameReadyEventArgs> FrameReady;

        public MJPEGDecoder()
        {
            _context = SynchronizationContext.Current;
        }

        public void ParseStream(Uri uri)
        {
            ParseStream(uri, null, null);
        }

        public void ParseStream(Uri uri, string username, string password)
        {
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
            if (!string.IsNullOrEmpty(username) || !string.IsNullOrEmpty(password))
                request.Credentials = new NetworkCredential(username, password);

            // asynchronously get a response
            request.BeginGetResponse(OnGetResponse, request);
        }

        public void StopStream()
        {
            _streamActive = false;
        }

        private void OnGetResponse(IAsyncResult asyncResult)
        {
            HttpWebResponse resp;
            byte[] buff;
            byte[] imageBuffer = new byte[1024 * 1024];
            Stream s;

            // get the response
            HttpWebRequest req = (HttpWebRequest)asyncResult.AsyncState;
            resp = (HttpWebResponse)req.EndGetResponse(asyncResult);

            // find our magic boundary value
            string contentType = resp.Headers["Content-Type"];
            if (!string.IsNullOrEmpty(contentType) && !contentType.Contains("="))
                throw new Exception("Invalid content-type header.  The camera is likely not returning a proper MJPEG stream.");
            string boundary = resp.Headers["Content-Type"].Split('=')[1].Replace("\"", "");
            byte[] boundaryBytes = Encoding.UTF8.GetBytes(boundary.StartsWith("--") ? boundary : "--" + boundary);

            s = resp.GetResponseStream();
            BinaryReader br = new BinaryReader(s);

            _streamActive = true;

            buff = br.ReadBytes(ChunkSize);

            while (_streamActive)
            {
                int size;

                // find the JPEG header
                int imageStart = GetArrayPosition(buff, JpegHeader);

                if (imageStart != -1)
                {
                    // copy the start of the JPEG image to the imageBuffer
                    size = buff.Length - imageStart;
                    Array.Copy(buff, imageStart, imageBuffer, 0, size);

                    while (true)
                    {
                        buff = br.ReadBytes(ChunkSize);

                        // find the boundary text
                        int imageEnd = GetArrayPosition(buff, boundaryBytes);
                        if (imageEnd != -1)
                        {
                            // copy the remainder of the JPEG to the imageBuffer
                            Array.Copy(buff, 0, imageBuffer, size, imageEnd);
                            size += imageEnd;

                            // create a single JPEG frame
                            CurrentFrame = new byte[size];
                            Array.Copy(imageBuffer, 0, CurrentFrame, 0, size);

                            ProcessFrame(CurrentFrame);

                            // copy the leftover data to the start
                            Array.Copy(buff, imageEnd, buff, 0, buff.Length - imageEnd);

                            // fill the remainder of the buffer with new data and start over
                            byte[] temp = br.ReadBytes(imageEnd);

                            Array.Copy(temp, 0, buff, buff.Length - imageEnd, temp.Length);
                            break;
                        }

                        // copy all of the data to the imageBuffer
                        Array.Copy(buff, 0, imageBuffer, size, buff.Length);
                        size += buff.Length;
                    }
                }
            }
            resp.Close();
        }

        private void ProcessFrame(byte[] frameBuffer)
        {
            _context.Post(delegate
            {
                // create a simple GDI+ happy Bitmap
                bitmap = new Bitmap(new MemoryStream(frameBuffer));

                // tell whoever's listening that we have a frame to draw
                FrameReady?.Invoke(this, new FrameReadyEventArgs { FrameBuffer = CurrentFrame, Bitmap = bitmap });
            }, null);
        }

        private int GetArrayPosition(byte[] mbyte, byte[] content)
        {
            int position = -1;

            for (int i = 0; i <= mbyte.Length - content.Length; i++)
            {
                if (mbyte[i] == content[0])
                {
                    bool isRight = true;
                    for(int j = 1; j < content.Length; j++)
                    {
                        if (mbyte[i + j] != content[j])
                        {
                            isRight = false;
                            break;
                        }
                    }
                    if (isRight) return i;
                }
            }

            return position;
        }
        
    }

為了觸發FrameReady事件 還要再加入一個EventArgs類別 如下

public class FrameReadyEventArgs : EventArgs
    {
        public byte[] FrameBuffer;
        public Bitmap Bitmap;
    }

到這裡已經萬事俱備了

去FormLoad來使用吧

    private void Form1_Load(object sender, EventArgs e)
        {
            var mjpeg = new MJPEGDecoder();
            mjpeg.FrameReady += Mjpeg_FrameReady;
            mjpeg.ParseStream(new Uri("http://192.168.0.17/airvideo/media?channel=channel7&streamtype=mjpeg&width=1280&height=720"), "JR", "123");
        }

    private void Mjpeg_FrameReady(object sender, FrameReadyEventArgs e)
        {
            pictureBox1.Image = e.Bitmap;
        }

結果如下

 

新手發文,有謬誤請告知,也請多多指教。