太久没写博客了,由于工作,过年还有孩子出生搞得自己焦头烂额,现在有些时间了就搞点东西。发现浏览量突破10万了,也是挺高兴的,虽然很多东西写的不好,可也看到了自己的进步,也是前年到现在的累积。刚开始我只是学习视频解码,渲染和视频编码,慢慢的也开始搞音频了,本来没想过搞视频编辑这一块的,慢慢的做着做着就接触到了,也没想到会搞成一个系列,等完成了再好好整理一下,废话不多说开始说正题。
转gif由于文件大小问题降低了帧率所以看起来卡其实不卡
这篇文章只讲画面部分,音频部分在我以前的文章有android 使用MediaCodec和lamemp3对多段音频进行截取和拼接,以后再写一篇相结合的文章,真正实现一个有画面有声音的视频裁剪和合并。做音频拼接知道有些参数要一样才可以进行拼接,视频也一样,主要是帧速率和分辨率要一样,因为用到opengl,所以分辨率这一块可以通过计算解决,而帧速率可以进行丢帧处理,选最小的帧速率为参数,对大帧速率的视频进行丢帧。
首先裁剪视频,裁剪视频用的是我的这篇文章android 简单的视频编辑功能
主要用到这些参数,这是纯画面的,先不处理声音
已经加入了音频
//视频路径
private String videoFile;
//裁剪的坐标长宽
private int cropLeft;
private int cropTop;
private int cropWidth;
private int cropHeight;
//开始结束时间
private long startTime;
private long endTime;
//帧时间
private long frameTime;
得到复数的视频开始统一参数
//这里遍历数据,选择最小的参数用来初始化编码器
cropWidth = decoderList.get(0).getCropWidth();
cropHeight = decoderList.get(0).getCropHeight();
int frameRate = decoderList.get(0).getFrameRate();
frameTime = decoderList.get(0).getFrameTime();
//宽高选择方案
//以最小宽高来显示还是根据宽高比计算最小显示分辨率
//我这选用根据宽高比计算最小显示分辨率
float sh = cropWidth*1.0f/cropHeight;
if(decoderList.size() != 1) {
for (VideoDecoder holder : decoderList) {
float vh = holder.getCropWidth()*1.0f/holder.getCropHeight();
if( sh < vh){
cropWidth = Math.min(cropWidth,holder.getCropWidth());
cropHeight = (int) (cropWidth*vh);
//宽高不能是奇数
if(cropHeight%2 != 0){
cropHeight = cropHeight - 1;
}
}
//cropWidth = Math.min(cropWidth,holder.getCropWidth());
//cropHeight = Math.min(cropHeight,holder.getCropHeight());
//选最小帧速率,对帧素率大的进行丢帧处理
frameRate = Math.min(frameRate, holder.getFrameRate());
//帧时间,帧速率越小,帧时间越大
frameTime = Math.max(frameTime, holder.getFrameTime());
}
}
//防止MediaFormat取不到帧速率
if(frameRate < 1){
frameRate = (int) (1000/(frameTime/1000));
}
//编码参数
int BIT_RATE = cropWidth*cropHeight*2*8;
MediaFormat mediaFormat = MediaFormat.createVideoFormat(VIDEO, cropWidth, cropHeight);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
初始化编码器
//创建编码器
try {
videoEncode = MediaCodec.createEncoderByType(VIDEO);
} catch (IOException e) {
e.printStackTrace();
}
videoEncode.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
将编码器生成的Surface传给opengl来获取opengl的显示画面
//将编码器生成的Surface传给opengl来获取opengl的显示画面
Surface surface = videoEncode.createInputSurface();
videoEncode.start();
//初始化opengl
eglUtils = new EGLUtils();
eglUtils.initEGL(surface);
framebuffer = new GLFramebuffer();
framebuffer.initFramebuffer(cropWidth,cropHeight);
surfaceTexture = framebuffer.getSurfaceTexture();
surfaceTexture.setDefaultBufferSize(cropWidth,cropHeight);
解码开始前要计算显示的画面
//计算裁剪的显示画面
float f = decoder.getCropLeft()*1.0f/decoder.getVideoWidth();
float t = 1.0f - decoder.getCropTop()*1.0f/decoder.getVideoHeight();
float r = (decoder.getCropLeft()+decoder.getCropWidth())*1.0f/decoder.getVideoWidth();
float b = 1.0f - (decoder.getCropTop()+decoder.getCropHeight())*1.0f/decoder.getVideoHeight();
float[] textureVertexData = {
r, b,
f, b,
r, t,
f, t
};
//将要显示的画面传给opengl
framebuffer.setVertexDat(textureVertexData);
framebuffer.setRect(decoder.getCropWidth(),decoder.getCropHeight());
创建解码器
//创建解码器
try {
videoDecoder = MediaCodec.createDecoderByType(getMime());
videoDecoder.configure(format, surface, null, 0);
videoDecoder.start();
} catch (IOException e) {
e.printStackTrace();
}
根据时间进行解码
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
if(outIndex >= 0){
//根据时间进行解码
if(info.presentationTimeUs >= videoHolder.getStartTime() &&
info.presentationTimeUs <= videoHolder.getEndTime()){
//解码一帧完成后回调
if(decoderListener != null){
decoderListener.onDecoder(info.presentationTimeUs,videoHolder);
}
}
videoDecoder.releaseOutputBuffer(outIndex, true);
//判断是否裁剪内部分解码完成
if(info.presentationTimeUs >= videoHolder.getEndTime()){
break;
}
}
解码出的图像进行处理
surfaceTexture.updateTexImage();
surfaceTexture.getTransformMatrix(mSTMatrix);
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(0f,0f,0f,0f);
GLES20.glViewport(rect.left, rect.top, rect.right, rect.bottom);
GLES20.glUseProgram(programId);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBuffers[0]);
GLES20.glEnableVertexAttribArray(aPositionHandle);
GLES20.glVertexAttribPointer(aPositionHandle, 3, GLES20.GL_FLOAT, false,
12, 0);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBuffers[1]);
GLES20.glEnableVertexAttribArray(aTextureCoordHandle);
GLES20.glVertexAttribPointer(aTextureCoordHandle, 2, GLES20.GL_FLOAT, false, 8, 0);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]);
GLES20.glUniform1i(uTextureSamplerHandle,0);
GLES20.glUniformMatrix4fv(uSTMMatrixHandle, 1, false, mSTMatrix, 0);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
进行编码
ByteBuffer encodedData = videoEncode.getOutputBuffer(inputIndex);
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
info.size = 0;
}
if (info.size != 0) {
encodedData.position(info.offset);
encodedData.limit(info.offset + info.size);
info.presentationTimeUs = timeUs;
//填充数据
mediaMuxer.writeSampleData(videoTrackIndex, encodedData, info);
Log.d("==============","timeUs = "+timeUs);
timeUs = timeUs+frameTime;
//结合presentationTimeUs - videoHolder.getStartTime() < startFrameTime
//对帧速率大的进行丢帧
startFrameTime = startFrameTime + frameTime;
}
videoEncode.releaseOutputBuffer(inputIndex, false);
详细的可以看我的demo
VideoMerge
现在没加进度条,处理的进度就看log打印,等整理好以后加上音频和进度条,成为一个可以看的demo
2019/3/27更新
因为音频要转码成MP3,再转成视频所以效率不太好,可是也找不到什么好方法将采样率,比特率和声道变一样的方法
在整合音频时代码做了大改动,画面核心代码没变只是放到其他类和函数内了,所以就不改博客了,可以下载demo,里面我也做了注释