WEBRTC基本概念

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WEBRTC基本概念相关的知识,希望对你有一定的参考价值。

参考技术A AIMD英文全称:Additive Increase Multiplicative Decrease。TCP/IP模型中,属于[运输层],为了解决[拥塞控制]的一个方法,即:加性增,乘性减,或者叫做“和式增加,积式减少”。

  aimd controller是TCP底层的码率调节概念,但是WebRTC并没有完全照搬TCP的机制,而是设计了套自己的算法。
  如果处于Incr状态,增加码率的方式分为两种:一种是通信会话刚刚开始,相当于TCP慢启动,它会进行一个倍数增加,当前使用的码率乘以系数,系数是1.08;如果是持续在通信状态,其增加的码率值是当前码率在一个RTT时间周期所能传输的数据速率。
  如果处于Decrease状态,递减原则是:过去500ms时间窗内的最大acked bitrate乘上系数0.85,acked bitrate通过feedback反馈过来的报文序号查找本地发送列表就可以得到。
aimd根据上面的规则最终计算到的码率就是基于延迟拥塞评估到的bwe bitrate码率。

WebRTC在发送端收到来自接收端的RTCP RR报文,根据其Report Block中携带的丢包率信息,动态调整发送端码率As。

基于延迟的码率控制运行在接收端,WebRTC根据数据包到达的时间延迟,通过到达时间滤波器,估算出网络延迟m(t),然后经过过载检测器判断当前网络的拥塞状况,最后在码率控制器根据规则计算出远端估计最大码率Ar。然后,通过RTCP REMB报文返回Ar等到发送端。

发送端综合As、Ar和预配置的上下限,计算出最终的目标码率A,该码率会作用到Encoder、RTP和PacedSender等模块,控制发送端的码率。

  GCC算法充分考虑丢包率和延迟对码率的影响,在实时通讯应用(如视频会议)中能够发挥良好效果。然而,在某些特定应用场景下(比如实时在线编辑),GCC算法的表现不太让人满意,主要体现在它应对峰值流量的能力上,具体表现在:
1)算法一开始基于Increase状态增加码率,当检测到Decrease状态时调用Ar[t(i)] = Alpha * Rr[t(i)],这个时候实时码率Rr(ti)可能远小于Ar[t(i-1)],这样在后续过程中Ar处于较低水平;此时若有视频关键帧冲击,则数据包大量在PacedSender的队列中排队,造成较大排队延迟。
2)基于1)中论述的情况,码率估计模块反馈给Codec的编码码率很低,但编码器需要编码关键帧时,内部的码率控制模块控制出的最小码率仍然大于反馈码率。这两种情况都会造成较大的发送端排队延迟,进而在接收端造成较大的JitterBuffer延迟,最终导致端到端延迟到达500ms的水平,这在实时在线编辑应用中是无法容忍的。
基于此,Google官方从WebRTC M55开始引入新的码率估计算法,把所有码率计算模块都移动到发送端,并采用全新的Trendline滤波器,基于码率探测机制快速准确地估计出实时码率。

WebRTC在评估延迟差的时候不是对每个包进行估算,而是采用了包组间进行延迟评估,这符合视频传输(视频帧是需要切分成多个UDP包)的特点,也减少了频繁计算带来的误差。那么什么是包组呢?就是距包组中第一个包的发送时刻t0小于5毫秒发送的所有的包成为一组,第一个超过5毫秒的包作为下一个包组第一个包。

Android端WebRTC本地音视频采集流程源码分析

WebRTC源码版本为:org.webrtc:google-webrtc:1.0.32006 本文仅分析Java层源码,在分析之前,先说明一下一些重要类的基本概念。

  • MediaSource:WebRTC媒体资源数据来源,它有两个子类:AudioSource(音频资源)、VideoSource(视频资源);
  • MediaStreamTrack:媒体资源轨,一个MediaStreamTrack对应一个MediaSource,创建媒体轨需要MediaSource,同样,它也有两个子类:AudioTrack(音频轨)对应AudioSource、VideoTrack(视频轨)对应VideoSource;
  • MediaStream:媒体流,一个媒体流可以添加多条AudioTrack和VideoTrack,一般来说我们对应分别只添加一条。

在使用WebRTC进行音视频通话时,需要构建一个PeerConnectionFactory,这是一个连接工厂,创建本地LocalMediaStream流及客户端PeerConnection等,都需要使用。构建大致代码如下:

