webrtc相关介绍

1. 介绍

WebRTC (Web Real-Time Communications) 是一项 实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。

2. MediaCodec介绍

MediaCodec 是 Android 提供的一个用于处理音频和视频数据的底层 API。它支持编码(将原始数据转换为压缩格式)和解码(将压缩数据转换回原始格式)的过程。MediaCodec 是自 Android 4.1(API 16)起引入的,(通常与MediaExtractorMediaSyncMediaMuxerMediaCryptoMediaDrmImageSurface一起使用)

  • MediaExtractor:用于从缓冲区中读取(readSampleData)媒体数据
  • MediaMuxer:用于将原始音频数据(pcm)和视频数据(yuv)写入(writeSampleData)音视频轨道中,从而可以以文件形式保存
  1. 创建和配置 MediaCodec

    1. 首先,需要根据所需的编解码器类型(例如 H.264、VP8、Opus 等)创建一个 MediaCodec 实例。
    2. 接下来,通过 MediaFormat 对象指定编解码器的一些参数,如分辨率、帧率、码率等。
    3. 然后,使用 configure() 方法配置 MediaCodec
    object MediaCodecUtil {
        // 音频源:音频输入-麦克风
        private const val AUDIO_INPUT = MediaRecorder.AudioSource.MIC
     
        // 采样率
        // 44100是目前的标准,但是某些设备仍然支持22050,16000,11025
        // 采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级
        private const val AUDIO_SAMPLE_RATE = 44100
     
        // 音频通道 单声道
        private const val AUDIO_CHANNEL = AudioFormat.CHANNEL_IN_MONO
     
        // 音频通道 立体声:CHANNEL_OUT_STEREO或CHANNEL_IN_STEREO
        private const val AUDIO_CHANNEL2 = AudioFormat.CHANNEL_IN_STEREO
     
        // 音频格式:PCM编码
        private const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT
     
        private var bufferSizeInBytes: Int = 0
     
        /**
         * 获取缓冲大小
         */
        fun getBufferSizeInBytes(): Int {
            return bufferSizeInBytes
        }
     
     	
        fun createVideoEncode(surfaceSize: Size): MediaCodec {
            // 1. 视频编码器
            val videoEncoder = MediaCodec.createEncoderByType("video/avc")
                
            // 2. 创建视频MediaFormat
            val videoFormat = MediaFormat.createVideoFormat(
                "video/avc", surfaceSize.width
                , surfaceSize.height)
            // 指定编码器颜色格式
            videoFormat.setInteger(
                MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
            // 指定编码器码率
            videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 0)
            // 指定编码器帧率
            videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
            // 指定编码器关键帧间隔
            videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
            // BITRATE_MODE_CBR输出码率恒定
            // BITRATE_MODE_CQ保证图像质量
            // BITRATE_MODE_VBR图像复杂则码率高,图像简单则码率低
            videoFormat.setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)
            videoFormat.setInteger(
                MediaFormat.KEY_COMPLEXITY,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)
                
            // 3. 配置 mediacodec
            videoEncoder.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
            return videoEncoder
        }
     
     
        fun createAudioEncoder(): MediaCodec {
            // 1. 音频编码器
            val audioEncoder = MediaCodec.createEncoderByType("audio/mp4a-latm")
                
            // 2. 创建音频MediaFormat,参数2:采样率,参数3:通道
            val audioFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 1)
            // 仅编码器指定比特率
            audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 4 * 1024)
            var bufferSizeInBytes = getBufferSizeInBytes()
            if (bufferSizeInBytes == 0) {
                bufferSizeInBytes = AudioRecord.getMinBufferSize(
                    AUDIO_SAMPLE_RATE ,
                    CHANNEL_IN_STEREO,
                    ENCODING_PCM_16BIT)
            }
            //可选的,输入数据缓冲区的最大大小
            audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSizeInBytes)
            audioFormat.setInteger(
                MediaFormat.KEY_AAC_PROFILE,
                MediaCodecInfo.CodecProfileLevel.AACObjectLC)
     
     		// 3. 配置mediacodec
            audioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
            return audioEncoder
        }
     
        // 默认获取单声道AudioRecord
        fun getSingleAudioRecord(
            channelConfig: Int = AUDIO_CHANNEL,
            audioSource: Int = AUDIO_INPUT,
            sampleRateInHz: Int = AUDIO_SAMPLE_RATE,
            audioFormat: Int = AUDIO_ENCODING): AudioRecord {
            //audioRecord能接受的最小的buffer大小
            bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
            return AudioRecord(
                audioSource,
                sampleRateInHz,
                channelConfig,
                audioFormat,
                bufferSizeInBytes)
        }
    }
    
  2. 基本使用

    fun encode(){
        val videoEncoder = MediaCodecUtil.createVideoEncode(size)
        // 设置 buffer (camera的surface or mediaprojection捕获的surface)
        videoEncoder.setInputSurface(surface)
        videoEncoder.start()
        
        //音频录制类
        val audioRecord = MediaCodecUtil.getSingleAudioRecord(AudioFormat.CHANNEL_IN_STEREO)
        //音频编码器
        val audioEncoder = MediaCodecUtil.createAudioEncoder()
        audioEncoder.start()
    }
     
     
    GlobalScope.launch (Dispatchers.IO) {
        while (isActive) {
            val length = AudioRecordUtil.getBufferSizeInBytes()
            audioRecord.read(mAudioBuffer, 0, length)
            // 1. 调用dequeueInputBuffer获取输入队列空闲数组下标
            val inputIndex = audioEncoder.dequeueInputBuffer(0)
            if (inputIndex >= 0) {
                // 2. 通过getInputBuffers获取输入队列
                val byteBuffer = audioEncoder.getInputBuffer(inputIndex)
                if (byteBuffer != null) {
                    byteBuffer.clear()
                    byteBuffer.put(mAudioBuffer)
                    byteBuffer.limit(length);// 设定上限值
                    // 3. queueInputBuffer把原始PCM数据送入编码器
                   audioEncoder.queueInputBuffer(inputIndex,0,length,System.nanoTime(),0); 
                }
            }
     
            // 4. dequeueOutputBuffer获取输出队列空闲数组角标
            val outputIndex = audioEncoder.dequeueOutputBuffer(mBufferInfo, 0)
            if (outputIndex >= 0) {
                // 5. 通过getOutputBuffers 获取输出流
                val byteBuffer = audioEncoder.getOutputBuffer(outputIndex)
                if (byteBuffer != null) {
                    val byte = byteBuffer.get(outputIndex)
                }
                // 6. 通过releaseOutputBuffer把输出buffer还给系统,重新放到输出队列中
                audioEncoder.releaseOutputBuffer(outputIndex, false)
            }
        }
    }
    
  3. 输入和输出缓冲区:MediaCodec 有两个缓冲区队列,一个用于输入,另一个用于输出。输入缓冲区用于接收原始数据(例如从摄像头捕获的视频帧),输出缓冲区用于存诸编码后的数据。在编解码过程中,需要将这些缓冲区填充或消费。img

  4. 编码器工作模式:MediaCodec 支持两种工作模式,分别是同步和异步。在同步模式下,需要手动管理输入和输出缓冲区。在异步模式下,通过设置回调函数(MediaCodec.Callback),可以在编解码事件发生时自动通知应用程序。

    同步模式

     MediaCodec codec = MediaCodec.createByCodecName(name);
     codec.configure(format,);
     MediaFormat outputFormat = codec.getOutputFormat(); // option B
     codec.start();
     for (;;) {
      int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
      if (inputBufferId >= 0) {
        ByteBuffer inputBuffer = codec.getInputBuffer();
        // fill inputBuffer with valid data
        …
        codec.queueInputBuffer(inputBufferId,);
      }
      int outputBufferId = codec.dequeueOutputBuffer();
      if (outputBufferId >= 0) {
        ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
        MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
        // bufferFormat is identical to outputFormat
        // outputBuffer is ready to be processed or rendered.
        …
        codec.releaseOutputBuffer(outputBufferId,);
      } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // Subsequent data will conform to new format.
        // Can ignore if using getOutputFormat(outputBufferId)
        outputFormat = codec.getOutputFormat(); // option B
      }
     }
     codec.stop();
     codec.release();
    
    

    异步模式(推荐)

     MediaCodec codec = MediaCodec.createByCodecName(name);
     MediaFormat mOutputFormat; // member variable
     codec.setCallback(new MediaCodec.Callback() {
      @Override
      void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
        ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
        // fill inputBuffer with valid data
        …
        codec.queueInputBuffer(inputBufferId,);
      }
     
      @Override
      void onOutputBufferAvailable(MediaCodec mc, int outputBufferId,) {
        ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
        MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
        // bufferFormat is equivalent to mOutputFormat
        // outputBuffer is ready to be processed or rendered.
        …
        codec.releaseOutputBuffer(outputBufferId,);
      }
     
      @Override
      void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
        // Subsequent data will conform to new format.
        // Can ignore if using getOutputFormat(outputBufferId)
        mOutputFormat = format; // option B
      }
     
      @Override
      void onError() {}
      @Override
      void onCryptoError() {}
     });
     codec.configure(format,);
     mOutputFormat = codec.getOutputFormat(); // option B
     codec.start();
     // wait for processing to complete
     codec.stop();
     codec.release();
    

