Android AVDemo(13):視訊渲染丨音視訊工程示例

語言: CN / TW / HK

塞尚《查德布凡光禿的樹木》

這個公眾號會 路線圖 式的遍歷分享音視訊技術 音視訊基礎(完成)  →  音視訊工具(完成)  →  音視訊工程示例(進行中)  →  音視訊工業實戰(準備) 關注一下成本不高,錯過乾貨損失不小 ↓↓↓

iOS/Android 客戶端開發同學如果想要開始學習音視訊開發,最絲滑的方式是對 音視訊基礎概念知識 有一定了解後,再借助 iOS/Android 平臺的音視訊能力上手去實踐音視訊的 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染 過程,並藉助 音視訊工具 來分析和理解對應的音視訊資料。

在音視訊工程示例這個欄目,我們將通過拆解 流程並實現 Demo 來向大家介紹如何在 iOS/Android 平臺上手音視訊開發。

這裡是 Android 第十三篇: Android 視訊渲染 Demo 。這個 Demo 裡包含以下內容:

  • 1)實現一個視訊採集裝模組;

  • 2)實現一個視訊渲染模組;

  • 3)串聯視訊採集和渲染模組,將採集的視訊資料輸入給渲染模組進行渲染;

  • 4)詳盡的程式碼註釋,幫你理解程式碼邏輯和原理。

在本文中,我們將詳解一下 Demo 的具體實現和原始碼。讀完本文內容相信就能幫你掌握相關知識。

不過,如果你的需求是:1)直接獲得全部工程原始碼;2)想進一步諮詢音視訊技術問題;3)諮詢音視訊職業發展問題。可以根據自己的需要考慮是否加入『關鍵幀的音視訊開發圈』。

長按識別二維碼→加入我們

1、視訊採集模組

在這個 Demo 中,視訊採集模組 的實現與 《Android 視訊採集 Demo》 中一樣,這裡就不再重複介紹了,其介面如下:

public interface KFIVideoCapture {
    ///< 視訊採集初始化。
    public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext);
    ///< 釋放採集例項。
    public void release();

    ///< 開始採集。
    public void startRunning();
    ///< 關閉採集。
    public void stopRunning();
    ///< 是否正在採集。
    public boolean isRunning();
    ///< 獲取 OpenGL 上下文。
    public EGLContext getEGLContext();
    ///< 切換攝像頭。
    public void switchCamera();
}

2、視訊渲染模組

在之前的 《Android 視訊採集 Demo》 那篇中,我們採集後的視訊資料是通過 來做預覽渲染的。這篇我們來介紹一下使用 內部管理 ,並且使用 OpenGL 實現渲染功能。

首先,我們在 中定義渲染回撥。

public interface KFRenderListener {
    void surfaceCreate(@NonNull Surface surface); ///< 渲染快取建立.
    void surfaceChanged(@NonNull Surface surface, int width, int height); ///< 渲染快取變更解析度。
    void surfaceDestroy(@NonNull Surface surface); ///< 渲染快取銷燬。
}

然後,我們在 中管理 以及具體渲染邏輯。

public class KFRenderView extends ViewGroup {
    private KFGLContext mEGLContext = null; ///< OpenGL 上下文。
    private KFGLFilter mFilter = null; ///< 特效渲染到指定 Surface。
    private EGLContext mShareContext = null; ///< 共享上下文。
    private View mRenderView = null; ///< 渲染檢視基類。
    private int mSurfaceWidth = 0; ///< 渲染快取寬。
    private int mSurfaceHeight = 0; ///< 渲染快取高。
    private FloatBuffer mSquareVerticesBuffer = null; ///< 自定義頂點.
    private KFRenderMode mRenderMode = KFRenderMode.KFRenderModeFill; ///< 自適應模式,黑邊,比例填衝。
    private boolean mSurfaceChanged = false; ///< 渲染快取是否變更。
    private Size mLastRenderSize = new Size(0,0); ///< 標記上次渲染 Size。

    public enum KFRenderMode {
        KFRenderStretch, ///< 拉伸滿,可能變形。
        KFRenderModeFit, ///< 黑邊。
        KFRenderModeFill ///< 比例填充。
    };

    public KFRenderView(Context context, EGLContext eglContext) {
        super(context);
        mShareContext = eglContext; ///< 共享上下文。
        _setupSquareVertices(); ///< 初始化頂點。

        boolean isSurfaceView  = false; ///< TextureView 與 SurfaceView 開關。
        if (isSurfaceView) {
            mRenderView = new KFSurfaceView(context, mListener);
        } else {
            mRenderView = new KFTextureView(context, mListener);
        }

        this.addView(mRenderView);// 新增檢視到父檢視
    }

