背景
实验室原有一个支持UVC协议的USB免驱摄像头,但是因为某种原因需要把该摄像头改装成WiFi无线网络摄像头。最后的解决方案是在原有摄像头的基础上再加上一个WiFi模块,把视频流转成WiFi信号进行传输。WiFi模块会提供一个热点信号,iPhone连接该热点信号后,Safari即可通过http://10.10.10.1:8080访问到实时的图片流(非视频流)。
需求
开发一个基于iOS的App来接收实时图片流,开发语言是Swift,UI框架是SwiftUI。
解决过程
Wireshark抓包信息图
响应头信息图
通过使用Wireshark抓包和查看响应头信息大概推断WiFi模块的数据传输方式:
WiFi模块传输的视频实际上是连续的实时二进制图片流(非视频流,这点卡了我好久......),且该图片格式是JPEG。比如视频的帧率是30fps,WiFi模块就会每秒传输30张JPEG格式的图片以此形成动态的视频,并且每一张图片会以一个分隔符分开,也就是上述响应头的boundary字段。
二进制图片流格式样式图
JPEG格式图片的一般以0xFFD8开始,
以0xFFD9结束,我打印了一部分数据流(转成十六进制)发现,我的数据都是0xFFD9后面紧跟着0xFFD8,也就是说该WiFi模块传输的数据直接是一张图紧跟着一张图,并没有用到分隔符。我就以此为根据获取到每一张图片,并把其显示在页面上,以此形成实时视频效果。
代码
import SwiftUI
class CameraModel: NSObject, ObservableObject {
@Published var image: UIImage?
private var session: URLSession?
private var dataTask: URLSessionDataTask?
private var receivedData = Data()
let videoStreamUrl: String = "http://10.10.10.1:8080"
override init() {
super.init()
self.session = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue.main)
}
// 开始捕捉画面
func startCaptureStream() {
guard let videoStreamUrl = URL(string: videoStreamUrl) else { return }
dataTask = session?.dataTask(with: videoStreamUrl)
dataTask?.resume()
}
// 停止捕捉画面
func stopCaptureStream() {
dataTask?.cancel()
// session = nil
dataTask = nil
image = nil
}
}
extension CameraModel: URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
receivedData.append(data)
// JPEG的图片流一般以0xFFD8开始,以0xFFD9结束(十六进制),根据打印出来的data观察,该图片流单纯传输了图片,没有用到分隔符标识
// upperBound 是闭区间的属性,表示闭区间的上界,也就是子序列的结束位置
if let imageEndIndex = receivedData.range(of: Data([0xFF, 0xD9]))?.upperBound{
// 截取一张JPEG的图片流
let imageData = receivedData.subdata(in: 0..<imageEndIndex)
if let image = UIImage(data: imageData) {
DispatchQueue.main.async {
self.image = image
}
}
// data清零,准备接收下一张图片流
receivedData.removeSubrange(..<imageEndIndex)
}
}
}
struct ContentView: View {
@StateObject private var cameraModel = CameraModel()
var body: some View {
VStack {
if let image = cameraModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
} else {
Text("Waiting for captureStream...")
}
HStack {
Button(action: {
cameraModel.startCaptureStream()
}, label: {
Text("开始")
})
.frame(width: 120, height: 50)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
.padding()
Button(action: {
cameraModel.stopCaptureStream()
}, label: {
Text("结束")
})
.frame(width: 120, height: 50)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
.padding()
}
}
}
}
#Preview {
ContentView()
}
PS:使用
URLSession
建立一个连接,但不是通过标准的数据任务(dataTask
),而是使用更低级别的网络接口来持续读取这种数据流。这点也卡了我很久,我直接用dataTask,发现请求一直进不去回调方法体中。
效果
效果图
结尾:目前正在学习iOS开发,技术小白,文章如有什么错误请指出,万分感谢!!!