1. 程式人生 > >Android 音視訊任務3

Android 音視訊任務3

任務3. 在 Android 平臺使用 Camera API 進行視訊的採集,分別使用 SurfaceView、TextureView 來預覽 Camera 資料,取到 NV21 的資料回撥

由於這次的任務牽涉面十分廣,所以用了很久才搞懂了一些知識,以後的任務也應該會做的越來越慢,不過十分合理,慢工出細活,上來一會兒就完成的東西,要不就是含金量不高,要不就是沒有深究,對於學習階段,雖然說有時候要管中窺豹不要太深究,但有些事情不搞清楚就不能算掌握知識了對吧?

下面是使用CameraAPI進行拍照和錄視訊,當然還有預覽的全過程。雖然CameraAPI在5.0後就被棄用了(原因之一是它不能同時預覽和拍攝),取而代之的是camera2,但由於眾所周知的原因,仍然要好好學習。

新增許可權後,如果是android6以上的,必須在設定的應用裡面手動開啟申請的許可權

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" android:required="true"/>
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"
/>

相機的角度,如果不加調整的開啟相機,預設的角度是0°,(這一點很奇怪。。)一般豎屏狀態下是90度,橫屏則為0或180度(應該是取決於感測器的方向)

拍攝照片

使用Camera拍攝照片的步驟:
要使用此類拍攝照片,請使用以下步驟:

  1. 從open(int)獲取Camera的例項。(首先使用以下方法獲得裝置攝像頭數目)
    int cameraNum = Camera.getNumberOfCameras();
    //上面open的有效取值是 0 ~ cameraNum-1 代表了攝像頭的裝置id
  2. 使用getParameters()獲取現有(預設)設定。如有必要,修改返回的Camera.Parameters物件並調setParameters(Camera.Parameters)
  3. 呼叫setDisplayOrientation(int)以確保正確的預覽方向。
    重要提示:將完全初始化的SurfaceHolder傳遞給 setPreviewDisplay(SurfaceHolder)。沒有surface,相機將無法啟動預覽。
    重要說明:呼叫startPreview()開始更新預覽曲面。必須先開始預覽才能拍照。
  4. 如果需要,可以呼叫takePicture(Camera.ShutterCallback,Camera.PictureCallback,Camera.PictureCallback,Camera.PictureCallback)來捕獲照片。等待回撥提供實際的影象資料。
  5. 拍照後,預覽顯示將停止。要拍攝更多照片,請先再次呼叫startPreview()。
  6. 呼叫stopPreview()以停止更新預覽surface。
    重要提示:呼叫release()以釋放相機以供其他應用程式使用。應用程式應立即在Activity.onPause()中釋放相機(並在Activity.onResume()中重新開啟它)。

一般自定義的來說,surface顯示的影象都會被拉伸,或者壓扁,反正就是影象比例不對,這是由於surfaceView和Camera.Size不匹配導致的,所以要根據surfaceView的寬高,在Camera所支援的Size裡面找一個最合適的

在啟用startPreview後,就可以通過Camera.takePicture()方法拍攝一張照片,返回的照片資料通過Callback介面獲取。takePicture()介面可以獲取三個型別的照片:

  • 第一個,ShutterCallback介面,在快門瞬間被回撥,通常用於播放“咔嚓”這樣的音效;
  • 第二個,PictureCallback介面,返回未經壓縮的RAW型別照片(位元組陣列);
  • 第三個,PictureCallback介面,返回經過壓縮的JPEG型別照片(位元組陣列);

這三個都可以不實現,第一個一般沒啥影響,但第二個第三個不實現則相當於拍的東西就沒了。(第二第三個可以選一個實現)呼叫此方法後,在返回JPEG回撥之前,不得呼叫startPreview()或拍攝另一張照片。

camera.setDisplayOrientation(90); //設定的是預覽的方向,預覽和資料是獨立的,如果只設置了DisplayOrientation,則在surfaceView中顯示正常,但儲存的資料仍然是0°的

