FFmpeg 编码和解码

news2025/1/3 16:11:38

文章目录

  • 音频格式
    • AAC
      • ADIF音频数据交换格式
      • ADTS音频数据传输流
    • 音频解码
    • 音频编码
  • 视频格式
    • H264
      • GOP图像组
      • I帧,P帧,B帧
      • H264压缩技术
      • H264压缩级别
      • H264视频级别
      • H264码流结构
      • SPS
      • PPS
    • 解码视频
    • 编码视频

音频格式

AAC

AAC全称 Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。出现于1997年,基于MPEG-2的音频编码技术。由Fraunhofer IIS、杜比实验室、AT&T、索尼等公司共同开发,目的是取代MP3格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的性价比。利用AAC格式,可使人感觉声音质量没有明显降低的前提下,更加小巧。

优点:相较于mp3,AAC格式的音质更佳,文件更小。
不足:AAC属于有损压缩的格式,与时下流行的APE、FLAC等无损格式相比,音质存在本质上的差距。加之传输速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC头上小巧的光环不复存在。

AAC共有9种规格,以适应不同场合的需要

  • MPEG-2 AAC LC 低复杂度规格(low complexity),比较简单,没有增益控制,但提高了编码效率,在中等码率的编码效率及音质方面,都能找到平衡点
  • MPEG-2 AAC Main 主规格
  • MPEG-2 AAC SSR 可变采样率规格(scaleable sample rate)
  • MPEG-4 AAC LC 低复杂度规格,现在的手机比较常见的MP4文件中的音频部分就包括了该规格音频文件
  • MPEG-4 AAC Main 主规格。包含了除增益控制之外的全部功能,其音质最好。
  • MPEG-4 AAC SSR 可变采样率规格
  • MPEG-4 AAC LTP 长时期预测规格(Long Term Predicition)
  • MPEG-4 AAC LD 低延迟规格 (Low Delay)
  • MPEG-4 AAC HE 高效率规格(High Efficiency)这种规格适合用于低码率编码,有Nero AAC编码器支持

在这里插入图片描述
HE:High Efficiency(高效性)。 HE-AAC v1(又称AACPlusV1,SBR),用容器的方法实现了AAC(LC)+SBR技术。SBR其实代表的是Spectral Band Replication(频段复制)。简要叙述一下,音乐的主要频谱集中在低频段,高频段幅度很小,但很重要,决定了音质。如果对整个频段编码,若是为了保护高频就会造成低频段编码过细以致文件巨大,若是保存了低频的主要成分而失去高频成分就会丧失音质。SBR把频谱切割开来,低频单独编码保存主要成分,高频单独放大编码保存音质,“统筹兼顾”了,在减少文件大小的情况下还保存了音质,完美地化解了这一矛盾。
HEv2:用容器的方法包含了HE-AAC v1和PS技术。PS指parametric stereo(参数立体声)。原来的立体声文件大小是一个声道的两倍。但是两个声道的声音存在某种相似性,根据香农信息熵编码定理,相关性应该被去掉才能减小文件大小。所以PS技术存储了一个声道的全部信息,然后,花很少的字节用参数描述另一个声道和它不同的地方

目前使用最多的是LC和HE(适合低码率)。流行的Nero AAC编码程序只支持LC,HE、HEv2这三种规格,编码后的AAC音频,规格显示都是LC,HE其实就是AAC(LC)+SBR技术,HEv2就是AAC(LC)+SBR+PS技术。

AAC音频格式分为ADIF和ADTS

  • ADIF:audio data interchange format,音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。
  • ADTS: audio data transport stream,音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始,它的特征类似于mp3数据流格式。
    简单的说,ADTS可以在任意帧解码,也就是说,它每一帧都有头信息。ADIF只有一个统一的头,所以必须得到所有的数据后解码。且这两种的header格式也是不同的,目前一般编码后的和抽取出的都是ADTS格式的音频流,两者具体的组织结构如下所示:
    在这里插入图片描述
    空白处表示前后帧

有时候当你编码AAC裸流的时候,会遇到写出来的AAC并不能在PC和手机上播放,很大可能就是AAC文件的每一帧里缺少了ADTS头信息文件的包装拼接。秩序奥加入头文件ADTS即可。

ADIF音频数据交换格式

其中adif_header如下表:
在这里插入图片描述
在这里插入图片描述
raw_data_stream如图所示
在这里插入图片描述
byte_alignment为了保持字节对齐用,
可以看到ADIF只有一个header,里面没有关于每帧的信息,因为每帧不是固定大小,故只能按照顺序进行解码,也无法跳播或快进快退。除非自己解码遍历整个文件,建立每帧位置表。

ADTS音频数据传输流

一个AAC原始数据块长度是可变的,对原始帧加上ADTS头进行ADTS的封装,就形成了ADTS帧,AAC音频文件的每一帧由ADTS Header和AAC Audio Data组成。结构体如下:
在这里插入图片描述
ADTS头部信息占据了整个文件中的前7或9个字节,分为2部分:

  • adts_fixed_header():固定头信息,头信息中的每一帧都相同,其中包括了一个固定的同步标记(syncword),该标记用于确定音频帧的边界位置。
  • adts_variable_header():可变头信息,头信息则在帧与帧之间可变
    一个完整的头信息就是固定头+可变头。
    以下是对头信息中各字段的详细介绍:
  1. 固定头
    在这里插入图片描述
  • syncword:同步字,12bit,同步字是ADTS文件的标志符,它用于确定音频帧的开始位置和结束位置,通常为0xFFF
  • ID:9bit,ID指示使用的MPEG版本。0表示MPEG-4,1表示MPEG-2
  • layer:2bit,Layer定义了音频流所属的层级,对于AAC来说,其值为0。
  • protection_absent:1bit,指示是否启动CRC错误校验。0表明数据经过CRC校验,否则未经过
  • profile:2bit,指示编码所使用的AAC规范类型,
    在MPEG-2 中定义了3种
    在这里插入图片描述
    MPEG-4中,profile的值=MPEG-4 Audio Object Type - 1
    在这里插入图片描述
  • sampling_frequency_index:4bit,表示采样率的索引,它告诉解码器当前音频数据的采样率。值的范围是0-15,每一个值表示一个特定的采样率。
    在这里插入图片描述
  • private_bit:1bit,为私有比特,通常被设置为0,没有实际作用。
  • channel_configuration:3bit,指示音频的通道数,范围也是0-15
    在这里插入图片描述
  • original_copy:1bit,指示编码数据是否被原始产生,通常为0
  • home:1bit,通常被设置为0,没有实际作用
  1. 可变头
    在这里插入图片描述
  • copyrighted_id_bit:编码时设置为0,解码时忽略
  • copyrighted_id_start:编码时设置为0,解码时忽略
  • aac_frame_length:ADTS帧长度,它包括ADTS长度和AAC声音数据长度。即aac_frame_length = ( protection_absent == 0 ? 9 : 7 ) + audio_data_length
  • adts_buffer_fullness: 固定位0x7FF。表示是码率可变的码流
  • number_of_raw_data_blocks_in_frame:ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。(一个AAC原始帧包含一段时间内1024个采样及相关数据)

