FFmpeg_note

FFmpeg第一次使用,总结一下

一、视频解码编解码的一些基本概念

数字图像的基本概念

  1. 数字图像的硬件介绍

    1. 图像输入设备:输入,采样量化,专用处理。(相机、摄像机、扫描仪等)
    • 线阵相机和面阵相机
      • 面阵相机:一次拍摄一个区域,视觉检测中绝大部分应用面阵相机
      • 线阵相机:一次拍摄一行像素,通过移动以及拼接来获取图像,分辨率更高,成像质量更高,价格更贵
    1. 电脑:数字图像处理(PC机,服务器集群,硬件电路等)
    2. 图像输出设备:专用处理,D/A转换,输出。(打印机、显示器等)
  2. 数字图像的几个基本概念

    1. 图像的采样和量化
      • 数字化坐标值称为采样
      • 数字化幅度值称为量化
    2. 图像的分辨率
      • 采样的程度通常用采样率来表示,也就是通常所说的分辨率。分辨率160 × 128 的意思是水平像素数为160个,垂直像素数128个。分辨率越高,像素的数目越多,感受的图像越精密
    3. 图像的灰度级
      • 最常见的图像为8位图像,灰度级为256级,即2的8次方。
      • 灰度级越多,可以展现的图像细节就越多,有时候也把灰度级称为灰度分辨率。
    4. 图像的坐标系
      • 在图像中,如果要表示图像中的某一个像素,可以用它的坐标来表示
      • 图像原点为图像的左上角,坐标记做[0,0]
      • 一副M × N的图像可以用一个矩阵来表示
    5. 像素的空间关系:8-邻接和4-邻接
  3. 数字图像的种类和色彩模型

    1. 图像的种类
      1. 二值图像:
        • 像素取值仅为0和1,“0“代表黑色,”1“代表白色。通常用来表示状态,如区分图像中的前景和背景
      2. 灰度图像:
        • 像素取值范围为[0,255],”0”表示纯黑,“255”表示纯白色,一些图像算法中需要使用灰度图进行运算
      3. 彩色图像:
        • 显示设备通常使用RGB格式的彩色图像,即红(red)绿(green)蓝(blue)三种颜色的组合叠加起来获得各种颜色。
        • 如果把RGB值看做是3个维度的坐标,构建的空间称为RGB色彩空间
        • 除了RGB外,常见的色彩模型还有HSV/HSI(数字图像算法常用),CMYK(主要用于印刷),YUV(用于图像传输)
    2. 色彩模型:通过数学模型表示颜色,所用的数学模型即颜色模型。
      1. CMYK色彩模型
        • 印刷业通过青(C)、品(M)、黄(Y)三原色油墨的不同网点面积率的叠印来表现颜色,一般采用青(C)、品(M)、黄(Y)、黑(BK)四色印刷。
        • CMYK可以看做是从黑色中减少颜色得到新的颜色,故可以称之为减色模型。而RGB是在白色上叠加颜色得到新的颜色,故称为加色模型。
      2. HSV色彩模型
        • HSV即色相(Hue)、饱和度(Saturation)、明度(Value),又称HSI(I即intensity)。常用于图像算法中的色彩分析,对光照具有较强的鲁棒性。
        • H:用角度表示,从红色开始按逆时针方向计算,红色为0度,绿色为120度,蓝色为240度,该值表示颜色接近于哪种纯色值
        • S:通常取值范围为0%~100%。圆锥的中心为0,该值越大表示颜色越饱满,直观的说即颜色深而艳
        • V:亮度,表示颜色的明亮程度
  4. 图像直方图

    1. 图像的直方图
      • 直方图(histogram)是图像处理中的一个非常重要工具,被广泛应用。直方图本质是概率分布的图形化,同时直方图也可以用来表示向量。
      • 直方图的作用
        • 图像匹配:比较两幅图像的直方图,可以得到两幅图像的相似程度,其本质是对比灰度出现的概率是否相似
        • 判断成像质量
        • 二值化阈值:所谓二值化即通过设置一个门限值,把灰度图像转换为二值化图像,通常的目的是分离前景和背景。