修改拍攝資料角度的兩種方法(以下均是豎屏情況,橫屏情況不用額外處理,因為預設是橫屏情況):

  1. 直接在設定camera引數的時候設定 parameters.setRotation(90); //這樣拍攝出的data就是修正過後的資料,直接寫到檔案裡即可
        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            File file = new File(fileName);
            try {
                FileOutputStream os = new FileOutputStream(file);
                os.write(data);
                os.flush();
                os.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            if (mCamera != null) {
                mCamera.startPreview();
            }
        }
  1. 不設定相機的Rotation,這樣得到的資料是橫屏情況下的,要使用Matrix和Bitmap進行修正
    parameters.setPreviewFormat(ImageFormat.NV21); //預設預覽幀格式為NV21,注意很多格式裝置可能不支援,程式就會崩,如nexus6p不支援YUY2

錄製視訊

使用surfaceView

  1. 像上面一樣獲得camera的例項並初始化它,並開啟preview
  2. 呼叫unlock()允許mediaRecorder的工作執行緒使用設定好引數的camera
  3. 把camera例項傳給MediaRecorder.setCamera(Camera)
  4. 設定用於預覽的surface:MediaRecorder.setPreviewDisplay(如果camera沒繫結surface,則此步驟會替換camera的surface為本引數)
    //以下為設定錄製的引數
    設定視訊源setVideoSource: 一般為攝像頭
    設定音訊源setAudioSource:一般是CAMCORDER(單純錄視訊是沒聲音的) 如果是用mediaRecorder單錄音頻的話,一般用MIC
    設定輸出檔案格式:setOutputFormat
    設定視訊的角度:setOrientationHint //預覽角度在camera的引數裡設定
    //下面的必須要放在輸出格式確定後
    設定視訊編碼:setVideoEncoder
    設定音訊編碼:setAudioEncoder
    //下面的必須要放在編碼確定後
    設定視訊解析度:setVideoSize; 這裡的解析度必須要選擇裝置攝像頭支援的解析度,否則報錯(Camera.getParameters().getSupportedPreviewSizes()的值之一)
    設定錄製視訊的捕獲幀速率:setVideoFrameRate(); //必須要選擇攝像頭支援的幀率,否則報錯(mCamera.getParameters().getSupportedPreviewFrameRates()的值之一)
    設定所錄製視訊的編碼位率:setVideoEncodingBitRate(3 * 1024 * 1024);
    設定記錄會話的最大持續時間(毫秒)setMaxDuration(30 * 1000);
    設定輸出檔案的路徑:setOutputFile
  5. 當完成錄製後,呼叫camera.reconnect把camera的使用權從mediaRecorder的工作執行緒轉回到設定了它的本執行緒
  6. 呼叫mediaRecorder呼叫stop和release 釋放資源
    一些總結性的概述:
    surfaceView 只是一個用來佔位置的控制元件,是用來顯示surface內容的,它會顯示它繫結的SurfaceHolder所持有的surface, 真正用於顯示的是surface(它是具體的要顯示的資料),surface的持有者是SurfaceHolder,每個surfaceView生來就有一個預設與其繫結的SurfaceHolder,通過getHolder來獲取,SurfaceHolder會決定如何去顯示surface,每個surface有且只有一個SurfaceHolder,
    surfaceView:畫布
    surface:畫的內容
    surfaceHolder,畫內容的持有者
    surfaceView、surfaceHolder、surface三位一體,是一個不可分的整體

一個camera設定好引數,它本身是被本執行緒持有的,持有的話是被lock的,mediaRecorder的工作是在單獨的執行緒中完成的
使用setCamera給mediarecorder設定一個被設定好相機。而如果不呼叫setCamera設定相機的話,分配給的是一個預設情況下的相機,也就是說什麼引數都沒有被設定,這樣一般不可能滿足拍攝需求,因此一般要給mediaRecorder設定camera

在mediaRecorder.start之前,它的camera必須要呼叫unlock(如果設定了的話),因為mediaRecorder的工作是在單獨的執行緒中完成的,而camera預設是被建立它的執行緒所持有的,這樣mediaRecorder的工作執行緒無法使用它),如果start順利結束,則會自動呼叫lock歸還camera,如果start呼叫失敗,則要手動lock來回收camera的所有權

mediaRecorder.setPreviewDisplay(Surface s)
給mediaRecorder設定surface,surface是用來顯示mediaRecorder的相機的預覽。如果s已經被一個設定給了camera(只是設定給了,還沒有被持有資源),而且這個camera已經通過mediaRecorder.setCamera被設定給了mediaRecorder,那麼這個方法不需要呼叫。如果s已經被設定給了一個另一個camera,只是設定給了,還沒有被持有資源),但這個camera沒有被設定給mediaRecorder,那麼這個s將會被設定給給mediaRecorder自己的camera(如果沒設定,則是預設的camera)。此時mediaRecorder自己的camera和另一個camera被設定了同一個surface.
如果s為空,則完全不會發生任何事