3. 引入关键库

第一个当然就是 WebRTC 库了,第二个是 socket.io 库,用它来与信令服务器互联。

...
dependencies {
    ...
    implementation 'org.webrtc:google-webrtc:1.0.+'
    implementation 'io.socket:socket.io-client:1.0.0'
    ...
}

4. PeerConnectionFactory

WebRTC程序的起源就是 PeerConnectionFactory

// 初始化
PeerConnectionFactory.initialize(...);

// 初始化之后,就可以通过 builder 模式来构造 PeerConnecitonFactory 对象了。
...
PeerConnectionFactory.Builder builder = 		
				PeerConnectionFactory.builder()
                	.setVideoEncoderFactory(encoderFactory)
                	.setVideoDecoderFactory(decoderFactory);
                
 ...

 return builder.createPeerConnectionFactory();

主要是方便调整建造 PeerConnectionFactory的组件,如编码器、解码器等。

5. 源

音视频源,Video/AudioTrack

...
VideoSource videoSource = 
					mPeerConnectionFactory.createVideoSource(false);
mVideoTrack = mPeerConnectionFactory.createVideoTrack(
													VIDEO_TRACK_ID, 
													videoSource);
													
...

AudioSource audioSource = 
					mPeerConnectionFactory.createAudioSource(new MediaConstraints());
