Camera开发系列之三-相机数据硬编码为h264git
Camera开发系列之四-使用MediaMuxer封装编码后的音视频到mp4容器github
Camera开发系列之五-使用MediaExtractor制做一个简易播放器网络
Camera开发系列之七-使用GLSurfaceviw绘制Camera预览画面 ide
视频的播放过程能够简单理解为一帧一帧的画面按照时间顺序呈现出来的过程,就像在一个本子的每一页画上画,而后快速翻动的感受。函数
有人就说了,这样不就简单了,直接把camera获取到的数据保存成文件,而后播放就好了。还须要编码不是画蛇添足么?你还别说,当初我就是这么想的,以致于被公司dalao鄙视了很久,终于知道了知识是多么重要。post
首先要讲一下为何须要将camera获取到的yuv数据进行编码,就拿视频直播举例,视频直播很是注重实时性,实时性就是视频图像从产生到消费完成整个过程人感受不到延迟,只要符合这个要求的视频业务均可以称为实时视频。要实时就要缩短延迟,要缩短延迟就要知道延迟是怎么产生的,视频从产生、编码、传输到最后播放消费,各个环节都会产生延迟,整体概括为下图: 编码
咱们知道从camera采集到的图像格式通常是YUV格式,这种格式的存储空间很是大,若是是 1080P 分辨率的图像空间:1920 *1080 * 3 /2= 3MB,就算转换为jpg也须要近200
k大小,若是是每秒12帧也须要近 2.4MB/S的带宽,这带宽在公网上传输是没法接受的。
视频编码器就是为了解决这个问题的,它会根据先后图像的变化作运动检测,经过各类压缩把变化的发送到对方,1080P 进行过 H.264 编码后带宽也就在 200KB/S ~ 300KB/S 左右。
在实际应用中,并非每一帧都是完整的画面,由于若是每一帧画面都是完整的图片,那么一个视频的体积就会很大,这样对于网络传输或者视频数据存储来讲成本过高,因此一般会对视频流中的一部分画面进行压缩(编码)处理。
所谓的硬编码就是用GPU对视频帧进行编码,相对于软编码来讲,硬编码的编码效率天差地别。更高的编码效率就意味着在相同帧率下可以得到更高的分辨率,更佳的画面质量。可是因为硬编码和手机硬件平台相关性较大,目前在部分机型上存在不兼容现象,因此并不能彻底抛弃软编码方案而是做为硬编码的补充。
Android平台提供了mediacodec类对视频进行硬编码 ,MediaCodec类可用于访问低级媒体编解码器,即编码/解码组件。它是Android低级别多媒体支持基础设施的一部分(一般一块儿使用MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack.)
#####1 初始化
mediacodec的初始化也很是简单,主要是下面三个方法:
static MediaCodec createByCodecName(String name);
static MediaCodec createEncoderByType(String type);
static MediaCodec createDecoderByType(String type);
复制代码
若是要使用createByCodecName初始化,须要提早知道编解码器的具体名字,第二个方法和第三个方法分别对应编码和解码,根据type建立,开头以video/打头,好比h264就是"video/avc" 。
初始化以后调用以下方法进行配置:
void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags) 复制代码
先看看第一个参数MediaFormat是干什么的,官方解释:
Encapsulates the information describing the format of media data, be it audio or video.
它是音视频数据格式的简单描述
The format of the media data is specified as string/value pairs.
这个格式有点特殊,是string/value键值对
Keys common to all audio/video formats, all keys not marked optional are mandatory:
键通常是音频/视频这样的格式,全部键都是必须的
简而言之,就是配置一些编解码时的格式,好比帧率,码率,颜色空间等等。
第二个参数是指定要在其上呈现此解码器输出的view,编码能够传入null
第三个参数用于视频加密
第四个是指定CONFIGURE_FLAG_ENCODE
将组件配置为编码器 ,若是是解码就传0
好吧,其实第三个是我随便说的,我也不知道具体能干什么,看官方文档上的解释也不详细,哪位知道的观众姥爷能够悄悄咪咪的告诉我,我悄悄的修改。
完整的代码以下:
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
//颜色空间设置为yuv420sp
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
//比特率,也就是码率 ,值越高视频画面更清晰画质更高
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
//帧率,通常设置为30帧就够了
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
//关键帧间隔
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
try {
//初始化mediacodec
mediaCodec = MediaCodec.createEncoderByType("video/avc");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//设置为编码模式和编码格式
mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
复制代码
首先建立一个队列,将获取到的视频数据逐帧的放入:
private static int yuvqueuesize = 10;
private ArrayBlockingQueue<byte[]> YUVQueue = new ArrayBlockingQueue<>(yuvqueuesize);
public void putYUVData(byte[] buffer) {
if (YUVQueue.size() >= 10) {
YUVQueue.poll();
}
YUVQueue.add(buffer);
}
复制代码
在camera数据回调方法中调用:
Camera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//给队列丢数据
putYUVData(data);
}
});
复制代码
其次初始化一个输出流,在构造函数中调用,往里面写编码后的数据:
private void createfile(){
File file = new File(path);
if(file.exists()){
file.delete();
}
try {
outputStream = new BufferedOutputStream(new FileOutputStream(file));
} catch (Exception e){
e.printStackTrace();
}
}
复制代码
而后新建一个线程从队列里取出帧数据进行编码,须要注意的是我设置的是YUV420SP格式的颜色空间,因此这里要将NV21转换为NV12格式的:
public void StartEncoderThread(){
Thread EncoderThread = new Thread(new Runnable() {
@SuppressLint("NewApi")
@Override
public void run() {
isRuning = true;
byte[] input = null;
long pts = 0;
long generateIndex = 0;
while (isRuning) {
if (YUVQueue.size() > 0){
//从缓冲队列中取出一帧
input = YUVQueue.poll();
byte[] yuv420sp = new byte[m_width*m_height*3/2];
//把待编码的视频帧转换为YUV420格式
NV21ToNV12(input,yuv420sp,m_width,m_height);
input = yuv420sp;
}
if (input != null) {
try {
long startMs = System.currentTimeMillis();
//编码器输入缓冲区
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();
//把转换后的YUV420格式的视频帧放到编码器输入缓冲区中
inputBuffer.put(input);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
generateIndex += 1;
}
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];
outputBuffer.get(outData);
if(bufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG){
configbyte = new byte[bufferInfo.size];
configbyte = outData;
}else if(bufferInfo.flags == BUFFER_FLAG_KEY_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 (Throwable t) {
t.printStackTrace();
}
} else {
try {
//这里能够根据实际状况调整编码速度
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
EncoderThread.start();
}
复制代码
NV21转NV21:
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];
}
}
复制代码
PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在何时显示这一帧的数据。虽然PTS 是用于指导播放端的行为,但它们是在编码的时候由编码器生成的。 下面是计算pts的方法:
/** * 计算pts * @param frameIndex * @return */
private long computePresentationTime(long frameIndex) {
return 132 + frameIndex * 1000000 / framerate;
}
public boolean isEncodering(){
return isRuning;
}
复制代码
最后写一个按钮,开启编码线程开始编码:
mBtnEncoder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//启动线程编码
StartEncoderThread();
}
});
复制代码
参考连接;
Android 硬解码MediaCodec配合SurfaceView的踏坑之旅
项目地址:camera数据采集硬编码H.264 欢迎start和fork