在看本篇文章之前请务必先查看这面三篇文章:
第一篇:Android音视频录制概述
第二篇Android音视频录制(1)——Surface录制
第三篇Android音视频录制(2)——Buffer录制
视频变速是一个非常有趣的东西,在我们平时看电影的时候,导演对某些镜头进行快放(比如动作片的拳脚片段),某些镜头进行慢放(比如一些火山喷发之类的),从而造成非常震撼的影视效果。最近非常火的一些app,能让普通群众都能拍出很精彩的快速/慢速的视频,而很多人对这种视频效果都感觉很赞,下面我就来讲述下视频录制过程中如何变速录制。
下面我先说下视频变速的原理:快速录制就是“丢”帧,慢速录制就是“加”帧,但帧率都保持不变,变的是时长。比如我4秒的视频,帧率是20帧/秒,那一共是80帧,把每一帧都编码0,1,2…,78,79,假设我定义的快速即为2倍变速,即4秒最后变成2秒的视频,视频帧的变化就是丢弃掉一半的帧,只取0, 2, 4…76, 78合成2秒的视频,帧率依然是20帧/秒。慢速录制也以1/2速度为例,不过慢速录制相对复杂些,毕竟删除总是比创建容易,4秒的视频最终要变成8秒的视频,帧率不变,所以肯定要“加”帧,其实就是复制帧,依然是0,1,2…78,79的视频,对每一帧复制一遍,重新编码,最后编程0,0A,1,1A….78,78A,79,79A一共160帧的8秒视频。这其中最最核心的点在哪里?三个字:时间戳。快速录制的时候,你需要把正常第2n的时间戳设置为n, 慢速录制的时候,需要把时间戳为n的帧变成2n。当然,talk is cheap, show me the code。下面我们看看如何实现。
代码的实现也是分两部分,第一部分是,Surface变速录制,第二部分是,Buffer变速录制。快速变速以2倍速为例,慢速变速以1/2倍速为例
在Android音视频录制(1)——Surface录制一文中并没有说到任何关于时间戳的代码,其实因为surface录制的时候egl默认给我们加上了时间戳,但是我们依然可以通过egl设置我们指定的时间戳,最终达到我们的目的。
首先定义几种模式:
public enum Speed{
NORMAL,//正常速度
SLOW,//慢速:0.5倍速
FAST//快速:2倍速
}
然后在VideoSurfaceEncoder中加入几个变量:具体看注释
private Speed mSpeed;//模式:快速/慢速/常速
private int mFrameIndex = 0;//实际编码器渲染帧数
private long mFirstTime;//第一帧渲染时间
private long mCurrPTS;//当前正在渲染的帧的时间戳
private int mDrainIndex = 0;//摄像头传递过来帧数
egl绘制的时候代码修改为如下,快速录制即每两次丢弃一次,慢速录制则是每次绘制重复绘制多一次
//egl 绘制
public void render(float[] surfaceTextureMatrix, float[] mvpMatrix) {
if(mSpeed == Speed.NORMAL) {//常速录制
draw(surfaceTextureMatrix, mvpMatrix);
}else if(mSpeed == Speed.SLOW){//慢速录制,则绘制两次
mCurrPTS = getPTS();
draw(surfaceTextureMatrix, mvpMatrix);
mCurrPTS = getPTS();
draw(surfaceTextureMatrix, mvpMatrix);
}else if(mSpeed == Speed.FAST){
if(mDrainIndex % 2 == 0){//快速录制
mCurrPTS = getPTS();
draw(surfaceTextureMatrix, mvpMatrix);
}
}
mDrainIndex++;
}
每次绘制,绘制帧数要加1:
private void draw(float[] surfaceTextureMatrix, float[] mvpMatrix) {
if(isAllKeyFrame()){
requestKeyFrame();
}
mRenderer.draw(surfaceTextureMatrix, mvpMatrix);
if(isAllKeyFrame()){
requestKeyFrame();
}
mFrameIndex++;//绘制帧加1
}
当然最重要的是时间戳的设定:常速的时候直接返回就好了,快速录制就是根据第一帧的时间戳,得出当前帧对应的当前时间与第一帧时间差的一半,加上第一帧的时间戳,即为正确的时间戳。慢速录制的时候时间戳就是第一帧时间戳,加上egl已经渲染的帧数乘上帧间隔即可。
private long getPTS() {
long time = System.nanoTime();
if(mFirstTime == -1){
mFirstTime = time;
}
if(mSpeed == Speed.NORMAL){
return time / 1000;
}
if(mSpeed == Speed.FAST){
return mFirstTime + (time - mFirstTime) / 2;
}
if(mSpeed == Speed.SLOW){
return mFirstTime + mFrameIndex * mFrameInterval;
}
return time / 1000;
}
opengl绘制的时候设置时间戳:在SurfaceEncoderRenderer每次绘制完之后,设置时间戳,之后再进行swap操作,时间戳才能真正写入到编码器:
while (mEncoder.isRecording()){
mLock.lock();
try {
Log.d(TAG, "await~~~~");
mDrawCondition.await();
mEgl.makeCurrent();
//makeCurrent表明opengl的操作是在egl环境下
// clear screen with yellow color so that you can see rendering rectangle
GLES20.glClearColor(1.0f, 1.0f, 0.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
mDrawer.setMatrix(mMatrix, 16);
mDrawer.draw(mTextureId, mMatrix);
if(!mEncoder.isNormalSpeed()) {
mEgl.setPTS(mEncoder.getCurrPTS());//设置时间戳
}
mEgl.swapBuffers();
mEncoder.singalOutput();//通知编码器线程要输出数据啦
Log.d(TAG, "draw------------textureId=" + mTextureId);
}finally {
mLock.unlock();
}
MEgl中设置时间戳:
/**
*设置时间戳
* @param pts 纳秒
*/
public void setPTS(long pts){
EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEGLSurface, pts);
}
这样surface变速录制就已经完成。
理解了surface的变速录制,buffer录制原理也一样
VideoEncoder需要增加下面的变量:
private Speed mSpeed = Speed.NORMAL;
private int mFrameIndex = 0;
private int mDrainIndex = 0;
private long mFirstFramePTS = 0;
摄像头提供帧数据:
public void addFrame(byte[] data){
Log.d(TAG, "drain frame-" + mDrainIndex + " frameIndex=" + mFrameIndex);
if(mSpeed == Speed.FAST){
if(mDrainIndex % 2 == 0){
addFrame(data, getPTS());//快速录制
}
}else if(mSpeed == Speed.SLOW){
addFrame(data, getPTS());
addFrame(data, getPTS());//慢速录制
}else{//normal
addFrame(data, getPTS());//正常录制
}
mDrainIndex++;
}
获取时间戳:这里和surface录制有区别,surface录制时间戳是纳秒,surface录制的时间戳是微妙
public long getPTS(){
if(mFrameIndex == 0){
mFirstFramePTS = System.nanoTime() / 1000;
return mFirstFramePTS;
}
long time = System.nanoTime() / 1000;
if(mSpeed == Speed.FAST){
return mFirstFramePTS + (time - mFirstFramePTS) / 2;//快速录制
}else if(mSpeed == Speed.NORMAL){//正常录制
return time;
}else if(mSpeed == Speed.SLOW){//慢速录制
return mFirstFramePTS + mFrameIndex * mFrameInterval;
}
return System.nanoTime() / 1000;
}
每次绘制的时候绘制帧都需要加1:
public void addFrame(Frame frame){
try {
mLock.lock();
mFrameList.add(frame);
mFrameIndex++;//绘制帧+1
Log.d(TAG, "add frame-" + frame.mTime + " frameIndex=" + mFrameIndex + " interval=" + mFrameInterval);
mCondition.signal();
}finally {
mLock.unlock();
}
}
自此,变速录制的就讲解完了,各位小伙伴有什么疑问的,欢迎反馈。