视频数据的一些基本概念

  1. 音视频领域早起采用模拟化技术,目前已发展为数字化技术。数字化后,音视频处理就进入了计算机技术领域,音视频处理本质上就是对计算机数据的处理
  2. 帧内编码:帧内编码是空间域编码,利用图像空间冗余度进行图像压缩,处理的是一副独立的图像,不会跨越多幅图像。空间域编码依赖于一幅图像中相邻像素间的相似性和图案区的主要空间域频率。(JPEG标准用于静止图像,即图片,只是用了空间域压缩,只是用帧内编码。)
    • 熵与冗余
      • 在所有的实际节目素材中,存在这两种类型的信号分量:即异常的、不可预见的信号分量和可以预见的信号分量。
      • 异常分量称为,它是信号中的真正信息
      • 其余部分称为冗余,因为它不是必须的信息。
        • 冗余可以是空间性的,例如在图像的大片区域中,邻近像素几乎具有相同的数值。
        • 冗余也可以是时间性的,例如连续图像之间的相似部分。
      • 在所有的压缩系统编码器中都是将熵与冗余分离,只有熵被编码和传输,而在解码器中再从编码器的发送的信号中计算出冗余。
  3. 帧间编码,是时间域编码,利用一组连续图像间的时间性冗余度进行图像压缩。如果某帧图像可被解码器使用,那么解码器只需利用两帧图像的差异即可得到下一阵图像。
    • 比如运动平缓的几帧图像的相似性大,差异性小,而运动剧烈的几幅图像则相似性小,差异性大。当得到一帧完整的图像信息后,可以利用与后一帧图像的差异值推算得到后一帧图像,这样就实现了数据量的压缩。时间域编码依赖于连续图像帧间的相似性,尽可能利用已接受的图像来“预测”生成当前图像。(MPEG标准用于运动图像,即视频,会使用空间域编码和时间域编码,因此是帧内编码和帧间编码结合使用)
  4. I帧:I帧(Intra-coded picture,帧内编码帧,常称为关键帧)包含一副完整的图像信息,属于帧内编码图像,不含运动矢量,在解码时不需要参考其他帧图像。因此,在I帧图像出可以切换频道,而不会导致图像丢失或无法解码。I帧图像用于组织误差的累计和扩散。在闭合式GOP中,每个GOP的第一个帧一定是I帧,且当前GOP的数据不会参考前后GOP的数据。
    • 运动矢量:一组连续图像记录了目标的运动。运动矢量用于衡量两帧图像间目标的运动程度运动矢量由水平位移和垂直位移二者构成
    • 闭合式GOP:
      • GOP(Group Of Pictures,图像组)是一组连续的图像,由一个I帧和多个B/P帧组成,是编解码存取的基本单位。GOP结构常用的两个参数M和N:M指定GOP中两个anhor frame(anchor frame指可被其他帧参考的帧,即I帧或P帧)之间的距离;N指定一个GOP的大小,例如M=2,N=13,GOP结构为:IBBPBBPBBPBB
      • B帧:(Bi-directionally predicted picture,双向预测编码图像帧),是帧间编码帧,利用之前和(或)之后的I帧或P帧进行双向预测编码。**B帧不可以作为参考帧。**B帧具有更高的压缩率,但需要更多的缓冲时间以及更高的CPU占用率,因此B帧适合本地存储以及视频点播,而不是用对实时性要求较高的直播系统。
        • 双向预测编码:
          • 连续的三幅图像中,目标块有垂直位置上的移动,背景块无位置移动,考虑如何得到当前帧图像(画面N):
            • 画面N中,目标向上移动后,露出背景块
            • 画面N-1中,因为背景块被目标快遮挡住了,因此没有背景块相关信息。
            • 画面N+1中,完整包含背景块的数据,因此画面N可以从画面N-1中取得背景块。
          • 如何可以得到画面N?
            • 解码器可以先解码得到画面N-1和画面N+1,通过画面N-1中的目标块数据结合运动矢量即可得到画面N中的目标块数据,通过画面N+1中的背景块数据则可得到画面N中的背景块数据。
            • 三幅画面的解码顺序为:N-1,N+1,N
            • 三幅画面的显示顺序为N-1,N,N+1
          • 画面N通过其前一幅画面N-1和后一副画面N+1推算(预测,predicted)得到,因此这种方式称为双向预测(或前向预测,双向参考)
      • P帧:(Predictive-coded picture,预测编码图像帧),是帧间编码帧,利用之前的I帧或P帧进行预测编码。
      • GOP有两种:闭合式GOP和开放式GOP
        • 闭合式GOP:闭合式GOP只需要参考本GOP内的图像即可,不需要参考前后GOP的数据。这种模式决定了,闭合式GOP的显示顺序总是以I帧开始,以P帧结束。(闭合式GOP是否一定是以P帧结束?可能未必有此定义,有些视频文件GOP以B帧结束)
        • 开放式GOP:开放式GOP中的B帧解码时可能要用到其前一个GOP或后一个GOP的某些帧。码流里面包含B帧的时候才会出现开放式GOP。