camera.setHolder
給camera設定一個holder,即surface;但此時holder的surface資源並不為camera所持有,只有當開啟預覽(即startPreview)時,surface資源才會被camera所持有(下面所說的surface和相應的surfaceHolder都是繫結的,彼此一體,surface資源也就相當於holder的資源)

如果對一個camera持有一個surface的資源,再次讓另一個camera再次去持有這個surface資源,則會報錯(mediaRecorder.start中會呼叫類似於它自己的camera.startPreview,startPreview會試圖去佔有surface資源)也就是說,同一個surfaceHolder可以被多個camera設定,但同時只能有一個camera持有它的surface的資源,否則就會報錯。可以對同一個camera連續多次試圖持有資源(反正資源就是你的,反覆持有還是這些).

當用startPreview成功開啟預覽後,surface的資源就被camera持有,此時單純呼叫stopPreview關閉預覽並不能釋放它所持有的surface資源,必須要用camera.release(完全釋放camera),或者直接setDisplayHolder(null)(只釋放camera佔有的surface資源,不改變camera的其他引數設定)才能釋放它佔用的surface資源。但不能直接把camera置為null來釋放資源,因為這樣只能減少它的引用計數,不是真正的釋放資源。

mediaRecorder 不會吧自己的surface主動連線到自己設定的camera, 除非是不設定camera而使用預設的camera,因為mediaRecorder會預設自己設定的camera有surface

camera.setDisplayOrientation設定相機預覽的角度,mediaRecorder.setOrientationHini設定儲存的視訊的角度,兩者互相獨立

使用TextureView

TextureViewView:
TextureView可用於顯示內容流。 這樣的內容流可以例如是視訊或OpenGL場景。 內容流可以來自應用程式的程序以及遠端程序。

TextureView只能在硬體加速視窗中使用。 在軟體中渲染時,TextureView將不會繪製任何內容。(例如在xml中給textureView設定背景色,就直接會報錯)

與SurfaceView不同,TextureView不會建立單獨的視窗,而是表現為常規View。 這個關鍵區別允許它進行移動,轉換,動畫等。例如,您可以通過呼叫myView.setAlpha(0.5f)使TextureView半透明。

使用TextureView很簡單:您需要做的就是獲得它的SurfaceTexture。 然後,把SurfaceTexture用來呈現內容。 以下示例演示如何將相機預覽渲染到TextureView中:

public class LiveCameraActivity extends Activity implements TextureView.SurfaceTextureListener {
      private Camera mCamera;
      private TextureView mTextureView;

      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);

          mTextureView = new TextureView(this);
          mTextureView.setSurfaceTextureListener(this);

          setContentView(mTextureView);
      }

      public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
          mCamera = Camera.open();

          try {
              mCamera.setPreviewTexture(surface);
              mCamera.startPreview();
          } catch (IOException ioe) {
              // Something bad happened
          }
      }

      public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
          // Ignored, Camera does all the work for us
      }

      public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
          mCamera.stopPreview();
          mCamera.release();
          return true;
      }

      public void onSurfaceTextureUpdated(SurfaceTexture surface) {
          // Invoked every time there's a new Camera preview frame
      }
  }

可以通過呼叫getSurfaceTexture()或使用TextureView.SurfaceTextureListener來獲取TextureView的SurfaceTexture。 重要的是要知道只有在將TextureView附加到視窗(並且已呼叫onAttachedToWindow()之後)才能使用SurfaceTexture。因此,強烈建議您使用listener在SurfaceTexture可用時進行通知。

值得注意的是,只有一個生產者可以使用TextureView。 例如,如果使用TextureView顯示相機預覽,則無法使用lockCanvas()同時繪製到TextureView。
如同surface一樣,TextureView的surfaceTexture資源同一時間也只能被一個物件所持有

———————————————————————————————————————————————————————————————
SurfaceTexture:
從影象流中捕獲幀作為OpenGL ES紋理。

影象流可以來自相機預覽或視訊解碼。從SurfaceTexture建立的Surface可以用作android.hardware.camera2,MediaCodec,MediaPlayer和Allocation API的輸出目標。呼叫updateTexImage()時,將更新建立SurfaceTexture時指定的紋理物件的內容,以包含影象流中的最新影象。這可能導致跳過一些流的幀。

