本文提供几种可行的在Android端进行录像的方案,对于MP4文件的格式请自行在网上查阅。其中的有些方法经过了个人实践,贴出了核心代码,有一些由于不满足项目需要,没有实践,贴出一些参考资料。
1 常规录制
1.1 场景描述
直接从摄像头和麦克风取数据,经过编码,保存为文件。
1.2 采用方法
这种情况可以直接调用Android的MediaRecorder类。该类获得数据后,通过硬编码,然后保存成文件。操作流程固定。
1.3 核心代码
MediaRecorder mMediaRecorder;
mMediaRecorder=new MediaRecorder();
//设置视频源
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.DEFAULT);
//设置音频源
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
//设置文件输出格式
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//设置视频编码方式
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
//设置音频编码方式
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
//设置视频高和宽,注意文档的说明:
//Must be called after setVideoSource().
//Call this after setOutFormat() but before prepare().
//设置录制的视频帧率,注意文档的说明:
//Must be called after setVideoSource().
//Call this after setOutFormat() but before prepare().
mMediaRecorder.setVideoFrameRate(15);
//设置预览画面
mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
//设置输出路径
mMediaRecorder.setOutputFile
(Environment.getExternalStorageDirectory()+File.separator+System.currentTimeMillis()+".mp4");
mMediaRecorder.setVideoSize(640, 480);
//设置视频的最大持续时间
mMediaRecorder.setMaxDuration(10000);
//为MediaRecorder设置监听
mMediaRecorder.setOnInfoListener(new OnInfoListener() {
public void onInfo(MediaRecorder mr, int what, int extra) {
if (what==MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
System.out.println("已经达到最长录制时间");
if (mMediaRecorder!=null) {
mMediaRecorder.stop();
mMediaRecorder.release();
mMediaRecorder=null;
}
}
}
});
2 可修改数据方式进行录像
2.1 场景描述
获取摄像头数据,进行处理,对处理过后视频数据进行编码,同时获得音频数据,进行处理,同样对处理后数据进行编码,将编码后的到数据保存为文件。
2.2 可选方法
(1)对于Android 4.3之后,可以通过MediaCodec和MediaMuxer配合进行录制。处理过后的数据,通过MediaCodec进行编码,然后通过MediaMuxer进行混合。
参考:http://blog.csdn.net/jinzhuojun/article/details/32163149
(2)利用开源库
ffmpeg:http://blog.csdn.net/leixiaohua1020/article/details/15811977这篇博文整理的很好。
http://blog.csdn.net/shaoyizhe2006/article/details/8525738写MP4的例子。
(3)利用MediaCodec库和MP4V2进行录制, MediaCodec只在Android 4.1之后存在。MediaCodec调用硬编码,不占CPU,因此该方法可以保证效率。利用MediaCodec进行编码后,调用封装的MP4V2的写MP4类写成文件。
2.3 方案三细节
2.3.1 MediaCodec的调用
该类主要对数据进行编码,具体操作流程如下:首先对编码器进行初始化,然后在编码器的输入数据队列中添加数据。编码得到的数据从编码器的输出队列中获取。
(1)初始化编码器
//video and audio encoder
public MediaCodec VideoCodecEncoder, AudioCodecEncoder;
//video frame property
int m_FrameWidth = 320; //视频帧宽
int m_FrameHeight = 240; //视频帧高
int m_VideoFrameRate = 20; //视频帧率
int m_VideoBitRate = 2000000; //视频比特率
int m_AudioSamleRate = 22050; //音频采样频率
int m_AudioChannelCout = 1; //音频通道数
int m_AudioBitRate = 128000; //音频比特率
private int InitEncoder(int width, int height, int videobitrate,
int videoframerate, int audiosampleRate, int audiochannelCount, int audiobitrate){
VideoCodecEncoder = MediaCodec.createEncoderByType("video/avc"); //"video/avc"为MIME类型,可以查阅支持的类型
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, videobitrate);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, videoframerate);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); //关键帧间隔时间 单位s
VideoCodecEncoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
VideoCodecEncoder.start();
AudioCodecEncoder = MediaCodec.createEncoderByType("audio/mp4a-latm");
MediaFormat AudioFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", audiosampleRate, audiochannelCount);
//尝试过的手机只支持这种MIME类型
AudioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
//测试发现,KEY_BIT_RAT必须设置,否则编码器无法设置成功
AudioFormat.setInteger(MediaFormat.KEY_BIT_RATE, audiobitrate);
//m.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, mBuffer_Size);
AudioCodecEncoder.configure(AudioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
AudioCodecEncoder.start();
return 0;
}
(2)在输入队列中添加数据
//write videodata h264
//输入视频帧数据格式为YUV420,即YYYYUVUVUVUV
//摄像头支持的数据YV12数据排列方式为YYYYUUUUVVVV,需要转换
public int WriteVideoData(byte[] YUV420Data){
ByteBuffer[] inputBuffers = VideoCodecEncoder.getInputBuffers();
int inputBufferIndex = VideoCodecEncoder.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0)
{
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(YUV420Data); //放入数据
VideoCodecEncoder.queueInputBuffer(inputBufferIndex, 0, YUV420Data.length, 0, 0);
}
return 0;
}
//wirte audiodata aac
//麦克风数据为PCM格式
public int WriteAudioData(byte[] PCMData){
ByteBuffer[] AudioinputBuffers = AudioCodecEncoder.getInputBuffers();
int AudioinputBufferIndex = AudioCodecEncoder.dequeueInputBuffer(-1);
if (AudioinputBufferIndex >= 0)
{
ByteBuffer inputBuffer = AudioinputBuffers[AudioinputBufferIndex];
inputBuffer.clear();
inputBuffer.put(PCMData);
AudioCodecEncoder.queueInputBuffer(AudioinputBufferIndex, 0, PCMData.length, 0, 0);
}
return 0;
}
(3)从输出队列取数据(以视频帧为例)
ByteBuffer[] outputBuffers = VideoCodecEncoder.getOutputBuffers();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = VideoCodecEncoder.dequeueOutputBuffer(bufferInfo,0);
int pos = 0;
while (outputBufferIndex >= 0)
{
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = new byte[bufferInfo.size];
outputBuffer.get(outData); //取编码后数据
if(m_Video_SPSPPS != null)
{
System.arraycopy(outData, 0, h264, pos, outData.length);
pos += outData.length;
}
else //保存pps sps 只有开始时 第一个帧里有, 保存起来后面用
//音频第一帧也有信息,一般为2个字节
{
ByteBuffer spsPpsBuffer = ByteBuffer.wrap(outData);
if (spsPpsBuffer.getInt() == 0x00000001)
{
m_Video_SPSPPS = new byte[outData.length];
System.arraycopy(outData, 0, m_Video_SPSPPS, 0, outData.length);
}
else
{
return -1;
}
}
VideoCodecEncoder.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = VideoCodecEncoder.dequeueOutputBuffer(bufferInfo, 0);
}
//音频不存在关键帧,因此需要添加头信息
byte input[] = new byte[h264.length];
if(h264[4] == 0x65) //key frame 编码器生成关键帧时只有 00 00 00 01 65 没有pps sps, 要加上
{
System.arraycopy(h264, 0, input, 0, pos);
System.arraycopy(m_Video_SPSPPS, 0, h264, 0, m_Video_SPSPPS.length);
System.arraycopy(input, 0, h264, m_Video_SPSPPS.length, pos);
pos += m_Video_SPSPPS.length;
}
2.3.2 Mp4v2调用
这是一个开源库:以下博文对该库的使用描述比较清楚。
http://blog.csdn.net/sweetloverft/article/details/29851309
包括编译方法,另外如果要在Android上使用,可以针对以下C++版本的调用程序(http://download.csdn.net/detail/nighterll/7592823)中的MP4Encoder类写个jni,然后和android库一起在cgwin下编译成so就可以使用。注意些Android.mk时按网上的写法,在需要编的.h和.cpp后加上MP4Encoder.h和MP4Encoder.cpp,以及自己写的jni的cpp和h文件。值得注意的是mp4v2在编译时cpp和h的编译时有顺序,尝试用通配符的方式(偷懒不想写那么多),出现错误,因此按各种帖子上的方式写,在最好加上自己的东西就可以了。值得一提的是C++版本的这个Demo非常好,已经测试过,很好用,结构清晰,操作简单。作者在CSDN上谦虚的称是封装的较好的,测试确实如此,程序直接可以编译运行,代码结构清晰,看起来不费力。可惜找了半天,找不到下载的地方了,我在上传一份,如果作者看到,请告知我一声,我把链接改到您的位置上。
具体调用过程为:先初始化文件,即建立一个MP4文件;然后写h264的track,需要给的参数就是sps和pps;接下来写AAC的track,给的信息也是一个头信息,似乎大部分都是2个字节;然后写h264的数据;接着写AAC数据;最后关闭文件。中间的步骤,只要对应的track在写数据之前写完,其他的没有什么顺序限制。
C++版本的调用实例:
MP4CreateFile("test.mp4", 5);
MP4AddH264Track(buf, len, 640, 480);
MP4AddAACTrack(buf, len);
MP4WriteH264Data(buf, len, pts);
MP4WriteAACData(buf, len, pts);
MP4ReleaseFile();
写Data的两个方法可以循环调用,直到写完所有数据。
3 遗留问题
到此,一个可行的方案完成了,测试了录像。存在以下问题:
(1)视频出现马赛克,个人认为是编码器的两个队列处理的问题,由于硬编码速度快,写文件速度慢,而编码器的输出队列只缓存了4个包,前面还没有写完,后面编码输出就覆盖了原来包,导致丢包。不知道理解对不对,希望有高手可以解释。
(2)同步问题,现在给pts是在写数据的时候分别给的PTS,但是同时采集的视频帧处理时间远大于音频的处理时间,如果用采集时间作为PTS,编码器输出的是一个个的包,没有时间信息,不知道怎么做同步。从实际效果来看,现在用写的时间来记录,文件播放似乎也是同步的,但是这始终是个隐患。
遗留的这两个问题希望做相关研究的朋友一起讨论解决方案。