//在创建PeerConnectionFactory之前,必须至少调用一次。不得在 PeerConnectionFactory 处于活动状态时调用。
PeerConnectionFactory.initialize(
    PeerConnectionFactory.InitializationOptions
        .builder(applicationContext).createInitializationOptions()
)
val eglBaseContext = EglBase.create().eglBaseContext
//视频编码工厂
val encoderFactory= DefaultVideoEncoderFactory(eglBaseContext, true, true)
//视频解码工厂
val decoderFactory = DefaultVideoDecoderFactory(eglBaseContext)
val audioDeviceModule = JavaAudioDeviceModule.builder(this)
            .setSamplesReadyCallback  audioSamples ->
                 //麦克风输入数据,即通话时LocalMediaStream的Audio的数据,pcm格式,通常用于录音。

            
            .createAudioDeviceModule()
val peerConnectionFactory = PeerConnectionFactory.builder()
            .setVideoEncoderFactory(encoderFactory)
            .setVideoDecoderFactory(decoderFactory)
            .setAudioDeviceModule(audioDeviceModule)
            .createPeerConnectionFactory()

若你有需求WebRTC视频编码要启用H.264,请参考Android端WebRTC启用H264编码

MediaSource

WebRTC媒体资源数据来源基类。

AudioSource(音频源)

通过 peerConnectionFactory.createAudioSource(MediaConstraints) 创建,参数为媒体约束,大致如下:

    //音频源
    val audioSource = peerConnectionFactory.createAudioSource(createAudioConstraints())
    private fun createAudioConstraints(): MediaConstraints 
        val audioConstraints = MediaConstraints()
        //回声消除
        audioConstraints.mandatory.add(
            MediaConstraints.KeyValuePair(
                "googEchoCancellation",
                "true"
            )
        )
        //自动增益
        audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
        //高音过滤
        audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true"))
        //噪音处理
        audioConstraints.mandatory.add(
            MediaConstraints.KeyValuePair(
                "googNoiseSuppression",
                "true"
            )
        )
        return audioConstraints
    

事实上音频的输入、输出具体处理在JavaAudioDeviceModule

package org.webrtc.audio;
/**
 * AudioDeviceModule implemented using android.media.AudioRecord as input and
 * android.media.AudioTrack as output.
 */
public class JavaAudioDeviceModule implements AudioDeviceModule 
    ...
    /**
     * 本地麦克风采集的输入数据,android.media.AudioRecord
     */
    private final WebRtcAudioRecord audioInput;
    /**
     * 用于播放通话时对方的音频数据,android.media.AudioTrack
     */
    private final WebRtcAudioTrack audioOutput;
    ...

记录本地麦克风采集的音频数据WebRtcAudioRecord

package org.webrtc.audio;

import android.media.AudioRecord;

class WebRtcAudioRecord 
    ...
    private AudioRecord audioRecord;
    /**
     * 读取线程
     */
    private AudioRecordThread audioThread;

  /**
   * Audio thread which keeps calling ByteBuffer.read() waiting for audio
   * to be recorded. Feeds recorded data to the native counterpart as a
   * periodic sequence of callbacks using DataIsRecorded().
   * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority.
   */
    private class AudioRecordThread extends Thread 
        private volatile boolean keepAlive = true;

        public AudioRecordThread(String name) 
            super(name);
        

        public void run() 
            Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
             //确保在采集数据状态
            assertTrue(audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING);
            //向外回调开始状态
            doAudioRecordStateCallback(AUDIO_RECORD_START);
            //存活状态
            while(keepAlive) 
                int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity());
                if (bytesRead == byteBuffer.capacity()) 
                    if (microphoneMute) 
                        //如果麦克风静音,则清空数据
                        byteBuffer.clear();
                        byteBuffer.put(WebRtcAudioRecord.this.emptyBytes);
                    

                    if (keepAlive) 
                        //调用native方法发送数据
                        nativeDataIsRecorded(nativeAudioRecord, bytesRead);
                    
                    //向外回调音频数据,pcm格式
                    if (audioSamplesReadyCallback != null) 
                        byte[] data = Arrays.copyOfRange(.byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.capacity() + byteBuffer.arrayOffset());
                        audioSamplesReadyCallback.onWebRtcAudioRecordSamplesReady(new AudioSamples(audioRecord.getAudioFormat(), audioRecord.getChannelCount(), audioRecord.getSampleRate(), data));
                    
                 else 
                    String errorMessage = "AudioRecord.read failed: " + bytesRead;
                    Logging.e("WebRtcAudioRecordExternal", errorMessage);
                    if (bytesRead ==AudioRecord.ERROR_INVALID_OPERATION) 
                        keepAlive = false;
                        reportWebRtcAudioRecordError(errorMessage);
                    
                
            

            try 
                if (audioRecord != null) 
                    audioRecord.stop();
                    //向外回调停止状态
                    doAudioRecordStateCallback(AUDIO_RECORD_STOP);
                
             catch (IllegalStateException e) 
                Logging.e("WebRtcAudioRecordExternal", "AudioRecord.stop failed: " + e.getMessage());
            
        
        /*
         * 结束读取
         */
        public void stopThread() 
            Logging.d("WebRtcAudioRecordExternal", "stopThread");
            this.keepAlive = false;
        
    
    ...