AAC ES是AAC音频编码的一种基本数据格式,也是AAC音频数据在流式传输和文件存储中的常见格式之一。
不同于其他容器格式,它不包含额外的元数据或结构信息,仅包含未经任何封装或压缩处理的原始音频数据。这些原始数据可以作为音频文件或流传输的基础,同时也可以用于对AAC音频进行转码、编辑或者重组。
通常由一系列连续的AAC音频帧组成,每个帧以一个特定的标志符开始,该标志符表示这是一个AAC音频帧。在AAC ES中,每个音频帧拥有相同的长度,但并一定包含相同数量的采样点,因为采样率和声道数量可能会发生变化。
另一个关键特征是其比特流顺序,即数字音频数据的组织方式,AAC ES采用大端字节顺序,其中高位字节排在前面,地位字节排在后面。此外,在AAC ES中,音频数据按照从左到右、自上而下的顺序排列,与典型的文本文件不同。
总之,AAC ES是AAC音频编码的一种基本数据格式,它通常由一系列AAC音频帧组成,并且不包含任何附加的元数据或结构信息。AAC ES可以作为音频文件或流传输的基础,同时也可以用于对AAC音频进行转码、编辑或重组。由于其简单性和灵活性,AAC ES受到了广泛的应用,并且成为了数字音频编码领域的标准之一。

音频解码

