一、實現目標
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);
五、最終效果