Android視頻編輯器(一)通過OpenGL預覽、錄製視頻以及斷點續錄等

前言

如今的視頻類app可謂是如日中天,火的不行。比如美拍、快手、VUE、火山小視頻、抖音小視頻等等。而這類視頻的最基礎和核心的功能就是視頻錄製和視頻編輯功能。包括了手機視頻錄製、美白、加濾鏡、加水印、給本地視頻美白、加水印、加濾鏡、視頻裁剪、視頻拼接和加bgm等等一系列音視頻的核心操作。而本系列的文章,就是作者在視頻編輯器開發上的一些個人心得,希望能幫助到大家,另外因個人水平有限,難免有不足之處,還希望大家不惜賜教。
      本系列的文章,計劃包括以下幾部分:
      1、android視頻編輯器之視頻錄製、斷點續錄、對焦等
      6、android視頻編輯器之通過OpenGL做本地視頻拼接
      7、android視頻編輯器之音視頻裁剪、增加背景音樂等
      通過這一系列的文章,大家就能自己開發出一個具有目前市面上完整功能的視頻類app最核心功能的視頻編輯器了(當然,如果作者能按計劃全部寫完的話。。。捂臉)。主要涉及到的核心知識點有Android音視頻編解碼、OpenGL開發、音視頻的基礎知識等等。整個過程會忽略掉一些基礎知識,只會講解一些比較核心的技術點。所有代碼都會上傳到github上面。有興趣的童鞋,可以從文章末尾進行下載。
      文章中也借鑑和學習了很多其他小夥伴們分享的知識。 每篇文章我都會貼出不完全的相關連接,非常感謝小夥伴們的分享。
      

方案選擇

     
      關於android平臺的視頻錄製,首先我們要確定我們的需求,錄製音視頻本地保存爲mp4文件。實現音視頻錄製的方案有很多,比如原生的sdk,通過Camera進行數據採集,注意:是Android.hardware包下的Camera而不是Android.graphics包下的,後者是用於矩陣變換的一個類,而前者纔是通過硬件採集攝像頭的數據,並且返回到java層。然後使用SurfaceView進行畫面預覽,使用MediaCodec對數據進行編解碼,最後通過MediaMuxer將音視頻混合打包成爲一個mp4文件。當然也可以直接使用MediaRecorder類進行錄製,該類是封裝好了的視頻錄製類,但是不利於功能擴展,比如如果我們想在錄製的視頻上加上我們自己的logo,也就是常說的加水印,或者是錄製一會兒 然後暫停 然後繼續錄製的功能,也就是斷點續錄的話 就不是那麼容易實現了。而本篇文章,作爲後面系列的基礎,我們就不講解常規的視頻錄製的方案了,有興趣的可以查看本文前面附上的一些鏈接。因爲我們後期會涉及到給視頻加濾鏡、加水印、加美顏等功能,所以就不能使用常規的視頻錄製方案了,而是採用Camera + OpengGL + MediaCodec +進MediaMuxer行視頻錄製。

視頻預覽

      爲了實現錄製的效果,首先我們得實現攝像頭數據預覽的功能。
Camera的使用
      android在5.0的版本加載了hardware.camera2包對android平臺的視頻錄製功能進行了增強,但是爲了兼容低版本,我們將使用的是Camera類,而不是5.0之後加入的新類。對新api感興趣的童鞋,可以自行查閱相關資料。

GLSurfaceView的作用
       其實視頻的預覽大致流程就是,從Camera中拿到當前攝像頭返回的數據,然後顯示在屏幕上,我們這裏是採用的GLSurfaceView類進行圖像的顯示。GLSurfaceView類有一個Renderer接口,這個Renderer其實就是GLSurfaceView中很重要的一個監聽器,你可以把他看成是GLSurfaceView的生命週期的回調。有三個回調函數:
 @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
       
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {

    }
    @Override
    public void onDrawFrame(GL10 gl) {
      
    }
      onSurfaceCreated:我們主要在這裏面做一些初始化的工作
      onSurfaceChanged:就是當surface大小發生變化的時候,會回調,我們主要會在這裏面做一些更改相關設置的工作
      onDrawFrame:這個就是返回當前幀的數據,我們對幀數據進行處理,主要就是在這裏面進行的
      所以,我們的大致流程就是按照這三個回調方法來進行的:
      