void Widget::decode()
{
    char errbuf[1024];                             // 错误信息缓冲区
    const char *inputFile = "../source/audio.aac"; // 输入文件
    const char *outputFile = "../output/out.pcm";  // 输出文件
    AVFormatContext *pInFormatCtx = NULL;          // 打开文件上下文

    // 打开输入文件
    if (avformat_open_input(&pInFormatCtx, inputFile, NULL, NULL) != 0)
    {
        av_strerror(1, errbuf, sizeof(errbuf));
        printf("无法打开输入文件:%s\n", errbuf);
        return;
    }
    // 获取输入文件信息
    if (avformat_find_stream_info(pInFormatCtx, NULL) < 0)
    {
        av_strerror(1, errbuf, sizeof(errbuf));
        printf("无法获取输入文件信息:%s\n", errbuf);
        return;
    }
    // 查找音频流
    int audioStreamIndex = av_find_best_stream(pInFormatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (audioStreamIndex < 0)
    {
        printf("无法找到音频流\n");
        return;
    }
    // 查找解码器
    const AVCodec *pCodec = avcodec_find_decoder(pInFormatCtx->streams[audioStreamIndex]->codecpar->codec_id);
    if (!pCodec)
    {
        printf("无法找到解码器\n");
        return;
    }
    // 创建解码器上下文
    AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);
    if (!pCodecCtx)
    {
        printf("无法创建解码器上下文\n");
        return;
    }
    // 复制解码器参数
    if (avcodec_parameters_to_context(pCodecCtx, pInFormatCtx->streams[audioStreamIndex]->codecpar) < 0)
    {
        printf("无法复制解码器参数\n");
        return;
    }
    // 打开解码器
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
    {
        printf("无法打开解码器\n");
        return;
    }
    // 创建输出文件
    FILE *pOutFile = fopen(outputFile, "wb");
    if (!pOutFile)
    {
        printf("无法创建输出文件\n");
        return;
    }
    // 解码数据
    AVPacket packet;                    // 数据包
    AVFrame *pFrame = av_frame_alloc(); // 解码后的数据帧
    while (av_read_frame(pInFormatCtx, &packet) >= 0)
    {
        // 解码数据包
        int ret = avcodec_send_packet(pCodecCtx, &packet);
        if (ret < 0)
        {
            return; // 解码失败
        }
        while (ret >= 0)
        {
            ret = avcodec_receive_frame(pCodecCtx, pFrame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            {
                break;
            }
            else if (ret < 0)
            {
                av_strerror(ret, errbuf, sizeof(errbuf));
                printf("解码数据帧失败:%s\n", errbuf);
                break;
            }

            int fmtByteSize = av_get_bytes_per_sample((AVSampleFormat)pFrame->format); // 每个样本的字节数
            for (int i = 0; i < pFrame->nb_samples; i++)                               // 每个样本
            {
                for (int ch = 0; ch < pFrame->ch_layout.nb_channels; ch++) // 每个声道
                {
                    fwrite(pFrame->data[ch] + i * fmtByteSize, 1, fmtByteSize, pOutFile); // 写入文件
                }
            }
            av_frame_unref(pFrame); // 释放数据帧
        }
        av_packet_unref(&packet); // 释放数据包
    }
    // 编码器刷新,将剩余的数据帧编码为数据包
    int ret = avcodec_send_packet(pCodecCtx, NULL);
    while (ret >= 0)
    {
        ret = avcodec_receive_frame(pCodecCtx, pFrame);
        if (ret < 0)
        {
            break;
        }
        int fmtByteSize = av_get_bytes_per_sample((AVSampleFormat)pFrame->format); // 每个样本的字节数
        for (int i = 0; i < pFrame->nb_samples; i++)                               // 每个样本
        {
            for (int ch = 0; ch < pFrame->ch_layout.nb_channels; ch++) // 每个声道
            {
                fwrite(pFrame->data[ch] + i * fmtByteSize, 1, fmtByteSize, pOutFile); // 写入文件
            }
        }
        av_frame_unref(pFrame); // 释放数据帧
    }

    qDebug() << "解码完成";
    // 释放资源
    fclose(pOutFile);
    av_frame_free(&pFrame);
    avcodec_free_context(&pCodecCtx);
    avformat_close_input(&pInFormatCtx);
}

音频编码

写入adts头的函数

void Widget::writeADTSHeader(std::ofstream &file, const AVCodecContext *codecContext, const int packetSize)
{
    uint8_t adtsHeader[7]{0};
    // 写入ADTS头,注意位运算优先级比较低,所以括号不能省略
    adtsHeader[0] = 0xFF;
    adtsHeader[1] = 0xF1; // 0xF1表示MPEG-4 AAAC 校验位为1
    adtsHeader[2] = (codecContext->profile << 6) + (4 << 2) + (codecContext->ch_layout.nb_channels >> 2);
    adtsHeader[3] = ((codecContext->ch_layout.nb_channels & 3) << 6) + ((packetSize + 7) >> 11); //  固定头的后4位+可变头的前4位
    adtsHeader[4] = ((packetSize + 7) >> 3) & 0xFF;
    adtsHeader[5] = (((packetSize + 7) & 0x07) << 5) + 0x1F;
    adtsHeader[6] = 0xFC;
    file.write((char *)adtsHeader, 7);
}

编码函数

void Widget::encode()
{
    char errbuf[1024];                                // 错误信息缓冲区
    const char *inputFile = "../../source/audio.pcm"; // 输入文件
    const char *outputFile = "../../output/out1.aac"; // 输出文件
    // 查找AAC编码器
    const AVCodec *pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
    if (!pCodec)
    {
        printf("无法找到编码器\n");
        return;
    }
    // 创建编码器上下文
    AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);
    if (!pCodecCtx)
    {
        printf("无法创建编码器上下文\n");
        return;
    }
    pCodecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP;           // 设置采样格式
    pCodecCtx->sample_rate = 44100;                       // 设置采样率
    AVChannelLayout ch_layout = AV_CHANNEL_LAYOUT_STEREO; // 设置声道布局
    av_channel_layout_copy(&pCodecCtx->ch_layout, &ch_layout);
    pCodecCtx->bit_rate = 128'000;                  // 设置比特率
    pCodecCtx->flags = AV_CODEC_FLAG_GLOBAL_HEADER; // 设置全局头标志
    pCodecCtx->profile = AV_PROFILE_AAC_HE_V2;      // 设置AAC编码器配置文件
    // 打开编码器
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
    {
        printf("无法打开编码器\n");
        return;
    }

    std::ifstream pInFile(inputFile, std::ifstream::binary);   // 打开输入文件
    std::ofstream pOutFile(outputFile, std::ofstream::binary); // 打开输出文件
    if (!pInFile.is_open() || !pOutFile.is_open())
    {
        return;
    }

    // 创建数据包
    AVPacket *packet = av_packet_alloc();
    if (!packet)
    {
        printf("无法创建数据包\n");
        return;
    }
    // 创建数据帧
    AVFrame *pFrame = av_frame_alloc();
    if (!pFrame)
    {
        printf("无法创建数据帧\n");
        return;
    }
    pFrame->sample_rate = pCodecCtx->sample_rate;                      // 设置采样率
    pFrame->format = pCodecCtx->sample_fmt;                            // 设置采样格式
    av_channel_layout_copy(&pFrame->ch_layout, &pCodecCtx->ch_layout); // 设置声道布局
    pFrame->nb_samples = pCodecCtx->frame_size;                        // 设置每帧的样本数
    // 分配内存
    if (av_frame_get_buffer(pFrame, 0) < 0)
    {
        printf("无法分配内存\n");
        return;
    }
    // 分配缓冲区
    int fmtByteSize = av_get_bytes_per_sample(pCodecCtx->sample_fmt);                                                // 获取每个样本的字节数
    uint8_t *pBuffer = (uint8_t *)av_malloc(fmtByteSize * pCodecCtx->frame_size * pCodecCtx->ch_layout.nb_channels); // 分配缓冲区
    if (!pBuffer)
    {
        printf("无法分配缓冲区\n");
        return;
    }
    // 读取数据
    while (!pInFile.eof())
    {
        // 读取数据
        for (int i = 0; i < pFrame->nb_samples * pFrame->ch_layout.nb_channels / 2; i++)
        {
            pInFile.read((char *)pBuffer, fmtByteSize * 2);
            if (pInFile.eof())
            {
                break;
            }
            for (int ch = 0; ch < pFrame->ch_layout.nb_channels; ch++) // 每个声道
            {
                memcpy(pFrame->data[ch] + i * fmtByteSize, pBuffer + ch * fmtByteSize, fmtByteSize); // 复制数据到数据帧
            }
        }
        // 编码
        int ret = avcodec_send_frame(pCodecCtx, pFrame);
        if (ret < 0)
            break;
        while (ret >= 0)
        {
            ret = avcodec_receive_packet(pCodecCtx, packet);
            if (ret < 0)
                break;
            writeADTSHeader(pOutFile, pCodecCtx, packet->size); // 写入ADTS头
            pOutFile.write((char *)packet->data, packet->size); // 写入数据
            av_packet_unref(packet);                            // 释放数据包
        }
        // 清空缓冲区
        memset(pBuffer, 0, fmtByteSize * pFrame->nb_samples * pFrame->ch_layout.nb_channels);
    }
    // 清空编码器,防止缓冲区有数据
    int ret = avcodec_send_frame(pCodecCtx, nullptr);
    while (ret >= 0)
    {
        ret = avcodec_receive_packet(pCodecCtx, packet);
        if (ret < 0)
            break;
        writeADTSHeader(pOutFile, pCodecCtx, packet->size); // 写入ADTS头
        pOutFile.write((char *)packet->data, packet->size); // 写入数据
        av_packet_unref(packet);                            // 释放数据包
    }
    // 释放资源
    av_frame_free(&pFrame);
    av_packet_free(&packet);
    avcodec_free_context(&pCodecCtx);
    pInFile.close();
    pOutFile.close();
    av_free(pBuffer);
    printf("转换完成\n");
}

