1. 程式人生 > >【Android 進階】仿抖音系列之視訊預覽和錄製(五)

【Android 進階】仿抖音系列之視訊預覽和錄製(五)

大家好,又見面了。在前幾篇中,我們通過2種方式實現了仿抖音的翻頁切換視訊,仿抖音列表播放視訊功能,這一篇,我們來說說視訊的錄製。

主流的視訊錄製,一般都採用的是FFmpeg 例如 騰訊短視訊,由於FFmpeg的學習成本較大,這裡我們就說說系統自帶的MediaRecorder。

首先,需要實現攝像頭的預覽,這裡我們就用SurfaceView 。

  • 1.在佈局中引入
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:
layout_height
="match_parent" tools:context=".activity.RecordActivity">
<SurfaceView android:id="@+id/sv_record" android:layout_width="match_parent" android:layout_height="match_parent" /> <Button android:id="@+id/btn_start" android:layout_width
="wrap_content" android:layout_height="wrap_content" android:text="start" app:layout_constraintBottom_toBottomOf="parent" />
<Button android:id="@+id/btn_switch" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="switch" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btn_end" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="end" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintRight_toRightOf="parent" /> </android.support.constraint.ConstraintLayout>
    1. 實現SurfaceHolder.Callback,重寫surfaceCreated、surfaceChanged、surfaceDestroyed 3個方法

其中surfaceCreated是SurfaceView建立成功時回撥,可以在這裡開始預覽;surfaceChanged是SurfaceView變化時回撥,這裡不做處理;surfaceDestroyed 是SurfaceView銷燬時回撥,可以在這裡釋放資源

         surfaceHolder = svRecord.getHolder();
        surfaceHolder.addCallback(this);
        //設定一些引數方便後面繪圖
        svRecord.setFocusable(true);
        svRecord.setKeepScreenOn(true);
        svRecord.setFocusableInTouchMode(true);

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        surfaceHolder = holder;
        requestPermision();
    }


    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        surfaceHolder = holder;
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        //停止預覽並釋放攝像頭資源
        stopPreview();
        //停止錄製
        startRecord();
    }
    1. 開始預覽,首先是請求許可權,這裡使用的是easypermissions,也可以使用其他的封裝庫
  private void requestPermision() {
        String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO};
        if (EasyPermissions.hasPermissions(this, perms)) {
            // Already have permission, do the thing
            startPreview();
            // ...
        } else {
            // Do not have permissions, request them now
            EasyPermissions.requestPermissions(this, "我們的app需要以下許可權",
                    RC_STORAGE, perms);
        }
    }

