乐趣区

关于ios:iOS-ReplayKit-与-RTC

作者:声网 Agora Cavan
在日益繁多的直播场景中,如果你也是某位游戏主播的粉丝的话,有一种直播形式是你肯定不生疏的,那就是咱们明天要聊的屏幕分享。

直播场景下的屏幕分享,不仅要将以后显示器所展现的画面分享给远端,也要将声音传输进来,包含利用的声音,以及主播的声音。鉴于这两点需要,咱们能够简略剖析出,进行一次屏幕分享的直播所须要的媒体流如下:

  1. 一条显示器画面的视频流
  2. 一条利用声音的音频流
  3. 一条主播声音的音频流

ReplayKit 是苹果提供的用于 iOS 零碎进行屏幕录制的框架。

首先咱们来看看苹果提供的用于屏幕录制的 ReplayKit 的数据回调接口:

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

帧率

通过打印接口的回调次数,能够晓得每秒可能获取的视频帧为 30 次,也就是帧率为 30。

格局与帧率都能合乎 Agora RTC 所能接管的范畴,所以通过 Agora RTC 的 pushExternalVideoFrame 就能够将视频分享到远端了。

agoraKit.pushExternalVideoFrame(frame)

插入一个小常识

显示器所显示的帧来自于一个帧缓存区,个别常见的为双缓存或三缓存。当屏幕显示完一帧后,收回一个垂直同步信号 (V-Sync),通知帧缓存区切换到下一帧的缓存上,而后显示器开始读取新的一帧数据做显示。

这个帧缓存区是零碎级别的,个别的开发者是无奈读取跟写入的。然而如果是苹果本身提供的录制框架 ReplayKit 可能间接读取到曾经渲染好且将用于显示器的帧,且这一过程不会影响渲染流程而造成掉帧,那就能缩小一次用于提供给 ReplayKit 回调数据的渲染过程。

音频

ReplayKit 能提供的音频有两种,分为麦克风录制进来的音频流,与以后响应的利用播放的音频流。(下文将前者称为 AudioMic,后者为 AudioApp)

能够通过上面的两行代码,来获取音频格式

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

AudioApp

AudioApp 会在不同的机型下有不一样的声道数。例如在 iPad 或 iPhone7 以下机型中,不具备双声道播放的设施,这时候 AudioApp 的数据就是单声道,反之则是双声道。

采样率在局部试过的机型里,都是 44100,但不排除在未测试过的机型会是其余的采样率。

AudioMic

AudioMic 在测试过的机型里,采样率为 32000,声道数为单声道。

音频前解决

如果咱们将 AudioApp 与 AudioMic 作为两条音频流去发送,那么流量必定是大于一条音频流的。咱们为了节俭一条音频流的流量,就须要将这两条音频流做混音 (交融)。

然而通过上述,咱们不难看出,两条音频流的格局是不一样的,而且不能保障随着机型的不同,是不是会呈现其余的格局。在测试的过程中还发现 OS 版本的不同,每次回调给到的音频数据长度也会呈现变动。那么咱们在对两条音频流做混音前,就须要进行格局对立,来应答 ReplayKit 给出的各种格局。所以咱们采取了以下几个重要的步骤:

     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,只有进来的流为单声道,咱们都将它转化为双声道;
     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,只有进来的流采样率不为 48000,咱们将它们重采样为 48000;
  memcpy(appAudio + appAudioIndex, dataPointer, totalBytes);
  appAudioIndex += totalSamples;
    memcpy(micAudio + micAudioIndex, dataPointer, totalBytes);
  micAudioIndex += totalSamples;
  • 通过第一步与第二步,咱们保障了两条音频流都为同样的音频格式。然而因为 ReplayKit 是一次回调给到一种数据的,所以在混音前咱们还得用两个缓存区来存储这两条流数据;
  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 有选项是否开启麦克风录制,所以在敞开麦克风录制的时候,咱们就只有一条 AudioApp 音频流。所以咱们以这条流为主,去读取 AudioMic 缓存区的数据长度,而后比照两个缓存区的数据长度,以最小的数据长度为咱们的混音长度。将混音长度的两个缓存区里的数据做交融,失去混音后的数据,写入一个新的混音缓存区 (或者间接写入 AudioApp 缓存区);
[AgoraAudioProcessing pushAudioFrame:(*unsigned* *char* *)pushBuffer
                                   withFrameSize:appAudioIndex * *sizeof*(int16_t)];
  • 最初咱们再将这段混音后的数据拷贝进 Agora RTC 的 C++ 录制回调接口里,这时候就能够把麦克风录制的声音与利用播放的声音传输到远端了。

通过对音视频流的解决,联合 Agora RTC SDK,咱们就实现了一个屏幕分享直播场景的实现了。

具体的实现上的细节,能够参考 https://github.com/AgoraIO/Ad…

退出移动版