播放接收的音频数据WebRtcAudioTrack(这里不属于采集流程,提了一嘴):

package org.webrtc.audio;

import android.media.AudioTrack;

class WebRtcAudioTrack 
    ...
    private AudioTrack audioTrack;
    /**
     * 读取线程
     */
    private AudioTrackThread audioThread;

  /**
   * Audio thread which keeps calling AudioTrack.write() to stream audio.
   * Data is periodically acquired from the native WebRTC layer using the
   * nativeGetPlayoutData callback function.
   * This thread uses a Process.THREAD_PRIORITY_URGENT_AUDIO priority.
   */
    private class AudioTrackThread extends Thread 
        private volatile boolean keepAlive = true;

        public AudioTrackThread(String name) 
            super(name);
        

        public void run() 
            Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
            //确认状态
            assertTrue(audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING);
            //向外回调开始状态
            doAudioTrackStateCallback(AUDIO_TRACK_START);

            for(int sizeInBytes = byteBuffer.capacity(); keepAlive; byteBuffer.rewind()) 
                //调用native方法将播放数据写入到byteBuffer中
                nativeGetPlayoutData(nativeAudioTrack, sizeInBytes);
                assertTrue(sizeInBytes <= byteBuffer.remaining());
                if (speakerMute) 
                    //扬声器静音,清空数据
                    byteBuffer.clear();
                    byteBuffer.put(emptyBytes);
                    byteBuffer.position(0);
                
                //写入到AudioTrack中,进行播放,pcm格式数据
                int bytesWritten = writeBytes(audioTrack, byteBuffer, sizeInBytes);
                if (bytesWritten != sizeInBytes) 
                    Logging.e("WebRtcAudioTrackExternal", "AudioTrack.write played invalid number of bytes: " + bytesWritten);
                    if (bytesWritten < 0) 
                        keepAlive = false;
                        reportWebRtcAudioTrackError("AudioTrack.write failed: " + bytesWritten);
                    
                
            

        

        private int writeBytes(AudioTrack audioTrack, ByteBuffer byteBuffer, int sizeInBytes) 
            return VERSION.SDK_INT >= 21 ? audioTrack.write(byteBuffer, sizeInBytes, 0) : audioTrack.write(byteBuffer.array(), byteBuffer.arrayOffset(), sizeInBytes);
        

        public void stopThread() 
            Logging.d("WebRtcAudioTrackExternal", "stopThread");
            keepAlive = false;
        
    
    ...

上述的WebRtcAudioRecordWebRtcAudioTrack两个类都不是public,在WebRTC中只对外暴露出了本地麦克风采集数据,未暴露音频输出数据,如果你有获取WebRTC通话接收到的音频数据需求,请参考Android端WebRTC音视频通话录音-获取音频输出数据

VideoSource(视频源)

VideoSource是一个视频源,用于二次处理视频数据(VideoProcessor)和调整大小、格式和发送视频数据(具体实现是jni的包装类NativeAndroidVideoTrackSource)。 VideoSource视频的获取需要通过CapturerObserver,视频捕捉器可以通过videoSource.getCapturerObserver()获取捕捉器观察者,将数据传给VideoSource。 先看一下VideoSource创建使用流程:

    //视频捕捉器
    val videoCapturer = createVideoCapture(context)
    videoCapturer?.let capturer->
        val videoSource = peerConnectionFactory.createVideoSource(capture.isScreencast)
        //使用 SurfaceTexture 创建 WebRTC VideoFrames 的帮助类。为了创建 WebRTC VideoFrames,渲染到 SurfaceTexture。帧将传递给侦听器。
        val surfaceTextureHelper =
                SurfaceTextureHelper.create("surface_texture_thread", eglBaseContext)
            //将surfaceTextureHelper和CapturerObserver传给VideoCapturer
            capture.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
    
    /**
     * 创建相机视频捕捉器
     */
    private fun createVideoCapture(context: Context): CameraVideoCapturer? 
        val enumerator: CameraEnumerator = if (Camera2Enumerator.isSupported(context)) 
            Camera2Enumerator(context)
         else 
            Camera1Enumerator()
        

        for (name in enumerator.deviceNames) 
            if (enumerator.isFrontFacing(name)) 
                return enumerator.createCapturer(name, null)
            
        
        for (name in enumerator.deviceNames) 
            if (enumerator.isBackFacing(name)) 
                return enumerator.createCapturer(name, null)
            
        
        return null
    

我们来看一下VideoSource关键代码:

package org.webrtc;