在指定舊版Camera API的輸出目標時,也可以使用SurfaceTexture代替SurfaceHolder。這樣做會導致影象流中的所有幀都被髮送到SurfaceTexture物件而不是裝置的顯示。

從紋理中取樣時,應首先使用通過getTransformMatrix(float [])查詢的矩陣變換紋理座標。每次呼叫updateTexImage()時變換矩陣都會改變,因此每次更新紋理影象時都應該重新查詢。該矩陣將形式為(s,t,0,1)的傳統2D OpenGL ES紋理座標列向量轉換為包含區間[0,1]上的s和t到流式紋理中的適當取樣位置。此變換可補償影象流源的任何屬性,使其看起來與傳統的OpenGL ES紋理不同。例如,可以通過使用查詢的矩陣變換列向量(0,0,0,1)來完成從影象左下角的取樣,而從影象的右上角進行取樣可以通過變換來完成( 1,1,0,1)。

紋理物件使用GL_TEXTURE_EXTERNAL_OES紋理目標,該目標由GL_OES_EGL_image_external OpenGL ES擴充套件定義。這限制了紋理的使用方式。每次繫結紋理時,它必須繫結到GL_TEXTURE_EXTERNAL_OES目標而不是GL_TEXTURE_2D目標。此外,從紋理中取樣的任何OpenGL ES 2.0著色器必須使用例如“#extension GL_OES_EGL_image_external:require”指令宣告其對此擴充套件的使用。此類著色器還必須使用samplerExternalOES GLSL取樣器型別訪問紋理。

可以在任何執行緒上建立SurfaceTexture物件。 updateTexImage()只能在包含紋理物件的OpenGL ES上下文的執行緒上呼叫。在任意執行緒上呼叫可用幀的回撥,因此除非特別小心,否則不應直接從回撥中呼叫updateTexImage()。
———————————————————————————————————————————————————————————————

錄製流程:
其他步驟均和surfaceView一樣,只有第四步有區別,由於此時不能獲得surfaceHolder,只要把TextureView的SurfaceTexture繫結到相機即可(給camera設定用於預覽的surfaceTexture:camera.setPreviewTexture(代替camera.setPreviewDisplay),同時而且也不需要給mediaRecorder呼叫setPreviewDisplay)

MediaRecorder:用來錄製音訊和視訊,錄製控制基於簡單的狀態機,如下圖:
在這裡插入圖片描述

使用MediaRecorder錄製音訊的流程:

A common case of using MediaRecorder to record audio works as follows:
MediaRecorder recorder = new MediaRecorder();
 recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
 recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
 recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
 recorder.setOutputFile(PATH_NAME);
 recorder.prepare();
 recorder.start();   // Recording is now started
 ...
 recorder.stop();
 recorder.reset();   // You can reuse the object by going back to setAudioSource() step
 recorder.release(); // Now the object cannot be reused

//這個不需要手動另開執行緒寫資料,體積小使用方便,但很大的一個缺點就是錄下的音質不太好。相比之下AudioRecord雖然麻煩一點,但錄製的更像是無損音質