视频格式

H264

H264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。H264是MPG-4标准所定义的最新编码格式,同时也是技术含量最高、代表最新技术水平的视频编码格式之一,在ITU的标准称为H.264,在MPEG的标准里是MPEG-4的一个组成部分(MPEG-4 Part10),又叫Advanced Video Codec,因此常常被称为MPG-4 AVC或者直接叫AVC。H264视频格式是经过有损压缩的,但在技术上尽可能做的降低存储体积下获得较好图像质量和低带宽图像快速传输。压缩比大约是1%

GOP图像组

GOP(Group of Pictures)顾名思义,就是一组图片,在实际操作中,就是一组完整的视频帧,怎么叫做完整的视频帧?就是说一个GOP拿出来,必须能够完整地播放、显示。也就是两个I帧之间的间隔。
在这里插入图片描述

I帧,P帧,B帧

  • I帧(intraframe frame),也叫关键帧,采用帧内压缩技术,将其解压出来就是一张完整的图片。GOP中第一个I帧也被称为IDR帧。
  • P帧(forward Predicted frame),向前参考帧,也叫预测帧。压缩时,只参考前面已经处理的帧,采用帧间压缩技术,它只占用I帧一半大小。
  • B帧(Bidirectionally predicted frame),双向参考帧,也叫双向预测帧。压缩时既参考前面已经处理的帧,也参考后面的帧,使用帧间压缩技术。它占I帧1/4大小

IDR帧和I帧的区别
一个序列的第一个图像叫做IDR帧(立即刷新图像),IDR帧都是I帧。I帧和IDR帧都是用帧内预测。不用参考任何帧但是之后的P帧和B帧是有可能参考这个I帧之前的帧的。IDR就不允许这样。

其核心作用是,当解码器解码到IDR帧时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,使错误不致传播,从IDR帧开始算新的序列,开始编码。

H264压缩技术

H264首先会把一张图片分成一个个的片,片里面又会分成一个个宏块。
宏块是视频压缩操作的基本单元。无论是帧内压缩还是帧间压缩,它们都以宏块为单位。
以下面的图片为例
在这里插入图片描述
划分片
在这里插入图片描述
划分宏块
H264默认是用16x16 大小的区域作为一个宏块,也可以划分成8x8大小。划分好宏块后,计算宏块的像素值
在这里插入图片描述
划分子块
H264对比较平坦的图像使用16x16大小的宏块。但是为了更高的压缩率,还可以再这个宏块上划分出更小的子块,子块的大小可以是8x16,16x8,8x8,4x8,8x4,4x4非常的灵活
在这里插入图片描述
上图红框内的16x16宏块大部分是蓝色背景,而三只鹰的部分图像被划在了该宏块内,为了更好地处理三只鹰部分的图像, 就在这个宏块内又划分出了多个子块。
这样再经过帧内压缩,可以得到更高效的数据。

常见的宏块尺寸如下

在这里插入图片描述

帧内压缩也称为空间压缩,当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。帧内一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立地解码、显示。帧内压缩一般达不到很高的压缩,跟编码JPEG差不多。

帧间压缩,相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点。也即连续的视频其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减小压缩比。帧间压缩也称为时间压缩,它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的,帧差值算法是一种典型的时间压缩法,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。

帧内预测
如图所示,左侧显示的是H264对于帧内预测提供的9种模式
在这里插入图片描述
9种帧内预测模式
在这里插入图片描述

  1. 第一种是纵列的即垂直模式,左边以及上边都是已经推测出来的宏块,这个时候就可以将下面4x4的宏块预测出来,对于垂直模式来说,上边A对应下面的一整列都是A,第二个B,对应一列下面也都是B,以此类推。
  2. 第二种是横向的水平模式,还是左边以及上边都是都是预测出来的数据,预测结果第一行是I,第二行是j,以此类推。
  3. 第三种是求平均值,在目标的4x4宏块内都是ABCD加上IJKL的求平均值,例如宏块内的小a,它的值是ABCD加IJKL求平均值得出,也就是下面每一个4x4内的像素值都是一样的。需要注意的是:像素a对应的值是(ABCD+IJKL)/2,而不是(A+I)/2。

H264压缩级别

H264 Profile 是对视频压缩特性的描述,Profile越高,说明采用了越高级的压缩特性。
在这里插入图片描述

在这里插入图片描述

H264视频级别

H264 Level是对视频的描述,Level越高,视频的码率、分辨率、FPS越高。
在这里插入图片描述

H264码流结构

H264的两种码流格式,它们分别为:字节流格式和RTP包格式。

  • 字节流格式(Annexb):这是在H264官方协议文档中规定的格式,处于文档附录B(Annex-B Byte stream format)中,所以它也成为了大多数编码器,默认的输出格式。它的基本数据单位为NAL单元,也即NALU。为了从字节流中提取出NALU,协议规定,在每个NALU的前面加上起始码:0x000001或0x00000001
  • RTP包格式:这种格式并没有在H264中规定,那为什么还要介绍它呢?是因为在H264的官方参考软件JM里,有这种封装格式的实现。在这种格式中,NALU并不需要起始码Start_Code来进行识别,而是在NALU开始的若干字节(1,2,4字节),代表NALU的长度。

码流分层图
在这里插入图片描述
接下来我们主要讲字节流格式,H264码流分为2层,NAL层和VCL层

  • NAL(Network Abstraction Layer)层,视频数据网络抽象层,是H264码流的基本单元。
  • VCL(Video Coding Layer)层,视频数据编码层,VCL结构关系如下图所示,在视频帧序列由图像组成,图像中包含分片,分片里面包含宏块,宏块里面又包含子块
    在这里插入图片描述
    接下来我们来看NALU,NALU由NALU头部和NALU载荷 (Raw Byte Sequence Payload ,RBSP) 组成,其中NALU头部包括禁止位 (forbidden_zero_bit)、参考标识(nal_ref_idc)和NALU类型(nal_uint_type)等信息。NALU载荷则是包含实际视频数据的原始字节序列。

