Camera开发系列之二-相机预览数据回调android
Camera开发系列之四-使用MediaMuxer封装编码后的音视频到mp4容器git
Camera开发系列之五-使用MediaExtractor制做一个简易播放器github
Camera开发系列之七-使用GLSurfaceviw绘制Camera预览画面 架构
前几篇的文章中,咱们已经可以获取到h264格式的视频裸流和pcm格式的音频数据了,而使用MediaMuxer这个工具,则能够将咱们处理过的音视频数据封装到mp4容器里。框架
学习一个历来没接触过的东西,固然先从官方文档给开始看啦,下面是MediaMuxer的主要方法:ide
int addTrack(@NonNull MediaFormat format):
一个视频文件是包含一个或多个音视频轨道的,而这个方法就是用于添加一个视频或视频轨道,并返回对应的ID。以后咱们能够经过这个ID向相应的轨道写入数据。void start():
当咱们添加完全部音视频轨道以后,须要调用这个方法告诉Muxer,我要开始写入数据了。须要注意的是,调用了这个方法以后,咱们是没法再次addTrack
了的。void writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):
用于向Muxer写入编码后的音视频数据。trackIndex是咱们addTrack的时候返回的ID,byteBuf即是要写入的数据,而bufferInfo是跟这一帧byteBuf相关的信息,包括时间戳、数据长度和数据在ByteBuffer中的位移。void stop() :
与start()
相对应,用于中止写入数据。MediaMuxer中使用的方法就介绍完了,真是个又短又实用的工具( ̄▽ ̄)/。那这玩意儿怎么用呢?也很简单,没有繁琐的调用方法,只须要四步就搞定:工具
具体的代码以下:
MediaMuxer mMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);//第一步,其中第一个参数为合成的mp4保存路径,第二个参数是格式为MP4
//第二步
public void addTrack(MediaFormat format,boolean isVideo){
Log.e( TAG,"添加音频轨和视频轨");
if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1){
new RuntimeException("already addTrack");
}
int track = mMuxer.addTrack(format);
if (isVideo){
mVideoFormat = format;
mVideoTrackIndex = track;
}else {
mAudioFormat = format;
mAudioTrackIndex = track;
}
if (mVideoTrackIndex != -1 && mAudioTrackIndex != -1){ //当音频轨和视频轨都添加,才start
mMuxer.start();
}
}
//第三步
public synchronized void putStrem(ByteBuffer outputBuffer,MediaCodec.BufferInfo bufferInfo,boolean isVideo){
if (mAudioTrackIndex == -1 || mVideoTrackIndex == -1){
Log.e( TAG,"音频轨和视频轨没有添加");
return;
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0){
// The codec config data was pulled out and fed to the muxer when we got
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
}else if (bufferInfo.size != 0){
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.size + bufferInfo.offset);
mMuxer.writeSampleData(isVideo?mVideoTrackIndex:mAudioTrackIndex,outputBuffer,bufferInfo);
}
}
//最后一步
public void release(){
if (mMuxer != null){
if (mAudioTrackIndex != -1 && mVideoTrackIndex != -1){
mMuxer.stop();
mMuxer.release();
mMuxer = null;
}
}
}
复制代码
其中第二步须要注意的是,必须在音频轨和视频轨都添加完成以后,才能调用start方法。
上面的代码可能让各位有点懵,道理你们都懂,可是在实际使用中何时添加音视频轨,何时喂数据??
在获取编码器输出缓冲区时,调用了mediaCodec.dequeueOutputBuffer(),这个方法的返回值是一个int类型的的索引 ,当这个索引等于MediaCodec.INFO_OUTPUT_FORMAT_CHANGED
(这个常量为-2)常量时,表示编码器输出缓存区格式改变,一般在存储数据以前且只会改变一次,因此这个时候添加音视频轨最合适。
当这个索引大于0,说明已成功解码的输出缓冲区,这个时候的数据是有效的,能够喂给MediaMuxer了,视频数据的写入具体代码以下:
//编码器输出缓冲区
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
boolean isAddKeyFrame = false;
int outputBufferIndex;
do {
outputBufferIndex = mediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
//Log.i(TAG, "得到编码器输出缓存区超时");
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// 若是API小于21,APP须要从新绑定编码器的输入缓存区;
// 若是API大于21,则无需处理INFO_OUTPUT_BUFFERS_CHANGED
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
outputBuffers = mediaCodec.getOutputBuffers();
}
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 编码器输出缓存区格式改变,一般在存储数据以前且只会改变一次
// 这里设置混合器视频轨道,若是音频已经添加则启动混合器(保证音视频同步)
synchronized (H264EncoderConsumer.this) {
MediaFormat newFormat = mediaCodec.getOutputFormat();
addTrack(newFormat, true);
}
//Log.i(TAG, "编码器输出缓存区格式改变,添加视频轨道到混合器");
} else {
//由于上面的addTrackIndex方法不必定会被调用,因此要在此处再判断并添加一次,这也是混合的难点之一
if (!mediaUtil.isAddVideoTrack()) {
synchronized (H264EncoderConsumer.this) {
MediaFormat newFormat = mediaCodec.getOutputFormat();
addTrack(newFormat, true);
}
}
ByteBuffer outputBuffer = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = outputBuffers[outputBufferIndex];
} else {
outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
}
// 若是API<=19,须要根据BufferInfo的offset偏移量调整ByteBuffer的位置
// 而且限定将要读取缓存区数据的长度,不然输出数据会混乱
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
}
// 判断输出数据是否为关键帧 必须在关键帧添加以后,再添加普通帧,否则会出现马赛克
boolean keyFrame = (mBufferInfo.flags & BUFFER_FLAG_KEY_FRAME) != 0;
if (keyFrame) {
// 录像时,第1秒画面会静止,这是因为音视轨没有彻底被添加
Log.i(TAG, "编码混合,视频关键帧数据(I帧)");
putStrem(outputBuffer, mBufferInfo, true);
isAddKeyFrame = true;
} else {
// 添加视频流到混合器
if (isAddKeyFrame) {
Log.i(TAG, "编码混合,视频普通帧数据(B帧,P帧)" + mBufferInfo.size);
putStrem(outputBuffer, mBufferInfo, true);
}
}
// 处理结束,释放输出缓存区资源
mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
}
} while (outputBufferIndex >= 0);
复制代码
以上是视频数据的写入,代码和以前的编码h264差很少,就不贴所有代码了。音频数据写入相似,这里不作过多阐述,我相信各位都是和我同样的聪明人,不用我再贴代码都能依葫芦画瓢写出来。
音频编码的代码以下:
public class AudioEncoder {
private MediaCodec.BufferInfo mBufferInfo;
private final String mime = "audio/mp4a-latm";
private int bitRate = 96000;
private FileOutputStream fileOutputStream;
private MediaCodec mMediaCodec;
private static volatile boolean isEncoding;
private static final String TAG = AudioEncoder.class.getSimpleName();
private AudioRecord mAudioRecord;
private int mAudioRecordBufferSize;
private static AudioEncoder mAudioEncoder;
private AudioEncoder() {
}
public static AudioEncoder getInstance() {
if (mAudioEncoder == null) {
synchronized (AudioEncoder.class) {
if (mAudioEncoder == null) {
mAudioEncoder = new AudioEncoder();
}
}
}
return mAudioEncoder;
}
public AudioEncoder setEncoderParams(EncoderParams params) {
try {
mMediaCodec = MediaCodec.createEncoderByType(mime);
MediaFormat mediaFormat = new MediaFormat();
mediaFormat.setString(MediaFormat.KEY_MIME, mime);
mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); //声道
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024 * 100);//做用于inputBuffer的大小
mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);//采样率
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//start()后进入执行状态,才能作后续的操做
mMediaCodec.start();
startAudioRecord(params);
mBufferInfo = new MediaCodec.BufferInfo();
if (null != params.getAudioPath()) {
File fileAAc = new File(params.getAudioPath());
if (!fileAAc.exists()) {
fileAAc.createNewFile();
}
fileOutputStream = new FileOutputStream(fileAAc.getAbsoluteFile());
}
} catch (IOException e) {
e.printStackTrace();
}
return mAudioEncoder;
}
public void startEncodeAacData() {
isEncoding = true;
Thread aacEncoderThread = new Thread(new Runnable() {
@Override
public void run() {
while (isEncoding) {
if (mAudioRecord != null && mMediaCodec != null) {
byte[] audioBuf = new byte[mAudioRecordBufferSize];
int readBytes = mAudioRecord.read(audioBuf, 0, mAudioRecordBufferSize);
if (readBytes > 0) {
try {
encodeAudioBytes(audioBuf, readBytes);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
stopEncodeAacSync();
}
});
aacEncoderThread.start();
}
public static boolean isEncoding() {
return isEncoding;
}
private void startAudioRecord(EncoderParams params) {
// 计算AudioRecord所需输入缓存空间大小
mAudioRecordBufferSize = AudioRecord.getMinBufferSize(params.getAudioSampleRate(), params.getAudioChannelConfig(),
params.getAudioFormat());
if (mAudioRecordBufferSize < 1600) {
mAudioRecordBufferSize = 1600;
}
Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO);
mAudioRecord = new AudioRecord(params.getAudioSouce(), params.getAudioSampleRate(),
params.getAudioChannelConfig(), params.getAudioFormat(), mAudioRecordBufferSize);
// 开始录音
mAudioRecord.startRecording();
}
private void encodeAudioBytes(byte[] audioBuf, int readBytes) {
//dequeueInputBuffer(time)须要传入一个时间值,-1表示一直等待,0表示不等待有可能会丢帧,其余表示等待多少毫秒
int inputIndex = mMediaCodec.dequeueInputBuffer(-1);//获取输入缓存的index
if (inputIndex >= 0) {
ByteBuffer inputByteBuf;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
inputByteBuf = mMediaCodec.getInputBuffer(inputIndex);
} else {
ByteBuffer[] inputBufferArray = mMediaCodec.getInputBuffers();
inputByteBuf = inputBufferArray[inputIndex];
}
if (audioBuf == null || readBytes <= 0) {
mMediaCodec.queueInputBuffer(inputIndex, 0, 0, getPTSUs(), MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
inputByteBuf.clear();
inputByteBuf.put(audioBuf);//添加数据
//inputByteBuf.limit(audioBuf.length);//限制ByteBuffer的访问长度
mMediaCodec.queueInputBuffer(inputIndex, 0, readBytes, getPTSUs(), 0);//把输入缓存塞回去给MediaCodec
}
}
int outputIndex;
byte[] frameBytes = null;
do {
outputIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 12000);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
//Log.i(TAG,"得到编码器输出缓存区超时");
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//设置混合器视频轨道,若是音频已经添加则启动混合器(保证音视频同步)
synchronized (AudioEncoder.class) {
MediaFormat format = mMediaCodec.getOutputFormat();
MediaUtil.getDefault().addTrack(format, false);
}
} else {
//获取缓存信息的长度
int byteBufSize = mBufferInfo.size;
// 当flag属性置为BUFFER_FLAG_CODEC_CONFIG后,说明输出缓存区的数据已经被消费了
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
Log.i(TAG, "编码数据被消费,BufferInfo的size属性置0");
byteBufSize = 0;
}
// 数据流结束标志,结束本次循环
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(TAG, "数据流结束,退出循环");
break;
}
ByteBuffer outPutBuf;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
outPutBuf = mMediaCodec.getOutputBuffer(outputIndex);
} else {
ByteBuffer[] outputBufferArray = mMediaCodec.getOutputBuffers();
outPutBuf = outputBufferArray[outputIndex];
}
if (byteBufSize != 0) {
//由于上面的addTrackIndex方法不必定会被调用,因此要在此处再判断并添加一次,这也是混合的难点之一
if (!MediaUtil.getDefault().isAddAudioTrack()) {
synchronized (AudioEncoder.this) {
MediaFormat newFormat = mMediaCodec.getOutputFormat();
MediaUtil.getDefault().addTrack(newFormat, false);
}
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
outPutBuf.position(mBufferInfo.offset);
outPutBuf.limit(mBufferInfo.offset + mBufferInfo.size);
}
MediaUtil.getDefault().putStrem(outPutBuf, mBufferInfo, false);
Log.i(TAG, "------编码混合音频数据-----" + mBufferInfo.size);
//给adts头字段空出7的字节
int length = mBufferInfo.size + 7;
if (frameBytes == null || frameBytes.length < length) {
frameBytes = new byte[length];
}
addADTStoPacket(frameBytes, length);
outPutBuf.get(frameBytes, 7, mBufferInfo.size);
if (audioListener != null) {
audioListener.onGetAac(frameBytes, length);
}
}
//释放
mMediaCodec.releaseOutputBuffer(outputIndex, false);
}
} while (outputIndex >= 0);
}
private long prevPresentationTimes = 0;
private long getPTSUs() {
long result = System.nanoTime() / 1000;
if (result < prevPresentationTimes) {
result = (prevPresentationTimes - result) + result;
}
return result;
}
/** * 给编码出的aac裸流添加adts头字段 * * @param packet 要空出前7个字节,不然会搞乱数据 * @param packetLen */
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; //AAC LC
int freqIdx = 4; //44.1KHz
int chanCfg = 2; //CPE
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
public void stopEncodeAac() {
isEncoding = false;
}
private void stopEncodeAacSync() {
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}
if (mMediaCodec != null) {
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
try {
if (fileOutputStream != null) {
fileOutputStream.flush();
fileOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
MediaUtil.getDefault().release();
if (audioListener != null) {
audioListener.onStopEncodeAacSuccess();
}
}
private AudioEncodeListener audioListener;
public void setEncodeAacListner(AudioEncodeListener listener) {
this.audioListener = listener;
}
public interface AudioEncodeListener {
void onGetAac(byte[] data, int length);
void onStopEncodeAacSuccess();
}
}
复制代码
音视频编码的mediacodec初始化参数都是差很少的,这里我用一个单独的类来设置录制时的参数:
public class EncoderParams {
public static final int DEFAULT_AUDIO_SAMPLE_RATE = 44100; //全部android系统都支持的采样率
public static final int DEFAULT_CHANNEL_COUNT = 1; //单声道
public static final int CHANNEL_COUNT_STEREO = 2; //立体声
public static final int DEFAULT_AUDIO_BIT_RATE = 96000; //默认比特率
public static final int LOW_VIDEO_BIT_RATE = 1; //默认比特率
public static final int MIDDLE_VIDEO_BIT_RATE = 3; //默认比特率
public static final int HIGH_VIDEO_BIT_RATE = 5; //默认比特率
private String videoPath; //视频文件的全路径
private String audioPath; //音频文件全路径
private int frameWidth;
private int frameHeight;
private int frameRate; // 帧率
private int videoQuality = MIDDLE_VIDEO_BIT_RATE; //码率等级
private int audioBitrate = DEFAULT_AUDIO_BIT_RATE; // 音频编码比特率
private int audioChannelCount = DEFAULT_CHANNEL_COUNT; // 通道数
private int audioSampleRate = DEFAULT_AUDIO_SAMPLE_RATE; // 采样率
private int audioChannelConfig ; // 单声道或立体声
private int audioFormat; // 采样精度
private int audioSouce; // 音频来源
public EncoderParams(){
}
//...省略set get方法
}
复制代码
最后在录制mp4的时候,同时启动编码音频数据和视频数据的线程就ok了:
H264EncoderConsumer.getInstance()
.setEncoderParams(params)
.StartEncodeH264Data();
AudioEncoder.getInstance()
.setEncoderParams(params)
.startEncodeAacData();
复制代码
使用以上的方法录制mp4视频,会出现不少奇怪的问题。
恭喜你,看到这儿才发现本篇文章是大坑,如今是否是想特别锤我呀,惋惜你打不着我,略略略
不一样的手机会出现不一样的状况,配置低的手机会出现录制的视频变慢的现象,配置高的手机会出现视频变快的现象。
其实出现这个问题很简单,以前从网上copy代码,都是用的ArrayBlockingQueue队列接收每一帧yuv格式的数据,而后mediacodec从队列中不停的读取数据,配置低的手机处理数据能力慢,配置高的手机处理数据能力快,就会形成这种状况。解决方法也很简单,不用队列接收数据了呗,直接从camera回调中获取数据编码。
你觉得大功告成了吗?不存在的,解决上面的问题以后,你还会发现录制的视频出现卡顿的现象,由于对yuv数据的处理太耗时了,在java中作旋转yuv数据耗时200ms左右,旋转以后还要转换为mediacodec支持的nv12的数据格式,耗时110ms左右。加起来有300多毫秒,固然卡了。既然java中作数据处理不太方便,那就在native层作吧,直接上cmake写c++,一鼓作气。
一顿操做猛如虎,一看效果卡如狗。套用java的两个转换方法(上篇文章有提供代码),放进native层,总共耗时在150ms之内,快了将近一倍,虽然没有那么卡顿了,可是录制出来的视频和MediaRecorder
录制出来的用肉眼看,仍是有很大的差异。没办法了,本身写的渣代码无法用,只能靠第三方库libyuv
了。
什么是libyuv
?看看官方解释:
libyuv是Google开源的实现各类YUV与RGB之间相互转换、旋转、缩放的库。它是跨平台的,可在Windows、Linux、Mac、Android等操做系统,x8六、x6四、arm架构上进行编译运行,支持SSE、AVX、NEON等SIMD指令加速。
看起开很屌的样子,下载libyuv
源码,导入android studio,让我来试试你的深浅!使用libyuv
,首先要将nv21格式的数据转换为I420格式,而后才能对数据进行其余操做。具体流程是这样的:
camera获取到nv21数据 -> 转换为I420 -> 旋转镜像I420数据 -> 转换为nv12 -> mediacodec编码为h264
这套流程感受比上面的方法还要耗时,由于多了I420的转换,可是实际测试总体耗时在20ms左右。侧面反映了google有多厉害,我写的代码有多渣。
libyuv
的使用这里不作过多介绍,由于android studio 支持cmake,我并无将其编译为so库使用。具体步骤就不细说了,能够上github看源码。后期可能会对录制的音频变声,以及对视频添加水印等处理。
项目地址:camera开发从入门到入土 欢迎start和fork