通过libfdk_aac编解码器实现AAC音频和PCM的编解码

这篇具有很好参考价值的文章主要介绍了通过libfdk_aac编解码器实现AAC音频和PCM的编解码。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

测试环境:

  • ffmpeg的4.3.2自行编译版本

  • windows环境

  • qt5.12


AAC编码是MP3格式的后继产品,通常在相同的比特率下可以获得比MP3更高的声音质量,是iPhone、iPod、iPad、iTunes的标准音频格式。

AAC相较于MP3的改进包含:

  • 更多的采样率选择:8kHz ~ 96kHz,MP3为16kHz ~ 48kHz

  • 更高的声道数上限:48个,MP3在MPEG-1模式下为最多双声道,MPEG-2模式下5.1声道

  • 改进的压缩功能:以较小的文件大小提供更高的质量

  • 改进的解码效率:需要较少的处理能力进行解码


AAC编码为了使用不同场景的需求,设计了很多规格

  • MPEG-2 AAC LC:低复杂度规格(Low Complexity)

  • MPEG-2 AAC Main:主规格

  • MPEG-2 AAC SSR:可变采样率规格(Scaleable Sample Rate)

  • MPEG-4 AAC LC:低复杂度规格(Low Complexity)

    • 现在的手机比较常见的MP4文件中的音频部分使用了该规格

  • MPEG-4 AAC Main:主规格

  • MPEG-4 AAC SSR:可变采样率规格(Scaleable Sample Rate)

  • MPEG-4 AAC LTP:长时期预测规格(Long Term Predicition)

  • MPEG-4 AAC LD:低延迟规格(Low Delay)

  • MPEG-4 AAC HE:高效率规格(High Efficiency)

众多规格中只需关注LC和HE


pcm与aac的转换需要AAC编解码器(如下列举几种常用的AAC编解码器)

  • Nero AAC

    • 支持LC/HE规格

    • 目前已经停止开发维护

  • FFmpeg AAC

    • 编解码器名字叫做aac

    • 在开发过程中通过这个名字找到编解码器

    • 支持LC规格

    • FFmpeg官方内置的AAC编解码器,在libavcodec库中

  • FAAC(Freeware Advanced Audio Coder)

    • 编解码器名字叫做libfaac

    • 在开发过程中通过这个名字找到编解码器,最后调用FAAC库的功能

    • 支持LC规格

    • 可以集成到FFmpeg的libavcodec中

    • 从2016年开始,FFmpeg已经移除了对FAAC的支持

  • Fraunhofer FDK AAC

    • 编解码器名字叫做libfdk_aac

    • 在开发过程中通过这个名字找到编解码器,最后调用FDK AAC库的功能

    • 支持LC/HE规格

    • 目前质量最高的AAC编解码器

    • 可以集成到FFmpeg的libavcodec中

编码质量排名:Fraunhofer FDK AAC > FFmpeg AAC > FAAC。

由于libfdk_aac最好,但是网上下载好的ffmpeg编译好的版本不带libfdk_aac编解码器。所以我们只能自行编译ffmpeg。

如下命令可以查看FFmpeg目前集成的AAC编解码器

ffmpeg -codecs | findstr aac


自己手动编译FFmpeg源码,将libfdk_aac集成到FFmpeg中,这种方式最好,但在windows环境下较为麻烦。

因为编译源码需要在类Unix系统上的(Linux、Mac等),默认无法直接用在Windows上。所以必须先用MSYS2软件在Windows上模拟出Linux环境,然后再在其中用MinGW软件对FFmpeg进行编译。

链接:windows下msys2编译64位的ffmpeg源码


编译好源码后,需要把.pro文件配置成新编译的源码。

fdk-aac对需要编解码的pcm音频有一定的格式要求

  • 采样格式必须为16位整数PCM

  • 采样率只支持:8000、11025、12000、16000、22050、24000、32000、44100、48000、64000、88200、96000

命令行将pcm和wav文件编码成aac音频

# pcm -> aacffmpeg -ar 44100 -ac 2 -f s16le -i in.pcm -c:a libfdk_aac out.aac-ar 44100 -ac 2 -f s16le   --PCM输入数据的参数-c:a	 设置音频编码器,c表示codec(编解码器),a表示audio(音频)。 等价写法 -codec:a或-acodec    
# wav -> aacffmpeg -i in.wav -c:a libfdk_aac out.aac


默认生成的aac文件是LC规格的。aac文件比之前的pcm文件小了很多很多。

