Android 拍攝(橫 \ 豎屏)視訊的懶人之路

戀貓de小郭發表於2016-11-23

hello,大家吼,我是那個愛貓的老司機,愛好是掀桌子的話嘮程式猿。回想起剛開始碼文章的時候,沒想到內向的自己也可以擼出那麼多文字,真是挖坑不止,且行且珍惜啊。有猜到今天聊的主角是誰嗎?猜到是不是要送紅包呢?

 
請捂著你的良心說話,對於貧窮的作者(我)不是應該打賞麼 ̄へ ̄!,接下來工作又要忙起來了,更新應該是放緩了呢╮(╯_╰)╭,好傷心。

Android 拍攝(橫 \ 豎屏)視訊的懶人之路

例牌飄過: github.com/CarGuo 請(bu yao)無視。

 想一想,我們聊過AudioReordAudioTrackMediaPlayer,那多媒體四大金剛,就剩下了MediaRecorder了(SoundPool?我這裡訊號不好···)。其實MediaRecorder個人用的也不多,很久前用它在拍攝視訊上確實趟過無視次坑,那今天就聊它吧,把它聊到躺下(ノQ益Q)ノ彡┻━┻。

MediaRecorder

 一般用在多媒體錄製上面,當然如果你只是簡單的想錄制音訊,用它最合適不過,不過如果你想更多樣化的錄製這裡推薦《Android MP3錄製,波形顯示,音訊許可權相容與播放》。今天的主題是錄製視訊,用的還是老式通用的Camera,不是新的camera2(這就尷尬了.....((/- -)/),反正個人秉承能用是王道的做法(懶)。之前也嘗試過FFMPEG的錄製合成音訊,大小和效果也不錯,只是有時候的相容性確實有些問題,最主要還是資料不多,不好改啊 ̄へ ̄(懶)。

 既然是錄製視訊,那麼少不了Camera,這貨也是讓人又愛又恨(哪裡有愛了┑( ̄Д  ̄)┍?),也許是因為Android碎片化的原因,所以用起來也是坑坑窪窪的,接下來就讓我們結束廢話吧:

  • 1、SurfaceView用於承載畫面。
  • 2、初始化相機Camera
  • 3、初始化重力旋轉用於橫豎屏。
  • 4、配置閃光燈和旋轉攝像頭功能。
  • 5、配置MediaRecorder的錄製引數後開始錄製。
  • 6、結束錄製預覽視訊。

1、SurfaceView顯示畫面

 
 舊專案用的都是SurfaceView,這次就就它吧。這裡我們需要首先是implements SurfaceHolder.Callback,這樣我們才能在surface建立的時候初始化相機渲染畫面,在畫面銷燬的時候銷燬相機(畫面都沒有要初始化相機何用)。

SurfaceHolder holder = cameraShowView.getHolder();
holder.addCallback(this);
// setType必須設定,要不出錯.
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

···此處略過無數只草泥馬

@Override
public void surfaceCreated(SurfaceHolder holder) {
    surfaceHolder = holder;
    //更具當前的相機型別(前,後)初始化相機,閃光燈不啟動
    initCamera(cameraType, false);
}

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

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    //結束錄製
    endRecord();
    //是否相機
    releaseCamera();
}複製程式碼

 

2、初始化Camera

 
 除了有點坑外,流程上還是比較簡單的:

  • 釋放已經初始化過的相機。
  • 根據當前攝像頭型別開啟相機。
  • 配置相機引數:預覽大小,對焦,閃光燈,豎屏顯示。
  • 設定顯示畫面的surface
  • 開始繪製
if (camera != null) {
    //如果已經初始化過,就先釋放
    releaseCamera();
}