FFmpeg解码涉及的一些基本概念

  1. 封装格式(container format):可以看做是编码流(音频流、视频流等)数据的一层外壳,将编码后的数据存储于此封装格式的文件之内。
    • 封装,又称容器,所谓容器,就是存放内容的器具。 容器的本质就是文件,是特定的视频文件,如mp4,mkv,flv等格式的音视频文件,其内部存放一帧帧的视频信息和音频信息。因此,视频文件内部尝尝包含不止一个信息流,而是包含一组信息流(若干视频流和若干音频流)
      • 所谓信息流,就是随时间分布的信息,比如视频可以看成是一组随时间分布的图片。
      • 视频流中的一个数据元通常被称为一帧(frame),每一种视频流都有属于自己的编解码器(encoder/decoder,在FFmpeg中被简写为coderc),用于说明该种视频流是如何编码和解码的。
      • 数据包(packets)则尝尝指从裸数据解析而来的数据片断。
    • 容器的作用:容器中可以存放音频、视频、字幕流等信息,将这些信息整合在一起,按照特定规则放置在容器中。
  2. 编解码器(Codec):数据帧(原始数据)与数据包(压缩数据)之间的转换工具
    • 数据帧(原始数据)-> 编解码器(编码)-> 数据包(压缩数据)
    • 数据包(压缩数据)-> 编解码器(解码)-> 数据帧(原始数据)
  3. 媒体流(Stream):时间上的一段连续数据。一段声音数据,称为音频流;一段视频数据,称为视频流;一段字母数据,称为字幕流
  4. 数据帧(Data Frame):媒体流,由若干数据帧构成;压缩格式中,数据帧是最小的处理单元。
    • 在容器中,如果有多个数据流,那么视频帧、音频帧、字母信息、交错存储,以保证实时性
    • 数据帧是未压缩的原始数据,如:视频帧每一帧都是一张完整的YUV图片,音频帧是PCM格式的。
  5. 数据包(Data Packet):将数据帧压缩后就是数据包,数据帧是未压缩的原始数据,数据包是压缩后的数据。
  6. 复用(Mux):将不通的媒体流,按照一定规则放入容器,复用的关键工具是:复用器(Muxer)
  7. 解复用(Demux):从容器中解析不通的流出来,解复用的关键工具是:解复用器(Demuxer)

FFmpeg开发环境构建

  1. ffmpeg官网:https://www.ffmpeg.org/
  2. SDL(Simple DirectMedia Layer):是一套开源的跨平台多媒体开发库。SDL提供了书中控制图像、声音、输出输入的函数,封装了复杂的音视频底层操作,简化了音视频处理的难度。目前SDL多用于开发游戏、模拟器、媒体播放器等多媒体应用领域。官网: https://www.libsdl.org/
  3. yasm/nasm
    • 旧版ffmpeg及x264/x265使用yasm汇编器
    • 新版ffmpeg及x264/x265改用nasm汇编器
      • NASM(Netwide Assembler),是一款基于英特尔x86架构的汇编与反汇编工具。官网:https://www.nasm.us/
  4. x264:是开源的h264编码器,使用非常广泛。ffmpeg工程中实现了h264解码器,但是没有264编码器。因此需要安装第三方编码器x264。官网:https://www.videolan.org/developers/x264.html
  5. x265:是开源的h265编码器。ffmpeg工程中实现了h265解码器,但是没有h265编码器,因此需要安装第三方编码器x265.官网:https://www.x265.org/ 下载地址:https://www.videolan.org/developers/x265.html
  6. libmp3lame:是开源的mp3编码器。官网:https://lame.sourceforge.net/
  7. librtmp:RTMPDump Read-Time Messaging Protocol API,又成rtmpdump,是用于处理RTMP流的工具。支持所有形式的RTMP,文档:https://rtmpdump.mplayerhq.hu/librtmp.3.html 官网:https://rtmpdump.mplayerhq.hu/

