H.264码流结构是怎么样的

        视频有很多种编码标准,H.264,H.265,AV1等等,其中我们可能最常见的便是H.264,因此,本文我们就主要来分下下H.264编码的码流结果具体是怎么样的。

一、前置知识

        了解H264码流结构之前,需要先了解一些前置的知识。

1、I/P/B帧和GOP

        需要先了解图像序列的层次结构,也就是一帧帧视频帧的一些概念。这些可以参考我前面的一篇文章 视频相关的一些基本概念 ,会比较系统和具体地讲解。

2、Slice

        上面主要是图像序列的层次结构,而图像的内部层次结构是怎么样的,这主要涉及Slice这个概念。

        Slice其实就是“片”的概念,图像内的层次结构就是一帧图像可以划分成一个或多个 Slice,而一个 Slice 包含多个宏块,且一个宏块又可以划分成多个不同尺寸的子块。大概就是类似下面这样的结构图:

二、H264码流结构

1、码流格式

        H264 码流有两种格式:一种是 Annexb 格式;一种是 MP4 格式。

(1)、Annexb 格式使用起始码来表示一个编码数据的开始。起始码本身不是图像编码的内容,只是用来分隔用的。起始码有两种,一种是 4 字节的“00 00 00 01”,一种是 3 字节的“00 00 01”。

        由于图像编码出来的数据中也有可能出现“00 00 00 01”和“00 00 01”的数据。为了防止出现这种情况,H264 会将图像编码数据中的下面的几种字节串做如下处理:

        “00 00 00”修改为“00 00 03 00”;

        “00 00 01”修改为“00 00 03 01”;

        “00 00 02”修改为“00 00 03 02”;

        “00 00 03”修改为“00 00 03 03”。

        其实也就是转义,同样地在解码端,我们在去掉起始码之后,也需要将对应的字节串转换回来。

 (2)、MP4 格式没有起始码,而是在图像编码数据的开始使用了 4 个字节作为长度标识,用来表示编码数据的长度,这样我们每次读取 4 个字节,计算出编码数据长度,然后取出编码数据,再继续读取 4 个字节得到长度,一直继续下去就可以取出所有的编码数据了。

         这两种格式差别不大,接下来我们主要使用 Annexb 格式来讲解 H264 码流结构。