public class VideoSource extends MediaSource 
    private final NativeAndroidVideoTrackSource nativeAndroidVideoTrackSource;
    /**
     * 视频处理器,可自行对图像数据进行二次处理;
     * 如:添加水印、视频旋转、剪裁等。
     */
    @Nullable
    private VideoProcessor videoProcessor;
    private final Object videoProcessorLock = new Object();
    private final CapturerObserver capturerObserver = new CapturerObserver() 
        /**
         * 开始捕捉
         */
        @Override
        public void onCapturerStarted(boolean success) 
            nativeAndroidVideoTrackSource.setState(success);
            synchronized(videoProcessorLock) 
                isCapturerRunning = success;
                if (videoProcessor != null) 
                    videoProcessor.onCapturerStarted(success);
                
            
        
        /**
         * 停止捕捉
         */
        @Override
        public void onCapturerStopped() 
            nativeAndroidVideoTrackSource.setState(false);
            synchronized(videoProcessorLock) 
                isCapturerRunning = false;
                if (videoProcessor != null) 
                    videoProcessor.onCapturerStopped();
                
            
        
        /**
         * 捕捉的视频数据
         * @param frame 视频数据
         */
        @Override
        public void onFrameCaptured(VideoFrame frame) 
            //应在传送任何帧之前调用此函数,以确定是否应丢弃该帧或裁剪、缩放参数是什么。如果FrameAdaptationParameters#drop为true,则应丢弃该帧,否则应在调用 onFrameCaptured() 之前根据帧适配参数对帧进行适配。
            FrameAdaptationParameters parameters = nativeAndroidVideoTrackSource.adaptFrame(frame);
            synchronized(videoProcessorLock) 
                if (videoProcessor != null) 
                    //如果添加了视频处理器,自行处理,直接return。
                    videoProcessor.onFrameCaptured(frame, parameters);
                    return;
                
            
            //根据参数调整帧数据,此步骤会调用VideoFrame.getBuffer().cropAndScale()进行裁剪缩放。
            VideoFrame adaptedFrame = VideoProcessor.applyFrameAdaptationParameters(frame, parameters);
            if (adaptedFrame != null) 
                //如果不为空,则通过native方法将帧数据传递过去
                nativeAndroidVideoTrackSource.onFrameCaptured(adaptedFrame);
                adaptedFrame.release();
            
        
    ;
    /**
     * 调整输出格式
     */
    public void adaptOutputFormat(int width, int height, int fps) 
        int maxSide = Math.max(width, height);
        int minSide = Math.min(width, height);
        this.adaptOutputFormat(maxSide, minSide, minSide, maxSide, fps);
    

    public void adaptOutputFormat(int landscapeWidth, int landscapeHeight, int portraitWidth, int portraitHeight, int fps) 
        this.adaptOutputFormat(new VideoSource.AspectRatio(landscapeWidth, landscapeHeight), landscapeWidth * landscapeHeight, new VideoSource.AspectRatio(portraitWidth, portraitHeight), portraitWidth * portraitHeight, fps);
    

    public void adaptOutputFormat(VideoSource.AspectRatio targetLandscapeAspectRatio, @Nullable Integer maxLandscapePixelCount, VideoSource.AspectRatio targetPortraitAspectRatio, @Nullable Integer maxPortraitPixelCount, @Nullable Integer maxFps) 
        nativeAndroidVideoTrackSource.adaptOutputFormat(targetLandscapeAspectRatio, maxLandscapePixelCount, targetPortraitAspectRatio, maxPortraitPixelCount, maxFps);
    

    /**
     * 设置视频处理器
     */
    public void setVideoProcessor(@Nullable VideoProcessor newVideoProcessor) 
        synchronized(videoProcessorLock) 
            //判断之前是否设置过
            if (videoProcessor != null) 
                videoProcessor.setSink(null);
                if (isCapturerRunning) 
                    videoProcessor.onCapturerStopped();
                
            

            videoProcessor = newVideoProcessor;
            if (newVideoProcessor != null) 
                newVideoProcessor.setSink(new VideoSink() 
                    @Override
                    public void onFrame(VideoFrame frame) 
                        //这里返回的是自行处理好的数据,再通过native方法将帧数据传递过去,和上面一样。
                        runWithReference(() -> 
                          nativeAndroidVideoTrackSource.onFrameCaptured(frame);
                        );
                    
                );
                if (isCapturerRunning) 
                    newVideoProcessor.onCapturerStarted(true);
                
            
        
    

    /**
     * 获取捕捉器观察者
     */
    public CapturerObserver getCapturerObserver() 
        

以上是关于WEBRTC基本概念的主要内容,如果未能解决你的问题,请参考以下文章

零基础快速入门WebRTC:基本概念关键技术与WebSocket的区别等

RTCPeerConnection基本概念 -- 以及创建和绑定音视频以及渲染远端视频时候的作用

WebRTC 通信原理

WebRTC概念简介

WebRTC 速成课程

Android端WebRTC本地音视频采集流程源码分析