CameraController類對camera進行控制
      Camera的使用過程,網上已經有很多資料了,這裏就不在過多的介紹了。但是 有幾個地方需要注意一下,首先就是你設置的視頻尺寸攝像頭並不一定支持,所以我們要選取攝像頭支持的,跟我們預設的相同或者相近的尺寸,主要代碼如下
     mCamera = Camera.open(cameraId);
        if (mCamera != null){
            /**選擇當前設備允許的預覽尺寸*/
            Camera.Parameters param = mCamera.getParameters();
            preSize = getPropPreviewSize(param.getSupportedPreviewSizes(), mConfig.rate,
                    mConfig.minPreviewWidth);
            picSize = getPropPictureSize(param.getSupportedPictureSizes(),mConfig.rate,
                    mConfig.minPictureWidth);
            param.setPictureSize(picSize.width, picSize.height);
            param.setPreviewSize(preSize.width,preSize.height);

            mCamera.setParameters(param);
    }

    private Camera.Size getPropPictureSize(List<Camera.Size> list, float th, int minWidth){
        Collections.sort(list, sizeComparator);
        int i = 0;
        for(Camera.Size s:list){
            if((s.height >= minWidth) && equalRate(s, th)){
                break;
            }
            i++;
        }
        if(i == list.size()){
            i = 0;
        }
        return list.get(i);
    }
    private Camera.Size getPropPreviewSize(List<Camera.Size> list, float th, int minWidth){
        Collections.sort(list, sizeComparator);

        int i = 0;
        for(Camera.Size s:list){
            if((s.height >= minWidth) && equalRate(s, th)){
                break;
            }
            i++;
        }
        if(i == list.size()){
            i = 0;
        }
        return list.get(i);
    }
    private static boolean equalRate(Camera.Size s, float rate){
        float r = (float)(s.width)/(float)(s.height);
        if(Math.abs(r - rate) <= 0.03) {
            return true;
        }else{
            return false;
        }
    }
    private Comparator<Camera.Size> sizeComparator=new Comparator<Camera.Size>(){
        public int compare(Camera.Size lhs, Camera.Size rhs) {
            if(lhs.height == rhs.height){
                return 0;
            }else if(lhs.height > rhs.height){
                return 1;
            }else{
                return -1;
            }
        }
    };
     這個代碼還是相當簡單,這裏就不過多介紹了,網上也有很多不同的但是類似功能的適配方法,大家可以多瞭解下,相互對照。
     第二個就是,攝像頭取數據的座標系和屏幕顯示的座標系不太相同,簡單的說就是,不管是前置還是後置攝像頭,我們都需要對攝像頭取的數據進行一些座標系旋轉操作,才能正常的顯示到屏幕上,不然的話就會出現畫面扭曲的情況。因爲我們是採用的OpengGL進行視頻錄製的,所以我們會有一系列的AFilter來進行shader的加載和畫面的渲染工作,所以我們將攝像頭數據的旋轉也放到這個裏面來做。這部分後面再說,CameraController類主要就是Camera的一個包裝類,還會包括一些視頻尺寸控制等代碼,具體的請下載完整demo,進行查看。

AFilter的作用   
       我們在這個項目中,我們使用了AFilter來完成加載shader、繪製圖像、清除數據等,主要代碼包括如下:
加載asset中的shader
public static int uLoadShader(int shaderType,String source){
        int shader= GLES20.glCreateShader(shaderType);
        if(0!=shader){
            GLES20.glShaderSource(shader,source);
            GLES20.glCompileShader(shader);
            int[] compiled=new int[1];
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS,compiled,0);
            if(compiled[0]==0){
                glError(1,"Could not compile shader:"+shaderType);
                glError(1,"GLES20 Error:"+ GLES20.glGetShaderInfoLog(shader));
                GLES20.glDeleteShader(shader);
                shader=0;
            }
        }
        return shader;
    }

      Buffer的初始化
/**
     * Buffer初始化
     */
    protected void initBuffer(){
        ByteBuffer a= ByteBuffer.allocateDirect(32);
        a.order(ByteOrder.nativeOrder());
        mVerBuffer=a.asFloatBuffer();
        mVerBuffer.put(pos);
        mVerBuffer.position(0);
        ByteBuffer b= ByteBuffer.allocateDirect(32);
        b.order(ByteOrder.nativeOrder());
        mTexBuffer=b.asFloatBuffer();
        mTexBuffer.put(coord);
        mTexBuffer.position(0);
    }

      綁定默認的紋理