mAudioTrack = mPeerConnectionFactory.createAudioTrack(
													AUDIO_TRACK_ID, 
													audioSource);

6. 源采集

视频采集,在 Android 系统下有两种 Camera,一种称为 Camera1, 是一种比较老的采集视频数据的方式,别一种称为 Camera2, 是一种新的采集视频的方法。它们之间的最大区别是 Camera1使用同步方式调用API,Camera2使用异步方式,所以Camera2更高效。

private VideoCapturer createVideoCapturer() {
        if (Camera2Enumerator.isSupported(this)) {
            return createCameraCapturer(new Camera2Enumerator(this));
        } else {
            return createCameraCapturer(new Camera1Enumerator(true));
        }
}


private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
        final String[] deviceNames = enumerator.getDeviceNames();

        // First, try to find front facing camera
        Log.d(TAG, "Looking for front facing cameras.");
        for (String deviceName : deviceNames) {
            if (enumerator.isFrontFacing(deviceName)) {
                Logging.d(TAG, "Creating front facing camera capturer.");
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, 
                                                                        null);
                if (videoCapturer != null) {
                    return videoCapturer;
                }
            }
        }

        // Front facing camera not found, try something else
        Log.d(TAG, "Looking for other cameras.");
        for (String deviceName : deviceNames) {
            if (!enumerator.isFrontFacing(deviceName)) {
                Logging.d(TAG, "Creating other camera capturer.");
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName,
                                                                        null);
                if (videoCapturer != null) {
                    return videoCapturer;
                }
            }
        }
        
        return null;
}

VideoCapture 是如何与 VideoSource 关联到一起的:

...

mSurfaceTextureHelper = 
			SurfaceTextureHelper.create("CaptureThread",
										mRootEglBase.getEglBaseContext());

mVideoCapturer.initialize(mSurfaceTextureHelper,
 						  getApplicationContext(), 
 						  videoSource.getCapturerObserver());

...

mVideoTrack.setEnabled(true);
...
    
    
@Override
protected void onResume() {
    super.onResume();
    mVideoCapturer.startCapture(VIDEO_RESOLUTION_WIDTH, 
    							VIDEO_RESOLUTION_HEIGHT, 
    							VIDEO_FPS);
}

7. 渲染