2、SPS和PPS

        视频编码的时候还有一些编码参数数据的,为了能够将一些通用的编码参数提取出来,不在图像编码数据中重复,H264 设计了两个重要的参数集:一个是 SPS(序列参数集);一个是 PPS(图像参数集)。

        SPS 主要包含的是图像的宽、高、YUV 格式和位深等基本信息;PPS 则主要包含熵编码类型、基础 QP 和最大参考帧数量等基本编码信息。

        如果没有 SPS、PPS 里面的基础信息,之后的 I 帧、P 帧、B 帧就都没办法进行解码。因此 SPS 和 PPS 是至关重要的。

        这样的话,H264码流主要包含了 SPS、PPS、I 帧、P 帧和 B 帧。由于帧又可以划分成一个或多个 Slice。因此,帧在码流中实际上是以 Slice 的形式呈现的。所以,H264 的码流主要是由 SPS、PPS、I Slice、P Slice和B Slice 组成的。如下图所示:

 3、NALU

        上面说到H264码流的组成部分,但是每个部分是如何区分开?为了解决这个问题,H264 设计了 NALU(网络抽象层单元)。SPS 是一个 NALU、PPS 是一个 NALU、每一个 Slice 也是一个 NALU。每一个 NALU 又都是由一个 1 字节的 NALU Header 和若干字节的 NALU Data 组成的。而对于每一个 Slice NALU,其 NALU Data 又是由 Slice Header 和 Slice Data 组成,并且 Slice Data 又是由一个个 MB Data 组成。其结构如下:

        其中,NALU Header总共占用 1 个字节,具体如下图所示。

        其中,

        --> F:forbidden_zero_bit,占 1bit,禁止位,H264 码流必须为 0;

        --> NRI: nal_ref_idc,占 2bits,可以取 00~11,表示当前 NALU 的重要性。参考帧、SPS 和 PPS 对应的 NALU 必须要大于 0;

        --> Type: nal_unit_type,占 5bits,表示 NALU 类型。其取值如下表所示。

         有了这个,我们解析出 NALU Header 的 Type 字段,查询表格就可以得到哪个 NALU 是 SPS,哪个是 PPS,以及哪个是 IDR 帧了。

        不过NALU 类型只区分了 IDR Slice 和非 IDR Slice,至于非 IDR Slice 是普通 I Slice、P Slice 还是 B Slice,则需要继续解析 Slice Header 中的 Slice Type 字段得到。

        下面我们通过两个例子来看看常见的 NALU 里的 NALU Header 是怎样的:

         下面我们再来看一个实际码流的例子,看看在实际编码出来的二进制数据中,各种 NALU 是怎么“放置”在数据中的。下图是用二进制查看工具打开实际编码后的码流数据。我们可以看到在码流的开始部分是一个起始码,之后紧接着是一个 SPS 的 NALU。在 SPS 后面是一个 PPS 的 NALU。然后就是一个 IDR Slice 的 NALU 和一个非 IDR Slice NALU。

 三、如何判断哪几个 Slice 是同一帧的

         根据上面的分析,在H264 码流中,帧是以 Slice 的方式呈现的,或者可以说在 H264 码流里是没有“帧“这种数据的,只有 Slice。

        那么有个问题,一帧有几个 Slice 是不知道的。也就是说码流中没有字段表示一帧包含几个 Slice。既然没有办法知道一帧有几个 Slice,那我们如何知道多 Slice 编码时一帧的开始和结束分别对应哪个 Slice 呢?

        其实,Slice NALU 由 NALU Header 和 NALU Data 组成,其中 NALU Data 里面就是 Slice 数据,而 Slice 数据又是由 Slice Header 和 Slice Data 组成。在 Slice Header 开始的地方有一个 first_mb_in_slice 的字段,表示当前 Slice 的第一个宏块 MB 在当前编码图像中的序号。我们只要解析出这个宏块的序号出来。

        --> 如果 first_mb_in_slice 的值等于 0,就代表了当前 Slice 的第一个宏块是一帧的第一个宏块,也就是说当前 Slice 就是一帧的第一个 Slice。

        --> 如果 first_mb_in_slice 的值不等于 0,就代表了当前 Slice 不是一帧的第一个 Slice。并且,使用同样的方式一直往下找,直到找到下一个 first_mb_in_slice 为 0 的 Slice,就代表新的一帧的开始,那么其前一个 Slice 就是前一帧的最后一个 Slice 了。

         其中,first_mb_in_slice 是以无符号指数哥伦布编码的,需要使用对应的解码方式才能解码出来。但是有一个小技巧,如果只是需要判断 first_mb_in_slice 是不是等于 0,不需要计算出实际值的话,只需要通过下面的方式计算就可以了。

         以上便是对H264码流结构的讲解。

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
由于FFmpeg是一个C语言库,因此在Java中使用FFmpeg时需要使用Java Native Interface(JNI)来进行交互。以下是一个简单的Java JNI代码示例,用于接收H.264/H.265的码流并将其解码为YUV格式。 首先,需要在Java中定义本地方法,以便调用FFmpeg库中的函数。这可以通过使用“native”关键字来完成。以下是一个示例方法,用于初始化FFmpeg并打开输入文件: ``` public native void init(String input_file); ``` 接下来,需要在C/C++代码中实现这个方法。以下是一个简单的示例,使用FFmpeg API初始化并打开输入文件: ``` JNIEXPORT void JNICALL Java_MyClass_init(JNIEnv *env, jobject obj, jstring input_file) { const char *in_filename = (*env)->GetStringUTFChars(env, input_file, NULL); // Initialize FFmpeg av_register_all(); // Open input file AVFormatContext *format_ctx = NULL; if (avformat_open_input(&format_ctx, in_filename, NULL, NULL) != 0) { printf("Error: Could not open input file\n"); return; } // Find stream information if (avformat_find_stream_info(format_ctx, NULL) < 0) { printf("Error: Could not find stream information\n"); avformat_close_input(&format_ctx); return; } // Close input file avformat_close_input(&format_ctx); (*env)->ReleaseStringUTFChars(env, input_file, in_filename); } ``` 这个方法首先获取Java字符串对象的UTF-8编码,并将其转换为C字符串。然后,它初始化FFmpeg库并打开输入文件。如果打开文件失败,则会输出错误消息并返回。否则,它将查找流信息并关闭输入文件。 接下来,需要定义另一个本地方法,用于读取视频帧。以下是一个示例方法,用于读取下一帧并将其解码为YUV格式: ``` public native byte[] readFrame(); ``` 为了实现这个方法,需要使用FFmpeg的AVPacket和AVFrame结构。以下是一个简单的示例,用于读取下一帧并将其解码为YUV格式: ``` JNIEXPORT jbyteArray JNICALL Java_MyClass_readFrame(JNIEnv *env, jobject obj) { // Read next packet AVPacket packet; av_init_packet(&packet); if (av_read_frame(format_ctx, &packet) < 0) { return NULL; } // Decode packet AVFrame *frame = av_frame_alloc(); int got_frame = 0; if (avcodec_decode_video2(codec_ctx, frame, &got_frame, &packet) < 0) { av_packet_unref(&packet); av_frame_free(&frame); return NULL; } // Convert frame to YUV format AVFrame *yuv_frame = av_frame_alloc(); uint8_t *buffer = (uint8_t *) av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height)); avpicture_fill((AVPicture *) yuv_frame, buffer, AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height); struct SwsContext *sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL); sws_scale(sws_ctx, (const uint8_t *const *) frame->data, frame->linesize, 0, codec_ctx->height, yuv_frame->data, yuv_frame->linesize); sws_freeContext(sws_ctx); av_frame_free(&frame); // Return YUV frame data as byte array jbyteArray result = (*env)->NewByteArray(env, yuv_frame->linesize[0] * codec_ctx->height * 3 / 2); (*env)->SetByteArrayRegion(env, result, 0, yuv_frame->linesize[0] * codec_ctx->height * 3 / 2, (jbyte *) yuv_frame->data[0]); av_frame_free(&yuv_frame); return result; } ``` 这个方法首先读取下一个AVPacket并将其解码为AVFrame。然后,它将AVFrame转换为YUV格式,使用SWScale库进行高质量的色彩空间转换。最后,它将YUV帧数据作为Java字节数组返回。 这只是一个简单的示例,用于演示如何在Java中使用FFmpeg API接收H.264/H.265的码流。实际应用中,需要更复杂的逻辑来处理不同的编码格式、分辨率和帧率。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值