aac的缩写还可以是m4a和mp4。虽然现在都只认为mp4是视频文件


首先是pcm编码为aac

完整代码

AacEncodeThread.h

#ifndef AACENCODETHREAD_H#define AACENCODETHREAD_H#include <QFile>#include <QObject>#include <QThread>extern "C" {#include <libavformat/avformat.h>}typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;} AudioEncodeSpec;class AacEncodeThread : public QThread{
    Q_OBJECT
public:
    explicit AacEncodeThread(QObject *parent = nullptr);
    ~AacEncodeThread();

    static int check_sample_fmt(const AVCodec *codec,enum AVSampleFormat sample_fmt);
    static int encode(AVCodecContext *ctx,AVFrame *frame,AVPacket *pkt,QFile &outFile);
    static void aacEncode(AudioEncodeSpec &in,const char *outFilename);signals:


    // QThread interfaceprotected:
    virtual void run() override;};#endif // AACENCODETHREAD_H


AacEncodeThread.cpp

#include "aacencodethread.h"#include <QDebug>#include <QFile>extern "C" {#include <libavcodec/avcodec.h>#include <libavutil/avutil.h>}#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));AacEncodeThread::AacEncodeThread(QObject *parent) : QThread(parent){
    // 当监听到线程结束时(finished),就调用deleteLater回收内存
    connect(this, &AacEncodeThread::finished,
            this, &AacEncodeThread::deleteLater);}AacEncodeThread::~AacEncodeThread(){
    // 断开所有的连接
    disconnect();
    // 内存回收之前,正常结束线程
    requestInterruption();
    // 安全退出
    quit();
    wait();
    qDebug() << this << "析构(内存被回收)";}// 检查采样格式int AacEncodeThread::check_sample_fmt(const AVCodec *codec,enum AVSampleFormat sample_fmt) {
    const enum AVSampleFormat *p = codec->sample_fmts;

    while (*p != AV_SAMPLE_FMT_NONE) {//        qDebug() << av_get_sample_fmt_name(*p);
        if (*p == sample_fmt) return 1;
        p++;
    }
    return 0;}// 音频编码// 返回负数:中途出现了错误// 返回0:编码操作正常完成int AacEncodeThread::AacEncodeThread::encode(AVCodecContext *ctx,
                  AVFrame *frame,
                  AVPacket *pkt,
                  QFile &outFile) {
    // 发送数据到编码器
    int ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_frame error" << errbuf;
        return ret;
    }

    // 不断从编码器中取出编码后的数据
    // while (ret >= 0)
    while (true) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            // 继续读取数据到frame,然后送到编码器
            return 0;
        } else if (ret < 0) { // 其他错误
            return ret;
        }

        // 成功从编码器拿到编码后的数据
        // 将编码后的数据写入文件
        outFile.write((char *) pkt->data, pkt->size);

        // 释放pkt内部的资源
        av_packet_unref(pkt);
    }}void AacEncodeThread::aacEncode(AudioEncodeSpec &in, const char *outFilename){
    // 文件
    QFile inFile(in.filename);
    QFile outFile(outFilename);

    // 返回结果
    int ret = 0;

    // 编码器
    AVCodec *codec = nullptr;

    // 编码上下文
    AVCodecContext *ctx = nullptr;

    // 存放编码前的数据(pcm)
    AVFrame *frame = nullptr;

    // 存放编码后的数据(aac)
    AVPacket *pkt = nullptr;

    // 获取编码器//    codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
    codec = avcodec_find_encoder_by_name("libfdk_aac");
    if (!codec) {
        qDebug() << "encoder not found";
        return;
    }

    // libfdk_aac对输入数据的要求:采样格式必须是16位整数
    // 检查输入数据的采样格式
    if (!check_sample_fmt(codec, in.sampleFmt)) {
        qDebug() << "unsupported sample format"
                 << av_get_sample_fmt_name(in.sampleFmt);
        return;
    }

    // 创建编码上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        qDebug() << "avcodec_alloc_context3 error";
        return;
    }

    // 设置PCM参数
    ctx->sample_rate = in.sampleRate;
    ctx->sample_fmt = in.sampleFmt;
    ctx->channel_layout = in.chLayout;
    // 比特率
    ctx->bit_rate = 32000;
    // 规格
    ctx->profile = FF_PROFILE_AAC_HE_V2;

    // 打开编码器//    AVDictionary *options = nullptr;//    av_dict_set(&options, "vbr", "5", 0);//    ret = avcodec_open2(ctx, codec, &options);
    ret = avcodec_open2(ctx, codec, nullptr);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_open2 error" << errbuf;
        goto end;
    }

    // 创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        goto end;
    }

    // frame缓冲区中的样本帧数量(由ctx->frame_size决定)
    frame->nb_samples = ctx->frame_size;
    frame->format = ctx->sample_fmt;
    frame->channel_layout = ctx->channel_layout;

    // 利用nb_samples、format、channel_layout创建缓冲区
    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "av_frame_get_buffer error" << errbuf;
        goto end;
    }

    // 创建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        qDebug() << "av_packet_alloc error";
        goto end;
    }

    // 打开文件
    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "file open error" << in.filename;
        goto end;
    }
    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "file open error" << outFilename;
        goto end;
    }

    // 读取数据到frame中
    while ((ret = inFile.read((char *) frame->data[0],
                              frame->linesize[0])) > 0) {
        // 从文件中读取的数据,不足以填满frame缓冲区
        if (ret < frame->linesize[0]) {
            int bytes = av_get_bytes_per_sample((AVSampleFormat) frame->format);
            int ch = av_get_channel_layout_nb_channels(frame->channel_layout);
            // 设置真正有效的样本帧数量
            // 防止编码器编码了一些冗余数据
            frame->nb_samples = ret / (bytes * ch);
        }

        // 进行编码
        if (encode(ctx, frame, pkt, outFile) < 0) {
            goto end;
        }
    }

    // 刷新缓冲区
    encode(ctx, nullptr, pkt, outFile);end:
    // 关闭文件
    inFile.close();
    outFile.close();

    // 释放资源
    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_free_context(&ctx);

    qDebug() << "线程正常结束";}void AacEncodeThread::run(){
    AudioEncodeSpec in;
    in.filename = "E:/media/test.pcm";
    in.sampleRate = 44100;
    in.sampleFmt = AV_SAMPLE_FMT_S16;
    in.chLayout = AV_CH_LAYOUT_STEREO;

    aacEncode(in, "E:/media/test.aac");}


