Android 編碼攝像頭數據爲h.264格式

之前自學了下ffmpeg,使用ffmpeg在ubuntu下編解碼比較方便,但是到了Android,發現使用比較多的編解碼類是MediaCodec,在工作之餘,抽點時間,學習下這個類的使用,做點記錄,以供後續查閱。
MediaCodec類可用於訪問Android底層的媒體編解碼器,它是Android爲多媒體支持提供的底層接口的一部分(通常與MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, 以及AudioTrack一起使用)—這是官網對MediaCodec類的簡介。閱讀了下API文檔,還是水裏霧裏的,於是設計了一個實驗,慢慢嘗試MediaCodec類的使用。
我設計的實驗非常簡單,就是從攝像頭拿到數據,然後使用MediaCodec將其壓縮爲h.264格式,壓縮完成後寫入一個文件中。
獲取攝像頭的數據的過程之前在 一文中已經說過了,是的,基於此,我們將拿到的數據進行編碼。
先看一下結果吧,因爲h.264文件是可以直接播放的:
這裏寫圖片描述

從攝像頭取出圖像數據

首先,創建一個TextureView對象或者SurfaceView,二者都可以,這裏使用TextureView,它直接在佈局文件中使用,然後在Activity中使用findViewById來找到它。
其次,給TextureView設置監聽器:textureView.setSurfaceTextureListener(this);Acitivity實現了這個監聽器接口。
然後,在onSurfaceTextureAvailable函數中初始化攝像頭,並打開,就像下面這樣:

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        Log.d("jw_liu","onPreviewFrame");

        mCamera = Camera.open();
        try {
            mCamera.setPreviewTexture(surface);
            mCamera.setPreviewCallback(new Camera.PreviewCallback() {
                @Override
                public void onPreviewFrame(byte[] data, Camera camera) {
                    Log.d("jinwei","l:"+data.length);
                    if(MediaCodecManager.frame != null){
                        System.arraycopy(data,0,MediaCodecManager.frame,0,data.length);
                    }
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        Camera.Parameters parameters = mCamera.getParameters();
        parameters.setPictureFormat(PixelFormat.JPEG);
        parameters.setPreviewFormat(PixelFormat.YCbCr_420_SP);
        parameters.setPreviewSize(480, 320);

        parameters.set("orientation", "portrait");
        parameters.set("rotation", 180);
        mCamera.setDisplayOrientation(180);

        mCamera.setParameters(parameters);
        mCamera.startPreview();
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
      //  Log.d("jw_liu","onSurfaceTextureSizeChanged");
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
       // Log.d("jw_liu","onSurfaceTextureDestroyed");
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
       // Log.d("jw_liu","onSurfaceTextureUpdated");
    }

這個流程非常簡單,我們需要關注的是攝像頭的配置參數。
我們將攝像頭的預覽格式配置成 了YCbCr_420_SP,預覽的大小爲420*320,這兩個參數非常重要,因爲編碼器也需要設置編碼圖像的大小和圖片格式,如果這兩者是一致的,那麼你就省去了在這之間做轉換的繁瑣工作。

我們給camera設置了預覽的回調函數,這樣,當攝像頭採集到數據以後,回調函數就會將預覽的數據傳給你,由此,我們獲取到了攝像頭的數據。
獲取到攝像頭的數據以後,我們便可以對其進行編碼了。我把這部分邏輯放在一個單獨的類MediaCodecManager中。這個類會創建一個MeidaCodec的實例,並使用它來對視屏編碼。

編碼的流程

首先,我們要獲得一個編碼器,這沒什麼好說的,我們可以通過類型來直接獲取:
mediaCodec = MediaCodec.createEncoderByType(“video/avc”);
其次,這個編碼器我們需要對他進行配置。官方文檔中對配置的條目有明確的書名,很多條目都是必須要配置的,配置的不對,調用mediaCodec.start()方法就會失敗。我對編碼器做的配置如下:

        try {
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 480, 320);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
        mediaCodec.setCallback(new MediaCodec.Callback() {
            @Override
            public void onInputBufferAvailable(MediaCodec codec, int index) {
                Log.d("jinwei","onInputBufferAvailable:"+index);
                ByteBuffer byteBuffer = codec.getInputBuffer(index);
                byteBuffer.put(frame);
                codec.queueInputBuffer(index,0,frame.length,1,BUFFER_FLAG_CODEC_CONFIG);
            }

            @Override
            public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
                Log.d("jinwei","onOutputBufferAvailable:"+index);
                if(index>-1){
                    ByteBuffer outputBuffer = codec.getOutputBuffer(index);
                    byte[] bb = new byte[info.size];
                    outputBuffer.get(bb);
                    try {
                        fileOutputStream.write(bb);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    codec.releaseOutputBuffer(index,false);
                }
            }

            @Override
            public void onError(MediaCodec codec, MediaCodec.CodecException e) {
                Log.d("jinwei","onError");
                codec.reset();
            }

            @Override
            public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
                Log.d("jinwei","onOutputFormatChanged");
            }
        });
        mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mediaCodec.start();

首先,我們創建視頻格式的時候,指定大小爲480*320,這和我們在攝像頭中預覽的大小一致。
MediaFormat mediaFormat = MediaFormat.createVideoFormat(“video/avc”, 480, 320);
KEY_BIT_RATE,波特率,這是必須的。
KEY_FRAME_RATE,一秒鐘多少幀,也是必須的。
圖片格式:COLOR_FormatYUV420Planar,攝像頭的圖片格式爲YCbCr_420_SP,加Planar是將YUV三個分量存儲在三個數組中,YCbCr_420_SP則是存儲在一個數組中。這沒有關係。
KEY_I_FRAME_INTERVAL,關鍵幀的間隔,這涉及到h.264具體的編碼過程,很多人都配置爲5,我也配置爲5吧。

yuv420格式的數據,存儲的大小爲width*height*1.5;因此,對於一幀480*320大小的圖像,我們需要分配480*320*1.5個字節,也就是230400。因此,程序中,在MediaCodecManager中我們分配了230400大小的一個byte數組,不斷把採集到的數據寫入這個數組,然後,編碼器從中取到數據,進行編碼,並將編碼後的數據寫入文件。

最後強調的是,mediaCodec.setCallback(new MediaCodec.Callback()這句代碼給編碼器設置了回調接口,它使得編碼器的工作是異步進行的。

如果你覺得理解起來有點費勁,那麼你需要了解下編碼器的工作流程,它其實是一個狀態機:
這裏寫圖片描述
當我們創建一個編碼器以後,它處於uninitialized狀態,我們調用configure函數配置它以後,它的狀態變爲了Configured,調用start後,它屬於Flushed狀態,這個時候,它的輸入緩衝區是空的,onInputBufferAvailable方法被回調告訴你你可以寫入數據了。這個時候我們可以將數據寫入它的輸入緩存,然後提交它。編碼器就對編碼我們提交的數據,編碼完成後onOutputBufferAvailable放法被回調。如果一個文件結束了,我們需要在最後一幀數據中標明這是最後一幀,這樣編碼器就會停止接受數據的輸入,吧當前緩存區的數據編碼完成後,編碼器就可以被release了。
如果看到這裏你還是沒搞明白編碼器怎麼用,那麼,好好看幾遍API文檔的介紹,然後再來做這個簡單的實驗,你肯定能成功的。

以下爲完整的代碼,非常少,兩個java文件和一個佈局文件而已,僅供參考:
當然,別忘了權限

    <uses-permission android:name = "android.permission.CAMERA" />
    <uses-feature android:name = "android.hardware.camera" />
    <uses-feature android:name = "android.hardware.camera.autofocus" />
    <!-- 在SDCard中創建於刪除文件的權限 -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
    <!-- 往SDCard中寫入數據的權限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.sharenew.cameraclarity.MainActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextureView
            android:id="@+id/texture_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
</RelativeLayout>

MediaCodecManager.java

import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Environment;
import android.util.Log;

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

import static android.media.MediaCodec.BUFFER_FLAG_CODEC_CONFIG;

/**
 * Created by Administrator on 2017/3/31 0031.
 */

public class MediaCodecManager {
    MediaCodec mediaCodec;
    private int count = 0;
    public static final byte[] frame = new byte[230400];
    FileOutputStream fileOutputStream;
    public MediaCodecManager(Context context){
        int numCodecs = MediaCodecList.getCodecCount();
        for (int i = 0; i < numCodecs; i++) {
            MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
            Log.d("jinwei",codecInfo.getName());
        }
        initCodec();
        try {
            File file=new File(Environment.getExternalStorageDirectory(), "test.avc");
            if(!file.exists()){
                file.createNewFile();
            }
            fileOutputStream  = new FileOutputStream(file,true);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void initCodec(){
        try {
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 480, 320);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
        mediaCodec.setCallback(new MediaCodec.Callback() {
            @Override
            public void onInputBufferAvailable(MediaCodec codec, int index) {
                Log.d("jinwei","onInputBufferAvailable:"+index);
                ByteBuffer byteBuffer = codec.getInputBuffer(index);
                byteBuffer.put(frame);
                codec.queueInputBuffer(index,0,frame.length,1,BUFFER_FLAG_CODEC_CONFIG);
            }

            @Override
            public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
                Log.d("jinwei","onOutputBufferAvailable:"+index);
                if(index>-1){
                    ByteBuffer outputBuffer = codec.getOutputBuffer(index);
                    byte[] bb = new byte[info.size];
                    outputBuffer.get(bb);
                    try {
                        fileOutputStream.write(bb);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    codec.releaseOutputBuffer(index,false);
                }
            }

            @Override
            public void onError(MediaCodec codec, MediaCodec.CodecException e) {
                Log.d("jinwei","onError");
                codec.reset();
            }

            @Override
            public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
                Log.d("jinwei","onOutputFormatChanged");
            }
        });
        mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mediaCodec.start();
    }
    public void release(){
        mediaCodec.release();
    }
}

MainActivity


import android.app.ActionBar;
import android.app.Activity;
import android.graphics.PixelFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.WindowManager;

import java.io.IOException;
import java.util.List;

public class MainActivity extends Activity implements TextureView.SurfaceTextureListener{
    TextureView textureView;
    Camera mCamera;
    boolean isProcess = false;
    MediaCodecManager mediaCodecManager ;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        setContentView(R.layout.activity_main);
        textureView = (TextureView) findViewById(R.id.texture_view);
        textureView.setSurfaceTextureListener(this);
        mediaCodecManager= new MediaCodecManager(this);
    }

    private long  clarityCalculator(byte[] data,int width,int height){
        long rest = 0;
        long restF = 0;
        if(width<400 || height< 400)return 0;
        int startX = (width-400)/2;
        int startY = (height-400)/2;
        int startBase = startY*1280+startX;
        for(int i=0;i<100;i+=4){
            for(int j=0;j<100;j+=4){
                rest = 10*data[startBase+j*width+i];
                rest -= 4*data[startBase+j*width+i+1];
                rest -= 4*data[startBase+(j+1)*width+j];
                rest -= data[startBase+(j-1)*width+i+1];
                rest -= data[startBase+(j+1)*width+i+1];
                restF+=rest*rest;
            }
        }
        return restF/10000;
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        Log.d("jw_liu","onPreviewFrame");

        mCamera = Camera.open();
        try {
            mCamera.setPreviewTexture(surface);
            mCamera.setPreviewCallback(new Camera.PreviewCallback() {
                @Override
                public void onPreviewFrame(byte[] data, Camera camera) {
                    Log.d("jinwei","l:"+data.length);
                    if(MediaCodecManager.frame != null){
                        System.arraycopy(data,0,MediaCodecManager.frame,0,data.length);
                    }
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        Camera.Parameters parameters = mCamera.getParameters();
        parameters.setPictureFormat(PixelFormat.JPEG);
        parameters.setPreviewFormat(PixelFormat.YCbCr_420_SP);
        parameters.setPreviewSize(480, 320);

        parameters.set("orientation", "portrait");
        parameters.set("rotation", 180);
        mCamera.setDisplayOrientation(180);

        mCamera.setParameters(parameters);
        mCamera.startPreview();
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
      //  Log.d("jw_liu","onSurfaceTextureSizeChanged");
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
       // Log.d("jw_liu","onSurfaceTextureDestroyed");
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
       // Log.d("jw_liu","onSurfaceTextureUpdated");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mCamera.release();
        mediaCodecManager.release();
    }
}

小結

上面的程序執行完成後,會在/sdcard/下生成test.avc文件,咦,不是h.264嗎?真麼是avc,還有,我們之前的編碼器類型也是avc啊?我一開始也有這樣的困惑,百度了下,發現他們是等價的。我們使用adb把test.avc pull出來,然後就可以播放它了。強烈建議你下載ffplay,ffmpeg等工具,播放h.264格式的文件只需要一個命令:ffplay test.avc
如果你想生成gif,你可以使用ffmpeg:ffmpeg -i test.avc -s 480x320 -r 10 -t 10 test.gif
這條命令能生成幀率爲10,大小爲480*320,在原視頻中時長爲10秒的gif。

發佈了116 篇原創文章 · 獲贊 247 · 訪問量 54萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章