Android-音視訊(7):使用Camera錄製視訊,並存檔案
阿新 • • 發佈:2018-12-13
1.MediaCodec的作用
因為這裡會用到,所以先介紹這個的用法。
MediaCodec類用於使用一些基本的多媒體編解碼操作。
主要的API如下:
- getInputBuffers:獲取需要編碼資料的輸入流佇列,返回的是一個ByteBuffer陣列
- queueInputBuffer:輸入流入佇列
- dequeueInputBuffer:從輸入流佇列中取資料進行編碼操作
- getOutputBuffers:獲取編解碼之後的資料輸出流佇列,返回的是一個ByteBuffer陣列
- dequeueOutputBuffer:從輸出佇列中取出編碼操作之後的資料
- releaseOutputBuffer:處理完成,釋放ByteBuffer資料
在編碼壓縮的過程中,有幾個關鍵的標誌如下:
- BUFFER_FLAG_CODEC_CONFIG 這表明標記為這樣的緩衝區包含編解碼器初始化/編解碼器特定資料,而不是媒體資料 常數:2
- BUFFER_FLAG_END_OF_STREAM 這意味著流的結束,即在此之後沒有緩衝區可用 常數:4
- BUFFER_FLAG_KEY_FRAME 這表示標記為這樣的(編碼的)緩衝區包含關鍵幀的資料 常數:1
- CONFIGURE_FLAG_ENCODE 如果要將此編解碼器用作編碼器,則傳遞此標誌 常數:1
2.本次程式碼的主要流程:
- 1.設定Camera引數,並開啟Camera錄製
- 2.收集Camera資料,先將NV21的資料格式轉換為NV12,然後用MediaCodec編碼為H.264(AVC)並存儲到mp4檔案。
3.解釋一些地方
- 1.AVC是什麼?
AVC是一種編碼。實際上是 H.264 協議的別名。自從H.264協議中增加了SVC的部分之後,人們習慣將不包含SVC的H.264協議那一部分稱為 AVC,而將SVC這一部分單獨稱為SVC。
- 2.ArrayBlockingQueue 作用?
ArrayBlockingQueue是一個阻塞式的佇列,執行緒安全,底層以陣列的形式儲存資料,其所含的物件是以FIFO(先入先出)順序排序的。
- 3.YUV420 資料格式到底是什麼?
YUV420是一類資料格式的總稱。不僅僅只是下面給出的 :
YUV420有平面格式(Planar),即Y、U、V是分開儲存的,其中Y為 width*height,而U、V合佔Y的一半,該種格式每個畫素佔12位元。根據U、V的順序,分出2種格式,U前V後即YUV420P,也叫 I420。V前U後,叫YV12(YV表示Y後面跟著V,12表示12bit)。
還有一種半平面格式(Semi-planar),即Y單獨佔一塊地 方,其後U、V緊挨著排在一起,根據U、V的順序,又有2種,( U前V後叫NV12,在國內好像很多人叫它為YUV420SP格式);V前U後叫 NV21。
I420: YYYYYYYY UU VV =>YUV420P
YV12: YYYYYYYY VV UU =>
NV12: YYYYYYYY UVUV =>YUV420SP
NV21: YYYYYYYY VUVU =>
所以由此得出:YUV420 資料在記憶體中的長度是 width * hight * 3 / 2 (Y佔1、UV佔0.5)
4.程式碼如下:
MainActivity.java
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback, Camera.PreviewCallback {
//預覽用SurfaceView,視訊採集用Camera類,視訊壓縮為H.264(avc)
Camera camera;
SurfaceView surfaceView;
SurfaceHolder surfaceHolder;
Button stopEncoder;
int width = 1280;
int height = 720;
int framerate = 30; //一秒30幀
H264Encoder encoder; //自定義的編碼操作類
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main5);
stopEncoder = (Button) findViewById(R.id.stopEncoder);
surfaceView = (SurfaceView) findViewById(R.id.surface_view);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(this);
if (supportH264Codec()){ //查詢手機是否支援AVC編碼
Log.e("TAG" , "support H264 hard codec");
}else {
Log.e("TAG" , "not support H264 hard codec");
}
stopEncoder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.i("TAG", "surfaceDestroyed Medthod");
//停止預覽,並釋放資源
if (camera != null){
camera.setPreviewCallback(null);
camera.stopPreview();
camera = null;
}
if (encoder != null){
//停止編碼
encoder.stopEncoder();
}
}
});
}
private boolean supportH264Codec() {
// 遍歷支援的編碼格式資訊,並查詢有沒有支援H.264(avc)的編碼
if (Build.VERSION.SDK_INT >= 18){
//計算可用的編解碼器數量
int number = MediaCodecList.getCodecCount();
for (int i=number-1 ; i >0 ; i--){
//獲得指定的編解碼器資訊
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
//得到支援的型別
String[] types = codecInfo.getSupportedTypes();
//查詢有沒有支援H.264(avc)的編碼
for (int j = 0 ; j < types.length ; j++){
if (types[j].equalsIgnoreCase("video/avc")){
return true;
}
}
}
}
return false;
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
Log.i("TAG", "surfaceCreated Medthod");
camera = Camera.open(); //開啟相機
camera.setDisplayOrientation(90);
Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21); //設定資料格式
parameters.setPreviewSize(1280,720);
try{
camera.setParameters(parameters);
camera.setPreviewDisplay(surfaceHolder);
camera.setPreviewCallback(this);
camera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
//編碼初始化
encoder = new H264Encoder(width,height,framerate);
encoder.startEncoder(); //開始編碼
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
Log.i("TAG", "surfaceChanged Medthod");
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
/*Log.i("TAG", "surfaceDestroyed Medthod");
//停止預覽,並釋放資源
if (camera != null){
camera.setPreviewCallback(null);
camera.stopPreview();
camera = null;
}
if (encoder != null){
//停止編碼
encoder.stopEncoder();
}*/
}
@Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
//返回相機預覽的視訊資料,並給H264Encoder編碼壓縮為H.264(avc)的檔案test.mp4
//這裡面的Bytes的資料就是NV21格式的資料
if (encoder != null){
encoder.putDate(bytes); //將一幀的資料傳過去處理
}
}
}
H264Encoder.java
public class H264Encoder {
private final static int TIMEOUT_USEC = 12000; //超時時間
private MediaCodec mediaCodec; //核心
public boolean isRunning = false; //flag
private int width ,height , framerate;
public byte[] configbyte;
private BufferedOutputStream outputStream;
//儲存camera返回的視訊資料yuv(NV21)
public ArrayBlockingQueue<byte[]> yuv420queue = new ArrayBlockingQueue<byte[]>(10);
public H264Encoder(int width, int height, int framerate) {
this.width = width;
this.height = height;
this.framerate = framerate;
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc",width,height);
//設定編碼器的資料格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar); //NV12(YUV420SP)的資料格式
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE,width*height*5); //位元率 bite/s
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,1);
try {
mediaCodec = MediaCodec.createEncoderByType("video/avc");
//建立編碼器
mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
createfile();//準備儲存視訊錄製資料的檔案
} catch (IOException e) {
e.printStackTrace();
}
}
private void createfile() {
String path = Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator+"test.mp4";
File file = new File(path);
if (file.exists()){
file.delete();
}
try {
outputStream = new BufferedOutputStream(new FileOutputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
//核心
public void startEncoder() {
new Thread(new Runnable() {
@Override
public void run() {
isRunning = true;
byte[] input = null;
long pts = 0;
long generateIndex = 0 ;
while (isRunning){
if (yuv420queue.size() > 0){
//得到一幀資料
input = yuv420queue.poll();
//YUV420 資料在記憶體中的長度是 width * hight * 3 / 2 (Y佔UV佔0.5)
byte[] yuv420sp = new byte[width*height*3/2];
//必須要轉換格式,否則視訊錄得內容播放出來顏色有偏差
NV21TONV12(input,yuv420sp,width,height);
input = yuv420sp;
}
if (input != null){
try {
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0){
//當前幀的時間戳
pts = computePresentationTime(generateIndex);
//得到編碼的輸入緩衝區
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
//向緩衝區新增資料
inputBuffer.put(input);
//緩衝區資料入編碼器
mediaCodec.queueInputBuffer(inputBufferIndex,0,input.length,pts,0);
generateIndex += 1;
}
//定義一個BufferInfo儲存outputBufferIndex的幀資訊
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
while (outputBufferIndex >= 0){
//得到輸出緩衝區
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = new byte[bufferInfo.size];
//將資料寫入outData
outputBuffer.get(outData);
if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG){
//關鍵幀資訊或初始化的資訊,只有一次
configbyte = new byte[bufferInfo.size];
configbyte = outData;
} else if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_SYNC_FRAME){
//關鍵幀
byte[] keyframe = new byte[bufferInfo.size + configbyte.length];
System.arraycopy(configbyte,0,keyframe,0,configbyte.length);
System.arraycopy(outData,0,keyframe,configbyte.length,outData.length);
outputStream.write(keyframe,0,keyframe.length);
}else {
outputStream.write(outData,0,outData.length);
}
//釋放輸出緩衝區,進行下一次編碼操作
mediaCodec.releaseOutputBuffer(outputBufferIndex,false);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
}
} catch (IOException e) {
e.printStackTrace();
}
}else {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//編解碼完成,停止編解碼,並釋放資源
mediaCodec.stop();;
mediaCodec.release();
//關閉資料流
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private long computePresentationTime(long frameIndex) {
return frameIndex * 1000000 / framerate;
}
private void NV21TONV12(byte[] nv21, byte[] nv12, int width, int height) {
if (nv21 == null || nv12 == null) return;
int frameSize = width*height;
int i=0 ,j=0;
System.arraycopy(nv21,0,nv12,0,frameSize);
for (i = 0 ; i < frameSize ; i++){
nv12[i] = nv21[i];
}
for (j = 0 ; j <frameSize/2 ; j+=2){
nv12[frameSize+j+1] = nv21[j+frameSize];
}
for (j=0 ; j < frameSize/2 ; j+=2){
nv12[frameSize+j] = nv21[j + frameSize + 1];
}
}
public void stopEncoder() {
isRunning = false;
}
public void putDate(byte[] bytes) {
if (yuv420queue.size() >= 10){
yuv420queue.poll();
}
yuv420queue.add(bytes);
}
}