处理音视频流的一般过程:

  1. 打开音视频文件,获取音视频流
  2. 从数据流中读取数据帧
  3. 如果数据帧不完整,就回到第二步
  4. 处理数据帧
  5. 回到第二步

二、FFmpeg视频解码过程

  1. 注册所支持的所有的文件(容器)格式及其对应的Codec:av_register_all()
  2. 打开输入文件:avformat_open_input()
  3. 解封装,从文件中提取流信息:avformat_find_stream_info()
  4. 查找video stream相对应的解码器:avcodec_find_decoder
  5. 给相应解码器的上下文容器分配内存:avcodec_alloc_context3() avcode_parameters_to_context()
  6. 打开解码器,并初始化解码器的上下文容器:avcodec_open2()
  7. 创建SwsContext对象,并初始化:av_image_fill_arrays(),sws_getContext()
  8. 为解码帧分配内存:av_frame_alloc()
  9. 从流中读取数据到Packet中:av_read_fream()
  10. 发送数据包到解码队列:avcodec_send_packet()
  11. 接受一帧解码数据,并解码:avcodec_receive_frame()
  12. 对视频帧(YUV)进行图像格式转换(RGB):sws_scale()

三、(用到的)结构体的功能和参数简要总结

关键的结构体分类:

  1. 解协议(http,rtsp,rtmp,mms)
    1. AVIOContext
    2. URLProtocol
      1. 存储输入视音频使用的封装格式,每种协议都对应一个URLProtocol结构
      2. FFmpeg中文件也被当做一种协议“file”
    3. URLContext
      1. 主要存储视音频使用的协议的类型
  2. 解封装(flv,avi,rmvb,mp4)
    1. AVFormatContext
      1. 主要存储视音频封装格式中包含的信息
      2. 几个主要变量(解码):
        1. AVIOContext * pb : 输入数据的缓存
        2. unsigned int nb_streams : 视音频流的个数
        3. AVStream **streams:视音频流
        4. char filename[1024]:文件名
        5. int64_t duration:时长(单位:微秒)
        6. int bit_rate:比特率(单位:bps)
    2. AVInputFormat
      1. 存储输入视音频使用的封装格式
      2. 每种视音频封装格式都对应一个AVInputFormat结构
  3. 解码(h264, mpeg2, aac, mp3)
    1. AVStream:
      1. 每一个AVStream存储一个视频/音频流的相关数据
    2. AVCodecContext:
      1. 存储该视频/音频流使用的解码方式的相关数据
      2. 每个AVStream对应一个AVCodecContext
    3. AVCodec:
      1. 包含视频/音频对应的解码器,每种解码器都对应一个AVCodec结构
      2. 每个AVCodecContext中对应一个AVCodec
  4. 存数据
    1. 视频:每个结构一般都是存一帧,音频可能是一帧,也可能是多帧
    2. 解码前的数据格式:AVPacket
    3. 解码后的数据格式:AVFrame

