项目有个需求,需要把ios设备上的操作画面实时传输出去,也就是类似推流手机直播画面的方案。
一番调研后发现在ios中,我们可以通过ios自带ReplayKit框架实现。
关于ReplayKit的讲解,这篇文章写的很好,可以看一下
iOS端使用replaykit录制屏幕的技术细节
文章详细介绍了ReplayKit的发展历程,从ios9~ios12的每个版本的功能迭代都有写,包括如何录制当前app内容,还是制系统层次的内容等。
不过由于我的需求是只录制当前App内容,所以下面只讲解这方面的。
我的测试demo流程大概这样
1、通过ReplayKit开启录屏
2、实时获取视频流CMSampleBuffer
3、对CMSampleBuffer处理发包或推流
为了效果快速呈现,这里我采取udp发包来传输内容
以下代码仅供参考逻辑。
1、开启录屏
/// 开启录制屏幕
func startRecord() {
if !RPScreenRecorder.shared().isAvailable{
print("暂不支持xxx功能")
return
}
if #available(iOS 11.0, *) {
printDebug(message: "start record")
if _udpSocket == nil{
//初始化udp
initUdp()
connectUdp()
queneConvertImage = DispatchQueue(label: "teacher.show.quene")
}
isScreenRecording = true
weak var weakself = self
//该方法只能录当前app,如果需要录系统的,用broadcastxxx那个方法
RPScreenRecorder.shared().startCapture(handler: { (sampleBuffer, sampleBufferType, error) in
if error == nil{
if CMSampleBufferDataIsReady(sampleBuffer) && sampleBufferType == RPSampleBufferType.video{
weakself?.queneConvertImage.async {
weakself?.getUIImageFromCMSampleBuffer(sampleBuffer: sampleBuffer)
}
}
}else{
printDebug(message: error.debugDescription)
}
}) { (finishError) in
}
} else {
// Fallback on earlier versions
print("xxx功能需要ios11版本及以上")
}
}
2、对视频文件进行处理
func getUIImageFromCMSampleBuffer(sampleBuffer:CMSampleBuffer){
/*
关于两种压缩系数结果测试如下:
UIImageJPEGRepresentation:
0.01-63000 0.1-63000 0.2-67000 0.3-73000 0.4-85000 0.5-97000 0.6-110000 0.9-150000 1-290000
UIImagePNGRepresentation:220000
*/
let image1 = K12SampleBufferTool.image(from: sampleBuffer)
if let data = UIImageJPEGRepresentation(image1!, 0.1),_udpSocket != nil{
//两次包相同,就忽略本次发送
if lastBufferlen > 0 && fabs(Double(lastBufferlen - data.count)) < 100{
return
}
lastBufferlen = data.count
compressData(data: data, image: image1!)
}
}
/// 压缩图片发送
///
/// - Parameters:
/// - odata: <#odata description#>
/// - image: <#image description#>
func compressData(data:Data,image:UIImage){
//质量压缩符合大小
if data.count < maxLength{
printDebug(message: "--- data:\(data) ")
_udpSocket?.send(data, withTimeout: -1, tag: 0)
return
}
let rate : Double = data.count > maxLength * 2 ? 1.0/Double(data.count/maxLength) : 0.5
//采用size压缩再次处理
if let d = UIImageJPEGRepresentation(image.compress(with: rate), 0.0),_udpSocket != nil{
//压缩过还超过最大值,就不发送
if d.count > maxLength{
return
}
printDebug(message: "----data:\(data) ------compressdata.size:\(d)")
_udpSocket?.send(d, withTimeout: -1, tag: 0)
}
}
buffer转iamge的方法
+ (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer{
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CGImageRef image = NULL;
if (@available(iOS 9.0, *)) {
OSStatus createdImage = VTCreateCGImageFromCVPixelBuffer(imageBuffer, NULL, &image);
UIImage * image1 = nil;
if (createdImage == noErr) {
image1 = [UIImage imageWithCGImage:image];
}
CGImageRelease(image);
return image1;
} else {
return nil;
}
}
以上是简单的测试,udp发送这块可以自行修改自己的逻辑。到这里,内容发出去了
3、停止录制
/// 停止录制屏幕
func stopRecord() {
if #available(iOS 11.0, *) {
printDebug(message: "stop record")
isScreenRecording = false
RPScreenRecorder.shared().stopCapture { (error) in
if error != nil{
printDebug(message: "stopRecord success")
}
}
} else {
// Fallback on earlier versions
}
//关闭udp
nilSocket()
}
结束之后记得调用一下关闭方法。
总结:
1、由于是采取图片方式udp发送,在CMSampleBuffer转image过程还是比较耗cpu的,但是,录屏本事对cpu和内存对占用是极少的。
2、CMSampleBuffer的大小是根据画面色彩度来的,如果画面色彩很多,bytes会比较大。
3、如果不需要实时录制,可以采用提供的结束统一获取视频的方式,那种更简单。
4、用这个方法实时录制需要ios11系统,这点是个硬伤;但关于另一个录制系统方法,好像ios10就可以了,不过这种实现方式,用户感知比较明显,具体看上文的文章连接。
5、录屏必须真机测试。