之前自學了下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。