线程调用:

void MainWindow::on_pushButton_aac_encode_clicked(){
    m_pAacEncodeThread=new AacEncodeThread(this);
    m_pAacEncodeThread->start();}


注意:.h文件中提前声明了以下全局变量

AacEncodeThread *m_pAacEncodeThread=nullptr;



下面是aac解码成pcm

完整代码

AacDecodeThread.h

#ifndef AACDECODETHREAD_H#define AACDECODETHREAD_H#include <QFile>#include <QObject>#include <QThread>extern "C" {#include <libavformat/avformat.h>}typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;} AudioDecodeSpec;class AacDecodeThread : public QThread{
    Q_OBJECT
public:
    explicit AacDecodeThread(QObject *parent = nullptr);
    ~AacDecodeThread();

    static int decode(AVCodecContext *ctx,
                      AVPacket *pkt,
                      AVFrame *frame,
                      QFile &outFile);
    static void aacDecode(const char *inFilename,AudioDecodeSpec &out);signals:


    // QThread interfaceprotected:
    virtual void run() override;};#endif // AACDECODETHREAD_H


AacDecodeThread.cpp

#include "aacdecodethread.h"#include <QDebug>extern "C" {#include <libavcodec/avcodec.h>#include <libavutil/avutil.h>}#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));// 输入缓冲区的大小#define IN_DATA_SIZE 20480// 需要再次读取输入文件数据的阈值#define REFILL_THRESH 4096AacDecodeThread::AacDecodeThread(QObject *parent) : QThread(parent){
    // 当监听到线程结束时(finished),就调用deleteLater回收内存
    connect(this, &AacDecodeThread::finished,
            this, &AacDecodeThread::deleteLater);}AacDecodeThread::~AacDecodeThread(){
    // 断开所有的连接
    disconnect();
    // 内存回收之前,正常结束线程
    requestInterruption();
    // 安全退出
    quit();
    wait();
    qDebug() << this << "析构(内存被回收)";}int AacDecodeThread::decode(AVCodecContext *ctx,
                  AVPacket *pkt,
                  AVFrame *frame,
                  QFile &outFile) {
    // 发送压缩数据到解码器
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_packet error" << errbuf;
        return ret;
    }

    while (true) {
        // 获取解码后的数据
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            ERROR_BUF(ret);
            qDebug() << "avcodec_receive_frame error" << errbuf;
            return ret;
        }//        for (int i = 0; i < frame->channels; i++) {//            frame->data[i];//        }

        // 将解码后的数据写入文件
        outFile.write((char *) frame->data[0], frame->linesize[0]);
    }}void AacDecodeThread::aacDecode(const char *inFilename, AudioDecodeSpec &out){
    // 返回结果
    int ret = 0;

    // 用来存放读取的输入文件数据(aac)
    // 加上AV_INPUT_BUFFER_PADDING_SIZE是为了防止某些优化过的reader一次性读取过多导致越界
    char inDataArray[IN_DATA_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    char *inData = inDataArray;

    // 每次从输入文件中读取的长度(aac)
    int inLen;
    // 是否已经读取到了输入文件的尾部
    int inEnd = 0;

    // 文件
    QFile inFile(inFilename);
    QFile outFile(out.filename);

    // 解码器
    AVCodec *codec = nullptr;
    // 上下文
    AVCodecContext *ctx = nullptr;
    // 解析器上下文
    AVCodecParserContext *parserCtx = nullptr;

    // 存放解码前的数据(aac)
    AVPacket *pkt = nullptr;
    // 存放解码后的数据(pcm)
    AVFrame *frame = nullptr;

    // 获取解码器
    codec = avcodec_find_decoder_by_name("libfdk_aac");
    if (!codec) {
        qDebug() << "decoder not found";
        return;
    }

    // 初始化解析器上下文
    parserCtx = av_parser_init(codec->id);
    if (!parserCtx) {
        qDebug() << "av_parser_init error";
        return;
    }

    // 创建上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        qDebug() << "avcodec_alloc_context3 error";
        goto end;
    }

    // 创建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        qDebug() << "av_packet_alloc error";
        goto end;
    }

    // 创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        goto end;
    }

    // 打开解码器
    ret = avcodec_open2(ctx, codec, nullptr);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_open2 error" << errbuf;
        goto end;
    }

    // 打开文件
    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "file open error:" << inFilename;
        goto end;
    }
    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "file open error:" << out.filename;
        goto end;
    }

    while ((inLen = inFile.read(inDataArray, IN_DATA_SIZE)) > 0) {
        inData = inDataArray;

        while (inLen > 0) {
            // 经过解析器解析
            // 内部调用的核心逻辑是:ff_aac_ac3_parse
            ret = av_parser_parse2(parserCtx, ctx,
                                   &pkt->data, &pkt->size,
                                   (uint8_t *) inData, inLen,
                                   AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

            if (ret < 0) {
                ERROR_BUF(ret);
                qDebug() << "av_parser_parse2 error" << errbuf;
                goto end;
            }

            // 跳过已经解析过的数据
            inData += ret;
            // 减去已经解析过的数据大小
            inLen -= ret;

            // 解码
            if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
                goto end;
            }
        }
    }
    decode(ctx, nullptr, frame, outFile);

    // 赋值输出参数
    out.sampleRate = ctx->sample_rate;
    out.sampleFmt = ctx->sample_fmt;
    out.chLayout = ctx->channel_layout;end:
    inFile.close();
    outFile.close();
    av_packet_free(&pkt);
    av_frame_free(&frame);
    av_parser_close(parserCtx);
    avcodec_free_context(&ctx);}void AacDecodeThread::run(){
    AudioDecodeSpec out;
    out.filename = "E:/media/test.pcm";

    aacDecode("E:/media/test.aac", out);

    qDebug() << "采样率:" << out.sampleRate;
    qDebug() << "采样格式:" << av_get_sample_fmt_name(out.sampleFmt);
    qDebug() << "声道数:" << av_get_channel_layout_nb_channels(out.chLayout);}

