iOS ReplayKit 與 RTC

  • 作者:聲網(wǎng)Agora Cavan*
    在日益繁多的直播場景中静暂,如果你也是某位游戲主播的粉絲的話济丘,有一種直播方式是你一定不陌生的,那就是我們今天要聊的屏幕分享。

直播場景下的屏幕分享摹迷,不僅要將當(dāng)前顯示器所展示的畫面分享給遠(yuǎn)端疟赊,也要將聲音傳輸出去,包括應(yīng)用的聲音峡碉,以及主播的聲音近哟。鑒于這兩點(diǎn)需求,我們可以簡單分析出异赫,進(jìn)行一次屏幕分享的直播所需要的媒體流如下:

  1. 一條顯示器畫面的視頻流
  2. 一條應(yīng)用聲音的音頻流
  3. 一條主播聲音的音頻流

ReplayKit 是蘋果提供的用于 iOS 系統(tǒng)進(jìn)行屏幕錄制的框架椅挣。

首先我們來看看蘋果提供的用于屏幕錄制的 ReplayKit 的數(shù)據(jù)回調(diào)接口:

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
        DispatchQueue.main.async {
            switch sampleBufferType {
            case .video:
                AgoraUploader.sendVideoBuffer(sampleBuffer)
            case .audioApp:
                AgoraUploader.sendAudioAppBuffer(sampleBuffer)
            case .audioMic:
                AgoraUploader.sendAudioMicBuffer(sampleBuffer)
            @unknown default:
                break
            }
        }
    }

從枚舉 sampleBufferType 上头岔,我們不難看出塔拳,剛好能符合我們上述對媒體流的需求。

視頻

格式

guard let videoFrame = CMSampleBufferGetImageBuffer(sampleBuffer) else {
    return
}
        
let type = CVPixelBufferGetPixelFormatType(videoFrame)
type = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange

通過 CVPixelBufferGetPixelFormatType峡竣,我們可以獲取到每幀的視頻格式為 yuv420靠抑。

幀率

通過打印接口的回調(diào)次數(shù),可以知道每秒能夠獲取的視頻幀為30次适掰,也就是幀率為 30颂碧。

格式與幀率都能符合 Agora RTC 所能接收的范圍,所以通過 Agora RTC 的 pushExternalVideoFrame 就可以將視頻分享到遠(yuǎn)端了类浪。

agoraKit.pushExternalVideoFrame(frame)

插入一個(gè)小知識

顯示器所顯示的幀來自于一個(gè)幀緩存區(qū)载城,一般常見的為雙緩存或三緩存。當(dāng)屏幕顯示完一幀后费就,發(fā)出一個(gè)垂直同步信號(V-Sync)诉瓦,告訴幀緩存區(qū)切換到下一幀的緩存上,然后顯示器開始讀取新的一幀數(shù)據(jù)做顯示力细。

這個(gè)幀緩存區(qū)是系統(tǒng)級別的睬澡,一般的開發(fā)者是無法讀取跟寫入的。但是如果是蘋果自身提供的錄制框架 ReplayKit 能夠直接讀取到已經(jīng)渲染好且將用于顯示器的幀眠蚂,且這一過程不會影響渲染流程而造成掉幀煞聪,那就能減少一次用于提供給 ReplayKit 回調(diào)數(shù)據(jù)的渲染過程。

音頻

ReplayKit 能提供的音頻有兩種逝慧,分為麥克風(fēng)錄制進(jìn)來的音頻流昔脯,與當(dāng)前響應(yīng)的應(yīng)用播放的音頻流。(下文將前者稱為 AudioMic笛臣,后者為 AudioApp)

可以通過下面的兩行代碼云稚,來獲取音頻格式

CMAudioFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
const AudioStreamBasicDescription *description = CMAudioFormatDescriptionGetStreamBasicDescription(format);

AudioApp

AudioApp 會在不同的機(jī)型下有不一樣的聲道數(shù)。例如在 iPad 或 iPhone7 以下機(jī)型中捐祠,不具備雙聲道播放的設(shè)備碱鳞,這時(shí)候 AudioApp 的數(shù)據(jù)就是單聲道,反之則是雙聲道踱蛀。

采樣率在部分試過的機(jī)型里窿给,都是 44100贵白,但不排除在未測試過的機(jī)型會是其他的采樣率。

AudioMic

AudioMic 在測試過的機(jī)型里崩泡,采樣率為 32000禁荒,聲道數(shù)為單聲道。

音頻前處理

如果我們將 AudioApp 與 AudioMic 作為兩條音頻流去發(fā)送角撞,那么流量肯定是大于一條音頻流的呛伴。我們?yōu)榱斯?jié)省一條音頻流的流量,就需要將這兩條音頻流做混音(融合)谒所。

