之前写了一篇
主要是研究ffmpeg在Android端的应用,那既然有了ffmpeg为什么还要去用其他的技术去做音视频合成呢,有两点:性能差,效果不够
1.性能差:
ffmpeg输入为一次性输入,然后逐帧渲染,再加上filtrer过滤器,添加各种效果,使得每次渲染都会去做大量操作,导致OOM,还有就是ffmpeg使用CPU进行软解码,OpenGl使用GPU硬解码
2.效果不够:
这里的效果主要是转场效果,视频之间的转场,ffmpeg没有转场的功能命令,所以需要自己用骚操作实现,但这样会导致大量的性能损耗,这一点之前的文章说过
使用OpenGl合成视频转场后的性能对比
对比数据表格之前记录了但找不到了,
图片处理速度明显提升,基本是毫秒级别
视频处理速度提升大概200%。
可以看出很明显的OpenGL对于性能的提升,2.0版本是进行了优化并且没有丰富的转场效果前提下才达到这个性能,3.0使用OpenGL做出了比较多的转场效果,可参考gl-transitions.com/
中间也遇到了比较多的问题,写出来记录一下:
1.OpenGL渲染要每一帧的图像资源,视频需要逐帧去获取图像,比较耗时
办法:合成开始前先用Android原生解码器把视频每一帧缓存下来,用的时候直接拿
2。OpenGL是图像处理库,不支持音频处理
办法:合成的时候把视频轨道和音频轨道分开用两个线程来处理,视频使用ffmpeg+OpenGl,音频使用ffmpeg+Android原生,最后用编码器把音轨混合进视频中导出一个完整视频
转场效果的实现:
思路还是跟之前一样,在第一个视频的末尾转场处同时去获取第二个视频的开头的textureId,同步渲染
public void exec() { //第二个视频的纹理图像Id
int texture2 = mContext.getToTextureId(mProgress , (int) mTransitionDuration);
if (texture2 != -1) {
mContext.attachOffScreenTexture(mContext.getOutputTextureId());
if (mDrawer == null) {
getDrawer();
}
mDrawer.setProgress(mProgress);//当前转场进度
mDrawer.setRatio(mContext.getRenderWidth() * 1.0f / mContext.getRenderHeight());
mDrawer.setToRatio(mContext.getNextAspectRatio());
setDrawerParams();
mDrawer.draw(mContext.getFromTextureId(),//当前视频的纹理图像Id
texture2,
mContext.getRenderLeft(),
mContext.getRenderBottom(),
mContext.getRenderWidth(),
mContext.getRenderHeight());
mContext.swapTexture();//交换缓冲区
}
}
创建离屏渲染区接受输出图像
private void createOffScreenTextures() {
GLES20.glGenTextures(mOffScreenTextureIds.length, mOffScreenTextureIds, 0);
for (int mTextureId : mOffScreenTextureIds) {
// bind to fbo texture cause we are going to do setting.
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mSurfaceWidth, mSurfaceHeight,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
// 设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
// 设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
// 设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
// 设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
// unbind fbo texture.
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
}
}
处理音频的时候,由于每个视频出现的位置不一样,每个视频原声什么时候开始播放也不一样,所以音画同步很重要,每个视频开始时间点的偏移量要跟音频同步,并且还要混合bgm,所以还是选用ffmpeg来实现
/**
*混合bgm和视频原声
*/
public static String[] mixBgmAuduio(String bgmPath , int duration , List<FFmpegBuilder.MixAudioBean> mixAudioBeans , String target ) {
boolean hasBgm = true;
if (TextUtils.isEmpty(bgmPath) || !new File(bgmPath).exists()){
hasBgm = false;
}
StringBuilder bd = new StringBuilder();
bd.append("ffmpeg -y "); //输入与总时长相等的静音音轨 bd.append(String.format("-f lavfi -t %f -i anullsrc=channel_layout=stereo:sample_rate=44100 ", duration/1000f ));
//bgm
if (hasBgm){
bd.append(String.format("-i %s ",codeString(bgmPath)));
}
//原声
for (FFmpegBuilder.MixAudioBean bean : mixAudioBeans){
bd.append(String.format("-i %s ",codeString(bean.audioPath)));
}
bd.append("-filter_complex ");
for(int i = 0 ; i < mixAudioBeans.size() ; i++) { //; MediaBean vedio : list //延迟播放声音
if (hasBgm){
bd.append(String.format("[%d:a]adelay=%d|%d,volume=15dB[a%d];" , i+2 , mixAudioBeans.get(i).delay , mixAudioBeans.get(i).delay ,i)); //,asetpts=PTS-STARTPTS+%f/TB
}else {
bd.append(String.format("[%d:a]adelay=%d|%d,volume=15dB[a%d];" , i+1 , mixAudioBeans.get(i).delay , mixAudioBeans.get(i).delay ,i)); //,asetpts=PTS-STARTPTS+%f/TB
}
}
//拼接音频
for(int i = 0 ; i < mixAudioBeans.size() ; i++){
bd.append(String.format("[a%d]",i));
}
//视频原声+音频数量
//bgm
if (hasBgm){
bd.append(String.format("[0:a]amix=inputs=%d:duration=longest:dropout_transition=0[outa];", mixAudioBeans.size()+1 ));
bd.append("[1:a]aloop=loop=-1:size=2e+09[bgm];[outa][bgm]amix=inputs=2:duration=first:dropout_transition=0");
}else {
bd.append(String.format("[0:a]amix=inputs=%d:duration=longest:dropout_transition=0", mixAudioBeans.size()+1 ));
}
bd.append(String.format(" %s", target));
String str = bd.toString();
String[] result = str.split(" ");
for (int i = 0;i<result.length;i++){
result[i] = decodeString(result[i]);
}
return result;
}
最后再混合视频和音频
private void mergeFile() {
MediaMuxer muxer = null;
MediaExtractor videoExtractor = null, bgmAudioExtractor = null;
ByteBuffer buffer = ByteBuffer.allocate(512 * 1024);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
try {
muxer = new MediaMuxer(mOutputFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// load video media format.
videoExtractor = new MediaExtractor();
videoExtractor.setDataSource(mVideoFile.getAbsolutePath());
mVideoTrackId = muxer.addTrack(videoExtractor.getTrackFormat(0));
// load audio media format.
bgmAudioExtractor = new MediaExtractor();
bgmAudioExtractor.setDataSource(mBgmSaveFile.getAbsolutePath());
mbgmAudioTrackId = muxer.addTrack(bgmAudioExtractor.getTrackFormat(0));
if (mAudioFiles != null && mAudioFiles.size() > 0){
for (int i =0 ; i< mAudioFiles.size() ; i++) {
SaveAudioBean saveAudioBean = mAudioFiles.get(i);
// String audioPath = saveAudioBean.savePath;
// load audio media format.
saveAudioBean.audioExtractor = new MediaExtractor();
saveAudioBean.audioExtractor.setDataSource(saveAudioBean.saveFile.getAbsolutePath());
saveAudioBean.audioTrackId = muxer.addTrack(getAudioTrackInVedioAndSelect(saveAudioBean.audioExtractor));
mAudioFiles.set(i,saveAudioBean);
}
}
// start muxer
muxer.start();
// read video first.
videoExtractor.selectTrack(0);
while (true) {
int sampleSize = videoExtractor.readSampleData(buffer, 0);
if (sampleSize != -1) {
bufferInfo.size = sampleSize;
bufferInfo.flags = videoExtractor.getSampleFlags();
bufferInfo.offset = 0;
bufferInfo.presentationTimeUs = videoExtractor.getSampleTime();
muxer.writeSampleData(mVideoTrackId, buffer, bufferInfo);
videoExtractor.advance();
} else {
Logger.d(TAG, "Read video done.");
break;
}
}
// clear buffer
buffer.clear();
// handle audio then
bgmAudioExtractor.selectTrack(0);
while (true) {
int sampleSize = bgmAudioExtractor.readSampleData(buffer, 0);
if (sampleSize != -1) {
bufferInfo.size = sampleSize;
bufferInfo.flags = bgmAudioExtractor.getSampleFlags();
bufferInfo.offset = 0;
bufferInfo.presentationTimeUs = bgmAudioExtractor.getSampleTime();
muxer.writeSampleData(mbgmAudioTrackId, buffer, bufferInfo);
bgmAudioExtractor.advance();
} else {
Logger.d(TAG, "Read video done.");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
......
}
}