博主在写这篇文章之前可以说是在
音视频这方面
,知识积累与经验几乎为0;所以在实现这个功能上也是费了好一番功夫和精力把它给搞出来了,所以以此篇文章纪念一下。
一、首先就是需要先打开摄像头,并拿到视频的每一帧数据
1、相机权限是必须要的,API>=6.0还需要动态申请 (动态申请权限代码略过,详情见文末源码)
2、在布局上放置一个SurfaceView
用来预览相机的画面
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 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();
}
}
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();
}
}
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;
}
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 = 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
命令下载到电脑进行播放就可以
Demo 下载地址请戳我
作者:Code-Porter