C# MJPEG 客户端简单实现
MJPEG协议在此不在过多描述,这里主要介绍一下使用C#中的PictureBox控件频繁刷新MJPEG传输过来的图片,高频率的图片刷新实现视频播放效果;
环境:
服务端
MJPEG服务器使用的是手机的DroidCam,很方便的一个MJPEG服务器,端口4747,打开软件就能使用,并且还附带了web端展示。
客户端
MJPEG客户端使用C# Http请求,并获取到响应MJPEG视频流,截取到图片数据部分,用PictureBox展示图片内容。
整体流程:
1. C# 向MJPEG发送请求URL,请求URL是MJPEG服务器定的,例如DroidCam,可以通过访问: {手机所在IP}:4747
图片中红框内容就是视频流的地址,使用GET请求后,服务端就会一直往这个请求的响应内容中写照片信息,直到这个GET请求断开为止(客户端、服务端其中一个主动退出)
ps: 如果身边没有MJPEG服务器,可以使用手机中DroidCam当服务器,建议使用手机热点、或者手机通过数据线共享链接方式和电脑链接,因为MJPEG实际是把视频的每一帧截成一张图片发送过来的,非常的占带宽,并且网速不好还有图片数据不完整情况,需要手动处理跳过.手机开WiFi热点电脑链接, 手机端IP一般是:192.168.43.1:4747,手机数据线连接usb网络共享,手机端IP一般是:192.168.43.129:4747;
2. C# 读响应头,找出视频流中每张图片的分隔符, 读取每张图片前Content-Length长度, 读图片;
3. 每每读到一张图片,刷新一次PictureBox控件,这样高频率的刷新图片,连续起来就实现了视频的效果;
具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | //创建一个HTTP请求,建立请求后MJPEG服务端会一直给请求的响应体中发送实时图片内容,知道这个HTTP(GET请求)请求断开位置 HttpWebRequest hwRequest = (System.Net.HttpWebRequest)WebRequest.Create( "请求URL地址" ); hwRequest.Method = "GET" ; HttpWebResponse hwResponse = (HttpWebResponse)hwRequest.GetResponse(); //读boundary指定的每张图片分隔符,DroidCam为:--dcmjpeg string contentType = hwResponse.Headers[ "Content-Type" ]; string boundryKey = "boundary=" ; string boundary = contentType.Substring(contentType.IndexOf(boundryKey) + boundryKey.Length); //拿到响应体流信息 Stream stream = hwResponse.GetResponseStream(); //Content-Length: 后边数字是这张图片大小,如果第一次使用可以先试着打印一张图片写到本地, Content-Length:所在行,下一行会有一个/r/n,然后就是实际图片数据 string headerName = "Content-Length:" ; //临时存储字符串数据 StringBuilder sb = new StringBuilder(); int len = 1024; while ( true ) { //读取一行数据 while ( true ) { char c = ( char )stream.ReadByte(); //Console.Write(c); if (c == '\n' ) { break ; } sb.Append(c); } string line = sb.ToString(); //清除临时信息 sb.Remove(0, sb.Length); //当前行中是否包含Content-Length: int i = line.IndexOf(headerName); if (i != -1) { //每张图片前有一段图片简单介绍(图片类型、长度),这里只关心长度(Content-Length:)后边的值,用于后续读取图片 int imageFileLength = Convert.ToInt32(line.Substring(i + headerName.Length).Trim()); //Content-Length:xxx 完后会有一个/r/n的换行符,换行符后才是真正的图片数据(不知道是DroidCam自己这样还是都这样...) //这里跳过/r/n stream.Read( new byte [2], 0, 2); //开始读取图片数据,imageFileLength就是读到的Content-Length:后的长度 byte [] imageFileBytes = new byte [imageFileLength]; stream.Read(imageFileBytes, 0, imageFileBytes.Length); //JPEG的文件头是: FF D8 FF ,文件尾是: FF D9,非常重要,调试时最好打印一下,便于区分读入的数据是否正好时图片的所有内容 //Console.WriteLine("文件头:" + imageFileBytes[0].ToString("X") + " " + imageFileBytes[1].ToString("X") + " " + imageFileBytes[2].ToString("X") + " " + imageFileBytes[3].ToString("X") + " " + imageFileBytes[4].ToString("X")); //Console.WriteLine("文件尾:" + imageFileBytes[imageFileLength - 2].ToString("X") + " " + imageFileBytes[imageFileLength - 1].ToString("X")); //此处做了一个如果读入文件不全时处理,图片越大,程序循环读取速度越快,越有可能导致读取文件不全情况...,如果有好的办法解决希望前辈们指教,非常感谢! //文件尾是否是FF D9,如果不是,表示图片接收不全, 当接受完一张图片后适当睡几十毫秒,可以适当降低接受不完情况 if (imageFileBytes[imageFileLength - 2].ToString( "X" ) != "FF" && imageFileBytes[imageFileLength - 1].ToString( "X" ) != "D9" ) { //读入文件内容不全,跳过次文件,让流位置跳到下次图片开始位置 //Console.WriteLine("开始矫正..."); char l = '0' ; while ( true ) { char c = ( char )stream.ReadByte(); //这里只判断了--dcmjpeg中的前两个字符--,当读到的流中连续两个字符是--时,表示流已读到下次图片开始位置 if (l == boundary[0] && c == boundary[1]) { break ; } l = c; } } else { //读取图片成功! //accessImageHandler是一个Action,用于把图片实时写到PictureBox控件中 //次数表示图片接收成功,imageFileBytes就是一张图片,可以把imageFileBytes数组写到文件中,用图片工具打开 accessImageHandler(imageFileBytes); } //这里适当睡几十毫秒,会降低点图片读入不全情况,还未找到图片随机读取不全情况原因... Thread.Sleep(sleep); } } stream.Close(); hwResponse.Close(); |
可以先试着读一张图片,通过FileStream 写成文件,看看写成的文件是否能用Windows图片查看器查看,如果不能并且机器上有PS的话,可以试着用PS打开一下,PS对图片支持的比较好,如果文件头多写两个其他字符(比如/r/n)它是可以过滤掉的,PS可以打开,就需要看看文件头和文件尾是否是:
1 | <strong>文件头: FF D8 FF ,文件尾: FF D9</strong> |
。但是最后的效果还是需要Windows图片查看器能看,只有Windows查看器能看,PictureBox才能正常显示内容,否则在打开图片时会报内存不足异常!
多调试几遍,查看一下请求头、请求尾是否正确。
如果有兴趣,可以看下我调试例子:链接: https://pan.baidu.com/s/1oihxe8ficnCm4gcaE9SQBg 提取码: atwh ,例子内容有点乱,并且很不完善,希望对你多少有些帮助!
20210227补充使用IP摄像头APP连接时有密码情况:
MJPEG协议中应该是没规定加密情况,这个加密(http auth)应该是IP摄像头APP规定的。
在使用IP摄像头App读MJPEG流时发现需要密码,使用浏览器直接访问会弹出输入账号密码框,通过解析请求发现其实就是在请求头中添加了一个请求头Authorization:
YWRtaW46YWRtaW4=是我在APP中设置的 用户名(admin):密码(admin) 拼接起来后转成Base64的字符串, admin:admin 转成base64为: YWRtaW46YWRtaW4=
所以在修改一下请求头就可以了:
hwRequest.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(user + ":" + pass)));
这里hwRequest就是HttpWebRequest
user是用户名,pass 是密码
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· 因为Apifox不支持离线,我果断选择了Apipost!