一些相关的类及其参数

  1. int av_image_get_buffer_size(enum AVPixelFormat pix_fmt, int width, int height, int align);
    1. 函数的作用:通过指定像素格式、图像宽、图像高来计算所需要的内存大小
    2. 重要参数:
      1. “align:设定内存对齐的对齐数,也就是按多大的字节对齐”
  2. int av_image_alloc(uint8_t* pointers[4], int linesize[4], int w, int h, enum AVPixelFormat pix_fmt, int align);
    1. 函数作用:按计算的内存大小申请所需要的内存
    2. 参数:
      1. pointers[4]:保存图像通道的地址。如果是RGB,则前三个指针分别指向R,G,B的内存地址,第四个保留不用
      2. linesize[4]:保存图像每个通道的内存对齐的步长,即一行的对齐内存的宽度,此值的大小等于图像的宽度
      3. w:要申请内存的图像宽度
      4. h:要申请内存的图像高度
      5. pix_fmt:要申请内存的图像的像素格式
      6. align:用于内存对齐的值
      7. @return:所申请的内存空间的总大小,如果为负值,申请失败
  3. int av_image_fill_arrays(uint8_t ** dst_data[4], int dst_linesize[4], const uint8_t *src, enum AVPixelFormat pix_fmt, int width, int height, int align);
    1. 函数作用:
      1. org:
        1. Setup the data pointers and linesizes based on the specified image parameters and provided array.(根据指定的图像参数和提供的数据设置数据指针和行宽的大小)The fields of the given image are filled in by using the src address which points to the image data buffer. Depending on the specified pixel format, one or multiple image data pointers and line sizes will be set.(给定图像的字段是使用指向图像数据缓冲区的src地址填充的,根据指定的像素格式,将设置一个或多个图像数据指针和行宽大小)
    2. 参数:
      1. dst_data[4]:data pointers to be filled in.(要填充的数据指针)对申请的内存格式分为三个通道后,分别保存其地址
      2. dst_linesize[4]:linesizes for the image in dst_data to be filled in.(dst_data中要填充的图像的行宽大小)格式化的内存步长(即内存对齐后的宽度)
      3. src:buffer which will contain or contains the actual image data can be NULL.(将包含或包含实际图像数据的缓冲区)av_alloc()函数申请的内存地址
      4. pix_fmt:the pixel format of the image.(图像的像素格式)申请src内存时的像素格式
      5. width:the width of the image in pixels. (图像的宽度,以像素为单位)申请src的宽度
      6. height:the height of the image in pixels. (图像的高度,以像素为单位)申请src的高度
      7. align:the value use in src for linesize alignment.(src中用于行大小对齐的值)用于内存对齐的值
      8. return:the size in bytes required for src.(src所需的大小,以字节为单位 )
  4. int avcodec_send_packet(AVCodecContext* avctx, const AVPacket* avpkt);
    1. 函数功能:Supple raw packet data as input to a decoder.(提供原始数据包数据做为解码器的输入)Internally, this call will copy relevant AVCodecContext fields, which can influence decoding per-packet, and apply them when the packet is actually decoded.(在内部,此调用将复制相关的AVCodecContext字段,这些字段可能会影响到每个数据包的解码,并在数据包实际解码时应用这些字段)
  5. int avcodec_receive_frame(AVCodecContext* acvtx, AVFrame* frame)
    1. 函数功能:Return decoded output data from a decoder.(从解码器返回解码数据)
    2. 参数:
      1. avctx: codec context.(编解码器上下文)
      2. frame: This will be set to a reference-counted video or audion frame(depending on the decoder type) allocated by the decoder.(这将被设置为解码器分配的参考计数视频或音频帧,取决于解码器类型)Note that the function will always call av_frame_unref(frame) before doing anything else.(在调用av_frame_unref函数之前,它可以做任何其他的事情。)
  6. int sws_scale(struct SwsContext* c, const uint8_t* const srcSlice[], const int srcStride[], int srcSliceY, int srcSliceH, uint8_t* const dst[], const int dstStride[]);
    1. 函数功能:Scale the image slice in srcSlice and put the resulting scaled slice in the image in dst.(在srcSlice中缩放图像切片,并将生成缩放切片放在dst中的图像)A slice is a sequence of consecutive rows in an image.(一个切片是图像中连续行的一个序列)Slice have to be provided in sequential order, either in top-bottom or bottom-top order. If slices are provided in non-sequential order the behavior of the function is undefined.(切片必须是按顺序提供的,可以是上下顺序,也可以是下上顺序。如果切片以非顺序提供,则函数的行为未定义)
    2. 参数:
      1. c:the scaling context previously created with sws_getContext
      2. srcSlice: the array containing the pointers to the planes of the source slice.(包含指向源切片平面的指针的数组)
      3. srcStride: the array containing the stride for each plane of the source image.(包含源图像的每个平面的步长的数组)
      4. srcSliceY: the position in the source image of the slice to process, that is the number(couted starting from zero) in the image of the first row of the slice.(要处理的切片的源图像的位置,即切片第一行图像张的数字(从零开始计数))
      5. srcSliceH: the height of the source slice, that is the number of rows in the slice.(源切片的高度,即切片中的行数)
      6. dst: the array containing the pointers to the planes of the destination image.(包含指向目标图像平面的指针的数组)
      7. dstStride : the array containing the strides for each plane of the destination image.(包含目标图像的每个平面的步长的数组)
      8. @return:the height of the output slice.(输出切片的高度)