对于一个H264裸流,它就是由一个起始码(StartCode)和一个NALU单元组成
在这里插入图片描述
由上图可以知道,NALU单元 = NALU Header + RBSP ,其中RBSP原始字节序列载荷,即在SODB的后面添加了trailing bit,即一个bit 1和若干个bit0,以使字节对齐,SODB(String of Data Bits)原始数据比特流,原始数据比特流长度不一定是8的倍数,故需要补齐,他是由VCL层产生的
在这里插入图片描述

在每一个I帧之前,都会存在一个SPS和PPS。

  • SPS(Sequence Parameter Set) 序列参数集,作用于一连串连续的视频图像。如seq_parameter_set_id、帧数及POC(picture order count)的约束、参考帧数目、解码图像尺寸和熵编码模式选择标识等
  • PPS(Picture Parameter Set)图像参数集,作用于视频序列中的图像。如pic_parameter_set_id、熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等
    在这里插入图片描述

SPS

SPS结构如下:
在这里插入图片描述

  • profile_idc:标识当前H264码流的profile。我们知道H264中定义了三种常用的档次profile:
    • 基准档次:baseline profile
    • 主要档次:main profile
    • 扩展档次:extended profile
      在H264的SPS中,第一个字节表示profile_idc,根据profile_idc的值可以确定码流符合哪一种档次。判断规律为:
    • profile_idc = 66 → baseline profile
    • profile_idc = 77 → main profile
    • profile_idc = 88 → extended profile
      在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra等,每一种都由不同的profile_idc表示。另外,constraint_set0_flag ~ constraint_set5_flag是在编码的档次方面对码流增加的其他一些额外限制性条件。
  • level_idc:标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的Level由level_idc指定。
  • seq_parameter_set_id:表示当前的序列参数集的id,通过该id值,图像参数集pps可以引用其代表的sps中的参数。
  • log2_max_frame_num_minus4:用于计算MaxFrameNum的值。计算公式为 M a x F r a m e N u m = 2 l o g 2 m a x f r a m e n u m m i n u s 4 + 4 MaxFrameNum = 2^{log2_max_frame_num_minus4 + 4} MaxFrameNum=2log2maxframenumminus4+4。MaxFrameNum是frame_num的上限值,frame_num是图像序号的一种表示方法,在帧间编码中常用作一种参考帧标记的手段。
  • pic_order_cnt_type:表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2
  • log2_max_pic_order_cnt_lsb_minus4:用于计算MaxPicOrderCntLsb的值,该值表示POC的上限。计算方法为 M a x P i c O r d e r C n t L s b = 2 l o g 2 m a x p i c o r d e r c n t l s b m i n u s 4 + 4 MaxPicOrderCntLsb = 2^{log2_max_pic_order_cnt_lsb_minus4 + 4} MaxPicOrderCntLsb=2log2maxpicordercntlsbminus4+4
    • max_num_ref_frames:用于表示参考帧的最大数目。
  • gaps_in_frame_num_value_allowed_flag:标识位,说明frame_num中是否允许不连续的值。

  • pic_width_in_mbs_minus1:用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:

    f r a m e w i d t h = 16 × ( p i c w i d t h i n m b s m i n u s 1 + 1 ) frame_width = 16 \times (pic_width_in_mbs_minus1 + 1) framewidth=16×(picwidthinmbsminus1+1)

  • pic_height_in_map_units_minus1:使用PicHeightInMapUnits来度量视频中一帧图像的高度。PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为:

    P i c H e i g h t I n M a p U n i t s = p i c h e i g h t i n m a p u n i t s m i n u s 1 + 1 PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1 PicHeightInMapUnits=picheightinmapunitsminus1+1

  • frame_mbs_only_flag:标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码;该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同,为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。

    按照宏块计算的图像实际高度FrameHeightInMbs的计算方法为:

    F r a m e H e i g h t I n M b s = ( 2 − f r a m e m b s o n l y f l a g ) × P i c H e i g h t I n M a p U n i t s FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) \times PicHeightInMapUnits FrameHeightInMbs=(2framembsonlyflag)×PicHeightInMapUnits

  • mb_adaptive_frame_field_flag

  • 标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。

  • direct_8x8_inference_flag:标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。

  • frame_cropping_flag:标识位,说明是否需要对输出的图像帧进行裁剪。

  • vui_parameters_present_flag:标识位,说明SPS中是否存在VUI信息。

  • 在这里插入图片描述

PPS

PPS结构如下:
在这里插入图片描述
其中的每一个语法元素及其含义如下:

  • pic_parameter_set_id:表示当前PPS的id。某个PPS在码流中会被相应的slice引用,slice引用PPS的方式就是在Slice header中保存PPS的id值。该值的取值范围为[0,255]。

  • seq_parameter_set_id:表示当前PPS所引用的激活的SPS的id。通过这种方式,PPS中也可以取到对应SPS中的参数。该值的取值范围为[0,31]。

  • entropy_coding_mode_flag:熵编码模式标识,该标识位表示码流中熵编码/解码选择的算法。对于部分语法元素,在不同的编码配置下,选择的熵编码方式不同。例如在一个宏块语法元素中,宏块类型mb_type的语法元素描述符为“ue(v)| ae(v)”,在baseline profile等设置下采用指数哥伦布编码,在main profile等设置下采用CABAC编码。

    标识位entropy_coding_mode_flag的作用就是控制这种算法选择。当该值为0时,选择左边的算法,通常为指数哥伦布编码或者CAVLC;当该值为1时,选择右边的算法,通常为CABAC。

  • bottom_field_pic_order_in_frame_present_flag:标识位,用于表示另外条带头中的两个语法元素delta_pic_order_cnt_bottom和delta_pic_order_cn是否存在的标识。这两个语法元素表示了某一帧的底场的POC的计算方法。

  • num_slice_groups_minus1:表示某一帧中slice group的个数。当该值为0时,一帧中所有的slice都属于一个slice group。slice group是一帧中宏块的组合方式,定义在协议文档的3.141部分。

  • num_ref_idx_l0_default_active_minus1、num_ref_idx_l0_default_active_minus1表示当Slice Header中的num_ref_idx_active_override_flag标识位为0时,P/SP/Bslice的语法元素num_ref_idx_l0_active_minus1和num_ref_idx_l1_active_minus1的默认值。

  • weighted_pred_flag:标识位,表示在P/SP slice中是否开启加权预测。

  • weighted_bipred_idc:表示在B Slice中加权预测的方法,取值范围为[0,2]。0表示默认加权预测,1表示显式加权预测,2表示隐式加权预测。

  • pic_init_qp_minus26和pic_init_qs_minus26:表示初始的量化参数。实际的量化参数由该参数、slice header中的 s l i c e q p d e l t a ÷ s l i c e q s d e l t a slice_qp_delta \div slice_qs_delta sliceqpdelta÷sliceqsdelta计算得到。

  • chroma_qp_index_offset:用于计算色度分量的量化参数,取值范围为[-12,12]。

  • deblocking_filter_control_present_flag:标识位,用于表示Slice header中是否存在用于去块滤波器控制的信息。当该标志位为1时,slice header中包含去块滤波相应的信息;当该标识位为0时,slice header中没有相应的信息。

  • constrained_intra_pred_flag:若该标识为1,表示I宏块在进行帧内预测时只能使用来自I和SI类型宏块的信息;若该标识位0,表示I宏块可以使用来自Inter类型宏块的信息。

  • redundant_pic_cnt_present_flag:标识位,用于表示Slice header中是否存在redundant_pic_cnt语法元素。当该标志位为1时,slice header中包含redundant_pic_cnt;当该标识位为0时,slice header中没有相应的信息。

