一 H264概述
H264,是由ITU-T视频编码专家组(VCEG)和ISO/IEC动态图像专家组(MPEG)联合组成的联合视频组(JVT,Joint Video Team)提出的高度压缩数字视频编解码器标准。也可以被称为H.264/AVC(或者AVC/H.264或者H.264/MPEG-4 AVC或MPEG-4/H.264 AVC)就是一个东西罢了,
二 H264起源
在 1988年 ITU-T发明了H261,ISO/IEC-MPEG 在 1991 年发明了 MPEG-1,都是用来做视频压缩的标准算法,但是这两个并不兼容,是不一样的东西,随着音视频的发展,在 2003年 这两个机构达成共识,组合成一个算法,H264/MPEG-4AVC,俗称H264。
三 为什么要对视频文件进行压缩,即使用 H264
比如一个1秒的视频,有30帧的话,视频分辨率是 1920 x 1080 , 则 每秒的数据是 就是 8个Y 对应2个U 两个 Y
1920x1080x30x1.5(YUV420格式是1.5个字节)/1024/1024 = 118MB (因为视频的是yuv不是arb)
也就是如果我们看网络视频的话需要每秒下载118M 才能去看视频。在当今短视频的风靡下,绝对是不可能的。
四 视频中那些地方可以压缩
- 空间冗余:比如一帧图片全是白色的话,我就存一个像素点的颜色就可以了
- 时间冗余:比如一个慢动作的视频,两个帧之间基本上一样,name只存一帧就可以了
- 视觉冗余:人类视觉对于图像的任何变化,并不是都能感知,
- 知识冗余:规律性的结构可由先验知识和背景知识得
四 H264的压缩原理
- 帧内压缩:根据帧内的像素趋于统一 而采用帧内预测编码技术
- 帧间压缩:使用以宏块为基础的运动补偿预测编码技术,从当前宏块从参考帧中产兆最佳匹配宏块
4.1 宏块
比如一个由左到右的一个黑白渐变的图片 我们就不用把所有的像素给存起来,我们可以把所有的像素,分成1616的方块,在这个1616的方块中我们只需记住top 和 left的像素值,宽高,起始和终止位置,这样我们就可以推算出所有的像素了。这样大大减少了内存,也就是一帧图片如果16*16的比较多,那么压缩出来的体积就越小,
4.1 关键帧
- I帧:I帧可以看成是一个图像经过压缩后的产物。自身可以通过视频解压算法解压成一张单独的完整的图片。大概压缩了 1/6
- P帧:与I帧作参考,这里面只有运动矢量,和差异的内容,压缩了 1/20,P帧也可以作为参考,前提是这个P帧前面有I帧,假如直播刚进来,BPBBI,此时这个P帧就算当做了参考也没用,必须有I帧才行
- B帧: B帧是由前面的I帧或P帧和后面的P帧来进行预测的。只有矢量,1/50
4.2 GOP
就是两个I帧之间间隔的帧数。 也就是说一个视频第一帧一定是I帧,如果不是I帧,则会黑屏一会,因为I帧是决定的一帧,比如直播的时候,我们一进来如果是P帧的话,就会黑屏,直到第一个I帧出现, 在短视频中I帧越少,GOP值越大,压缩出来的体积就越小,但是在直播的过程中,我们不能把GOP设置那么的大,因为大了的话会影响第一次进来的黑屏的概率会变大。所以直播的GOP是比较哦小
4.3 SPS 和 PPS
sps 配置信息(宽高帧率分辨率,编码方式等等),pps(只有宽高) 也就是说I帧之前肯定有 sps和pps帧,每帧都是通过 0x000001 分割的
4.4 码流总体结构
分为两层: 视频编码层(VCL)和网络提取层/网络抽象层(NAL)(磁盘也可以认为他,没有网络也有他)
H.264 的编码视频序列包括一系列的NAL 单元,每个NAL 单元包含一个RBSP。一个原始的H.264 由N个NALU单元组成、 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组 成,其中 Start Code 用于标示这是一个NALU 单元的开始,必须是"00 00 00 01" 或"00 00 01"
4.5 网络传输 NAL
分隔符 0x000001 就在 NAL中。如果NALU对应的Slice为一帧的开始,则用4字节表示,即0x00000001;否则用3字节表示, 0x000001。 NAL Header:forbidden_bit,nal_reference_bit(优先级),nal_unit_type(类型)。 脱壳操作:为了使NALU主体不包括起始码,在编码时每遇到两个字节(连续)的0,就插入一字节 0x03,以和起始码相区别。解码时,则将相应的0x03删除掉。
五 andriod底层视频编解码器 DSP芯片
比如先来了一个I帧,直接会走到传输编码器,当来了I帧,会缓存在传输缓冲器,等来了个P帧之后,P帧出来之后I帧在出来,所以 H264 码流的顺序和视频播放顺序一定是不一样的
六 什么视频编码采用YUV而不是rgb
- Rgb原理: 定义RGB 是从颜色发光的原理来设计定的,由红、绿、蓝三盏灯,当它们的光相互叠合的时候,色彩相混,而亮度却等于两者亮度之总和(两盏灯的亮度嘛!),越混合亮度越高,即加法混合。RGB24 是指 R , G , B 三个分量各占 8 位 也就是 3个字节
1280 * 720 * 3(一个像素3个字节) / 1024 / 1024 = 2.63MB
- YUV的原理是把亮度和色度分离,人眼对亮度的敏感度超过色度。YUV三个字母中,其中”Y”表示明亮度(Lumina nce或Luma),也就是灰阶值;而”U”和”V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。
,YUV 主要用于优化彩色视频信号的传输,与 RGB 视频信号传输相比,它最大的优点在于只需占用极少的频宽( RGB 要求三个独立的视频信号同时传输)其中 “Y” 表示明亮度也就是灰阶值;而 “U” 和 “V” 表示的则是色度
1280 * 720 * 2(一个像素2个字节) / 1024 / 1024 = 1.76MB 使用 YUV4:2:2 节省了 1/3
五 在android中使用MediaCodec 播放 h254格式的数据
MediaCodec 是android 提供的对于音视频提供的解码的 访问底层的解码器,底层的解码器就是 DSP芯片,所有app公用一个DSP芯片。我们可以利用ffmpeg 抽取视频中的 h264。用下面命令,我们使用
ffmpeg -i demo.mp4 -vcodec hevc out.h265
// 解码器,指定H264格式
MediaCodec mediaCodec = MediaCodec.createDecoderByType("video/avc");
// 解码的格式是 264 宽和高
MediaFormat mediaFormat = MediaFormat.createAudioFormat("video/avc", 100, 100);
// 设置颜色格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 设置帧率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 0);
// 如果直接显示在surfaceview上 就传入 surface ,如果要用到解码后的数据 就写null
mediaCodec.configure(mediaFormat, surface, null, 0);
// 我们需要在一个子线程中去解码每一帧
@Override
public void run() {
byte[] bytes = h64文件的byte数组;
// 就是从底层解码器拿到的输入buffer
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
// 开始索引,因为前面的肯定有文件格式所以随便加一个值
int startFrameIndex = 0;
// 总大小
int total = bytes.length;
while(true){
if(total == 0 || startFrameIndex>=total){
// 如果 下一个位置大于 totalSize 就体质
break;
}
// 寻找写一帧的点,因为我们知道每一帧是通过 0x0000001 分割的
int nextFrameStart = 0;
for((int i = startFrameIndex+2;i<total;i++){
if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x00 && bytes[i + 3] == 0x01)||(bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x01)) {
nextFrameStart = i;
break;
}
}
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int index = mediaCodec.dequeueInputBuffer(10000);
if(index>=0){
ByteBuffer byteBuffer = inputBuffers[index];
byteBuffer.clear();
byteBuffer.put(bytes, startFrameIndex, nextFrameStart - startFrameIndex);
mediaCodec.queueInputBuffer(startFrameIndex, 0, nextFrameStart - startFrameIndex, 0, 0);
startFrameIndex = nextFrameStart;
}else{
// 没有可用,证明别的app占用着呢 继续轮训
continue;
}
int outIndex= mediaCodec.dequeueOutputBuffer(info, 10000);
if (outIndex >= 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mediaCodec.releaseOutputBuffer(outIndex, true);
}
}
}
把录屏编码成h264
public class MainActivity2 extends AppCompatActivity {
public static final String TAG = "MainActivity2";
private MediaProjectionManager mediaProjectionManager;
private MediaProjection mediaProjection;
private MediaCodec mediaCodec;
private Surface surface;
// 在 30 之后进制录屏
// 比如要对mp4剪辑,直接剪辑文件肯定是不行的,你得剪辑h264 数据
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
Intent captureIntent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(captureIntent, 100);
checkPermission();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && resultCode == Activity.RESULT_OK) {
// 拿到录屏的 mediaProjection
mediaProjection = mediaProjectionManager.getMediaProjection
(resultCode, data);
//
initMediaCodec();
}
}
// H264 一帧数据 就是 一个NALU单元的
private void initMediaCodec() {
try {
// createEncoderByType 是 编码 ,把 一个yuv视频编码成 h264 也会有 分隔符 , 第一帧永远会把 sps pps 当成一帧输出出来
// 在一个视频 会出现多次 sps 0000000167 pps 68EFBCB0 00 00 01 I帧之前肯定是 sps pps
// createDecoderByType 是 解码 ,把 一个 h264 解码成 yuv视频
mediaCodec = MediaCodec.createEncoderByType("video/avc");
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 540, 960);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 每秒15帧
format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
// 每2秒一个I帧
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);
//码率 码率越大 视频越清晰。控制在编码或者解码视频画面的清晰度 ,编码一帧的长度会不一样
format.setInteger(MediaFormat.KEY_BIT_RATE,1200_000);
// 创建一个虚拟的Surface
surface = mediaCodec.createInputSurface();
// Surface 是否显示在 Surface上
// 加解密
// 要编码
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
new Thread() {
@Override
public void run() {
super.run();
mediaCodec.start();
// 把录屏的 mediaProjection 和 Surface关联起来,把录制好的每帧数据丢到 Surface中
// 1 = dp = 1像素
// VIRTUAL_DISPLAY_FLAG_PUBLIC 虚拟的Surface
mediaProjection.createVirtualDisplay("screen-codec", 540, 960, 1, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
// https://blog.csdn.net/mcsbary/article/details/95883818
// 我们需要把 输入 264 输出一帧数据,这里录屏已经输入了数据
// 只需要关注输出就行了
while (true) {
// 10ms的超时 返回数据索引 当我们执行这句话的时候 bufferInfo 就会size数据
int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 100000);
if (index > 0) {
// 这里是压缩数据 这里做的事情是把录屏转化成压缩数据 h264 这里是 encode 编码
// 如果 我们拿到h264数据,去解码 MediaCodec.createDecoderByType,那这里就是yuv视频数据
ByteBuffer buffer = mediaCodec.getOutputBuffer(index);
// 这里的 ByteBuffer 是 dsp芯片的 不能操作的,只能读,最后得销毁掉
byte[] outData = new byte[bufferInfo.size];
buffer.get(outData);
// 以字符串方式写入到一个文件 这样有利于我们去查看
writeContent(outData);
// 以16进制写入一个文件,这样可以播放
writeBytes(outData);
} else {
// -1 就是失败
Log.e(TAG,"-1");
}
}
}
}.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, 1);
}
return false;
}
public void writeBytes(byte[] array) {
FileOutputStream writer = null;
try {
// 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
writer = new FileOutputStream(Environment.getExternalStorageDirectory() + "/codec.h264", true);
writer.write(array);
writer.write('\n');
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String writeContent(byte[] array) {
char[] HEX_CHAR_TABLE = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
StringBuilder sb = new StringBuilder();
for (byte b : array) {
sb.append(HEX_CHAR_TABLE[(b & 0xf0) >> 4]);
sb.append(HEX_CHAR_TABLE[b & 0x0f]);
}
Log.i(TAG, "writeContent: " + sb.toString());
FileWriter writer = null;
try {
// 打开一个写文件器,构造函数中的第二个参数true表示以追加形式写文件
writer = new FileWriter(Environment.getExternalStorageDirectory() + "/codec.txt", true);
writer.write(sb.toString());
writer.write("\n");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}
}
vcl 视频编码层。
H265
H264 的缺点是对单个宏块压缩。帧间预测,从03年到现在一直没变,没想到现在分辨率发展这么快,比如8k,使用h264 就会很大。
H264 16*16的,上面记录16,左边记录15 = 31,16x16-31 =
h264 宏块 1616 比较多 h265 6464 或者是 128x128
树的递归划分,最终到4x4 就不再划分了,他很智能,在一个颜色变化比较大的,会递归划分,如果有变化的的就划分,找到左,上的像素点, 如果没啥差距的直接可以 是 64x64 预测方向也会比较多
占用CPU会很大,
I帧 是 H265 比 h264 大的,树形编码会增加复杂度 ,额外信息比较多 B 帧 P帧 会比 h264 小十倍左右
h265 第一帧多了个 vps包括裸眼3D,虽然对普通视频没啥用,但是也会解析,找到偏移量,找到 sps和 pps
sps pps
比如分隔符之后如果是26 就是I帧 分隔符之后是 02 是 P帧, 01 是B帧, h265 00 是B帧 h265 会收费的 ,4k 8k电视
CTU 跟 宏块的定义是一样的,树的CTU
算法
h264 无论 像素还是yuv 还有配置,都在255 之内。所以用了 哥伦布,对短数据非常友好,变长编码,哥伦布编码,
分析 码流 的 软件
mac Elecard hevc analyzer,window vedio eye
H264 和 H265 区别
一. 版本:
H.265是新的编码协议,也即是H.264的升级版,H265 对CPU的要求比较高,比较适合4K或者 8K的电影
二 降码率 和 存储空间
H.264中每个宏块(macroblock/MB)大小都是固定的16x16像素,而H.265的编码单位可以选择从最小的8x8到最大的64x64; 所以H264 比较占内存,
三 采用了块的四叉树划分结构
H.265相比H.264最主要的改变是采用了块的四叉树划分结构,采用了从64x64~8x8像素的自适应块划分,并基于这种块划分结构采用一系列自适应的预测和变换等编码技术;比如一个不复杂的64*64 直接就一个大块,复杂点的先分成 4 个 8x8的,然后再看谁复杂 就在分谁,最终到4x4
帧
I帧会265会比264大,树形编码会增加复杂度 ,额外信息比较多,B帧 P帧 比264 小很多。
帧内预测
h264 的预测方向 有 9中, h265 会有35种方向,更精确