/**
     * 綁定默認紋理
     */
    protected void onBindTexture(){
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0+textureType);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,getTextureId());
        GLES20.glUniform1i(mHTexture,textureType);
    }

     每次繪製前需要清理畫布
/**
     * 清理畫布
     */
    protected void onClear(){
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
    }

      這些代碼在不同的filter中其實都是公用的,所以我們通過一個抽象類,來進行管理。
      上面我們說了攝像頭的數據需要進行旋轉,所以我們通過一個ShowAFilter來進行畫面的旋轉操作,核心代碼如下,通過傳入是前置還是後置攝像頭的flag來進行畫面旋轉
public void setFlag(int flag) {
        super.setFlag(flag);
        float[] coord;
        if(getFlag()==1){    //前置攝像頭 順時針旋轉90,並上下顛倒
            coord=new float[]{
                    1.0f, 1.0f,
                    0.0f, 1.0f,
                    1.0f, 0.0f,
                    0.0f, 0.0f,
            };
        }else{               //後置攝像頭 順時針旋轉90度
            coord=new float[]{
                    0.0f, 1.0f,
                    1.0f, 1.0f,
                    0.0f, 0.0f,
                    1.0f, 0.0f,
            };
        }
        mTexBuffer.clear();
        mTexBuffer.put(coord);
        mTexBuffer.position(0);
    }

      攝像頭和AFilter我們都已經準備好了,下一步,就是我們需要把Camera取的數據顯示在GLSurfaceView上面了,也就是需要將AFilter、CameraController和GLSurfaceView聯繫起來。然後,因爲我們後續會涉及到很多不同AFilter的管理,所以我們創建一個CameraDraw類,來管理AFilter。讓其實現GLSurfaceView.Renderer接口,便於管理。

CameraDraw類
      首先實現GLSurfaceView.Renderer接口
    public class CameraDrawer implements GLSurfaceView.Renderer
       然後,在類的構造函數中,進行AFilter的初始化
   public CameraDrawer(Resources resources){
        //初始化一個濾鏡 也可以叫控制器
        showFilter = new ShowFilter(resources);      
   }
      在onSurfaceCreated中,進行SurfaceTextured的創建,並且和AFilter進行綁定
 @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        textureID = createTextureID();
        mSurfaceTextrue = new SurfaceTexture(textureID);
        showFilter.create();
        showFilter.setTextureId(textureID);   
       
    }
     在onSurfaceChanged中,進行一些參數的更改和紋理的重新綁定
    @Override
    public void onSurfaceChanged(GL10 gl10, int i, int i1) {
        width = i;
        height = i1;
        /**創建一個幀染緩衝區對象*/
        GLES20.glGenFramebuffers(1,fFrame,0);
        /**根據紋理數量 返回的紋理索引*/
        GLES20.glGenTextures(1, fTexture, 0);
        /**將生產的紋理名稱和對應紋理進行綁定*/
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fTexture[0]);
        /**根據指定的參數 生產一個2D的紋理 調用該函數前  必須調用glBindTexture以指定要操作的紋理*/
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mPreviewWidth, mPreviewHeight,
                0,  GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
        useTexParameter();
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
    }
      在onDrawFrame中進行圖像的繪製工作。
@Override
    public void onDrawFrame(GL10 gl10) {
        /**更新界面中的數據*/
        mSurfaceTextrue.updateTexImage();

        /**繪製顯示的filter*/
        GLES20.glViewport(0,0,width,height);
        showFilter.draw();
    }
     CameraDraw目前所做的主要工作就是這樣,然後我們將CameraController、CameraDraw和自定義的CameraView控件進行綁定,就可以實現攝像頭數據預覽了。