    public void release() {
        ///< 釋放 GL 上下文、特效。
        if (mEGLContext != null) {
            mEGLContext.bind();
            if (mFilter != null){ 
                mFilter.release();
                mFilter = null;
            }
            mEGLContext.unbind();

            mEGLContext.release();
            mEGLContext = null;
        }
    }

    public void render(KFTextureFrame inputFrame) {
        if (inputFrame == null) {
            return;
        }

        ///< 輸入紋理使用自定義特效渲染到 View 的 Surface 上。
        if (mEGLContext != null && mFilter != null) {
            boolean frameResolutionChanged = inputFrame.textureSize.getWidth() != mLastRenderSize.getWidth() || inputFrame.textureSize.getHeight() != mLastRenderSize.getHeight();
            ///< 渲染快取變更或者檢視大小變更重新設定頂點。
            if (mSurfaceChanged || frameResolutionChanged) {
                _recalculateVertices(inputFrame.textureSize);
                mSurfaceChanged = false;
                mLastRenderSize = inputFrame.textureSize;
            }

            ///< 渲染到指定 Surface。
            mEGLContext.bind();
            mFilter.setSquareVerticesBuffer(mSquareVerticesBuffer);
            GLES20.glViewport(0, 0, mSurfaceWidth, mSurfaceHeight);
            mFilter.render(inputFrame);
            mEGLContext.swapBuffers();
            mEGLContext.unbind();
        }
    }

    private KFRenderListener mListener = new KFRenderListener() {
        @Override
        ///< 渲染快取建立。
        public void surfaceCreate(@NonNull Surface surface) {
            mEGLContext = new KFGLContext(mShareContext,surface);
            ///< 初始化特效。
            mEGLContext.bind();
            _setupFilter();
            mEGLContext.unbind();
        }

        @Override
        ///< 渲染快取變更。
        public void surfaceChanged(@NonNull Surface surface, int width, int height) {
            mSurfaceWidth = width;
            mSurfaceHeight = height;
            mSurfaceChanged = true;
            ///< 設定 GL 上下文 Surface。
            mEGLContext.bind();
            mEGLContext.setSurface(surface);
            mEGLContext.unbind();
        }

        @Override
        public void surfaceDestroy(@NonNull Surface surface) {

        }
    };

    private void _setupFilter() {
        ///< 初始化特效。
        if (mFilter == null) {
            mFilter = new KFGLFilter(true, KFGLBase.defaultVertexShader,KFGLBase.defaultFragmentShader);
        }
    }

    private void _setupSquareVertices() {
        ///< 初始化頂點快取。
        final float squareVertices[] = {
                -1.0f, -1.0f,
                1.0f, -1.0f,
                -1.0f,  1.0f,
                1.0f,  1.0f,
        };

        ByteBuffer squareVerticesByteBuffer = ByteBuffer.allocateDirect(4 * squareVertices.length);
        squareVerticesByteBuffer.order(ByteOrder.nativeOrder());
        mSquareVerticesBuffer = squareVerticesByteBuffer.asFloatBuffer();
        mSquareVerticesBuffer.put(squareVertices);
        mSquareVerticesBuffer.position(0);
    }

