前言:
博主在寫這篇文章之前可以說是在音視頻這方面
,知識積累與經驗幾乎爲0;所以在實現這個功能上也是費了好一番功夫和精力把它給搞出來了,所以以此篇文章紀念一下。
一、首先就是需要先打開攝像頭,並拿到視頻的每一幀數據
- 1、相機權限是必須要的,API>=6.0還需要動態申請 (動態申請權限代碼略過,詳情見文末源碼)
<uses-permission android:name="android.permission.CAMERA" />
- 2、在佈局上放置一個
SurfaceView
用來預覽相機的畫面
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/sfv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
- 3、初始化
SurfaceView
,併爲SurfaceHolder
添加一個addCallback
來獲取SurfaceView
的狀態
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {
private SurfaceHolder holder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
SurfaceView surfaceView = findViewById(R.id.sfv);
holder = surfaceView.getHolder();
holder.addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
二、當SurfaceView
就緒好後就可以開啓相機獲取視頻幀數據了
- 1、在
surfaceCreated
處打開相機
private Camera camera;
@Override
public void surfaceCreated(SurfaceHolder holder) {
//打開相機
openCamera();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//關閉相機
releaseCamera(camera);
}
/**
* 打開相機
*/
private void openCamera() {
camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
//獲取相機參數
Camera.Parameters parameters = camera.getParameters();
//獲取相機支持的預覽的大小
Camera.Size previewSize = getCameraPreviewSize(parameters);
int width = previewSize.width;
int height = previewSize.height;
//設置預覽格式(也就是每一幀的視頻格式)YUV420下的NV21
parameters.setPreviewFormat(ImageFormat.NV21);
//設置預覽圖像分辨率
parameters.setPreviewSize(width, height);
//相機旋轉90度
camera.setDisplayOrientation(90);
//配置camera參數
camera.setParameters(parameters);
try {
camera.setPreviewDisplay(holder);
} catch (IOException e) {
e.printStackTrace();
}
//設置監聽獲取視頻流的每一幀
camera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
}
});
//調用startPreview()用以更新preview的surface
camera.startPreview();
}
/**
* 獲取設備支持的最大分辨率
*/
private Camera.Size getCameraPreviewSize(Camera.Parameters parameters) {
List<Camera.Size> list = parameters.getSupportedPreviewSizes();
Camera.Size needSize = null;
for (Camera.Size size : list) {
if (needSize == null) {
needSize = size;
continue;
}
if (size.width >= needSize.width) {
if (size.height > needSize.height) {
needSize = size;
}
}
}
return needSize;
}
/**
* 關閉相機
*/
public void releaseCamera(Camera camera) {
if (camera != null) {
camera.setPreviewCallback(null);
camera.stopPreview();
camera.release();
}
}
- 2、到這裏不出意外的話就可以在界面上可以看到攝像頭的畫面了
而視頻每一幀的數據就在onPreviewFrame(byte[] data, Camera camera)
回調函數中獲取
- 3、注意:這裏獲取到的視頻數據是
YUV420
編碼格式的原始數據,並不是我們要的H264編碼格式;所以接下來就是要對這每一幀的視頻數據進行轉碼了。對於編碼格式想要了解的大家可以出門右轉問下度娘^ _ ^
三、通過MediaCodec編碼成H264格式
-
編碼步驟
- 將從攝像頭獲取到的
NV21
數據編碼成NV12
,因爲MediaCodec
不支持NV21
格式 - 需要將
NV21
格式數據進行順時針旋轉90度
,因爲從攝像頭拿到的畫面已經被逆時針旋轉了90度 - 使用
MediaCodec
進行編碼
- 將從攝像頭獲取到的
-
1、代碼有點多,這裏就貼上整個類代碼了
public class NV21EncoderH264 {
private int width, height;
private int frameRate = 30;
private MediaCodec mediaCodec;
private EncoderListener encoderListener;
public NV21EncoderH264(int width, int height) {
this.width = width;
this.height = height;
initMediaCodec();
}
private void initMediaCodec() {
try {
mediaCodec = MediaCodec.createEncoderByType("video/avc");
//height和width一般都是照相機的height和width。
//TODO 因爲獲取到的視頻幀數據是逆時針旋轉了90度的,所以這裏寬高需要對調
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", height, width);
//描述平均位速率(以位/秒爲單位)的鍵。 關聯的值是一個整數
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
//描述視頻格式的幀速率(以幀/秒爲單位)的鍵。幀率,一般在15至30之內,太小容易造成視頻卡頓。
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
//色彩格式,具體查看相關API,不同設備支持的色彩格式不盡相同
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
//關鍵幀間隔時間,單位是秒
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//開始編碼
mediaCodec.start();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 將NV21編碼成H264
*/
public void encoderH264(byte[] data) {
//將NV21編碼成NV12
byte[] bytes = NV21ToNV12(data, width, height);
//視頻順時針旋轉90度
byte[] nv12 = rotateNV290(bytes, width, height);
try {
//拿到輸入緩衝區,用於傳送數據進行編碼
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
//拿到輸出緩衝區,用於取到編碼後的數據
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
//當輸入緩衝區有效時,就是>=0
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
//往輸入緩衝區寫入數據
inputBuffer.put(nv12);
//五個參數,第一個是輸入緩衝區的索引,第二個數據是輸入緩衝區起始索引,第三個是放入的數據大小,第四個是時間戳,保證遞增就是
mediaCodec.queueInputBuffer(inputBufferIndex, 0, nv12.length, System.nanoTime() / 1000, 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//拿到輸出緩衝區的索引
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = new byte[bufferInfo.size];
outputBuffer.get(outData);
//outData就是輸出的h264數據
if (encoderListener != null) {
encoderListener.h264(outData);
}
mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
} catch (Throwable t) {
t.printStackTrace();
}
}
/**
* 因爲從MediaCodec不支持NV21的數據編碼,所以需要先講NV21的數據轉碼爲NV12
*/
private byte[] NV21ToNV12(byte[] nv21, int width, int height) {
byte[] nv12 = new byte[width * height * 3 / 2];
int frameSize = width * height;
int i, j;
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];
}
return nv12;
}
/**
* 此處爲順時針旋轉旋轉90度
*
* @param data 旋轉前的數據
* @param imageWidth 旋轉前數據的寬
* @param imageHeight 旋轉前數據的高
* @return 旋轉後的數據
*/
private byte[] rotateNV290(byte[] data, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
// Rotate the Y luma
int i = 0;
for (int x = 0; x < imageWidth; x++) {
for (int y = imageHeight - 1; y >= 0; y--) {
yuv[i] = data[y * imageWidth + x];
i++;
}
}
// Rotate the U and V color components
i = imageWidth * imageHeight * 3 / 2 - 1;
for (int x = imageWidth - 1; x > 0; x = x - 2) {
for (int y = 0; y < imageHeight / 2; y++) {
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
i--;
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
i--;
}
}
return yuv;
}
/**
* 設置編碼成功後數據回調
*/
public void setEncoderListener(EncoderListener listener) {
encoderListener = listener;
}
public interface EncoderListener {
void h264(byte[] data);
}
}
- 2、這裏需要注意一點:因爲我們將視頻旋轉了90度,所以需要將原來的寬變成高,高變成寬;所以在初始化MediaFormat的時候,需要將傳入的寬高對調一下,不然畫面會顯示花屏
MediaCodec mediaCodec = MediaCodec.createEncoderByType("video/avc");
//height和width一般都是照相機的height和width。
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", height, width);
四、使用寫好的工具類進行編碼
- 需要在打開相機之前創建好實例
/**
* 打開相機
*/
private void openCamera() {
//....省略若干代碼
Camera.Parameters parameters = camera.getParameters();
//獲取相機支持的預覽的大小
Camera.Size previewSize = getCameraPreviewSize(parameters);
int width = previewSize.width;
int height = previewSize.height;
//....省略若干代碼
final NV21EncoderH264 nv21EncoderH264 = new NV21EncoderH264(width, height);
nv21EncoderH264.setEncoderListener(this);
//設置監聽獲取視頻流的每一幀
camera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
nv21EncoderH264.encoderH264(data);
}
});
//調用startPreview()用以更新preview的surface
camera.startPreview();
}
//編碼成功的回調
@Override
public void h264(byte[] data) {
Log.e("TAG", data.length + "");
}
- 運行的效果
- 既然已經把視頻幀數據編碼好了,那就可以把它寫入到一個文件裏拿來播放了
五、保存編碼好的數據爲視頻文件
- 在
onCreate()
的時候創建寫入的文件
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createFile();
}
@Override
public void h264(byte[] data) {
try {
outputStream.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
private void createFile() {
File file = new File(getExternalCacheDir(), "test.h264");
try {
outputStream = new FileOutputStream(file);
} catch (Exception e) {
e.printStackTrace();
}
}
- 生成的文件
- 使用
adb pull
命令下載到電腦進行播放就可以