需要實現EasyPermissions.PermissionCallbacks,以及處理授權回撥


    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        // Forward results to EasyPermissions
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }

    @Override
    public void onPermissionsGranted(int requestCode, @NonNull List<String> perms) {
        // Some permissions have been granted
        startPreview();
    }

    @Override
    public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
        // Some permissions have been denied
        finish();
    }
 /**
     * 開始預覽
     */
    private void startPreview() {
        if (svRecord == null || surfaceHolder == null) {
            return;
        }


        if (camera == null) {
            camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
            currentCameraType = 1;
            btnSwitch.setText("後");
        }


        try {
            camera.setPreviewDisplay(surfaceHolder);
            Camera.Parameters parameters = camera.getParameters();

            camera.setDisplayOrientation(90);

            //實現Camera自動對焦
            List<String> focusModes = parameters.getSupportedFocusModes();
            if (focusModes != null) {
                for (String mode : focusModes) {
                    mode.contains("continuous-video");
                    parameters.setFocusMode("continuous-video");
                }
            }

            List<Camera.Size> sizes = parameters.getSupportedVideoSizes();
            if (sizes.size() > 0) {
                size = sizes.get(sizes.size() - 1);
            }

            camera.setParameters(parameters);
            camera.startPreview();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

其中,Camera.CameraInfo.CAMERA_FACING_BACK 代表後置攝像頭,保險起見,應該檢查裝置是否有後置攝像頭,這裡就不檢查了;還需要注意,當時後置時,應該旋轉攝像頭90度,否則預覽是斜的

    1. 切換攝像頭,這裡如上程式碼所見,使用了一個int 型變數currentCameraType來記錄前後攝像頭;
                stopPreview();
                if (currentCameraType == 1) {
                    camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
                    currentCameraType = 2;
                    btnSwitch.setText("前");
                } else {
                    camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
                    currentCameraType = 1;
                    btnSwitch.setText("後");
                }

                startPreview();
 /**
     * 停止預覽
     */
    private void stopPreview() {
        //停止預覽並釋放攝像頭資源
        if (camera == null) {
            return;
        }

        camera.setPreviewCallback(null);
        camera.stopPreview();
        camera.release();
        camera = null;
    }

需要注意,需要先停止預覽,切換攝像頭之後,再開始預覽;

到這裡攝像頭已經實現了攝像頭預覽。

開始錄製視訊


    /**
     * 開始錄製
     */
    private void startRecord() {
        if (mediaRecorder == null) {
            mediaRecorder = new MediaRecorder();
        }
        temFile = getTemFile();


        try {
            camera.unlock();
            mediaRecorder.setCamera(camera);
            //從相機採集視訊
            mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
            // 從麥克採集音訊資訊
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            //編碼格式
            mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

            mediaRecorder.setVideoSize(size.width, size.height);

            //每秒的幀數
            mediaRecorder.setVideoFrameRate(24);
            // 設定幀頻率,然後就清晰了
            mediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024 * 100);


            mediaRecorder.setOutputFile(temFile.getAbsolutePath());
            mediaRecorder.setPreviewDisplay(surfaceHolder.getSurface());
            //解決錄製視訊, 播放器橫向問題
            if (currentCameraType == 1) {
                //後置
                mediaRecorder.setOrientationHint(90);
            } else {
                //前置
                mediaRecorder.setOrientationHint(270);
            }
            mediaRecorder.prepare();
            //正式錄製
            mediaRecorder.start();

       
            isRecording = true;
            showtoast("開始錄製");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 獲取臨時檔案目錄
     *
     * @return
     */
    private File getTemFile() {
        String basePath = Environment.getExternalStorageDirectory().getPath() + "/doudemo/";

        File baseFile = new File(basePath);
        if (!baseFile.exists()) {
            baseFile.mkdirs();
        }

        File temp = new File(basePath + System.currentTimeMillis() + ".mp4");

        return temp;
    }

這裡的坑還是比較多的

  • 1.首先需要解鎖相機,呼叫camera.unlock();
  • 2.關於視訊的size ,應該通過parameters.getSupportedVideoSizes(); 獲取該手機支援的寬高,如果設定手機不支援,會報錯;
  • 3.注意各個方法呼叫順序,否則會報一些奇怪的錯,無奈…
  • 4.攝像機角度問題,後置時,旋轉90度,前置時,旋轉270度

停止錄製,需要鎖定相機,需要預覽時,跳到預覽(播放)介面

  /**
     * 停止錄製
     */
    private void stopRecord(boolean delete) {
        if (mediaRecorder == null) {
            return;
        }
        if (myTimer != null) {
            myTimer.cancel();
        }

        try {
            mediaRecorder.stop();
        } catch (Exception e) {
            e.printStackTrace();
        }
        mediaRecorder.reset();
        mediaRecorder.release();
        mediaRecorder = null;
        if (camera != null) {
            camera.lock();
        }
        isRecording = false;

        if (delete) {
            if (temFile != null && temFile.exists()) {
                temFile.delete();
            }
        } else {
            //停止預覽
            stopPreview();

            Intent intent = new Intent(RecordActivity.this, PrepareActivity.class);
            intent.putExtra(PrepareActivity.VIDEO_PATH, temFile.getPath());
            startActivity(intent);

        }
        showtoast("停止錄製");
    }

最後,獻上完整程式碼。Github