自定義的CameraView控件
      首先,在構造函數中進行OpenGL、CameraController、CameraDraw的初始化
    private void init() {
        /**初始化OpenGL的相關信息*/
        setEGLContextClientVersion(2);//設置版本
        setRenderer(this);//設置Renderer
        setRenderMode(RENDERMODE_WHEN_DIRTY);//主動調用渲染
        setPreserveEGLContextOnPause(true);//保存Context當pause時
        setCameraDistance(100);//相機距離

        /**初始化Camera的繪製類*/
        mCameraDrawer = new CameraDrawer(getResources());
        /**初始化相機的管理類*/
        mCamera = new CameraController();
     }
      然後,分別在三個生命週期的函數中調用CameraController和CameraDrawer的相關方法,以及打開攝像頭
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        mCameraDrawer.onSurfaceCreated(gl,config);
        if (!isSetParm){
            open(0);
            stickerInit();
        }
        mCameraDrawer.setPreviewSize(dataWidth,dataHeight);
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        mCameraDrawer.onSurfaceChanged(gl,width,height);
    }
    @Override
    public void onDrawFrame(GL10 gl) {
        if (isSetParm){
            mCameraDrawer.onDrawFrame(gl);
        }
    }
     然後在onFrameAvailable函數中,調用即可
    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        this.requestRender();
    }
 
     我們的視頻預覽的主體流程就是這樣,然後我們可以直接在佈局中使用CameraView類即可。
<Image_1>

視頻錄製和斷點錄製

       在上面部分,我們實現了通過OpenGL預覽視頻,下面部分,我們就需要實現錄製視頻了.我們使用的opengl錄製視頻方案,採用的是谷歌的工程師編寫的grafika 這個項目,項目鏈接 在文章開頭部分。這個項目包含了很多GLSurfaceView和視頻編解碼的知識,還是非常值得學習一下的。主要核心的類就是TextureMovieEncoder和VideoEncoderCore類。採用的是MediaMuxer和MeidaCodec類進行視頻的編碼和音視頻合成。後面我們涉及到音視頻編解碼、視頻拼接、音視頻裁剪的時候會詳細的介紹一下android裏面音視頻編解碼的相關類的用法。這裏就暫時先不深入講解了。當然音視頻的編解碼也可以使用FFmpeg進行軟編碼,但是因爲硬編碼的速度比軟編碼要快得多,所以我們這個項目,不會涉及到FFmpeg的使用。
        好了,現在回到我們的從GLSurfaceView讀取數據,通過TexureMovieEncoder進行視頻的錄製和斷點續錄。
        上面我們說了我們通過AFilter的相關類,進行opengl的相關操作,實現了視頻的預覽,這裏我們還需要一個AFilter類將攝像頭的數據交給我們的編碼類進行編碼。所以初始化的時候 再初始化一個
     drawFilter = new ShowFilter(resources);
        這裏需要注意一下,爲了顯示在屏幕上是正常的,我們進行了旋轉的操作。所以,我們在錄製的AFilter裏面需要加上矩陣翻轉的控制。
    OM= MatrixUtils.getOriginalMatrix();
    MatrixUtils.flip(OM,false,true);//矩陣上下翻轉
    drawFilter.setMatrix(OM);
     然後同樣分別進行drawFilter的create,在onDrawFrame裏面講textureId進行綁定以及繪製。還有就是添加錄製控制的相關代碼
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
                GLES20.GL_TEXTURE_2D, fTexture[0], 0);
        GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);
        drawFilter.setTextureId(fTexture[0]);
        drawFilter.draw();
        //解綁
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);

        if (recordingEnabled){
            /**說明是錄製狀態*/
            switch (recordingStatus){
                case RECORDING_OFF:
                    videoEncoder = new TextureMovieEncoder();
                    videoEncoder.setPreviewSize(mPreviewWidth,mPreviewHeight);
                    videoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(
                            savePath, mPreviewWidth, mPreviewHeight,
                            3500000, EGL14.eglGetCurrentContext(),
                            null));
                    recordingStatus = RECORDING_ON;
                    break;
                case RECORDING_ON:
                case RECORDING_PAUSED:
                    break;
                case RECORDING_RESUMED:
                    videoEncoder.updateSharedContext(EGL14.eglGetCurrentContext());
                    videoEncoder.resumeRecording();
                    recordingStatus = RECORDING_ON;
                    break;

                case RECORDING_RESUME:
                    videoEncoder.resumeRecording();
                    recordingStatus=RECORDING_ON;
                    break;
                case RECORDING_PAUSE:
                    videoEncoder.pauseRecording();
                    recordingStatus=RECORDING_PAUSED;

                    break;
                default:
                    throw new RuntimeException("unknown recording status "+recordingStatus);
            }

        }else {
            switch (recordingStatus) {
                case RECORDING_ON:
                case RECORDING_RESUMED:
                case RECORDING_PAUSE:
                case RECORDING_RESUME:
                case RECORDING_PAUSED:
                    videoEncoder.stopRecording();
                    recordingStatus = RECORDING_OFF;
                    break;
                case RECORDING_OFF:
                    break;
                default:
                    throw new RuntimeException("unknown recording status " + recordingStatus);
            }
        }

        if (videoEncoder != null && recordingEnabled && recordingStatus == RECORDING_ON){
            videoEncoder.setTextureId(fTexture[0]);
            videoEncoder.frameAvailable(mSurfaceTextrue);
        }
      上面主要邏輯是,在數據返回的視頻判斷當前的錄製狀態,如果是正在錄製,就將SurfaceTexture給到VideoEncoder進行數據的編碼,如果沒有錄製,就跳過該幀,這樣就可以實現斷點續錄,即錄製 —>暫停錄製—>繼續錄製,而且這樣錄製出來的是一個整體的視頻文件。
       這裏就不貼出TexureMovieEncoder和VideoEncoderCore類的詳細代碼了。
       這樣我們就完成了,攝像頭數據的預覽和視頻的錄製功能。然後呢,還有一些額外的功能。