渲染视频。 在 Android 下 WebRTC 使用OpenGL ES 进行视频渲染,用于展示视频的控件是 WebRTC 对 Android 系统控件 SurfaceView 的封装 。 WebRTC 封装后的 SurfaceView 类为 org.webrtc.SurfaceViewRenderer

  1. 本地渲染

    <org.webrtc.SurfaceViewRenderer
            android:id="@+id/LocalSurfaceView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />
    

    定义好 surfaceview 后,还需要进行设置

    ...
    
    mLocalSurfaceView.init(mRootEglBase.getEglBaseContext(), null);
    mLocalSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
    mLocalSurfaceView.setMirror(true);
    mLocalSurfaceView.setEnableHardwareScaler(false /* enabled */);
    

    其含义是:

    • 使用 OpenGL ES 的上下文初始化 View。
    • 设置图像的拉伸比例。
    • 设置图像显示时反转,不然视频显示的内容与实际内容正好相反。
    • 是否打开便件进行拉伸。

    接下来将从摄像头采集的数据设置到该view里就可以显示了。设置非常的简单,代码如下:

    ...
    mVideoTrack.addSink(mLocalSurfaceView);
    ...
    
  2. 远端渲染

    要想从远端获取数据,我们就必须创建 PeerConnection 对象。该对象的用处就是与远端建立联接,并最终为双方通讯提供网络通道。

    ...
    PeerConnection.RTCConfiguration rtcConfig = 
                    new PeerConnection.RTCConfiguration(iceServers);
    ...
    PeerConnection connection =
                    mPeerConnectionFactory.createPeerConnection(rtcConfig,
                                                                mPeerConnectionObserver);
    
    ...
    connection.addTrack(mVideoTrack, mediaStreamLabels);
    connection.addTrack(mAudioTrack, mediaStreamLabels);
    ...
    

    WebRTC 在建立连接时使用 ICE 架构,一些参数需要在创建 PeerConnection 时设置进去。

    8. 信令驱动

    在整个 WebRTC 双方交互的过程中,其业务逻辑的核心是信令, 所有的模块都是通过信令串联起来的。通过socket.io与之前搭建的信令服备器互联的。

    客户端命令有:

    • join: 用户加入房间
    • leave: 用户离开房间
    • message: 端到端命令(offer、answer、candidate)

    服务端命令:

    • joined: 用户已加入
    • leaved: 用户已离开
    • other_joined:其它用户已加入
    • bye: 其它用户已离开
    • full: 房间已满

    通过以上几条信令就可以实现一对一实时互动的要求,是不是非常的简单?

9. 直播协议

9.1 HLS

HTTP Live Streaming 简称为 HLS, 是一个基于 HTTP 的视频流协议,由 APPLE 公司提出和实现。苹果公司的很多产品都支持 HLS 协议,譬如 Mac OS 上的 QuickTime、Safari 以及 iOS 上的 Safari。苹果 2009 年提出该协议,HLS 是 iOS 设备默认要求的视频流标准。安卓也支持HLS。

HLS 因为以下几个原因比较受欢迎。

  • HLS 几乎可随处播放。 几个大平台 web、mobile、tv 基本都有免费的HLS 播放器支持。
  • 苹果 要求 HLS。 如果你想在 iOS 设备直播,逃不了的。
  • HLS 相对简单。 它使用了普遍且已经存储的视频格式(MP4 或 TS,伴随着 H.264 和 AAC 等编解码器), 另外附加了一个丑陋但人类可读的文本格式(m3u8).
  • 它通过 HTTP 工作。 不需要跑特殊的服务(不像老旧校风派的 RTMP 协议或者新潮的 WebRTC 协议)。HLS 可以方便的透过防火墙或者代理服务器,而且可以很方便的利用 CDN 进行分发加速,并且客户端实现起来也很方便。

9.2 RTMP

Real Time Messaging Protocol(简称 RTMP)是 Macromedia 开发的一套视频直播协议,现在属于 Adobe。

协议基于 TCP,是一个协议族,包括 RTMP 基本协议及 RTMPT/RTMPS/RTMPE 等多种变种。RTMP 是一种设计用来进行实时数据通信的网络协议,主要用来在 Flash/AIR 平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。

无法支持移动端 WEB 播放是它的硬伤。虽然无法在iOS的H5页面播放,但是iOS原生应用可以写解码去解析的。浏览器端,HTML5 video标签无法播放 RTMP 协议的视频,可以通过 video.js 来实现。

其主要优点:

  • 实时性非常好,延时较小,通常为 1-3s

  • 基于 TCP 长连接,不需要多次建连。

    image.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值