try {
    //根據前後攝像頭開啟攝像頭
    camera = Camera.open(type);
    if (camera == null) {
        //拿不到可能是沒許可權
        showCameraPermission();
        return;
    }
    camera.lock();

    //Point screen = new Point(getScreenWidth(this), getScreenHeight(this));
    //現在不用獲取最高的顯示效果
    //Point show = getBestCameraShow(camera.getParameters(), screen);

    Camera.Parameters parameters = camera.getParameters();
    if (type == 0) {
        //基本是都支援這個比例
        parameters.setPreviewSize(SIZE_1, SIZE_2);
        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);//1連續對焦
        camera.cancelAutoFocus();// 2如果要實現連續的自動對焦,這一句必須加上
    }
    camera.setParameters(parameters);
    FlashLogic(camera.getParameters(), flashType, flashDo);
    if (cameraType == 1) {
        frontCameraRotate();
        camera.setDisplayOrientation(frontRotate);
    } else {
        camera.setDisplayOrientation(90);
    }
    camera.setPreviewDisplay(surfaceHolder);
    camera.startPreview();
    camera.unlock();
} catch (Exception e) {
    e.printStackTrace();
    releaseCamera();
}複製程式碼

 這裡需要注意坑(畫面變形)問題,那就是你配置的相機解析度畫面,在錄製的時候可能會因為和錄製的解析度畫面不一致,導致開始錄製的時候畫面奇怪的突變,所以CameraMediaRecorder的解析度最好一致。

 問題又來了CameraMediaRecorder不是什麼解析度都支援的,他們分別都有對應的介面:getSupportedPreviewSizesCamcorderProfile等來獲取對應支援的解析度的,路迢迢啊。
 
 經過輪番的嘗試,還有上傳對大小要求,所以最終選擇寫死,對,寫死了640 * 480這樣的大小,這個解析度基本都支援(不支援那手機的尊嚴何在( ‵o′)凸),對於十來秒的視訊,這個解析度的尺寸還算可以(如果對畫質有需要可以另外配置,如果FFMPEG壓縮效能堪憂啊)。

 那麼問題又來了(哪來那麼多問題),但是手機螢幕大部分情況下是16:9,而這個解析度明顯是4:3(萬惡的需求啊(ノQ益Q)ノ彡┻━┻)。這時候因為Surface的最外層是FrameLayout(搞不懂為什麼超出螢幕的時候RelativeLayout有時候會有問題),個人的做法是調整surface的比例。如果是不充滿螢幕高度的,就通過螢幕寬度比例算出surface的高度;如果充滿螢幕高度,就算出surface的寬度。

 如此以來,不變形啦,在點選錄製的瞬間也不跳動啦,唯一有點小問題的就是充滿高度的時候,畫面是超過了螢幕寬度的一點的,所以可能錄到了什麼不想錄制的♂,但是剛好沒看到︿( ̄︶ ̄)︿。

int screenWidth = getScreenWidth(this);
int screenHeight = getScreenHeight(this);
//根據比例設定surface的寬度
setViewSize(cameraShowView, screenWidth * SIZE_1 / SIZE_2, screenHeight);複製程式碼

3、重力感應旋轉

 
 當時看到IOS微博的視訊錄製是可以支援橫豎屏錄製,覺得挺有意思的,這裡用的是OrientationEventListener,具體的之前IJKPlayer視訊文章裡已經說過(懶),有興趣的可以去看看。我們是在畫面旋轉的時候把對應的logo用屬性動畫也旋轉了,然後得到當前的旋轉角度,告訴MediaRecorder,拍攝出來的視訊元資訊裡就帶有了角度資訊,播放的時候畫面會就旋轉為橫屏或者豎屏啦。

orientationEventListener = new OrientationEventListener(this) {
    @Override
    public void onOrientationChanged(int rotation) {
        if (!flagRecord) {
            if (((rotation >= 0) && (rotation <= 30)) || (rotation >= 330)) {
                // 豎屏拍攝
                if (rotationFlag != 0) {
                    //旋轉logo
                    rotationAnimation(rotationFlag, 0);
                    //這是豎屏視訊需要的角度
                    rotationRecord = 90;
                    //這是記錄當前角度的flag
                    rotationFlag = 0;
                }
            } else if (((rotation >= 230) && (rotation <= 310))) {
                // 橫屏拍攝
                if (rotationFlag != 90) {
                    //旋轉logo
                    rotationAnimation(rotationFlag, 90); 
                    //這是正橫屏視訊需要的角度
                    rotationRecord = 0;
                    //這是記錄當前角度的flag
                    rotationFlag = 90;
                }
            } else if (rotation > 30 && rotation < 95) {
                // 反橫屏拍攝
                if (rotationFlag != 270) {
                    //旋轉logo
                    rotationAnimation(rotationFlag, 270);
                    //這是反橫屏視訊需要的角度
                    rotationRecord = 180;
                    //這是記錄當前角度的flag
                    rotationFlag = 270;
                }
            }
            //倒過來就算了,你又不是小米MIX
        }
    }
};
orientationEventListener.enable();複製程式碼

前置攝像頭

 此處有,還不止一個,如果你還需要支援前置攝像頭(能說不嗎?),直接使用上面的rotationRecord去配置MediaRecorder是會有問題的。

 首先說Camera,如果測試說你的前置Camera在某些手機上畫面角度不對,這時候你可以偷偷把手機砸了,因為這是相容問題。如果你沒有勇氣砸手機,看下面。

 傳說中,只要拿下面的frontRotate去配置Camera就正常顯示啦,偉人說的!而其中的frontOri,我們是用到配置後面MediaRecorder,具體看程式碼的,這是調出來的結果(。・・)ノ。

 /**
 * 旋轉前置攝像頭為正的
 */
