Android音視訊錄製(4)——變速錄製
概述
在看本篇文章之前請務必先檢視這面三篇文章:
視訊變速是一個非常有趣的東西,在我們平時看電影的時候,導演對某些鏡頭進行快放(比如動作片的拳腳片段),某些鏡頭進行慢放(比如一些火山噴發之類的),從而造成非常震撼的影視效果。最近非常火的一些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倍速為例
Surface變速錄製
在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變速錄製就已經完成。
Buffer 變速錄製
理解了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();
}
}
自此,變速錄製的就講解完了,各位小夥伴有什麼疑問的,歡迎反饋。