WebRTC (Web Real-Time Communications) 是一项 实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。
MediaCodec 是 Android 提供的一个用于处理音频和视频数据的底层 API。它支持编码(将原始数据转换为压缩格式)和解码(将压缩数据转换回原始格式)的过程。MediaCodec 是自 Android 4.1(API 16)起引入的,(通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、 MediaDrm、Image、Surface一起使用)
MediaExtractor:用于从缓冲区中读取(readSampleData)媒体数据MediaMuxer:用于将原始音频数据(pcm)和视频数据(yuv)写入(writeSampleData)音视频轨道中,从而可以以文件形式保存创建和配置 MediaCodec:
MediaCodec 实例。MediaFormat 对象指定编解码器的一些参数,如分辨率、帧率、码率等。configure() 方法配置 MediaCodecobject 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)
}
}
基本使用
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();
第一个当然就是 WebRTC 库了,第二个是 socket.io 库,用它来与信令服务器互联。
...
dependencies {
...
implementation 'org.webrtc:google-webrtc:1.0.+'
implementation 'io.socket:socket.io-client:1.0.0'
...
}
WebRTC程序的起源就是 PeerConnectionFactory。
// 初始化
PeerConnectionFactory.initialize(...);
// 初始化之后,就可以通过 builder 模式来构造 PeerConnecitonFactory 对象了。
...
PeerConnectionFactory.Builder builder =
PeerConnectionFactory.builder()
.setVideoEncoderFactory(encoderFactory)
.setVideoDecoderFactory(decoderFactory);
...
return builder.createPeerConnectionFactory();
主要是方便调整建造 PeerConnectionFactory的组件,如编码器、解码器等。
音视频源,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);
视频采集,在 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);
}
渲染视频。 在 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 */);
其含义是:
接下来将从摄像头采集的数据设置到该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 时设置进去。
在整个 WebRTC 双方交互的过程中,其业务逻辑的核心是信令, 所有的模块都是通过信令串联起来的。通过socket.io与之前搭建的信令服备器互联的。
客户端命令有:
服务端命令:
通过以上几条信令就可以实现一对一实时互动的要求,是不是非常的简单?
HTTP Live Streaming 简称为 HLS, 是一个基于 HTTP 的视频流协议,由 APPLE 公司提出和实现。苹果公司的很多产品都支持 HLS 协议,譬如 Mac OS 上的 QuickTime、Safari 以及 iOS 上的 Safari。苹果 2009 年提出该协议,HLS 是 iOS 设备默认要求的视频流标准。安卓也支持HLS。
HLS 因为以下几个原因比较受欢迎。
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 长连接,不需要多次建连。
