1. 程式人生 > >Android Camera模組解析之視訊錄製

Android Camera模組解析之視訊錄製

《Android Camera架構》
《Android Camera程序間通訊類總結》
《Android Camera模組解析之拍照》
《Android Camera模組解析之視訊錄製》
《Android Camera原理之CameraDeviceCallbacks回撥模組》
《Android Camera原理之openCamera模組(一)》
《Android Camera原理之openCamera模組(二)》
《Android Camera原理之createCaptureSession模組》
《Android Camera原理之setRepeatingRequest與capture模組》

《Android Camera原理之編譯》
《Android Camera原理之camera provider啟動》
《Android Camera原理之cameraserver與cameraprovider是怎樣聯絡的》
《Android Camera原理之camera service與camera provider session會話與capture request輪轉》
《Android Camera原理之camera HAL底層資料結構與類總結》
《Android Camera原理之camera service類與介面關係》

之前講解過camera2 api之間的關係以及使用camera2 api拍照的功能,本文我們講解一下如何利用camera2 實現錄製視訊的功能。在講解功能實現的基礎上多探討一些。
拍照和錄製視訊的前期功能都是類似的,在拍照之前會有camera preview功能,錄製視訊之前也是有這個功能的,唯一的不同就是抓取的資料不同,拍照抓取的是image,視訊抓取的video,資料組織格式不一樣。

Android L版本引入了Camera2 api,之前《Android Camera模組解析之拍照》中已經詳細介紹了camera2 api主要類之間的呼叫關係。錄製視訊主要是呼叫了CameraDevice與CameraCaptureSession來錄製視訊,使用一個自定義的TextureView來渲染輸出的資料,preview介面使用TextureView來承載。

  • 1.佈局中建立一個自定義的TextureView,前文已經介紹了為什麼使用TextureView來渲染camera preview介面了。
  • 2.實現TextureView.SurfaceTextureListener
    方法,監聽當前的TextureView來監聽Camera preview介面。
  • 3.實現 CameraDevice.StateCallback 來監聽CameraDevice的狀態 ,可以監聽到Camera device 開啟、連線、斷開等狀態,在這些狀態中可以操作錄製、停止錄製等等。
  • 4.開始啟動camera preview,設定MediaRecorder接受的視訊格式。
  • 5.使用CameraDevice例項呼叫createCaptureRequest(CameraDevice.TEMPLATE_RECORD),建立一個CaptureRequest.Builder物件。
  • 6.實現CameraCaptureSession.StateCallback方法,使用CameraDevice例項呼叫createCaptureSession(surfaces, new CameraCaptureSession.StateCallback(){})
  • 7.MediaRecorder 的例項呼叫start()stop()方法開始視訊錄製和停止錄製操作。
  • 8.在onResume()onPause()中做好控制方法。

一、啟動設定預覽介面

1.1 設定TextureView顯示介面

    private TextureView.SurfaceTextureListener mSurfaceTextureListener
            = new TextureView.SurfaceTextureListener() {

        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
                                              int width, int height) {
            openCamera(width, height);
        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,
                                                int width, int height) {
            configureTransform(width, height);
        }

        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
            return true;
        }

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
        }

    };
        if (mTextureView.isAvailable()) {
            openCamera(mTextureView.getWidth(), mTextureView.getHeight());
        } else {
            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
        }

在設定SurfaceTextureListener之前,有一個判斷,mTextureView.isAvailable()判斷d當前TextureView設定的mSurface是否存在,這個mSurface就是SurfaceTexture,SurfaceTexture是從圖片流中捕捉圖片幀的介質

    public boolean isAvailable() {
        return mSurface != null;
    }

這個mSurface是怎麼來的呢?

Surface設定流程.jpg

在TextureView 繪製的時候,獲取當前的Texture繪製層。

    public final void draw(Canvas canvas) {
        // NOTE: Maintain this carefully (see View#draw)
        mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /* Simplify drawing to guarantee the layer is the only thing drawn - so e.g. no background,
        scrolling, or fading edges. This guarantees all drawing is in the layer, so drawing
        properties (alpha, layer paint) affect all of the content of a TextureView. */

        if (canvas.isHardwareAccelerated()) {
            DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas;

            TextureLayer layer = getTextureLayer();
            if (layer != null) {
                applyUpdate();
                applyTransformMatrix();

                mLayer.setLayerPaint(mLayerPaint); // ensure layer paint is up to date
                displayListCanvas.drawTextureLayer(layer);
            }
        }
    }

關鍵的判斷:canvas.isHardwareAccelerated() 硬體加速開啟的情況下才能進一步使用TextureView來渲染surface。然後判斷當前是否有SurfaceTexture,如果沒有的化,構造一個新的SurfaceTexture物件,當前的TextureView就有一個SurfaceTexture物件了。

1.2 執行openCamera

《Android Camera原理之openCamera模組(一)》
《Android Camera原理之openCamera模組(二)》
兩篇文章已經介紹了openCamera的底層邏輯。