    private void _recalculateVertices(Size inputImageSize) {
        ///< 按照適應模式建立頂點。
        if (mSurfaceWidth == 0 || mSurfaceHeight == 0) {
            return;
        }

        Size renderSize = new Size(mSurfaceWidth,mSurfaceHeight);
        float heightScaling = 1, widthScaling = 1;
        Size insetSize = new Size(0,0);
        float inputAspectRatio = (float) inputImageSize.getWidth() / (float)inputImageSize.getHeight();
        float outputAspectRatio = (float)renderSize.getWidth() / (float)renderSize.getHeight();
        boolean isAutomaticHeight = inputAspectRatio <= outputAspectRatio ? false : true;

        if (isAutomaticHeight) {
            float insetSizeHeight = (float)inputImageSize.getHeight() / ((float)inputImageSize.getWidth() / (float)renderSize.getWidth());
            insetSize = new Size(renderSize.getWidth(),(int)insetSizeHeight);
        } else {
            float insetSizeWidth = (float)inputImageSize.getWidth() / ((float)inputImageSize.getHeight() / (float)renderSize.getHeight());
            insetSize = new Size((int)insetSizeWidth,renderSize.getHeight());
        }

        switch (mRenderMode) {
            case KFRenderStretch: {
                widthScaling = 1;
                heightScaling = 1;
            }; break;
            case KFRenderModeFit: {
                widthScaling = (float)insetSize.getWidth() / (float)renderSize.getWidth();
                heightScaling = (float)insetSize.getHeight() / (float)renderSize.getHeight();
            }; break;
            case KFRenderModeFill: {
                widthScaling = (float) renderSize.getHeight() / (float)insetSize.getHeight();
                heightScaling = (float)renderSize.getWidth() / (float)insetSize.getWidth();
            }; break;
        }

        final float squareVertices[] = {
                -1.0f, -1.0f,
                1.0f, -1.0f,
                -1.0f,  1.0f,
                1.0f,  1.0f,
        };

        final float customVertices[] = {
                -widthScaling, -heightScaling,
                widthScaling, -heightScaling,
                -widthScaling,  heightScaling,
                widthScaling,  heightScaling,
        };
        ByteBuffer squareVerticesByteBuffer = ByteBuffer.allocateDirect(4 * customVertices.length);
        squareVerticesByteBuffer.order(ByteOrder.nativeOrder());
        mSquareVerticesBuffer = squareVerticesByteBuffer.asFloatBuffer();
        mSquareVerticesBuffer.put(customVertices);
        mSquareVerticesBuffer.position(0);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        ///< 檢視變更 Size。
        this.mRenderView.layout(left,top,right,bottom);
    }
}

上面是 的實現,繼承自 ViewGroup,其中主要包含這幾個部分:

  • 1)管理
    • 通過 開關來控制使用
    • 效能高一些,可以在自執行緒更新 UI 的 View,遵循雙緩衝機制,但無法像正常檢視實現動畫。
    • 效能稍微差一點,過載了 Draw 方法,可以像正常檢視實現動畫。
  • 2)建立 OpenGL 上下文。

    • 這裡需要注意的是,我們通過 管理上下文,初始化方法需要輸入渲染檢視 Surface 作為引數,這樣就可以將繪製結果直接渲染到指定 Surface。
  • 3)建立渲染特效。

    • 通過 將紋理資料渲染到渲染檢視 Surface。
    • 通過 控制自定義渲染比例,在方法 中實時計算頂點資料來實現。
  • 4)渲染回撥通知 Surface 生命週期。

    • 回撥中通知 Surface 建立。
    • 回撥中通知 Surface 變更。
    • 回撥中通知 Surface 銷燬。

接下來是內部渲染檢視 的實現,需要注意 Surface 是通過 方法 獲取:

public class KFSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    private KFRenderListener mListener = null; ///< 回撥。
    private SurfaceHolder mHolder = null; ///< Surface 的抽象介面。

    public KFSurfaceView(Context context, KFRenderListener listener) {
        super(context);
        mListener = listener;
        getHolder().addCallback(this);
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
        ///< Surface 建立。
        mHolder = surfaceHolder;
        ///< 根據 SurfaceHolder 建立 Surface。
        if (mListener != null) {
            mListener.surfaceCreate(surfaceHolder.getSurface());
        }
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int format, int width, int height) {
        ///< Surface 解析度變更。
        if (mListener != null) {
            mListener.surfaceChanged(surfaceHolder.getSurface(),width,height);
        }
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
        ///< Surface 銷燬。
        if (mListener != null) {
            mListener.surfaceDestroy(surfaceHolder.getSurface());
        }
    }
}

接下來是內部渲染檢視 的實現,需要注意 Surface 是通過 建立生成:

public class KFTextureView extends TextureView implements TextureView.SurfaceTextureListener {
    private KFRenderListener mListener = null; ///< 回撥。
    private Surface mSurface = null; ///< 渲染快取。
    private SurfaceTexture mSurfaceTexture = null; ///< 紋理快取。

    public KFTextureView(Context context, KFRenderListener listener) {
        super(context);
        this.setSurfaceTextureListener(this);
        mListener = listener;
    }

    @Override
    public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
        ///< 紋理快取建立。
        mSurfaceTexture = surfaceTexture;
        ///< 根據 SurfaceTexture 建立 Surface。
        mSurface = new Surface(surfaceTexture);
        if (mListener != null) {
            ///< 建立時候回撥一次解析度變更,對其 SurfaceView 介面。
            mListener.surfaceCreate(mSurface);
            mListener.surfaceChanged(mSurface,width,height);
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
        ///< 紋理快取變更解析度。
        if (mListener != null) {
            mListener.surfaceChanged(mSurface,width,height);
        }
    }