private void frontCameraRotate() {
    Camera.CameraInfo info = new Camera.CameraInfo();
    Camera.getCameraInfo(1, info);
    int degrees = getDisplayRotation(this);
    int result;
    if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        result = (info.orientation + degrees) % 360;
        result = (360 - result) % 360; // compensate the mirror
    } else { // back-facing
        result = (info.orientation - degrees + 360) % 360;
    }
    frontOri = info.orientation;
    frontRotate = result;
}

/**
 * 獲取旋轉角度
 */
private int getDisplayRotation(Activity activity) {
    int rotation = activity.getWindowManager().getDefaultDisplay()
            .getRotation();
    switch (rotation) {
        case Surface.ROTATION_0:
            return 0;
        case Surface.ROTATION_90:
            return 90;
        case Surface.ROTATION_180:
            return 180;
        case Surface.ROTATION_270:
            return 270;
    }
    return 0;
}

···此處無數字草泥馬
//配置錄製角度
int frontRotation;
if (rotationRecord == 180) {
    //反向橫屏的前置角度
    frontRotation = 180;
} else {
    //豎屏和正向橫屏的前置角度
    //錄製下來的視屏選擇角度,此處為前置
    frontRotation = (rotationRecord == 0) ? 270 - frontOri : frontOri; 
}
//根據前後攝像頭給角度
recorder.setOrientationHint((cameraType == 1) ? frontRotation : rotationRecord);複製程式碼

4、閃光燈和旋轉攝像頭

 閃光燈的開啟關閉遇到過一個問題,就是有的手機還沒有開啟錄製,一配置開啟它就亮了。(砸手機)最後解決的是在配置的時候標誌型別,設定好MediaRecorder之後拍攝才開始閃光燈。(其他的什麼一閃一閃的模式就算了吧= =)

 至於旋轉切換相機,主要還是針對前置camera需要做如上面所說的畫面預覽旋轉。

/**
* 閃光燈邏輯
*
* @param p    相機引數
* @param type 開啟還是關閉
* @param isOn 是否立即啟動
*/
private void FlashLogic(Camera.Parameters p, int type, boolean isOn) {
    flashType = type;
    if (type == 0) {
        if (isOn) {
            p.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
            camera.setParameters(p);
        }
        videoFlashLight.setImageResource(R.drawable.flash_off);
    } else {
        if (isOn) {
            p.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
            camera.setParameters(p);
        }
        videoFlashLight.setImageResource(R.drawable.flash);
    }
    if (cameraFlag == 0) {
        videoFlashLight.setVisibility(View.GONE);
    } else {
        videoFlashLight.setVisibility(View.VISIBLE);
    }
}

/**
 * 切換攝像頭
 */
public void switchCamera() {
    Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
    int cameraCount = Camera.getNumberOfCameras();//得到攝像頭的個數0或者1;

    try {
        for (int i = 0; i < cameraCount; i++) {
            Camera.getCameraInfo(i, cameraInfo);//得到每一個攝像頭的資訊
            if (cameraFlag == 1) {
                //後置到前置
                if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {//代表攝像頭的方位,CAMERA_FACING_FRONT前置      CAMERA_FACING_BACK後置
                    frontCameraRotate();//前置旋轉攝像頭度數
                    switchCameraLogic(i, 0, frontRotate);
                    break;
                }
            } else {
                //前置到後置
                if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {//代表攝像頭的方位,CAMERA_FACING_FRONT前置      CAMERA_FACING_BACK後置
                    switchCameraLogic(i, 1, 90);
                    break;
                }
            }
        }
    } catch (Exception exception) {
        exception.printStackTrace();
    }
}複製程式碼

 

5、配置MediaRecorder的錄製引數、生成視訊。

 這裡最坑的就是MediaRecorder的配置引數是有前後關係的,先生小孩後再洞房這種綠色模式是不行的,具體順序參照下方程式碼,位元速率和幀數都是配置相對較小,適合拍攝上傳。此處還需要注意,如果應用沒有獲取到錄音許可權,在錄製的時候是會走catch裡面的。

 停止錄製相對就簡單了,只要順序正常即可,之後就可以把視訊傳到VideoView快速實現預覽啦。作為谷歌親兒子,VideoView自帶對setOrientationHint的角度解析,只要根據視訊大小配置好介面顯示的效果即可。比起 之前本人擼的播放器 ,兒子還是自己的親┑( ̄Д  ̄)┍,如果需求不高用起來還是可以閉著眼睛的用的。(之前還有小夥伴自己用MediaPlayer播放呢)