解码视频

void Widget::H264Decode()
{
    char errbuf[1024];
    const char *infile = "../../source/video.h264";
    const char *outfile = "../../output/out.yuv";

    AVFormatContext *fmt_ctx = nullptr; // 输入文件上下文
    // 打开输入文件
    if (avformat_open_input(&fmt_ctx, infile, nullptr, nullptr) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "无法打开资源文件 %s\n", infile);
        return;
    }
    // 获取资源信息
    if (avformat_find_stream_info(fmt_ctx, nullptr) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "无法获取资源信息\n");
        return;
    }
    // 查找视频流
    int video_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    if (video_stream_index < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "无法找到视频流\n");
        return;
    }
    // 查找解码器
    const AVCodec *codec = avcodec_find_decoder(fmt_ctx->streams[video_stream_index]->codecpar->codec_id);
    if (!codec)
    {
        av_log(NULL, AV_LOG_ERROR, "无法找到解码器\n");
        return;
    }
    // 创建解码器上下文
    AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx)
    {
        av_log(NULL, AV_LOG_ERROR, "无法创建解码器上下文\n");
        return;
    }
    // 填充解码器上下文
    if (avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "无法填充解码器上下文\n");
        return;
    }
    // 打开解码器
    if (avcodec_open2(codec_ctx, codec, nullptr) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "无法打开解码器\n");
        return;
    }
    // 打开输出文件
    std::ofstream outfileSream(outfile, std::ios::binary);
    if (!outfileSream.is_open())
    {
        av_log(NULL, AV_LOG_ERROR, "无法打开输出文件\n");
        return;
    }
    // 读取数据包
    AVPacket *pkt = av_packet_alloc(); // 分配数据包
    AVFrame *frame = av_frame_alloc(); // 分配帧
    int ret = 0;
    while (av_read_frame(fmt_ctx, pkt) >= 0)
    {
        // 解码
        ret = avcodec_send_packet(codec_ctx, pkt); // 发送数据包到解码器
        if (pkt->stream_index != video_stream_index)
            continue; // 不是视频流就跳过
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "解码失败\n");
            break;
        }
        while (ret >= 0)
        {
            ret = avcodec_receive_frame(codec_ctx, frame); // 从解码器接收解码后的帧
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                break;
            else if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "解码失败\n");
                break;
            }
            // 写入文件
            for (int i = 0; i < frame->height; i++)
            {
                outfileSream.write((char *)frame->data[0] + i * frame->linesize[0], frame->width); // 写入Y分量
            }
            for (int i = 0; i < frame->height / 2; i++)
            {
                outfileSream.write((char *)frame->data[1] + i * frame->linesize[1], frame->width / 2); // 写入U分量
            }
            for (int i = 0; i < frame->height / 2; i++)
            {
                outfileSream.write((char *)frame->data[2] + i * frame->linesize[2], frame->width / 2); // 写入V分量
            }
            av_frame_unref(frame); // 清空帧
        }
        av_packet_unref(pkt); // 清空数据包
    }
    // 清空编码器
    ret = avcodec_send_packet(codec_ctx, nullptr);
    while (ret >= 0)
    {
        ret = avcodec_receive_frame(codec_ctx, frame);
        if (ret < 0)
            break;
        // 写入文件
        for (int i = 0; i < frame->height; i++)
        {
            outfileSream.write((char *)frame->data[0] + i * frame->linesize[0], frame->width); // 写入Y分量
        }
        for (int i = 0; i < frame->height / 2; i++)
        {
            outfileSream.write((char *)frame->data[1] + i * frame->linesize[1], frame->width / 2); // 写入U分量
        }
        for (int i = 0; i < frame->height / 2; i++)
        {
            outfileSream.write((char *)frame->data[2] + i * frame->linesize[2], frame->width / 2); // 写入V分量
        }
    }
    qDebug() << "解码完成";
    // 释放资源
    av_frame_free(&frame);
    av_packet_free(&pkt);
    outfileSream.close();
    avcodec_free_context(&codec_ctx);
    avformat_close_input(&fmt_ctx);
}

编码视频