    @Override
    public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surfaceTexture) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surfaceTexture) {
        ///< 紋理快取銷燬。
        if (mListener != null) {
            mListener.surfaceDestroy(mSurface);
        }
        if (mSurface != null) {
            mSurface.release();
            mSurface = null;
        }
        return false;
    }
}

更具體細節見上述程式碼及其註釋。

3、採集視訊資料並渲染

我們在一個 中來實現對採集的視訊資料進行渲染播放。

public class MainActivity extends AppCompatActivity {

    private KFIVideoCapture mCapture; ///< 相機採集。
    private KFVideoCaptureConfig mCaptureConfig; ///< 相機採集配置。
    private KFRenderView mRenderView; ///< 渲染檢視。
    private KFGLContext mGLContext; ///< OpenGL 上下文。


    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ///< 檢測採集相關許可權。
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions((Activity) this,
                    new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    1);
        }

        ///< OpenGL 上下文。
        mGLContext = new KFGLContext(null);
        ///< 渲染檢視。
        mRenderView = new KFRenderView(this,mGLContext.getContext());
        WindowManager windowManager = (WindowManager)this.getSystemService(this.WINDOW_SERVICE);
        Rect outRect = new Rect();
        windowManager.getDefaultDisplay().getRectSize(outRect);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(outRect.width(), outRect.height());
        addContentView(mRenderView,params);

        ///< 採集配置:攝像頭方向、解析度、幀率。
        mCaptureConfig = new KFVideoCaptureConfig();
        mCaptureConfig.cameraFacing = LENS_FACING_FRONT;
        mCaptureConfig.resolution = new Size(720,1280);
        mCaptureConfig.fps = 30;
        boolean useCamera2 = false;
        if (useCamera2) {
            mCapture = new KFVideoCaptureV2();
        } else {
            mCapture = new KFVideoCaptureV1();
        }
        mCapture.setup(this,mCaptureConfig,mVideoCaptureListener,mGLContext.getContext());
        mCapture.startRunning();
    }

    private KFVideoCaptureListener mVideoCaptureListener = new KFVideoCaptureListener() {
        @Override
        ///< 相機打開回調。
        public void cameraOnOpened(){}

        @Override
        ///< 相機關閉回撥。
        public void cameraOnClosed() {
        }

        @Override
        ///< 相機出錯回撥。
        public void cameraOnError(int error,String errorMsg) {

        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        ///< 相機資料回撥。
        public void onFrameAvailable(KFFrame frame) {
            mRenderView.render((KFTextureFrame) frame);
        }
    };
}

上面是 的實現,主要分為以下幾個部分:

  • 1)建立 OpenGL 上下文。

    • 建立上下文 ,這樣好處是採集與預覽可以共享,提高擴充套件性。
  • 2)建立採集例項。

    • 這裡需要注意的是,我們通過開關 選擇
    • 引數配置 ,可自定義攝像頭方向、幀率、解析度。
  • 3)採集資料回撥 ,將資料輸入給渲染檢視進行預覽。

更具體細節見上述程式碼及其註釋。

- 完 -

推薦閱讀
《Android AVDemo(12):視訊解碼》
《Android AVDemo(11):視訊轉封裝》
《Android AVDemo(10):視訊解封裝》
《Android AVDemo(9):視訊封裝》
《Android AVDemo(8):視訊編碼》
《Android AVDemo(7):視訊採集》
《Android AVDemo(6):音訊渲染》
《Android AVDemo(5):音訊解碼》
《Android AVDemo(4):音訊解封裝》
《Android AVDemo(3):音訊封裝》
《Android AVDemo(2):音訊編碼》
《Android AVDemo(1):音訊採集》
《iOS AVDemo(7):視訊採集》
《iOS 音訊處理框架及重點 API 合集》
《iOS AVDemo(6):音訊渲染》
《iOS AVDemo(5):音訊解碼》
《iOS AVDemo(4):音訊解封裝》
《iOS AVDemo(3):音訊封裝》
《iOS AVDemo(2):音訊編碼》
《iOS AVDemo(1):音訊採集》
加我微信,拉你入群

謝謝看完全文,也點一下『贊』和 『在看』吧 ↓