一、编码基础概念
1.1 为什么要进行视频编码
视频是由一帧帧图像组成,就如常见的gif图片,如果打开一张gif图片,可以发现里面是由很多张图片组成。一般视频为了不让观众感觉到卡顿,一秒钟至少需要24帧画面(一般是30帧),假如该视频是一个1280x720分辨率的视频,那么不经过编码一秒钟的大小:
结果:1280x720x4x24/(1024*1024)≈84.375M
所以不经过编码的视频根本没法保存,更不用说传输了。
1.2 视频压缩编码标准
视频中存在很多冗余信息,比如图像相邻像素之间有较强的相关性,视频序列的相邻图像之间内容相似,人的视觉系统对某些细节不敏感等,对这部分冗余信息进行处理的过程就是视频编码。
H.26X系列(由ITU[国际电传视讯联盟]主导)
H.261:主要在老的视频会议和视频电话产品中使用
H.263:主要用在视频会议、视频电话和网络视频上
H.264:H.264/MPEG-4第十部分,或称AVC(Advanced Video Coding,高级视频编码),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。
H.265:高效率视频编码(High Efficiency Video Coding,简称HEVC)是一种视频压缩标准,H.264/MPEG-4 AVC的继任者。可支持4K分辨率甚至到超高画质电视,最高分辨率可达到8192×4320(8K分辨率),这是目前发展的趋势,尚未有大众化编码软件出现
MPEG系列(由ISO[国际标准组织机构]下属的MPEG[运动图象专家组]开发)
MPEG-1第二部分:MPEG-1第二部分主要使用在VCD上,有些在线视频也使用这种格式
MPEG-2第二部分(MPEG-2第二部分等同于H.262,使用在DVD、SVCD和大多数数字视频广播系统中
MPEG-4第二部分(MPEG-4第二部分标准可以使用在网络传输、广播和媒体存储上
1.3 编码流程
在进行当前信号编码时,编码器首先会产生对当前信号做预测的信号,称作预测信号(predicted signal)
预测的方式
时间上的预测(interprediction),亦即使用先前帧的信号做预测
空间上的预测 (intra prediction),亦即使用同一张帧之中相邻像素的信号做预测
得到预测信号后,编码器会将当前信号与预测信号相减得到残余信号(residual signal),并只对残余信号进行编码,如此一来,可以去除一部份时间上或是空间上的冗余信息。
编码器并不会直接对残余信号进行编码,而是先将残余信号经过变换(通常为离散余弦变换)然后量化以进一步去除空间上和感知上的冗余信息。量化后得到的量化系数会再透过熵编码,去除统计上的冗余信息。
二、H.264编码详解(AVC)
2.1 H.264是新一代的编码标准,以高压缩高质量和支持多种网络的流媒体传输著称。
相关理解:
在相邻几幅图像画面中,一般有差别的像素只有10%以内的点,亮度差值变化不超过2%,而色度差值的变化只有1%以内,所以对于一段变化不大图像画面,我们可以先编码出一个完整的图像帧A,随后的B帧就不编码全部图像,只写入与A帧的差别,这样B帧的大小就只有完整帧的1/10或更小,B帧之后的C帧如果变化不大,我们可以继续以参考B的方式编码C帧,这样循环下去。
这段图像我们称为一个序列,序列就是有相同特点的一段数据,当某个图像与之前的图像变化很大,无法参考前面的帧来生成,那我们就结束上一个序列,开始下一段序列,也就是对这个图像生成一个完整帧A1,随后的图像就参考A1生成,只写入与A1的差别内容。
主要目标:
高的视频压缩比;
良好的网络亲和性;
2.2 H.264三种帧
在H.264中定义了三种帧:
I帧:完整编码的帧叫I帧
P帧:参考之前的I帧生成的只包含差异部分编码的帧叫P帧
B帧:参考前后的帧编码的帧叫B帧
H264采用的核心算法是帧内压缩和帧间压缩:
帧内压缩是生成I帧的算法
帧间压缩是生成B帧和P帧的算法
压缩方法:
分组:把几帧图像分为一组(GOP,也就是一个序列),为防止运动变化,帧数不宜取多
定义帧:将每组内各帧图像定义为三种类型,即I帧、B帧和P帧;
预测帧:以I帧做为基础帧,以I帧预测P帧,再由I帧和P帧预测B帧;
数据传输:最后将I帧数据与预测的差值信息进行存储和传输。
GOP序列:
在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流。
一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像
2.3 编码YUV为H.26X编码视频格式
YUV数据
YUV通过Y,U,V三个分量表示颜色空间,Y表示亮度,UV表示色度。RGB颜色空间每个像素点都有独立的RGB三个颜色分量值。YUV却不同:
YUV根据UV采样数目的不同,分为YUV444,YUV422,YUV420等。
YUV420
表示每个像素点有一个独立的亮度表示,即Y;色度UV分量由每四个像素点共享一个。例如一个4X4的图片,在YUV420格式下,有16个Y,UV各四个。
YUV420根据UV色度的存储顺序不同,又分为不同的格式。它分为两个YUV420P和YUV420SP两个大类,YUV420P的UV顺序存储,YUV420SP的UV交错存储。以4X4的图片格式为例部分格式如下:
三、H.264分层设计
3.1 H264算法在概念上分为两层:
1、VCL:(Video Coding Layer)视频编码层,负责高效的内容表示。
2、NAL:(Network Abstraction Layer)网络提取层,负责以网络所要求的恰当的方式对数据进行打包和传送。
VCL数据即被压缩编码后的视频数据序列。在VCL数据要封装到NAL单元中之后,才可以用来传输或存储。
H.264 的编码视频序列包括一系列的NAL 单元,每个NAL 单元包含一个RBSP。一个原始的H.264 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成,其中 Start Code 用于标示这是一个NALU 单元的开始,必须是"00 00 00 01" 或"00 00 01"。
其中RBPS有分为几种类型:
3.2 NAL头
占一个字节(8bit),由三部分组成forbidden_bit(1bit),nal_reference_bit(2bits)(优先级),nal_unit_type(5bits)(类型)。
3.2.1 forbidden_bit
禁止位,H264文档规定,这个值应该为0,当它不为0时,表示网络传输过程中,当前NALU中可能存在错误,解码器可以考虑不对这个NALU进行解码。
3.2.2 nal_reference_bit
当前NAL的优先级,值越大,该NAL越重要。
3.2.3 nal_unit_type
它表示NALU Header后面的RBSP的数据结构的类型。下图为nal_unit_type所有可能的取值,和对应的语义:
可以看到,nal_unit_type的值为1-5时,表示RBSP里面包含的数据为条带(片/Slice)数据,所以值为1-5的NALU统称为VCL(视像编码层)单元,其他的NALU则称为非VCL NAL单元。
当nal_unit_type为7时,代表当前NALU为序列参数集,为8时为图像参数集。这也是我们打开.h264文件后,遇到的前两个NALU,它们位于码流的最前面。
而且当nal_unit_type为14-31时,我们可以不用理睬,目前几乎用不到。
PPS(Picture Parameter Sets):图像参数集
SPS(Sequence Parameter Set):序列参数集
接下来说说SPS、PPS和SEI。
SPS和PPS是用来初始化解码器的,没这些数据,视频数据是无法解析出来的。另外如果我们分析单独的H264文件,可以发现有的文件每个IDR帧前面都有PPS和SPS,有的只是开头才有。针对SPS和PPS,一般来说:
1)、如果是在直播的话,每个IDR帧前面都应该加上SPS和PPS,因为有的观众会中途进来观看。
2)、如果是本地稳定文件,可以在开头加上SPS和PPS,或者都加上,这个根据具体需要来。
另外说明下SEI,有的H264文件有SEI,有的则没有,这说明SEI对文件的播放并无太大影响。
SEI(Supplemental Enhancement Information):辅助增强信息。这里面可以存放一些影片简介,版权信息或者作者自己添加的一些信息。
四、实例
4.1 MediaCodec解码本地视频 H.264文件,然后在SurfaceView上预览
4.1.1 从本地H264文件中获取文件流
public void getFileInputStream() {
try {
File file = new File(H264_FILE);
mInputStream = new DataInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
try {
mInputStream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
4.1.2 初始化解码器
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
private void initMediaCodec() {
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
//创建编码器
mCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
} catch (IOException e) {
e.printStackTrace();
}
//初始化编码器
final MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,holder.getSurfaceFrame().width(),holder.getSurfaceFrame().height());
/*h264常见的帧头数据为:
00 00 00 01 67 (SPS)
00 00 00 01 68 (PPS)
00 00 00 01 65 (IDR帧)
00 00 00 01 61 (P帧)*/
//获取H264文件中的pps和sps数据
if (UseSPSandPPS) {
byte[] header_sps = {0, 0, 0, 1, 67, 66, 0, 42, (byte) 149, (byte) 168, 30, 0, (byte) 137, (byte) 249, 102, (byte) 224, 32, 32, 32, 64};
byte[] header_pps = {0, 0, 0, 1, 68, (byte) 206, 60, (byte) 128, 0, 0, 0, 1, 6, (byte) 229, 1, (byte) 151, (byte) 128};
mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(header_sps));
mediaFormat.setByteBuffer("csd-1", ByteBuffer.wrap(header_pps));
}
//设置帧率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,40);
mCodec.configure(mediaFormat,holder.getSurface(),null,0);
startDecodingThread();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
}
4.1.3 开启新的线程进行解码
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
private class DecodeThread implements Runnable{
@Override
public void run() {
//循环解码
decodeLoop();
}
private void decodeLoop(){
//获取一组输入缓存区
ByteBuffer[] inputBuffers = mCodec.getInputBuffers();
//解码后的数据,包含每一个buffer的元数据信息
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
long startMs = System.currentTimeMillis();
long timeoutUs = 10000;
//用于检测文件头
byte[] maker0 = new byte[]{0,0,0,1};
byte[] dummyFrame = new byte[]{0x00,0x00,0x01,0x20};
byte[] streamBuffer = null;
try {
//返回可用的字节数组
streamBuffer = getBytes(mInputStream);
} catch (IOException e) {
e.printStackTrace();
}
int bytes_cnt = 0;
while (mStopFlag == false){
//得到可用字节数组长度
bytes_cnt = streamBuffer.length;
if (bytes_cnt == 0){
streamBuffer = dummyFrame;
}
int startIndex = 0;
//定义记录剩余字节的变量
int remaining = bytes_cnt;
while (true) {
//当剩余的字节=0或者开始的读取的字节下标大于可用的字节数时 不在继续读取
if (remaining == 0 || startIndex >= remaining) {
break;
}
//寻找帧头部
int nextFrameStart = KMPMatch(maker0,streamBuffer,startIndex + 2,remaining);
//找不到头部返回-1
if (nextFrameStart == -1) {
nextFrameStart = remaining;
}
//得到可用的缓存区
int inputIndex = mCodec.dequeueInputBuffer(timeoutUs);
//有可用缓存区
if (inputIndex >= 0) {
ByteBuffer byteBuffer = inputBuffers[inputIndex];
byteBuffer.clear();
//将可用的字节数组,传入缓冲区
byteBuffer.put(streamBuffer, startIndex, nextFrameStart - startIndex);
//把数据传递给解码器
mCodec.queueInputBuffer(inputIndex, 0, nextFrameStart - startIndex, 0, 0);
//指定下一帧的位置
startIndex = nextFrameStart;
} else {
continue;
}
int outputIndex = mCodec.dequeueOutputBuffer(info,timeoutUs);
if (outputIndex >= 0) {
//帧控制是不在这种情况下工作,因为没有PTS H264是可用的
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
boolean doRender = (info.size != 0);
//对outputbuffer的处理完后,调用这个函数把buffer重新返回给codec类。
mCodec.releaseOutputBuffer(outputIndex, doRender);
} else {
}
}
mStopFlag = true;
}
}
}
源码地址:https://github.com/Xiaoben336/MediaCodecDecodeH264Demo
4.2 收集Camera数据,并转码为H264存储到文件
4.2.1 实现方式
视频采集用Camera 2采集YV12数据编码为H264视频文件并保存。
1、我们要实现的功能位通过Camera采集到每帧YUV原始数据
2、录制视频后,编码YUV原始数据为H.264视频格式
3、保存H.264编码格式的视频文件
在使用Camera的时候,设置预览的数据格式为NV21
在使用Camera2的时候,无法支持预览数据为NV21,我设置的预览数据格式YV12
4.2.2 编码实现
4.2.2.1 初始化MediaCodec
public AvcEncoder(int width, int height, int framerate, File outFile,boolean isCamera) {
this.mIsCamera = isCamera;
mWidth = width;
mHeight = height;
mFrameRate = framerate;
mOutFile = outFile;
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);//YUV420SP
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);//*5表示较高的比特率,也可以是1、3
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
try {
mMediaCodec = MediaCodec.createEncoderByType("video/avc");
} catch (IOException e) {
e.printStackTrace();
}
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
createfile();//创建H264文件的保存位置
}
4.2.2.2 开始编码
public boolean isRunning = false;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void startEncoderThread(){
Thread encoderThread = new Thread(new Runnable() {
@Override
public void run() {
isRunning = true;
byte[] input = null;
long pts = 0;
long generateIndex = 0;
while (isRunning) {
if (mYuvQueue.size() > 0) {
input = mYuvQueue.poll();
if (mIsCamera) {//Camera NV21
//NV12数据所需空间为如下,所以建立如下缓冲区
//y=W*h;u=W*H/4;v=W*H/4,so total add is W*H*3/2 (1 + 1/4 + 1/4 = 3/2)
byte[] yuv420sp = new byte[mWidth * mHeight *3 /2];
NV21ToNV12(input, yuv420sp, mWidth, mHeight);
input = yuv420sp;
} else {//Camera 2
byte[] yuv420sp = new byte[mWidth * mHeight *3 /2];
YV12toNV12(input, yuv420sp, mWidth, mHeight);
input = yuv420sp;
}
}
if (input != null) {
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
pts = computePresentationTime(generateIndex);
ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(input);
mMediaCodec.queueInputBuffer(inputBufferIndex,0,input.length,pts,0);
generateIndex += 1;
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
byte[] outData = new byte[bufferInfo.size];
outputBuffer.get(outData);
if (bufferInfo.flags == 2) {
mConfigByte = new byte[bufferInfo.size];
mConfigByte = outData;
} else if (bufferInfo.flags == 1) {
byte[] keyframe = new byte[bufferInfo.size + mConfigByte.length];
System.arraycopy(mConfigByte, 0, keyframe, 0, mConfigByte.length);
System.arraycopy(outData, 0, keyframe, mConfigByte.length, outData.length);
try {
outputStream.write(keyframe, 0, keyframe.length);
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
outputStream.write(outData, 0, outData.length);
} catch (IOException e) {
e.printStackTrace();
}
}
mMediaCodec.releaseOutputBuffer(outputBufferIndex,false);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
}
} else {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
encoderThread.start();
}
需要把Camera的NV21的预览数据和Camera2的YV12预览数据都转换成了NV12格式的数据。
H.264编码必须要用NV12,所以我们拿到预览数据要做格式转换 .
4.2.3 获取预览数据
开启摄像头后,获取预览数据,参考之前的实现了Camera有回掉很简单(前面有写Camera 2的使用),Camera2通过ImageRender来获取。
private static final int STATE_PREVIEW = 0;
private static final int STATE_RECORD = 1;
private int mState = STATE_PREVIEW;
private AvcEncoder mAvcEncoder;
private int mFrameRate = 30;
private void setupImageReader() {
//2代表ImageReader中最多可以获取两帧图像流
mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(),mPreviewSize.getHeight(), ImageFormat.YV12,1);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Log.e(TAG, "onImageAvailable: "+Thread.currentThread().getName() );
//这里一定要调用reader.acquireNextImage()和img.close方法否则不会一直回掉了
Image img = reader.acquireNextImage();
switch (mState){
case STATE_PREVIEW:
Log.e(TAG, "mState: STATE_PREVIEW");
if (mAvcEncoder != null) {
mAvcEncoder.stopThread();
mAvcEncoder = null;
Toast.makeText(mContext,"停止录制视频成功",Toast.LENGTH_SHORT).show();
}
break;
case STATE_RECORD:
Log.e(TAG, "mState: STATE_RECORD");
Image.Plane[] planes = img.getPlanes();
byte[] dataYUV = null;
if (planes.length >= 3) {
ByteBuffer bufferY = planes[0].getBuffer();
ByteBuffer bufferU = planes[1].getBuffer();
ByteBuffer bufferV = planes[2].getBuffer();
int lengthY = bufferY.remaining();
int lengthU = bufferU.remaining();
int lengthV = bufferV.remaining();
dataYUV = new byte[lengthY + lengthU + lengthV];
bufferY.get(dataYUV, 0, lengthY);
bufferU.get(dataYUV, lengthY, lengthU);
bufferV.get(dataYUV, lengthY + lengthU, lengthV);
}
if (mAvcEncoder == null) {
mAvcEncoder = new AvcEncoder(mPreviewSize.getWidth(),
mPreviewSize.getHeight(), mFrameRate,
getOutputMediaFile(MEDIA_TYPE_VIDEO), false);
mAvcEncoder.startEncoderThread();
Toast.makeText(mContext, "开始录制视频成功", Toast.LENGTH_SHORT).show();
}
mAvcEncoder.putYUVData(dataYUV);
break;
default:
break;
}
img.close();
}
},mBackgroundHandler);
}