Camera的手動對焦

     不管是視頻錄製還是拍照的時候,對焦是非常重要的,如果沒有對焦功能,那錄製出來的視頻的效果會非常的差,但是網上的很多講解android的攝像頭對焦功能的文章,其實並不準確,他們實現的功能其實有一些小問題的,主要是涉及到攝像頭座標系和屏幕顯示座標系的變化。手動聚焦,主要是點擊屏幕 然後就調用Camera聚焦的相關函數 進行對焦。
     聚焦的主要代碼如下,在CameraConroller類裏面 
   Camera.Parameters parameters = mCamera.getParameters();
        boolean supportFocus=true;
        boolean supportMetering=true;
        //不支持設置自定義聚焦,則使用自動聚焦,返回
        if (parameters.getMaxNumFocusAreas() <= 0) {
            supportFocus=false;
        }
        if (parameters.getMaxNumMeteringAreas() <= 0){
            supportMetering=false;
        }
        List<Camera.Area> areas = new ArrayList<Camera.Area>();
        List<Camera.Area> areas1 = new ArrayList<Camera.Area>();
        //再次進行轉換
        point.x= (int) (((float)point.x)/ MyApplication.screenWidth*2000-1000);
        point.y= (int) (((float)point.y)/MyApplication.screenHeight*2000-1000);

        int left = point.x - 300;
        int top = point.y - 300;
        int right = point.x + 300;
        int bottom = point.y + 300;
        left = left < -1000 ? -1000 : left;
        top = top < -1000 ? -1000 : top;
        right = right > 1000 ? 1000 : right;
        bottom = bottom > 1000 ? 1000 : bottom;
        areas.add(new Camera.Area(new Rect(left, top, right, bottom), 100));
        areas1.add(new Camera.Area(new Rect(left, top, right, bottom), 100));
        if(supportFocus){
            parameters.setFocusAreas(areas);
        }
        if(supportMetering){
            parameters.setMeteringAreas(areas1);
        }

        try {
            mCamera.setParameters(parameters);// 部分手機 會出Exception(紅米)
            mCamera.autoFocus(callback);
        } catch (Exception e) {
            e.printStackTrace();
        }
      主要涉及到了一下座標變換,因爲大部分的手機的前置攝像頭不支持對焦功能,所以我們不進行前置攝像頭的對焦。
      

結語

      到這裏的話,本篇文章的主要內容就已經結束了,再次回顧一下,我們其實本篇文章主要涉及到的內容有通過OpenGl預覽視頻,通過MediaCodec錄製視頻,以及一些其他的知識點。這裏並沒有講解OpenGL的一些基礎知識,比如頂點着色器等等,這部分如果要涉及到的話,也是一個很龐大的內容,所以就不會在系列文章中進行介紹了。大家不太清楚的話,請自行查詢相關資料。
     本篇僅僅是一個開始,下一篇文章,我們就會在錄製視頻的時候通過opengl加上水印和美白效果。請大家持續關注。
     因爲個人水平有限,難免有錯誤和不足之處,還望大家能包涵和提醒。謝謝啦!!!

其他
      項目的github地址



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章