但是通過上述热康,我們不難看出,兩條音頻流的格式是不一樣的劣领,而且不能保證隨著機(jī)型的不同姐军,是不是會出現(xiàn)其他的格式。在測試的過程中還發(fā)現(xiàn) OS 版本的不同尖淘,每次回調(diào)給到的音頻數(shù)據(jù)長度也會出現(xiàn)變化奕锌。那么我們在對兩條音頻流做混音前,就需要進(jìn)行格式統(tǒng)一村生,來應(yīng)對 ReplayKit 給出的各種格式惊暴。所以我們采取了以下幾個(gè)重要的步驟:

     if (channels == 1) {
        int16_t* intData = (int16_t*)dataPointer;
        int16_t newBuffer[totalSamples * 2];
                
        for (int i = 0; i < totalSamples; i++) {
            newBuffer[2 * i] = intData[i];
            newBuffer[2 * i + 1] = intData[i];
        }
        totalSamples *= 2;
        memcpy(dataPointer, newBuffer, sizeof(int16_t) * totalSamples);
        totalBytes *= 2;
        channels = 2;
    }
  • 無論是 AudioMic 還是 AudioApp,只要進(jìn)來的流為單聲道趁桃,我們都將它轉(zhuǎn)化為雙聲道辽话;
     if (sampleRate != resampleRate) {
        int inDataSamplesPer10ms = sampleRate / 100;
        int outDataSamplesPer10ms = (int)resampleRate / 100;
        
        int16_t* intData = (int16_t*)dataPointer;
        
        switch (type) {
            case AudioTypeApp:
                totalSamples = resampleApp(intData, dataPointerSize, totalSamples,
                                           inDataSamplesPer10ms, outDataSamplesPer10ms, channels, sampleRate, (int)resampleRate);
                break;
            case AudioTypeMic:
                totalSamples = resampleMic(intData, dataPointerSize, totalSamples,
                                           inDataSamplesPer10ms, outDataSamplesPer10ms, channels, sampleRate, (int)resampleRate);
                break;
        }
        
        totalBytes = totalSamples * sizeof(int16_t);
    }
  • 無論是 AudioMic 還是 AudioApp,只要進(jìn)來的流采樣率不為 48000镇辉,我們將它們重采樣為 48000屡穗;
  memcpy(appAudio + appAudioIndex, dataPointer, totalBytes);
  appAudioIndex += totalSamples;
    memcpy(micAudio + micAudioIndex, dataPointer, totalBytes);
  micAudioIndex += totalSamples;
  • 通過第一步與第二步,我們保證了兩條音頻流都為同樣的音頻格式忽肛。但是由于 ReplayKit 是一次回調(diào)給到一種數(shù)據(jù)的村砂,所以在混音前我們還得用兩個(gè)緩存區(qū)來存儲這兩條流數(shù)據(jù);
  int64_t mixIndex = appAudioIndex > micAudioIndex ? micAudioIndex : appAudioIndex;
            
  int16_t pushBuffer[appAudioIndex];
            
  memcpy(pushBuffer, appAudio, appAudioIndex * sizeof(int16_t));
            
  for (int i = 0; i < mixIndex; i ++) {
       pushBuffer[i] = (appAudio[i] + micAudio[i]) / 2;
  }
  • ReplayKit 有選項(xiàng)是否開啟麥克風(fēng)錄制屹逛,所以在關(guān)閉麥克風(fēng)錄制的時(shí)候础废,我們就只有一條 AudioApp 音頻流。所以我們以這條流為主罕模,去讀取 AudioMic 緩存區(qū)的數(shù)據(jù)長度评腺,然后對比兩個(gè)緩存區(qū)的數(shù)據(jù)長度,以最小的數(shù)據(jù)長度為我們的混音長度淑掌。將混音長度的兩個(gè)緩存區(qū)里的數(shù)據(jù)做融合蒿讥,得到混音后的數(shù)據(jù),寫入一個(gè)新的混音緩存區(qū)(或者直接寫入 AudioApp 緩存區(qū));
[AgoraAudioProcessing pushAudioFrame:(*unsigned* *char* *)pushBuffer
                                   withFrameSize:appAudioIndex * *sizeof*(int16_t)];
  • 最后我們再將這段混音后的數(shù)據(jù)拷貝進(jìn) Agora RTC 的 C++ 錄制回調(diào)接口里芋绸,這時(shí)候就可以把麥克風(fēng)錄制的聲音與應(yīng)用播放的聲音傳輸?shù)竭h(yuǎn)端了媒殉。

通過對音視頻流的處理,結(jié)合 Agora RTC SDK摔敛,我們就完成了一個(gè)屏幕分享直播場景的實(shí)現(xiàn)了廷蓉。

具體的實(shí)現(xiàn)上的細(xì)節(jié),可以參考 https://github.com/AgoraIO/Advanced-Video/tree/master/iOS%26macOS/Agora-Screen-Sharing/Agora-Screen-Sharing-iOS

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末马昙,一起剝皮案震驚了整個(gè)濱河市桃犬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌行楞,老刑警劉巖攒暇,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異敢伸,居然都是意外死亡扯饶,警方通過查閱死者的電腦和手機(jī)恒削,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門池颈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钓丰,你說我怎么就攤上這事躯砰。” “怎么了携丁?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵琢歇,是天一觀的道長。 經(jīng)常有香客問我梦鉴,道長李茫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任肥橙,我火速辦了婚禮魄宏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘存筏。我一直安慰自己宠互,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布椭坚。 她就那樣靜靜地躺著予跌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪善茎。 梳的紋絲不亂的頭發(fā)上券册,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼烁焙。 笑死略吨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的考阱。 我是一名探鬼主播翠忠,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼乞榨!你這毒婦竟也來了秽之?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吃既,失蹤者是張志新(化名)和其女友劉穎考榨,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鹦倚,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡河质,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了震叙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掀鹅。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖媒楼,靈堂內(nèi)的尸體忽然破棺而出乐尊,到底是詐尸還是另有隱情,我是刑警寧澤划址,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布扔嵌,位于F島的核電站,受9級特大地震影響夺颤,放射性物質(zhì)發(fā)生泄漏痢缎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一世澜、第九天 我趴在偏房一處隱蔽的房頂上張望独旷。 院中可真熱鬧,春花似錦宜狐、人聲如沸势告。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咱台。三九已至,卻和暖如春俭驮,著一層夾襖步出監(jiān)牢的瞬間回溺,已是汗流浹背春贸。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遗遵,地道東北人萍恕。 一個(gè)月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像车要,于是被迫代替她去往敵國和親允粤。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容