application可能希望註冊資訊和錯誤事件,以便在錄製期間獲知某些內部更新和可能的執行時錯誤。 通過設定適當的監聽器(通過呼叫(到setOnInfoListener(OnInfoListener)setOnInfoListener和/或setOnErrorListener(OnErrorListener)setOnErrorListener)來註冊此類事件。為了接收與這些偵聽器關聯的相應回撥,應用程式需要在執行Looper的執行緒上建立MediaRecorder物件 (預設情況下,主UI執行緒已經運行了Looper)。

使用MediaRecorder報錯at android.media.MediaRecorder.start(Native Method):很可能是如下原因: 
1.使用下面兩個函式但引數不被當前硬體所支援
mediaRecorder.setVideoFrameRate()和mediaRecorder.setVideoSize(videoWidth, videoHeight)
//註釋掉後,會使用硬體支援的預設的引數,就不會出錯了,
要獲得硬體支援的引數用如下的函式:
mCamera.getParameters().getSupportedPreviewSizes()的值之一
mCamera.getParameters().getSupportedPreviewFrameRates()的值之一

2.已經有其他攝像頭開啟過預覽,持有了SurfaceView或SurfaceTexture的surface資源,但停止預覽後沒有釋放資源

另:mediaRecorder.setVideoSize不能設定的太大,否則點選錄影後畫面會停住
相機預覽資料的獲取

轉自:https://blog.csdn.net/lb377463323/article/details/53338045
首先定義一個類實現Camera.PreviewCallback介面,然後在它的onPreviewFrame(byte[] data, Camera camera)方法中即可接收到每一幀的預覽資料,也就是引數data。
然後使用setPreviewCallback()、setOneShotPreviewCallback或setPreviewCallbackWithBuffer()註冊回撥介面,下面介紹一下這些方法:

1,void setPreviewCallback (Camera.PreviewCallback cb) 

一旦使用此方法註冊預覽回撥介面,onPreviewFrame()方法會一直被呼叫,直到camera preview銷燬

注意,onPreviewFrame()方法跟Camera.open()是運行於同一個執行緒,所以為了防止onPreviewFrame()會阻塞UI執行緒,將Camera.open()放置在子執行緒中執行。

2,void setOneShotPreviewCallback (Camera.PreviewCallback cb) 

使用此方法註冊預覽回撥介面時,會將下一幀資料回撥給onPreviewFrame()方法,呼叫完成後這個回撥介面將被銷燬。也就是隻會回撥一次預覽幀資料。

3,void setPreviewCallbackWithBuffer (Camera.PreviewCallback cb) 

它跟setPreviewCallback的工作方式一樣,但是要求指定一個位元組陣列作為緩衝區,用於預覽幀資料,這樣能夠更好的管理預覽幀資料時使用的記憶體。它一般搭配addCallbackBuffer方法使用,虛擬碼如下:

byte[] mPreBuffer = new byte[size];//首先分配一塊記憶體作為緩衝區,size的計算方式見第四點中
mCamera.addCallbackBuffer(mPreBuffer);
mCamera.setPreviewCallbackWithBuffer(Camera.PreviewCallback cb);
mCamera.startPreview();

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    if (mPreBuffer == null) {
        mPreBuffer = new byte[size];
    }
    mCamera.addCallbackBuffer(mPreBuffer);//將此緩衝區新增到預覽回撥緩衝區佇列中
}

setPreviewCallbackWithBuffer需要在startPreview()之前呼叫,因為setPreviewCallbackWithBuffer使用時需要指定一個位元組陣列作為緩衝區,用於預覽幀資料,所以我們需要在setPreviewCallbackWithBuffer之前呼叫addCallbackBuffer,這樣onPreviewFrame的data才有值。

總結一下,設定addCallbackBuffer的地方有兩個,一個是在startPreview之前,一個是在onPreviewFrame中,這兩個都需要呼叫,如果在onPreviewFrame中不呼叫,那麼預覽幀資料就不會回撥給onPreviewFrame了

4,void addCallbackBuffer (byte[] callbackBuffer) 

新增一個預分配的緩衝區到預覽回撥緩衝區佇列中。應用程式可一新增一個或多個緩衝器到這個佇列中。當預覽幀資料到達時並且緩衝區佇列仍然有至少一個可用的緩衝區時,這個 緩衝區將會被消耗掉然後從佇列中移除,然後這個緩衝區會呼叫預覽回撥介面。如果預覽幀資料到達時沒有剩餘的緩衝區,這幀資料將會被丟棄。當緩衝區中的資料處理完成後,應用程式應該將這個緩衝區添加回緩衝區佇列中。
對於非YV12的格式,緩衝區的Size是預覽影象的寬、高和每個畫素的位元組數的乘積。寬高可以使用getPreviewSize()方法獲取。每個畫素的位元組數可以使用ImageFormat.getBitsPerPixel(mCameraParameters.getPreviewFormat()) / 8獲取。
對於YU12的格式,緩衝區的Size可以使用setPreviewFormat(int)裡面的公式計算,具體詳見官方文件。
這個方法只有在使用setPreviewCallbackWithBuffer(PreviewCallback)時才有必要使用。當使用setPreviewCallback(PreviewCallback) 或者setOneShotPreviewCallback(PreviewCallback)時,緩衝區會自動分配。當提供的緩衝區如果太小了,不能支援預覽幀資料時,預覽回撥介面將會return null,然後從緩衝區佇列中移除此緩衝區。

完整程式碼:只需在activity裡面呼叫runTask3即可

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;

