文章目录
1. 介绍
WebRTC
(Web Real-Time Communications) 是一项 实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接
,实现视频流和(或)音频流或者其他任意数据的传输。
2. MediaCodec介绍
MediaCodec
是 Android 提供的一个用于处理音频和视频数据的底层 API。它支持编码(将原始数据转换为压缩格式)和解码(将压缩数据转换回原始格式)的过程。MediaCodec
是自 Android 4.1(API 16)起引入的,(通常与MediaExtractor
、MediaSync
、MediaMuxer
、MediaCrypto
、 MediaDrm
、Image
、Surface
一起使用)
MediaExtractor
:用于从缓冲区中读取(readSampleData
)媒体数据MediaMuxer
:用于将原始音频数据(pcm)和视频数据(yuv)写入(writeSampleData
)音视频轨道中,从而可以以文件形式保存
-
创建和配置
MediaCodec
:- 首先,需要根据所需的编解码器类型(例如 H.264、VP8、Opus 等)创建一个
MediaCodec
实例。 - 接下来,通过
MediaFormat
对象指定编解码器的一些参数,如分辨率、帧率、码率等。 - 然后,使用
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) } }
- 首先,需要根据所需的编解码器类型(例如 H.264、VP8、Opus 等)创建一个
-
基本使用
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) } } }
-
输入和输出缓冲区:
MediaCodec
有两个缓冲区队列,一个用于输入,另一个用于输出。输入缓冲区用于接收原始数据(例如从摄像头捕获的视频帧),输出缓冲区用于存诸编码后的数据。在编解码过程中,需要将这些缓冲区填充或消费。 -
编码器工作模式:
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
。
-
本地渲染
<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); ...
-
远端渲染
要想从远端获取数据,我们就必须创建
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 长连接,不需要多次建连。