之前写过一个开源项目仿微信视频拍摄UI基于 ffmpeg的视频录制编辑
使用的是VCamera库来录制,效果很差, 主要是因为ffmpeg的so库编译版本不支持targetSdkVersion26以上, 导致现在大部分项目都用不了, 而且优化不是很好
所以我用了基于Camera录制源+MediaCodec编码 替换了原本的录制方法, 并更新了ffmpeg库LanSoEditor 速度更快 兼容更好
1. 首先是打开camera预览画面
//正常初始化Camera对象, 注意getCameraDisplayOrientation()这个方法,
//因为Camera得到的画面是歪的, 所以我们需要旋转一下
public void openCamera(Activity activity, int cameraId, SurfaceHolder surfaceHolder){
...
mCamera = Camera.open(cameraId);
//视频旋转角度 因为Camera得到的画面是歪的, 所以我们需要旋转一下播放
displayOrientation = getCameraDisplayOrientation(activity, cameraId);
mCamera.setDisplayOrientation(displayOrientation);
mCamera.setPreviewDisplay(surfaceHolder);
mCamera.setPreviewCallback(previewCallback);
Camera.Parameters parameters = mCamera.getParameters();
previewSize = getPreviewSize();
//视频录制尺寸
parameters.setPreviewSize(previewSize[0], previewSize[1]);
//录制焦点
parameters.setFocusMode(getAutoFocus());
parameters.setPictureFormat(ImageFormat.JPEG);
//在后面会转成nv12在进行编码
parameters.setPreviewFormat(ImageFormat.NV21);
mCamera.setParameters(parameters);
mCamera.startPreview();
...
}
//在这里cemara返回的数据是YUV420-NV21格式的数据
mCameraHelp.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
...
mYUVQueue.add(data);
...
}
});
2. 当开始录制的时候 初始化AvcEncoder(重点)
//首先是构造函数里面, 我们初始化MediaCodec编码器(视频宽高,码率,yuv420色彩,帧数,编码格式avc)
public AvcEncoder(int width, int height, ArrayBlockingQueue YUVQueue) {
...
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width*height*5);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
...
}
//开始编码, 编译出来的数据是h264
public void startEncoder(final String videoPath, final boolean isFrontCamera){
//循环从YUVQueue里面拿取每帧数据
while (isRunning.get()) {
//把nv21转nv12
NV21ToNV12(input,yuv420sp, width, height);
//如果是前置摄像头的话 要上下颠倒一下画面
rotateYUV420Degree180(yuv420sp, width, height);
//此步就是mediaCodec转码 有兴趣去看详细代码, 在后面我会详细讲解
mediaCodec.getInputBuffers();
}
}
3. 最后一步就是使用ffmpeg把h264转mp4
//ffmpeg -i in -vcodec copy -f mp4 put
//因为我的功能里有分段录制 所以我是先把h264转ts 然后多个ts合成mp4
4. 视频录制就完成了, 接下来是音频录制编码 初始化AudioRecordUtil
public AudioRecordUtil(final String path){
bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
//麦克风 采样率 单声道 音频格式, 缓存大小
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
...
}
//和视频编码一样 也是循环
public void startRecord(){
while (isRecording.get()) {
//拿到音频数据
audioRecord.read(data, 0, bufferSize);
//pcm转aac 有兴趣去看详细代码
encodeData(data);
}
}
5. 最后录制完成 使用ffppeg把视频和音频文件合成在一起
//ffmpeg -i video -1 audio -t vDuration -map 0:v -map 1:a -vcodec copy -acodec copy -absf aac_adtstoasc -y out
6. 音视频使用的MediaCodec除了编码的数据不一样 原理是一致的
//得到编码器的输入和输出流, 输入流写入源数据 输出流读取编码后的数据
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
//得到要使用的缓存序列角标
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
pts = computePresentationTime(generateIndex);
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
//把要编码的数据添加进去
inputBuffer.put(input);
//塞到编码序列中, 等待MediaCodec编码
mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
generateIndex += 1;
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//读取MediaCodec编码后的数据
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = new byte[bufferInfo.size];
//这步就是编码后的h264数据了
outputBuffer.get(outData);
if(bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG){//视频信息
configByte = new byte[bufferInfo.size];
configByte = outData;
}else if(bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME){//关键帧
byte[] keyframe = new byte[bufferInfo.size + configByte.length];
System.arraycopy(configByte, 0, keyframe, 0, configByte.length);
System.arraycopy(outData, 0, keyframe, configByte.length, outData.length);
outputStream.write(keyframe, 0, keyframe.length);
}else{//正常的媒体数据
outputStream.write(outData, 0, outData.length);
}
//数据写入本地成功 通知MediaCodec释放data
mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
//读取下一次编码数据
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
}
7. 还有拍照的逻辑 其实很简单 就是把一帧NV21数据转成jpeg
//把NV21数据输出成jpeg
YuvImage yuvimage = new YuvImage(data, ImageFormat.NV21, mCameraHelp.getWidth(), mCameraHelp.getHeight(), null);
yuvimage.compressToJpeg(new Rect(0, 0, yuvimage.getWidth(), yuvimage.getHeight()), 100, fos);
fos.close();
//注意这里, 因为之前初始化camera就说过了, camera出来的画面是旋转的 所以我们这里也手动把图片旋转一下
Matrix matrix = new Matrix();
if(mCameraHelp.getCameraId() == Camera.CameraInfo.CAMERA_FACING_FRONT){
matrix.setRotate(360-mCameraHelp.getDisplayOrientation());
//如果是前置摄像头, 那么镜像一下
matrix.postScale(-1, 1);
}else{
matrix.setRotate(mCameraHelp.getDisplayOrientation());
}
到这里 其实主要逻辑就结束了 其余的视频编辑操作(裁剪, 截取, 添加水印, 播放速度等等)这些其实就是简单的ffmpeg语句, 大家自行看代码也能理解, 之前我也有文章讲过这些
仿微信视频拍摄UI, 基于ffmpeg的视频录制编辑(上)
仿微信视频拍摄UI, 基于ffmpeg的视频录制编辑(下)