//開始
private boolean startRecord() {

    //懶人模式,根據閃光燈和攝像頭前後重新初始化一遍,開期閃光燈工作模式
    initCamera(cameraType, true);

    if (recorder == null) {
        recorder = new MediaRecorder();
    }
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
            || camera == null || recorder == null) {
        camera = null;
        recorder = null;
        //還是沒許可權啊
        showCameraPermission();
        return false;
    }

    try {

        recorder.setCamera(camera);
        // 這兩項需要放在setOutputFormat之前
        recorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
        recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
        // Set output file format,輸出格式
        recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);

        //必須在setEncoder之前
        recorder.setVideoFrameRate(15);  //幀數  一分鐘幀,15幀就夠了
        recorder.setVideoSize(SIZE_1, SIZE_2);//這個大小就夠了

        // 這兩項需要放在setOutputFormat之後
        recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
        recorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);

        recorder.setVideoEncodingBitRate(3 * SIZE_1 * SIZE_2);//第一個數字越大,清晰度就越高,考慮檔案大小的緣故,就調整為1
        int frontRotation;
        if (rotationRecord == 180) {
            //反向的前置
            frontRotation = 180;
        } else {
            //正向的前置
            frontRotation = (rotationRecord == 0) ? 270 - frontOri : frontOri; //錄製下來的視屏選擇角度,此處為前置
        }
        recorder.setOrientationHint((cameraType == 1) ? frontRotation : rotationRecord);
        //把攝像頭的畫面給它
        recorder.setPreviewDisplay(surfaceHolder.getSurface());
        //建立好視訊檔案用來儲存
        videoDir();
        if (videoFile != null) {
            //設定建立好的輸入路徑
            recorder.setOutputFile(videoFile.getPath());
            recorder.prepare();
            recorder.start();
            //不能旋轉啦
            orientationEventListener.disable();
            flagRecord = true;
        }
    } catch (Exception e) {
        //一般沒有錄製許可權或者錄製引數出現問題都走這裡
        e.printStackTrace();
        //還是沒許可權啊
        recorder.reset();
        recorder.release();
        recorder = null;
        showCameraPermission();
        FileUtils.deleteFile(videoFile.getPath());
        return false;
    }
    return true;

}

//結束錄製
private void endRecord() {
    //反正多次進入,比如surface的destroy和介面onPause
    if (!flagRecord) {
        return;
    }
    flagRecord = false;
    try {
        if (recorder != null) {
            recorder.stop();
            recorder.reset();
            recorder.release();
            orientationEventListener.enable();
            recorder = null;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    videoTime.stop();
    videoTime.setBase(SystemClock.elapsedRealtime());
    Intent intent = new Intent(this, PlayActivity.class);
    intent.putExtra(PlayActivity.DATA, videoFile.getAbsolutePath());
    startActivityForResult(intent, 2222);
    overridePendingTransition(R.anim.fab_in, R.anim.fab_out);
}複製程式碼
最後

 
 總的來說,錄製視訊還是蠻簡單的,主要還是視訊的角度問題需要考慮:

  • Camera的前置攝像頭角度注意。
  • Android本身預設的是橫屏錄製效果,所以需要配置橫屏和豎屏的錄製角度。
  • MediaRecorder引數的配置順序。
  • CameraMediaRecorder的解析度和拉伸問題。
  • 閃光燈要在開始錄製的時候才開啟。
  • 初始化攝像頭和釋放攝像頭需要在surface的surfaceCreatedsurfaceDestroyed
  • 注意鎖屏、退到後臺、onPuase的是會走surface的surfaceDestroyed
  • 如果是要一次性上傳很長很長的拍攝視訊,推薦還是找FFMPEG的錄製方式吧,畢經錄製好了再壓縮的做法很費時。 
  • 告訴IOS,讓他支援視訊元資訊的角度旋轉播放。(不支援?網上那麼多視訊有角度資訊,難道歪著看?)
  • 測試如果說前置畫面拍攝出來的視訊左右翻轉,用本機拍一個前置視訊或者照片給他看,不然你只能接FFMPEG了。  

<( ̄︶ ̄)↗知道你想說什麼,DMEO在這裡 : github.com/CarGuo/Vide…

Android 拍攝(橫 \ 豎屏)視訊的懶人之路
有人支援麼,好累啊

相關文章