import com.example.lll.va.R;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Task3 implements View.OnClickListener,
        TextureView.SurfaceTextureListener,
        SurfaceHolder.Callback {
    private final static String tag = Task3.class.getName();
    private Camera mCamera;
    private SurfaceHolder mHolder;
    private CameraUtil mCameraUtil;
    private Context mContext;
    private int height;
    private int width;
    private SurfaceView surfaceView;
    private TextureView textureView;
    private static Task3 task3;
    private Activity mActivity;
    private boolean isSurfaceMode;  //切換surfaceview和textureview的演示

    static String videoFileName = Environment.getExternalStorageDirectory() + "/test_vedio.mp4";

    public static void runTask3(Activity activity) {
        task3 = new Task3(activity);
        task3.initView();

    }

    public Task3(Activity activity) {
        mActivity = activity;
        mContext = activity;
    }

    private void initView() {
        mActivity.setContentView(R.layout.activity_task3);

        surfaceView = mActivity.findViewById(R.id.sv_t3);
        textureView = mActivity.findViewById(R.id.texture_view_1);

        //為了知道surface的生命週期,一般只有surface建好後才開始下一步動作
        //surfaceView必須給holder新增callback,TextureView必須給自己新增listener
        if (surfaceView.getVisibility() == View.VISIBLE) {
            isSurfaceMode = true;
            surfaceView.getHolder().addCallback(this);
            mHolder = surfaceView.getHolder();
        } else {
            isSurfaceMode = false;
            textureView.setSurfaceTextureListener(this);
        }

        Button btnTakePic = mActivity.findViewById(R.id.btn_take_pic);
        Button btnStartVideo = mActivity.findViewById(R.id.btn_start_video);
        Button btnStopVideo = mActivity.findViewById(R.id.btn_stop_video);
        btnTakePic.setOnClickListener(this);
        btnStartVideo.setOnClickListener(this);
        btnStopVideo.setOnClickListener(this);
    }

    private void getWidthAndHeight() {
        width = isSurfaceMode ? surfaceView.getWidth() : textureView.getWidth();
        height = isSurfaceMode ? surfaceView.getHeight() : textureView.getHeight();
    }

    public void initCamera() {
        if (mCamera != null) {
            mCamera.release();
            mCamera = null;
        }
        Log.d(tag, "is surfaceview = " + isSurfaceMode);
        mCameraUtil = CameraUtil.getInstance();
        mCamera = mCameraUtil.openCamera(0);
        Bundle paramBundle = new Bundle();  //感覺乾脆用bundle傳參好了
        getWidthAndHeight();
        paramBundle.putInt("width", width);
        paramBundle.putInt("height", height);
        mCameraUtil.setCameraParamter(mContext, mCamera, paramBundle);

        //根據不同的view,選擇不同的預覽載體
        if (isSurfaceMode) {
            try {

                mCamera.setPreviewDisplay(surfaceView.getHolder());
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            try {
                mCamera.setPreviewTexture(textureView.getSurfaceTexture());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //預覽監聽
        mCamera.setPreviewCallback(mCameraCallback);
//        mCamera.setPreviewCallbackWithBuffer(mCameraCallback);
        //開始預覽
        mCamera.startPreview();

    }


    /**************************  錄視訊部分   *******************************/
    MediaRecorder mediaRecorder;

    public void startVideoRecord() {

        mediaRecorder = new MediaRecorder();// 建立mediarecorder物件
        // 設定錄製視訊源為設定好引數的Camera(相機)
        mediaRecorder.setOnInfoListener(mediaCallbackListener);
        mediaRecorder.setOnErrorListener(mediaCallbackListener);
        mCamera.unlock();

        mediaRecorder.setCamera(mCamera);

        if (isSurfaceMode)//如果給camera設定了setPreviewDisplay,則這句可以不加
            mediaRecorder.setPreviewDisplay(mHolder.getSurface());


        Log.d(tag, "camera + " + mCamera);

        
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
        // 設定錄製完成後視訊的封裝格式THREE_GPP為3gp.MPEG_4為mp4
        mediaRecorder
                .setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        // 設定錄製的視訊編碼h264
        //音訊編碼為AAC
        mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
        mediaRecorder.setOrientationHint(90);
        // 設定視訊錄製的解析度。必須放在設定編碼和格式的後面,否則報錯
        mediaRecorder.setVideoSize(1600, 1200);
        mediaRecorder.setVideoEncodingBitRate(1024 * 1024 * 5)