manager.openCamera(cameraId, mStateCallback, null);
    private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {

        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            mCameraDevice = cameraDevice;
            startPreview();
            mCameraOpenCloseLock.release();
            if (null != mTextureView) {
                configureTransform(mTextureView.getWidth(), mTextureView.getHeight());
            }
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            mCameraDevice = null;
        }

        @Override
        public void onError(@NonNull CameraDevice cameraDevice, int error) {
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            mCameraDevice = null;
            Activity activity = getActivity();
            if (null != activity) {
                activity.finish();
            }
        }

    };

在得到當前camera device已經onOpened回撥之後,我們真正開始預覽功能。

1.3 設定預覽

            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            assert texture != null;
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
            mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

            Surface previewSurface = new Surface(texture);
            mPreviewBuilder.addTarget(previewSurface);

            mCameraDevice.createCaptureSession(Collections.singletonList(previewSurface),
                    new CameraCaptureSession.StateCallback() {

                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession session) {
                            mPreviewSession = session;
                            updatePreview();
                        }

                        @Override
                        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                            Activity activity = getActivity();
                            if (null != activity) {
                                Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
                            }
                        }

                    }, mBackgroundHandler);

Surface封裝的SurfaceTexture是CameraDevice預覽渲染的主要媒介,image stream渲染回執就在這上面進行的。

mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

這兒是設定預覽介面,這是一個CaptureRequest.Builder物件,在camera2 api中CaptureRequest是一個重要的創舉,可以設定camera request請求快取,稍後會講解這兒的底層原理。

    public void createCaptureSession(List<Surface> outputs,
            CameraCaptureSession.StateCallback callback, Handler handler)
            throws CameraAccessException {
        List<OutputConfiguration> outConfigurations = new ArrayList<>(outputs.size());
        for (Surface surface : outputs) {
            outConfigurations.add(new OutputConfiguration(surface));
        }
        createCaptureSessionInternal(null, outConfigurations, callback,
                checkAndWrapHandler(handler), /*operatingMode*/ICameraDeviceUser.NORMAL_MODE,
                /*sessionParams*/ null);
    }

捕獲當前的surface流,可以實現渲染出當前camera device前的影像。至此,camera preview流程已經完成工作,接下來開始錄製視訊的工作。

二、錄製視訊

2.1 設定MediaRecorder屬性

    private void setUpMediaRecorder() throws IOException {
        final Activity activity = getActivity();
        if (null == activity) {
            return;
        }
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        if (mNextVideoAbsolutePath == null || mNextVideoAbsolutePath.isEmpty()) {
            mNextVideoAbsolutePath = getVideoFilePath(getActivity());
        }
        mMediaRecorder.setOutputFile(mNextVideoAbsolutePath);
        mMediaRecorder.setVideoEncodingBitRate(10000000);
        mMediaRecorder.setVideoFrameRate(30);
        mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        switch (mSensorOrientation) {
            case SENSOR_ORIENTATION_DEFAULT_DEGREES:
                mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
                break;
            case SENSOR_ORIENTATION_INVERSE_DEGREES:
                mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation));
                break;
        }
        mMediaRecorder.prepare();
    }
  • 設定音訊和視訊源,就是聲音從麥克風中取,視訊從Surface介面上取,就是從螢幕上取。
  • 設定輸出檔案格式和輸出檔案。
  • 設定視訊編碼位元速率和幀率,位元速率和幀率可以顯示當前視訊是否卡頓。
  • 設定視訊寬高。
  • 設定音訊和視訊編碼,音訊使用 AAC編碼,視訊使用H264編碼。
MediaRecorder.prepare();

執行MediaRecorder.prepare();開始啟動MediaRecorder錄製。
prepare()函式中主要的工作是設定輸出檔案File,準備開始IO;設定底層音視訊編碼快取,開始執行編碼工作。底層的解析放在後續進行。

    public void prepare() throws IllegalStateException, IOException
    {
        if (mPath != null) {
            RandomAccessFile file = new RandomAccessFile(mPath, "rw");
            try {
                _setOutputFile(file.getFD());
            } finally {
                file.close();
            }
        } else if (mFd != null) {
            _setOutputFile(mFd);
        } else if (mFile != null) {
            RandomAccessFile file = new RandomAccessFile(mFile, "rw");
            try {
                _setOutputFile(file.getFD());
            } finally {
                file.close();
            }
        } else {
            throw new IOException("No valid output file");
        }

        _prepare();
    }

2.2 開始錄製工作

                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            // UI
                            mButtonVideo.setText(R.string.stop);
                            mIsRecordingVideo = true;

                            // Start recording
                            mMediaRecorder.start();
                        }
                    });

錄製工作需要放在主執行緒中進行,不然獲取不到UI介面的資訊。
MediaRecorder涉及到很多native方法,在本文中不一一展開,但是後續詳細分析的時候回談到這些native方法的具體是做什麼的。
mMediaRecorder.start();一定要在MediaRecorder.prepare();之後,因為prepare()不設定輸出檔案和準備音視訊編碼方式,後續的start()便不能繼續工作了。

專案原始碼:https://github.com/googlesamples/android-Camera2Video

 

小禮物走一走,來簡書關