ReplayKit2 有线投屏项目总结
一、实现目标
iOS11.0以上设备通过USB线连接电脑,在电脑端实时看到手机屏幕内容
画质达到超清720级别,码率可达到1Mbps以上
二、实现技术方案设计
1、手机端采用ReplayKit2框架,在Upload Extension 进程中采集到屏幕内容YUV和系统声音PCM+麦克风声音PCM
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType { switch (sampleBufferType) { case RPSampleBufferTypeVideo: break; case RPSampleBufferTypeAudioApp: break; case RPSampleBufferTypeAudioMic: break; default: break; } }
2、考虑在在Upload Extension 进程中或者主App进程中对图像和声音进行编码,编码成H264+AAC ,然后封装为FLV格式的包,利用RTMP协议进行推流
因为目前已经存在一套推流的接口,所以考虑在PC端增加RTMP收流服务,进行解析视频流,然后渲染
3、在PC端建立RTMP收流服务端,解码,渲染;目前OBS已经存在相关模块
三、遇到的问题以及解决方案
1、如果在局域网中,目前的基础上,无线推流到PC和推流到远程直播服务器流程基本一样
2、如何规避局域网的网络抖动环境,实现高清推流?局域网可能因为多人使用导致带宽分配原因,以及信道干扰原因导致上行速率达不到标称要求
采用有线方案可以解决这个问题,那么手机如何利用USB线传递数据?
3、USB传递有线数据有两种方案:
第一种是MIFI认证,使用iOS外设通信的库,ExternalAccessory
第二种是通过iproxy , 在PC端执行"iproxy pcport mobileport"的方式实现端口转发,PC上连接pcport会连接到手机的mobileport,当一条TCP连接建立成功之后手机就可以利用USB线和PC实现双向通信了
这里为什么不能像安卓一样,实现正向的转发,将手机的端口转发到PC上呢?这就是iOS系统相对封闭的原因;
猜测安卓连接USB线的时候,PC端执行命令会在手机端出发操作实现端口转发规则;而iOS不行
那么最终采用的是第二种方案。
四、推流SDK协议改造
对于采用的第二种方案,实施的时候遇到两个问题?
第一个如何实现由PC主动连接手机的过程,连接手机的哪个端口?
对于这个问题,这里解决方案是,第一个在socket上面设置套接字为REUSE相关的属性,保证端口能够重复绑定成功,这里假定这个1397端口只有这个程序使用
第二个是在有线投屏的时候,手机要先扫码得到PC的一个key,手机在启动一个TCP监听后将端口号联系这个key一起发给我们的后台,后台通过push或者pc pull的方式,将这个信息通知到PC端,也就是建立信道的方式
第二个问题,如何在一个RTMP.c的主动发起连接中,修改原有的方式,先尝试被动连接(先启动一个同步阻塞的监听socket等待PC连接)。在这个逻辑中,因为等待过程是阻塞的,必然涉及到延时,在这里遇到了坑
我们希望在 tcp socket bind一个端口,然后listen,然后accept的时候,希望在accept这个方法实现超时逻辑,最开始是这样实现的
int ret = ::setsockopt(m_nRealServerSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*) &tv, sizeof(tv)); LOGW("socket accept start 1, set timeout ret = %d", ret); ret = ::setsockopt(m_nRealServerSocket, SOL_SOCKET, SO_SNDTIMEO, (const char*) &tv, sizeof(tv));
上述的代码在安卓和PC上面生效,但是在iOS平台上面无效,虽然设置了一个超时时间,但是这个超时永远不会触发,accept永久阻塞
为了规避这个问题,我采用select监听文件描述符的方式,select跨平台兼容性效果更好
采用以下代码实现accept超时逻辑:
int fd = -1; fd_set fdflag; sockaddr_in client_addr; memset(&client_addr, 0, sizeof(client_addr)); FD_ZERO(&fdflag); FD_SET(m_nRealServerSocket, &fdflag); LOGW("socket accept start, timeout = %d secs", tv.tv_sec); bool hasProcessConnect = false; if(!hasProcessConnect && select(m_nRealServerSocket + 1, &fdflag, NULL, NULL, &tv) > 0) { hasProcessConnect = true; fd = accept(m_nRealServerSocket, (struct sockaddr*)NULL, NULL); } // 一次事件触发之后, 清理监控的描述符 FD_ZERO(&fdflag);
五、最终效果