void Widget::H264Encode()
{
    char errorBuffer[1024];
    const char *inputFile = "../../source/video.yuv";
    const char *outputFile = "../../output/out.h264";

    // 查找H264编码器
    const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec)
    {
        qDebug() << "找不到编码器";
        return;
    }
    // 创建编码器上下文
    AVCodecContext *codecContext = avcodec_alloc_context3(codec);
    if (!codecContext)
    {
        qDebug() << "创建编码器上下文失败";
        return;
    }
    // 设置编码器参数
    codecContext->profile = AV_PROFILE_H264_HIGH_444; // 设置编码器压缩级别
    codecContext->level = 50;                         // 设置编码器视频质量级别,50表示5.0
    codecContext->width = 1280;                       // 设置编码器视频宽度
    codecContext->height = 720;                       // 设置编码器视频高度
    codecContext->pix_fmt = AV_PIX_FMT_YUV420P;       // 设置编码器视频像素格式
    codecContext->time_base = AVRational{1, 30};      // 设置编码器时间基准
    codecContext->framerate = AVRational{30, 1};      // 设置编码器帧率
    codecContext->gop_size = 30;                      // 设置编码器I帧间隔
    codecContext->max_b_frames = 3;                   // 设置编码器B帧最大数量
    codecContext->keyint_min = 3;                     // 设置编码器最小I帧间隔
    codecContext->has_b_frames = 1;                   // 设置编码器是否支持B帧
    codecContext->refs = 3;                           // 设置编码器参考帧数量
    codecContext->bit_rate = 2000000;                 // 设置编码器比特率

    // 打开编码器
    CoUninitialize(); // 初始化COM库,头文件<objbase.h>,这里防止报错COM must not be in STA mode
    int ret = avcodec_open2(codecContext, codec, nullptr);
    if (ret < 0)
    {
        av_strerror(ret, errorBuffer, sizeof(errorBuffer));
        qDebug() << "打开编码器失败:" << errorBuffer;
        return;
    }
    // 打开输入文件
    FILE *inputFilePtr = fopen(inputFile, "rb");
    if (!inputFilePtr)
    {
        qDebug() << "Could not open input file";
        return;
    }
    // 打开输出文件
    FILE *outputFilePtr = fopen(outputFile, "wb");
    if (!outputFilePtr)
    {
        qDebug() << "Could not open output file";
        return;
    }
    // 创建AVFrame对象
    AVFrame *frame = av_frame_alloc();
    if (!frame)
    {
        qDebug() << "Could not allocate video frame";
        return;
    }
    frame->format = codecContext->pix_fmt;
    frame->width = codecContext->width;
    frame->height = codecContext->height;
    // 分配AVFrame对象缓冲区
    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0)
    {
        av_strerror(ret, errorBuffer, sizeof(errorBuffer));
        qDebug() << "不能为帧分配缓冲区:" << errorBuffer;
        return;
    }
    // 创建AVPacket对象
    AVPacket *packet = av_packet_alloc();
    if (!packet)
    {
        qDebug() << "不能分配数据包";
        return;
    }
    // 读取输入文件数据
    int ySize = codecContext->width * codecContext->height;
    int frameCount = 0;
    while (!feof(inputFilePtr))
    {
        fread(frame->data[0], ySize, 1, inputFilePtr);     // Y分量
        fread(frame->data[1], ySize / 4, 1, inputFilePtr); // U分量
        fread(frame->data[2], ySize / 4, 1, inputFilePtr); // V分量
        frame->pts = frameCount;                           // 设置帧时间戳
        // 编码一帧数据
        ret = avcodec_send_frame(codecContext, frame);
        if (ret < 0)
        {
            av_strerror(ret, errorBuffer, sizeof(errorBuffer));
            qDebug() << "编码失败:" << errorBuffer;
            return;
        }
        while (ret >= 0)
        {
            ret = avcodec_receive_packet(codecContext, packet);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                break;
            else if (ret < 0)
            {
                av_strerror(ret, errorBuffer, sizeof(errorBuffer));
                qDebug() << "编码失败:" << errorBuffer;
                return;
            }
            // 写入输出文件
            fwrite(packet->data, packet->size, 1, outputFilePtr);
            av_packet_unref(packet);
        }
        frameCount++; // 帧计数器加1
    }
    // 清空编码器
    ret = avcodec_send_frame(codecContext, nullptr);
    while (ret >= 0)
    {
        ret = avcodec_receive_packet(codecContext, packet);
        if (ret < 0)
            break;
        fwrite(packet->data, packet->size, 1, outputFilePtr); // 写入输出文件
    }
    // 释放资源
    av_packet_free(&packet);
    av_frame_free(&frame);
    avcodec_free_context(&codecContext);
    fclose(inputFilePtr);
    fclose(outputFilePtr);
    qDebug() << "编码完成!";
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2268718.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

游戏引擎学习第67天

reviewing “apron”概念以更新区域 我们正在进行模拟区域的扩展工作&#xff0c;目标是通过增加一个更大的区域来支持更丰富的互动&#xff0c;尤其是那些可能超出摄像机视野的内容。现有的模拟区域包括摄像机能看到的区域和其周围的环境区域&#xff0c;但为了保证更高效的游…

3D线上艺术展:艺术与技术的完美融合

随着数字技术的飞速发展&#xff0c;未来的艺术展览正逐步迈向线上线下融合的新阶段。其中&#xff0c;3D线上展览以其独特的魅力&#xff0c;成为线下展览的延伸与拓展&#xff0c;为艺术爱好者们开辟了全新的观赏途径。 对于艺术家和策展人而言&#xff0c;3D线上展览不仅打…

Qt制作一个简单闹钟

增加testtospeech模块 QT core gui texttospeech 头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimerEvent> #include <QTimer> #include <QTime> #include <QMouseEvent> #include <QTextToSpeech>…

Java编程题_面向对象和常用API01_B级

Java编程题_面向对象和常用API01_B级 第1题 面向对象、异常、集合、IO 题干: 请编写程序&#xff0c;完成键盘录入学生信息&#xff0c;并计算总分将学生信息与总分一同写入文本文件 需求&#xff1a;键盘录入3个学生信息(姓名,语文成绩,数学成绩) 求出每个学生的总分 ,并…

tcpdump 网络数据包分析工具

简介 用简单的话来定义tcpdump&#xff0c;就是&#xff1a;dump the traffic on a network&#xff0c;根据使用者的定义对网络上的数据包进行截获的包分析工具。 tcpdump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的…

24个未授权访问漏洞详情