补充
  1. 字节对齐、内存对齐
    1. 现代计算机中内存空间都是按照byte划分的,从理论上将似乎对任何类型的变量额访问可以从任何地址开始,但是实际上的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常是4或者8)的背书,这就是所谓的内存对齐
    2. 图像在存储或传输的过程中,一般一个行宽会是某个数的倍数,行宽一般是以字节为单位的,所以便有字节对齐。(例如:一个图像的行宽为1023,想向1024对齐,差的1个字节,就是字节对齐数)
      1. 行宽 = 宽度 × 通道数
      2. 高度 = 就是行数,因为每一行是在高度上是1
      3. 那么申请的内存大小为:height * width * channels
      4. 图像在电脑里存储的是二维数据,也就是数字矩阵。(不管是1通道还是3通道,都是以数字矩阵(二维)方式存储,不通的是一个像素是对应一个值还是三个值)
      5. 而现实中,对图像存储的描述,可以是立体的,例如三通道的图像,就是立方体,长是行宽,宽是高度,高是通道数。(这样一说,好像更迷了哈哈哈,但它就是立方体,第三个坐标轴代表的是通道数
  2. AVFrame结构体中的几个参数:
    1. width、height:Video dimensions. Video frames only. The coded dimensions(in pixels) of the video frame, i.e. the size of the rectangle that contains some well-defined values.(仅限视频帧,视频帧的编码尺寸(以像素为单位),即包含一些明确定义的值的矩形大小)
    2. linesize :for video, size in bytes of each picture line.(对于视频,每条图片行的大小(以字节为单位))for video the linesizes should be multiples of the CPUs alignment preference, this is 16 or 32 for modern destop CPUs.(对于视频,线条大小应该是CPU对齐首选项的倍数,对于现代桌面CPU,这是16或32.)
    3. 别人的代码可以读,复制,但是一定要弄清楚每行代码是干什么的,学会阅读头文件里的注释,最终还是要看官方的文档。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
敬告:该系列的课程在抓紧录制更新中,敬请大家关注。敬告: 该系列的课程涉及:FFmpeg,WebRTC,SRS,Nginx,Darwin,Live555,OpenCV,等。包括:音视频、流媒体、直播、Android、视频监控28181、等。 我将带领大家一起来学习OpenCV4的图像处理原理和编程知识,并动手操练58案例代码。具体内容包括: 一、小白入门与初体验:禁果尝鲜二、图像基本操作:懵懵懂懂学图像三、图像统计操作:七七八八有收获四、图像卷积:不入虎穴焉得虎子五、磨皮美颜:柳暗花明又一村六、二值图像:阴阳合一法自然七、图像形态学:登高望远天地阔 音视频与流媒体是一门很复杂的技术,涉及的概念、原理、理论非常多,很多初学者不学 基础理论,而是直接做项目,往往会看到c/c++的代码时一头雾水,不知道代码到底是什么意思,这是为什么呢? 因为没有学习音视频和流媒体的基础理论,就比如学习英语,不学习基本单词,而是天天听英语新闻,总也听不懂。所以呢,一定要认真学习基础理论,然后再学习播放器、转码器、非编、流媒体直播、视频监控、等等。 梅老师从事音视频与流媒体行业18年;曾在永新视博、中科大洋、百度、美国Harris广播事业部等公司就职,经验丰富;曾亲手主导广电直播全套项目,精通h.264/h.265/aac,曾亲自参与百度app上的网页播放器等实战产品。目前全身心自主创业,主要聚焦音视频+流媒体行业,精通音视频加密、流媒体在线转码快编等热门产品。     
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张张张张张#张#

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值