注意:本文为个人记录,新手照搬可能会出现各种问题,请谨慎使用

码字不易,如果这篇博客对你有帮助,麻烦点赞收藏,非常感谢!有不对的地方文章来源地址https://www.toymoban.com/news/detail-759053.html

到了这里,关于通过libfdk_aac编解码器实现AAC音频和PCM的编解码的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 解码器 | 基于 Transformers 的编码器-解码器模型

    基于 transformer 的编码器-解码器模型是 表征学习 和 模型架构 这两个领域多年研究成果的结晶。本文简要介绍了神经编码器-解码器模型的历史,更多背景知识,建议读者阅读由 Sebastion Ruder 撰写的这篇精彩 博文。此外,建议读者对 自注意力 (self-attention) 架构 有一个基本了解

    2024年02月08日
    浏览(48)
  • Netty编解码器,Netty自定义编解码器解决粘包拆包问题,Netty编解码器的执行过程详解

    当Netty发送或者接收一个消息的时候,就会发生一次数据转换。入站消息会被解码(从字节转换为另一种格式,比如java对象);出站消息会被编码成字节。 Netty 提供一系列实用的编解码器,他们都实现了 ChannelInboundHadnler 或者 ChannelOutboundHandler 接口。在这些类中,channelRead 方

    2023年04月23日
    浏览(43)
  • 【计算机视觉 | 目标检测】术语理解9:AIGC的理解,对比学习,解码器,Mask解码器,耦合蒸馏,半耦合,图像编码器和组合解码器的耦合优化

    AIGC指的是使用人工智能技术自动生成的各类数字内容,包括文本、图像、音频、视频等。它利用机器学习模型进行智能化内容生成。 主要的技术手段包括: 自然语言生成(NLG):使用RNN、GPT等语言模型生成文本。 生成对抗网络(GAN):使用GAN生成高质量图片。 自动语音合成(TTS):使用

    2024年02月04日
    浏览(67)
  • 编码器 | 基于 Transformers 的编码器-解码器模型

    基于 transformer 的编码器-解码器模型是 表征学习 和 模型架构 这两个领域多年研究成果的结晶。本文简要介绍了神经编码器-解码器模型的历史,更多背景知识,建议读者阅读由 Sebastion Ruder 撰写的这篇精彩 博文。此外,建议读者对 自注意力 (self-attention) 架构 有一个基本了解

    2024年02月08日
    浏览(61)
  • 【Transformer系列(1)】encoder(编码器)和decoder(解码器)

    前言 这个专栏我们开始学习transformer,自推出以来transformer在深度学习中占有重要地位,不仅在NLP领域,在CV领域中也被广泛应用,尤其是2021年,transformer在CV领域可谓大杀四方。 在论文的学习之前,我们先来介绍一些专业术语。本篇就让我们先来认识一下encoder和decoder吧!

    2024年03月25日
    浏览(61)
  • 【Netty】Netty 解码器(十二)

    回顾Netty系列文章: Netty 概述(一) Netty 架构设计(二) Netty Channel 概述(三) Netty ChannelHandler(四) ChannelPipeline源码分析(五) 字节缓冲区 ByteBuf (六)(上) 字节缓冲区 ByteBuf(七)(下) Netty 如何实现零拷贝(八) Netty 程序引导类(九) Reactor 模型(十) 工作原理

    2024年02月07日
    浏览(54)
  • 22.Netty源码之解码器

    https://mp.weixin.qq.com/s/526p5f9fgtZu7yYq5j7LiQ Netty 常用解码器类型: ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象; MessageToMessageDecoder 将一种消息类型解码为另外一种消息类型。 自定义一次解码器ByteToMessageDecoder解码器,如果读到的字节大小为4,那么认为读取到了1个完整的数

    2024年02月14日
    浏览(46)
  • 23.Netty源码之内置解码器

    在前两节课我们介绍了 TCP 拆包/粘包的问题,以及如何使用 Netty 实现自定义协议的编解码。可以看到,网络通信的底层实现,Netty 都已经帮我们封装好了,我们只需要扩展 ChannelHandler 实现自定义的编解码逻辑即可。 更加人性化的是,Netty 提供了很多开箱即用的解码器,这些

    2024年02月13日
    浏览(34)
  • flutter 视频解码器fijkplayer使用

           本人做视频监控项目的时候,需要去展示视频流到用户端,一开始使用flutter自带的VideoPlayer播放监控视频,一开始没有发现有什么问题,因为使用多的是Android模拟器,一直没有使用iso模拟器或者真机测试能不能播放,直到开发接近尾声,在ios模拟器上测试的时候发现

    2023年04月10日
    浏览(46)
  • ffmpeg中的avs解码器综述

    最近拿了一个avs的视频流,用硬件可以解码,但是ffmpeg自带的却无法解码。 所以研究了一下,首先看ffmpeg的avs解码器: 可以看到avs有两个,第一个是avs 第二个是cavs. 我们先用avs来解码,解码的视频是通过【 avs编码器 】编码的: 结果发现有问题,尺寸本来是640 360,结果被强

    2024年02月08日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包