最近公司在搞执法仪项目,涉及到音视频的录制和推流需求,因为之前并没有这方面的经验,花费了一段时间恶补相关知识,主要涉及到音视频数据的采集和编码,给我的感觉就是音视频这块还是很复杂的,目前也是出于学习阶段,本篇文档主要讲述一下在做执法仪的过程中,用到的一些知识和相关问题和大家分享一下,下面就进入正题。
音频采集
android原生camera录制视频一般是通过MediaRecorder来实现的,录制完成后直接生成mp4或者是3gp文件,所以我们不需要单独考虑视频数据如何采集,音频数据如何采集,但是如果采用MediaCodec来实现的话,我们就要单独考虑音频数据和视频数据如何采集了,我们先来说一下音频数据的采集方法。
音频数据的采集android为我们提供了AudioRecord类,通常情况下,如果不需要对采集到的数据进行处理的话,完全可以采用MediaRecorder来去实现,但是如果你需要将采集到的数据转化为PCM格式,AAC格式,MP3格式等,就需要使用AudioRecord类进行采集了,使用AudioRecord类之前先需要了解几个概念:
- 采样率
采样率就是采样的频率
- 量化精度
量化精度可以理解为位宽,采用多少位来存储数据
- 声道数
声音录制时音源数量或播放时相应的扬声器数量。一般分为单声道(Mono)和双声道(Stereo)
- bufferSizeInBytes
表示AudioRecord内部音频缓冲区大小,一般通过getMinBufferSize来获取
使用步骤如下:
- 定义需要设置的参数
private int samplingRate = 44100;
private int bitRate = 16000;
private int BUFFER_SIZE = 1920;
int mSamplingRateIndex = 0;
AudioRecord mAudioRecord;
- 获取音频缓冲区的大小
int bufferSize = AudioRecord.getMinBufferSize(samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
- 创建并实例化AudioRecord
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
- 调用AudioRecord的startRecording开始录制
mAudioRecord.startRecording();
- 调用AudioRecord的read方法写入数据
mAudioRecord.read(audioBuffer, BUFFER_SIZE);
- 停止录制并释放资源
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}
经过以上步骤以后,我们就可以在本地获取到PCM格式的音频数据,PCM意为脉冲编码调制,是一种原始的音频格式数据,一般播放器不可以进行直接播放。
视频采集
上一小节对音频的采集做了一个介绍,接下来说一下视频数据的采集,视频数据的采集一般是获取Camera的callback数据,获取camera的callback数据一般有如下两种方案:
- setPreviewCallback
- setPreviewCallbackWithBuffer
然后利用 Camera.PreviewCallback 回调接口收集到 YUV数据:
public void onPreviewFrame(byte[] bytes, Camera camera)
但是考虑到实际场景,如果业务中涉及到推流直播的需求,对实时性要求较高,这就需要我们提高预览数据回调的效率,而通过setPreviewCallback获取预览数据的话,每产生一帧都要开辟一个新的buffer,进行存储帧数据,这样不断开辟和回收内存,GC会很频繁,效率很低。所以我们一般采用第二种方案来获取camera的callback数据,这种方案在每次回调数据后都会回收缓存,不需要开辟新的内存,达到复用内存的效果,很大程度上提高了帧数据的回调效率(这种方案可以解决推流效果抖动的问题)。
mCamera.addCallbackBuffer(new byte[size]);
mCamera.setPreviewCallbackWithBuffer(previewCallback);
previewCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (data == null)
return;
int result;
if (camInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (camInfo.orientation + mDgree) % 360;
} else { // back-facing
result = (camInfo.orientation - mDgree + 360) % 360;
}
if (i420_buffer == null || i420_buffer.length != data.length) {
i420_buffer = new byte[data.length];
}
JNIUtil.ConvertToI420(data, i420_buffer, width, height, 0, 0, width, height, result % 360, 2);
}
};
上面代码就是获取camera callback数据的实现方法,从上面代码我们可以看到在onPreviewFrame方法里我们对callback上来的数据进行了一次旋转操作,这是因为camera模组通常情况下并不是竖直安装的,而是和手机竖直方向成90度(前摄270度),所以我们需要对callback数据进行一次旋转操作才能达到实际的效果,这里会有一个问题,为什么前置摄像头安装角度是270度却只要旋转90度呢,这个其实在hal层,平台已经帮我们做了一次旋转操作,所以我们只需要再次旋转90度即可。
MediaCodec编码
通过前面两小节的讲述,我们对音频数据和视频数据的采集已经有了一定的了解,通过camera的onPreviewFrame获取到了yuv格式的视频数据,通过AudioRecord获取到了pcm格式的音频数据,不过这两种格式的数据都是原始数据,如果需要混合成mp4格式的文件是不行的,音频数据需要编码为aac格式的数据,视频数据需要编码为h264或者h265格式的数据,这个时候MediaCodec就登场了,MediaCodec是android为我们提供的硬编码api,依赖于设备内部的硬编码器,效率较高,具体可以参考另一篇文档《MediaCodec原理解析》,下面来了解一下MediaCodec的使用步骤:
音频编码
上面已经分析了通过AudioRecord来采集音频的步骤,不过获取到的是PCM格式的音频数据,但是由于其占用带宽较大,不利于直接进行传输,所以在视频合成和推流之前,需要通过MediaCodec来进行编码。具体实现步骤和视频编码类似
- 实例化MediaCodec实例
mMediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
- 设置MediaFormat参数
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, samplingRate);
format.setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, BUFFER_SIZE);
-
MediaFormat.KEY_MIME
设置编码类型,audio/mp4a-latm为aac格式。 -
MediaFormat.KEY_BIT_RATE
设置比特率也就是码率,一般设置为44.1khz可以兼容大部分设备 -
MediaFormat.KEY_AAC_PROFILE
设置可用于编解码器组件的配置文件
配置好MediaFormat后,执行MediaCodec的configure方法然后调用start让MediaCodec进入到工作状态
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
4.开始编码
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
bufferIndex = mMediaCodec.dequeueInputBuffer(1000);
len = mAudioRecord.read(audioBuffer, BUFFER_SIZE);
mMediaCodec.queueInputBuffer(bufferIndex, 0, len, presentationTimeUs, 0);
获取到MediaCodec的输入缓冲区,通过AudioRecord的read方法把采集到的PCM格式数据循环填充到MediaCodec的输入缓冲区内,MediaCodec编码完成以后,接下来就是从输出缓冲区内读取编码后的音频数据了:
mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000);
outputBuffer = mMediaCodec.getOutputBuffer(index);
addADTStoPacket(mBuffer.array(), mBufferInfo.size + 7);
mMediaCodec.releaseOutputBuffer(index, false);
获取输出缓冲区以后,这里调用了addADTStoPacket方法来添加一些头部信息,最后调用releaseOutputBuffer释放掉输出缓冲区。
视频编码
- 实例化MediaCodec
try {
mMediaCodec = MediaCodec.createByCodecName(info.mName);
} catch (IOException e) {
e.printStackTrace();
throw new IllegalStateException(e);
}
createByCodecName参数我们一般设置如下两种类型:
- “video/avc” - H.264
- “video/hevc” - H.265
- 配置MediaCodec
public void configure(
MediaFormat format,
Surface surface,
MediaCrypto crypto,
int flags
);
-
Surface surface
一般用于解码的时候使用,通过指定surface,编码后的数据显示在surface上 -
MediaCrypto crypto
用于媒体数据的加密,一般会传入null -
int flags
常量值,如果作为编码使用传入
MediaCodec.CONFIGURE_FLAG_ENCODE -
MediaFormat format
输入或输出数据的格式,这个参数比较重要,使用步骤如下:
- 实例化MediaFormat实例
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight);
- 设置编码参数
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate + 3000);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, info.mColorFormat);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
-
MediaFormat.KEY_BIT_RATE
比特率也就是码率,单位时间内数据的传输量,影响视频的清晰度。 -
MediaFormat.KEY_FRAME_RATE
帧率,影响视频的流畅性。 -
MediaFormat.KEY_COLOR_FORMAT
颜色空间,这个需要获取设备支持的编码器以及编码器所支持的颜色空间,android设备camera一般callback数据为NV21或者YV12格式,所以在编码视频数据之前,需要把callback数据转换为设备编码器所支持的格式,否则编码得到的数据可能会出现花屏、叠影、颜色失真等现象。 -
MediaFormat.KEY_I_FRAME_INTERVAL
关键帧间隔,通常情况下设置为1,单位是s
配置好MediaCodec后,调用
mMediaCodec.start();
让MediaCodec进入工作状态。
- 数据流处理
准备工作已经就绪,接下来就是编码视频数据的具体实现了:
int bufferIndex = mMediaCodec.dequeueInputBuffer(0);
ByteBuffer buffer = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
buffer = mMediaCodec.getInputBuffer(bufferIndex);
} else {
buffer = inputBuffers[bufferIndex];
}
mMediaCodec.queueInputBuffer(bufferIndex, 0, data.length, System.nanoTime() / 1000, MediaCodec.BUFFER_FLAG_KEY_FRAME);
MediaCodec维护着一组输入输出buffer,编码的时候首先需要通过
mMediaCodec.dequeueInputBuffer(0)
获取输入buffer的索引,然后通过
mMediaCodec.getInputBuffer(bufferIndex);
获取到输入buffer,获取到输入buffer以后,就可以通过
mMediaCodec.queueInputBuffer(bufferIndex, 0, data.length, System.nanoTime() / 1000, MediaCodec.BUFFER_FLAG_KEY_FRAME);
填充我们的数据交给MediaCodec进行编码处理了。
MediaCodec编码完成以后,我们就可以从输出缓冲区来获取编码后的数据了
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
outputBuffers = mMediaCodec.getOutputBuffers(outputBufferIndex);
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
首先通过
mMediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
获取到输出缓冲区索引,然后通过
mMediaCodec.getOutputBuffers(outputBufferIndex);
获取一组输出缓冲区来消耗掉里面的数据,处理完成以后,调用
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
来释放掉输出缓冲区。
至此视频数据已经编码完成,编码后的视频数据格式为h264/h265,后面的视频合成以及推流都会用到此类型的文件。
至此音视频数据的采集以及编码就完成了,编码后我们得到了h264/h265的视频数据和aac格式的音频数据,接下来就是音视频合成和推流了。
音视频合成
android平台Camera录制视频一般采用MediaRecorder来实现,MediaRecorder封装的比较完善,可以直接录制mp4,3gp视频文件,如果没有特殊需求的话,采用MediaRecorder就可以实现大部分功能,但是如果想了解MP4录制的整个过程,从采集、编码、封装成MP4到解析、解码、播放就需要采用MediaCodec + MediaMuxer来实现了。
先来了解一下MediaMuxer:
MediaMuxer facilitates muxing elementary streams. Currently MediaMuxer supports MP4, Webm and 3GP file as the output. It also supports muxing B-frames in MP4 since Android Nougat.
这是谷歌官方文档对MediaMuxer的解释,MediaMuxer用来产生一个混合的音频和视频的多媒体文件,目前支持的格式有MP4,3GP,Webm。下面是谷歌的官方使用实例:
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
// More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
// or MediaExtractor.getTrackFormat().
MediaFormat audioFormat = new MediaFormat(...);
MediaFormat videoFormat = new MediaFormat(...);
int audioTrackIndex = muxer.addTrack(audioFormat);
int videoTrackIndex = muxer.addTrack(videoFormat);
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
boolean finished = false;
BufferInfo bufferInfo = new BufferInfo();
muxer.start();
while(!finished) {
// getInputBuffer() will fill the inputBuffer with one frame of encoded
// sample from either MediaCodec or MediaExtractor, set isAudioSample to
// true when the sample is audio data, set up all the fields of bufferInfo,
// and return true if there are no more samples.
finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
if (!finished) {
int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
}
};
muxer.stop();
muxer.release();
更多具体解释可以参考谷歌官方文档
https://developer.android.google.cn/reference/android/media/MediaMuxer?hl=en
下面就来讲一下MediaMuxer的具体使用步骤:
- 创建MediaMuxer实例
mMuxer = new MediaMuxer(mFilePath + ".mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
MediaMuxer的构造函数接收两个参数,第一个参数传入的是我们需要生成视频文件的路径,第二个参数传入的是生成视频文件的格式,这里我们传入了MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,表示我们期望生成的视频文件为MP4格式
- 添加媒体通道
public synchronized void addTrack(MediaFormat format, boolean isVideo) {
// now that we have the Magic Goodies, start the muxer
if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1)
throw new RuntimeException("already add all tracks");
try {
int track = mMuxer.addTrack(format);
Log.i(TAG, String.format("addTrack %s result %d", isVideo ? "video" : "audio", track));
if (isVideo) {
mVideoFormat = format;
mVideoTrackIndex = track;
if (mAudioTrackIndex != -1) {
Log.i(TAG, "both audio and video added,and muxer is started");
mMuxer.start();
mBeginMillis = System.currentTimeMillis();
}
} else {
mAudioFormat = format;
mAudioTrackIndex = track;
if (mVideoTrackIndex != -1) {
mMuxer.start();
mBeginMillis = System.currentTimeMillis();
}
}
} catch (RuntimeException e) {
Log.i(TAG, "add track exception :" + e.getMessage());
}
}
添加媒体通道就是添加音频和视频通道,通过调用addTrack来实现,addTrack接收一个MediaFormat类型的参数,可以通过MediaCodec的getOutputFormat方法获取。添加好媒体通道后就可以调用
mMuxer.start();
开始合成了。
- 音视频数据写入
muxer.writeSampleData(videoTrackIndex, byteBuf, bufferInfo);
- videoTrackIndex
媒体通道索引
- byteBuf
MediaCodec输出缓冲区
- bufferInfo
记录MediaCodec输出缓冲区的信息
writeSampleData就是写入我们需要合成的音视频数据,这里传入的需要注意每次只能添加一帧视频数据或者单个Sample的音频数据,并且BufferInfo对象的值一定要设置正确
。
- 关闭并释放资源
mMuxer.stop();
mMuxer.release();
音视频合成完成以后,调用如上代码来关闭并释放资源。
以上就是MediaMuxer音视频合成MP4文件的具体流程,更详细的流程可以参考执法仪app源码的EasyMuxer类。
音视频推流
手机直播在最近几年开始兴起,南抖音,北快手,相对于图片文字,视频传递信息更加清晰直接,用户体验更好,直播的原理其实就是设备端采集音视频数据然后经过相应处理后推流到对应的流媒体服务器,流媒体服务器负责分发数据,这样我们就可以用手机看到设备端实时的画面。
先来看一张流程图:
这张图描述了设备端音视频采集到编码然后推流到流媒体服务器的流程,关于摄像头和音频的的采集以及视频数据的编码我们上面已经了解过,音频采集为PCM格式的原始数据经过编码为aac,视频采集为yuv的原始数据经过编码为h264/h265.
目前常用的推流协议有如下三种:
- RTMP(Real Time Messaging Protocol)
RTMP协议基于 TCP,是一种设计用来进行实时数据通信的网络协议,主要用来在 flash/AIR 平台和支持 RTMP 协议的流媒体/交互服务器之间进行音视频和数据通信。
RTMP 是目前主流的流媒体传输协议,广泛用于直播领域,可以说市面上绝大多数的直播产品都采用了这个协议
- RTSP(Real Time Streaming Protocol)
实时流传送协议,是用来控制声音或影像的多媒体串流协议,相对于RTMP来说,实时性更好,如果对延时性要求较高的话,建议采用RTSP传输协议
- HLS(HTTP Live Streaming)
苹果公司(AppleInc.)实现的基于HTTP的流媒体传输协议
音视频推流到流媒体服务器后,就是服务器的分发了,流媒体服务器的作用是负责直播流的发布和转播分发功能。
流媒体服务器有诸多选择,我在本地调试的时候采用的是开源的EasyDarwin,使用起来还是比较方便的,还有Nginx,可以根据个人喜好去选择。
最后我们就可以通过播放器来去流媒体服务器去拉流观看音视频了,常用的播放器vlc,输入相应的媒体服务地址就可以观看了,类似如下:
rtsp://172.16.2.34:8554/live/abc
总结一下,视频采集处理后推流到流媒体服务器,第一部分功能完成。第二部分就是流媒体服务器,负责把从第一部分接收到的流进行处理并分发给观众。第三部分就是观众啦,只需要拥有支持流传输协议的播放器即可。
目前做的执法仪app,采用的是EasyDarwin开源的推流框架,具体实现流程如下:
- 实例化Push对象
mEasyPusher = new EasyPusher();
- 初始化push
mEasyPusher.initPush(mApplicationContext, callback);
这里我们传入了一个callback实例,用于监听推流的状态
:
mMediaStream.startStream(ip, port, id, new InitCallback() {
@Override
public void onCallback(int code) {
switch (code) {
case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_INVALID_KEY:
sendMessage("无效Key");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_SUCCESS:
sendMessage("激活成功");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_CONNECTING:
sendMessage("连接中");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_CONNECTED:
sendMessage("连接成功");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_CONNECT_FAILED:
sendMessage("连接失败");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_CONNECT_ABORT:
sendMessage("连接异常中断");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_PUSHING:
sendMessage("推流中");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_PUSH_STATE_DISCONNECTED:
sendMessage("断开连接");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_PLATFORM_ERR:
sendMessage("平台不匹配");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_COMPANY_ID_LEN_ERR:
sendMessage("COMPANY不匹配");
break;
case EasyPusher.OnInitPusherCallback.CODE.EASY_ACTIVATE_PROCESS_NAME_LEN_ERR:
sendMessage("进程名称长度不匹配");
break;
}
}
});
- 设置推流相关参数
mEasyPusher.setMediaInfo(!mSWCodec && info.hevcEncode ? Pusher.Codec.EASY_SDK_VIDEO_CODEC_H265 : Pusher.Codec.EASY_SDK_VIDEO_CODEC_H264, 25, Pusher.Codec.EASY_SDK_AUDIO_CODEC_AAC, 1, 8000, 16);
这个方法用来设置推流音视频的相关参数,包括音视频格式,帧率和码率,音频格式为aac,视频格式为h265
4. 开始推流
mEasyPusher.start(ip, port, String.format("%s.sdp", id), Pusher.TransType.EASY_RTP_OVER_TCP);
设置流媒体服务器的地址,因为我们采用的是TCP协议,所以这里直接设置Pusher.TransType.EASY_RTP_OVER_TCP即可。
在Push.java下面,我们会看到还有如下的一个方法:
public void push(byte[] data, int offset, int length, long timestamp, int type);
从方法命名我们其实就可以猜测到,这个方法就是用来推送我们编码的音视频数据的,音频数据推送在AudioStream.java下面:
ps.push(mBuffer.array(), 0, mBufferInfo.size, mBufferInfo.presentationTimeUs / 1000, 0);
这里就是传入了MediaCodec编码后的aac格式的音频数据。
视频数据的推送在HwConsumer.java下面:
mPusher.push(h264/h265, 0, mPpsSps.length + bufferInfo.size, bufferInfo.presentationTimeUs / 1000, 2);
这里传入的是MediaCodec编码后的h264/h265格式的视频数据(执法仪app采用的是h265)。
总结
至此,执法仪app音视频的采集,编码,音视频合成以及如何推送到流媒体服务器就讲完了,下面来总结一下:
- 音频采集
通过AudioRecord进行音频的采集,具体过程为采集,量化,编码(此编码非MediaCodec编码)得到pcm格式的音频文件。
- 视频采集
设置Camera的setPreviewCallbackWithBuffer回调,在onPreviewFrame下面实时获取到YUV格式的预览帧数据,采集到的YUV数据需要旋转为自然方向。
- 音频编码
采集到的pcm格式的音频文件经过MediaCodec编码为aac格式的音频文件。
- 视频编码
原始的YUV格式视频流数据经过MediaCodec编码为h264/h265格式的视频文件。
- 音视频合成为MP4文件
音视频合成采用MediaMuxer来实现,视频数据源为编码后的h264/h265数据,音频数据源为编码后的aac数据。
- 推流
编码后的h264/h265的视频数据和aac音频数据推送到流媒体服务器。
由于水平有限,目前还在学习中,文中表述难免有不当之处,文中涉及到的代码具体可以参考:
http://192.168.11.104/gitweb/?p=SprocommLawApp.git;a=summary
参考开源项目:https://github.com/EasyDarwin/EasyPusher-Android