一、Redis未授权访问漏洞 第一步&#xff1a;进⼊vulhub⽬录使⽤以下命令启动靶机 进⼊⽬录&#xff1a;cd /vulhub-master/redis/4-unacc 启动&#xff1a;docker-compose up -d 检查&#xff1a;docker ps 第二步&#xff1a;在Kali上安装redis程序进⾏服务的链接 #安装r…

python3中条件判断语句:if 语句与if嵌套语句

一. 简介 本文开始学习 python3中条件判断语句。Python 条件语句是通过一条或多条语句的执行结果&#xff08;True 或者 False&#xff09;来决定执行的代码块。 二. python3中条件判断语句&#xff1a;if 语句与 if嵌套语句 1. if 语句 Python中if语句的一般形式如下所示&…

vue实现下拉多选、可搜索、全选功能

最后的效果就是树形的下拉多选&#xff0c;可选择任意一级选项&#xff0c;下拉框中有一个按钮可以实现全选&#xff0c;也支持搜索功能。 在mounted生命周期里面获取全部部门的数据&#xff0c;handleTree是讲接口返回的数据整理成树形结构&#xff0c;可以自行解决 <div c…

在 Unity 6 中为您的世界创建全局照明的新方法(三)

本文是Unity 6全局光照功能介绍系列中的最后一篇文章&#xff0c;在上篇文章中我们为大家介绍了关于使用沙漠绿洲场景创建实时光照的案例&#xff0c;在本文中我们将承接上篇文章继续为大家介绍如何使用Unity 6中的APV探针功能在URP管道中的另一个案例“花园场景”中创建实时光…

《计算机组成及汇编语言原理》阅读笔记:p116-p120

《计算机组成及汇编语言原理》学习第 7 天&#xff0c;p116-p120 总结&#xff0c;总计 5 页。 一、技术总结 1.CPU优化 (1)increase overall performance number 例如&#xff1a;16位电脑提升到32位电脑。 (2)multiprocessing One way to make computers more useful i…

Python的简单爬虫框架

爬虫为网络爬虫&#xff08;又称为网页蜘蛛&#xff0c;网络机器人&#xff0c;在FOAF社区中间&#xff0c;更经常的称为网页追逐者&#xff09;&#xff0c;是一种按照一定的规则&#xff0c;自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、…

优化站群SEO:使用苹果CMS泛目录插件实现泛目录页面刷新不变

优化站群SEO&#xff1a;使用苹果CMS泛目录插件实现泛目录页面刷新不变 在当今数字营销环境中&#xff0c;搜索引擎优化&#xff08;SEO&#xff09;是提升网站流量和可见性的关键策略。苹果CMS作为一款灵活的内容管理系统&#xff0c;提供了丰富的插件功能&#xff0c;尤其是…

Spring Cloud由入门到精通

文章目录 1.初识微服务1.1. 单体架构1.2.分布式架构1.3.微服务1.4 微服务技术比对1.5.Spring Cloud1.6. 总结2.服务拆分和远程调用2.1.服务拆分原则2.2.服务拆分示例2.2.1.项目工程结构设计2.2.2.创建Maven项目工程2.3.实现远程调用案例2.3.1.案例需求:2.3.2. 注册 Rest Templ…

【RK3588 Linux 5.x 内核编程】-内核I2C子系统介绍

内核I2C子系统介绍 文章目录 内核I2C子系统介绍1、内核中的I2C子系统2、内核中的I2C驱动2.1 获取I2C适合器2.2 创建i2c_board_info与设备2.3 创建设备ID和I2C驱动2.4 数据传输2.4.1 发送数据2.4.2 读取数据3、I2C总线如何工作I2C 是一种用于双线接口的串行协议,用于连接低速设…

GPIO相关寄存器,点灯

目录 一.输入模式 1.浮空输入 2.上拉输入 3.下拉输入 4.模拟输入 二.输出模式 1.推挽输出 2.开漏输出 三.寄存器 1.寄存器的作用 2.功能与类型 3.控制某一引脚输出电压来点灯所需要控制的寄存器 1.打开对应时钟开关 2.端口模式寄存器 ---输出模式 3.输出类型寄存…

【基础篇】三、MySQL表结构的操作

文章目录 Ⅰ. 创建表1、语法2、创建表样例3、创建和其它表一样结构的表 Ⅱ. 查看表结构1、查看数据库中的表2、查看指定表的属性3、获取表的创建语句 Ⅲ. 删除表Ⅳ. 修改表结构1、向表中插入新的字段2、删除表中的字段3、修改表名4、修改字段属性 Ⅰ. 创建表 1、语法 create …

Ethernet 系列(12)-- 基础学习::SOME/IP

目录 1. SOME/IP简介&#xff1a; 1.1 什么是SOME/IP&#xff1a; 1.2 什么时候使用SOME/IP&#xff1a; 2. SOME/IP的特点&#xff1a; 2.1 序列化&#xff1a; 2.2 远程过程调用&#xff08;RPC&#xff09;: 2.3 服务发现&#xff1a; 2.4 发布/订阅&#xff1a; 2.5 UDP消息…

前端小案例——轮播图(HTML+CSS+JS, 附源码)

一、前言 实现功能&#xff1a; 显示多张图片&#xff0c;每张图片占据轮播图容器的一部分空间。实现向左和向右的切换按钮&#xff0c;可以点击按钮切换到上一张或下一张图片。在底部显示小圆点&#xff0c;表示当前显示的图片&#xff0c;点击小圆点可以跳转到对应的图片。…

vue封装弹窗元素拖动指令

项目开发过程中我们通常会遇到需要到一些弹窗鼠标可以随意拖动位置去放置&#xff0c;vue里面直接通过封装对应的指令即可&#xff0c;于是封装了一个出来&#xff0c;希望可以用到。 Vue.directive(draggable-dom, draggableDom); 组件节点添加对应指令就可以 v-draggable-…

Supermap iClient Webgl 粒子特效案例-消防场景

作者&#xff1a;Lzzzz 前言 WebGL 粒子特效的应用场景非常广泛&#xff0c;几乎可以在任何需要丰富视觉效果或动态表现的地方看到其身影。通过灵活运用颗粒系统&#xff0c;开发者可以创造出引人入胜的用户体验和视觉表现。 一、效果展示 二、实现步骤 1&#xff0c;构建…