当前位置:   article > 正文

qt下ffmpeg录制mp4经验分享,支持音视频(h264、h265,AAC,G711 aLaw, G711muLaw)_qt h265

qt h265

前言

    MP4,是最常见的国际通用格式,在常见的播放软件中都可以使用和播放,磁盘空间占地小,画质一般清晰,它本身是支持h264、AAC的编码格式,对于其他编码的话,需要进行额外处理。本文提供了ffmpeg录制mp4的封装代码,经测试视频上它支持h264、h265编码,音频支持了AAC、G711的aLaw、muLaw编码。对于以上编码的支持,部分是需要修改ffmpeg的源码,本文也有提供已编译好的ffmpeg以及说明源码上需要修改的地方。

一、时间戳处理

    在mp4录制中,有碰到一个问题,即在录制实时流后,用播放器进行播放,播放时间没有从0秒开始。windows自带的media play播放时,一开始都是静止的画面,从第n秒后,才开始正式播放,用VLC可以直接跳到n秒进行播放。这个问题的原因是时间戳没有处理好,需要记录下首帧,指定首帧时间戳为0,然后后续视频帧的时间戳等于当前帧的时间戳减去首帧时间戳。代码如下:
在这里插入图片描述

二、添加h264、h265、AAC解码头信息

    解码头信息是保存在解码器上下文(AVCodecContext)的extradata中,这些信息包含h264的SPS、PPS头信息,AAC的adts头信息,h265的VPS、SPS、PPS,我们需要使用比特流过滤器(AVBitStreamFilter)来为每一种格式添加相应的头部信息,这样才能在解码器中正常进行解码。以下为添加解码头信息的相关代码:
    初始化时视频:
在这里插入图片描述
    循环读帧中,视频:
在这里插入图片描述
    初始化时音频:
在这里插入图片描述
    循环读帧中,音频:
在这里插入图片描述

三、ffmpeg支持g711 aLaw muLaw

在ffmpeg源码movenc.c文件中,找到mov_write_audio_tag函数,修改以下:
在这里插入图片描述
和在该文件中增加以下:
在这里插入图片描述
muLaw修改类似,它的MKTAG为 ‘u’,‘l’, ‘a’,‘w’。

四、代码分享

mp4recorder.h

#ifndef MP4RECORDER_H
#define MP4RECORDER_H

extern "C"
{
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libavfilter/avfilter.h"
    #include "libswscale/swscale.h"
    #include "libavutil/frame.h"
    #include "libavutil/imgutils.h"
    #include "libavcodec/bsf.h"
}

#include <QObject>
#include <QMutex>

class mp4Recorder : public QObject
{
    Q_OBJECT
public:
    explicit mp4Recorder(QObject *parent = nullptr);
    virtual ~mp4Recorder();

    bool Init(AVFormatContext *pIfmtCtx, int nCodecType, int nAudioCodecType, QString& sFile);
    bool DeInit();
    bool isInit() {return m_bInit;}
    bool saveOneFrame(AVPacket& pkt, int nCodecType, int nAudioCodecType);

private:
    uint64_t         m_nCounts;
    bool             m_bFirstGoP;
    bool             m_bInit;
    QString          m_sRecordFile;
    AVFormatContext *m_pIfmtCtx;
    AVFormatContext *m_pOfmtCtx; // output stream format. copy from instream format.
    const AVOutputFormat  *m_pOfmt; // save file format.
    QMutex           m_lock;
    int64_t          m_nVideoTimeStamp;
    int              m_nVideoDuration;
    int              m_nVideoIndex = -1;
    int              m_nAudioIndex = -1;
    int				 m_nSpsPpsSize = 0;
    AVBSFContext    *m_pBsfc = nullptr;
    AVBSFContext    *m_pBsfcAAC = nullptr;
    AVPacket        *m_pktFilter = nullptr;
    AVPacket        *m_pktFilterAudio = nullptr;
    int64_t         m_nFirstVideoPts = 0;
    int64_t         m_nFirstAudioPts = 0;
    bool            m_bTransCode = false;

    // stream map.
    int  *m_pStreamMapping;
    int   m_nMappingSize;

};

#endif // MP4RECORDER_H
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

mp4recorder.cpp

#include "mp4recorder.h"
#include "commondef.h"
#include "cteasyaacencoder.h"

#define TRANSCODE 0

mp4Recorder::mp4Recorder(QObject *parent) : QObject(parent)
{
    QMutexLocker guard(&m_lock);
    m_sRecordFile.clear();
    m_pIfmtCtx = nullptr;
    m_pOfmtCtx = nullptr;
    m_pOfmt = nullptr;
    m_pStreamMapping = nullptr;
    m_nMappingSize = 0;
    m_nCounts = 0;
    m_bFirstGoP = false;
    m_bInit = false;
}

mp4Recorder::~mp4Recorder()
{
    DeInit();
}

bool mp4Recorder::Init(AVFormatContext *pIfmtCtx, int nCodecType, int nAudioCodecType, QString &sFile)
{
    QMutexLocker guard(&m_lock);
    if(!pIfmtCtx || sFile.isEmpty())
    {
        MY_DEBUG << "sFile.isEmpty().";
        return false;
    }

    m_sRecordFile = sFile;
    m_pIfmtCtx = pIfmtCtx;

    QByteArray ba = m_sRecordFile.toLatin1();
    const char* pOutFile = ba.data();

    qDebug() << "pOutFile:" << pOutFile;

    unsigned i = 0;
    int ret = 0;
    int stream_index = 0;

    // 1. create output context
    avformat_alloc_output_context2(&m_pOfmtCtx, nullptr, nullptr, pOutFile);
    if (!m_pOfmtCtx)
    {
        MY_DEBUG << "Could not create output context.";
        ret = AVERROR_UNKNOWN;
        goto end;
    }

    // 2. get memory.
    m_nMappingSize = pIfmtCtx->nb_streams;
    m_pStreamMapping = (int*)av_mallocz_array(m_nMappingSize, sizeof(*m_pStreamMapping));
    if (!m_pStreamMapping)
    {
        MY_DEBUG << "av_mallocz_array fail.";
        ret = AVERROR(ENOMEM);
        goto end;
    }

    // 3. copy steam information.
    m_pOfmt = m_pOfmtCtx->oformat;
    for (i = 0; i < pIfmtCtx->nb_streams; i++)
    {
        AVStream *pOutStream;
        AVStream *pInStream = pIfmtCtx->streams[i];
        AVCodecParameters *pInCodecpar = pInStream->codecpar;
        if (pInCodecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
            pInCodecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
            pInCodecpar->codec_type != AVMEDIA_TYPE_SUBTITLE)
        {
            m_pStreamMapping[i] = -1;
            continue;
        }

        if(pInCodecpar->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            m_nVideoIndex = i;

            //1.找到相应解码器的过滤器
            if(nCodecType == AV_CODEC_ID_HEVC)
            {
                const AVBitStreamFilter *bsf = av_bsf_get_by_name("hevc_mp4toannexb");
                if (!bsf)
                {
                    MY_DEBUG << "av_bsf_get_by_name() video failed";
                    return false;
                }
                //2.过滤器分配内存
                av_bsf_alloc(bsf, &m_pBsfc);
            }
            else
            {
                const AVBitStreamFilter *bsf = av_bsf_get_by_name("h264_mp4toannexb");
                if (!bsf)
                {
                    MY_DEBUG << "av_bsf_get_by_name() video failed";
                    return false;
                }
                //2.过滤器分配内存
                av_bsf_alloc(bsf, &m_pBsfc);
            }

            //3.添加解码器属性
            avcodec_parameters_copy(m_pBsfc->par_in, pInCodecpar);

            //4. 初始化过滤器上下文
            av_bsf_init(m_pBsfc);
        }
        else if(pInCodecpar->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            m_nAudioIndex = i;

#if TRANSCODE
            if(nAudioCodecType == AV_CODEC_ID_PCM_ALAW || nAudioCodecType == AV_CODEC_ID_PCM_MULAW)
            {
                MY_DEBUG << "ctEasyAACEncoder Init";
                if(nAudioCodecType == AV_CODEC_ID_PCM_ALAW)
                    ctEasyAACEncoder::getInstance().Init(Law_ALaw);
                else
                    ctEasyAACEncoder::getInstance().Init(Law_ULaw);
                m_bTransCode = true;
            }
            else
                m_bTransCode = false;

#endif
            if(m_bTransCode || nAudioCodecType == AV_CODEC_ID_AAC)
            {
                //1. 找到相应解码器的过滤器
                const AVBitStreamFilter *bsf = av_bsf_get_by_name("aac_adtstoasc");
                if (!bsf)
                {
                    MY_DEBUG << "av_bsf_get_by_name() audio failed";
                    return false;
                }

                //2.过滤器分配内存
                av_bsf_alloc(bsf, &m_pBsfcAAC);

                //3.添加解码器属性
                avcodec_parameters_copy(m_pBsfcAAC->par_in, pInCodecpar);

                //4. 初始化过滤器上下文
                av_bsf_init(m_pBsfcAAC);
            }

#if TRANSCODE
            if(m_bTransCode)
                m_pBsfcAAC->par_in->codec_id = AV_CODEC_ID_AAC;
#endif

        }

        // fill the stream index.
        m_pStreamMapping[i] = stream_index++;

        // copy the new codec prameters.
        pOutStream = avformat_new_stream(m_pOfmtCtx, nullptr);
        if (!pOutStream)
        {
            MY_DEBUG << "Failed allocating output stream";
            ret = AVERROR_UNKNOWN;
            goto end;
        }

        ret = avcodec_parameters_copy(pOutStream->codecpar, pInCodecpar);
        if (ret < 0)
        {
            MY_DEBUG << "Failed to copy codec parameters";
            goto end;
        }
#if TRANSCODE
        if(m_bTransCode && pInCodecpar->codec_type == AVMEDIA_TYPE_AUDIO)
            pOutStream->codecpar->codec_id = AV_CODEC_ID_AAC;
#endif

        //pOutStream->codecpar->bit_rate = 2000000;
        //pOutStream->codecpar->codec_tag = 0;
    }

    // 4. create MP4 header.
    if (!(m_pOfmt->flags & AVFMT_NOFILE)) // network stream
    {
        ret = avio_open(&m_pOfmtCtx->pb, pOutFile, AVIO_FLAG_WRITE);
        if (ret < 0)
        {
            MY_DEBUG << "Could not open output file " << m_sRecordFile;
            goto end;
        }
    }
    // 5. write file header.
    ret = avformat_write_header(m_pOfmtCtx, nullptr);
    if (ret < 0)
    {
        MY_DEBUG << "Error occurred when opening output file ret:" << ret;
        goto end;
    }

    m_pktFilter = new AVPacket;
    av_init_packet(m_pktFilter);
    m_pktFilter->data = NULL;
    m_pktFilter->size = 0;

    m_pktFilterAudio = new AVPacket;
    av_init_packet(m_pktFilterAudio);
    m_pktFilterAudio->data = NULL;
    m_pktFilterAudio->size = 0;

    m_nFirstVideoPts = 0;
    m_nFirstAudioPts = 0;


    m_bFirstGoP = false;
    m_bInit = true;
    m_nCounts = 0;

    return true;

end:
    DeInit();
    if (ret < 0 && ret != AVERROR_EOF)
    {
        MY_DEBUG << "Error occurred.";
    }
    return false;
}

bool mp4Recorder::DeInit()
{
    // 1. save tail.
    if(m_bInit && m_pOfmtCtx)
    {
        av_write_trailer(m_pOfmtCtx);
    }
    m_bInit = false;

    // 2. close output
    if (m_pOfmtCtx && !(m_pOfmt->flags & AVFMT_NOFILE))
    {
        avio_closep(&m_pOfmtCtx->pb);
    }

    // 3. free contex.
    if(m_pOfmtCtx)
    {
        avformat_free_context(m_pOfmtCtx);
        m_pOfmtCtx = nullptr;
    }
    av_freep(&m_pStreamMapping);

    if(m_pBsfc)
    {
        av_bsf_free(&m_pBsfc);
        m_pBsfc = nullptr;
    }

    if(m_pBsfcAAC)
    {
        av_bsf_free(&m_pBsfcAAC);
        m_pBsfcAAC = nullptr;
    }

#if TRANSCODE
    if(m_bTransCode)
    {
        ctEasyAACEncoder::getInstance().DeInit();
        m_bTransCode = false;
    }
#endif

    return true;
}

bool mp4Recorder::saveOneFrame(AVPacket &pkt, int nCodecType, int nAudioCodecType)
{
    int ret = 0;
    if(!m_bInit)
    {
        return false;
    }
    AVStream *pInStream, *pOutStream;

    if(nCodecType == AV_CODEC_ID_H264)
    {
        if(m_bFirstGoP == false)
        {
            if(pkt.flags != AV_PKT_FLAG_KEY)
            {
                av_packet_unref(&pkt);
                return false; // first frame must be Iframe.
            }
            else
            {
                m_bFirstGoP = true;
            }
        }
    }

    pInStream  = m_pIfmtCtx->streams[pkt.stream_index];
    if (pkt.stream_index >= m_nMappingSize ||
        m_pStreamMapping[pkt.stream_index] < 0)
    {
        av_packet_unref(&pkt);
        return true;
    }

    pkt.stream_index = m_pStreamMapping[pkt.stream_index];
    pOutStream = m_pOfmtCtx->streams[pkt.stream_index];

    if(pInStream->codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
            pInStream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
    {
        av_packet_unref(&pkt);
        return false;
    }

    if(pInStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
    {
        av_bsf_send_packet(m_pBsfc, &pkt);
        av_bsf_receive_packet(m_pBsfc, m_pktFilter);

        m_pktFilter->pts = av_rescale_q_rnd(m_pktFilter->pts, pInStream->time_base, pOutStream->time_base, AV_ROUND_NEAR_INF);
        m_pktFilter->dts = av_rescale_q_rnd(m_pktFilter->dts, pInStream->time_base, pOutStream->time_base, AV_ROUND_NEAR_INF);
        m_pktFilter->duration = av_rescale_q_rnd(m_pktFilter->duration, pInStream->time_base, pOutStream->time_base, AV_ROUND_NEAR_INF);
        m_pktFilter->stream_index = pOutStream->index;


        //时间戳处理
        if(m_nFirstVideoPts == 0)
        {
            m_nFirstVideoPts = m_pktFilter->pts;
            m_pktFilter->pts = 0;
            m_pktFilter->dts = 0;
        }
        else
        {
            m_pktFilter->pts = m_pktFilter->pts - m_nFirstVideoPts;
            m_pktFilter->dts = m_pktFilter->dts - m_nFirstVideoPts;
        }

        //av_packet_rescale_ts(&pkt, pInStream->time_base, pOutStream->time_base);
        m_pktFilter->pos = -1;
        m_pktFilter->flags |= AV_PKT_FLAG_KEY;

        ret = av_interleaved_write_frame(m_pOfmtCtx, m_pktFilter);
        av_packet_unref(&pkt);
        if (ret < 0)
        {
            qDebug() << "Video Error muxing packet";
        }
    }
    else
    {
#if TRANSCODE

        if(m_bTransCode)
        {
            AVPacket* pAACPkt = av_packet_clone(&pkt);
            if(ctEasyAACEncoder::getInstance().G711ToAAC(pkt.data, pkt.size, pAACPkt->data, pAACPkt->size) == false)
            {
                av_packet_unref(&pkt);
                return false;
            }

            av_bsf_send_packet(m_pBsfcAAC, pAACPkt);
            av_bsf_receive_packet(m_pBsfcAAC, m_pktFilterAudio);
        }
        else
#endif
        if(m_bTransCode || nAudioCodecType == AV_CODEC_ID_AAC)
        {
            av_bsf_send_packet(m_pBsfcAAC, &pkt);
            av_bsf_receive_packet(m_pBsfcAAC, m_pktFilterAudio);

            m_pktFilterAudio->pts = av_rescale_q_rnd(m_pktFilterAudio->pts, pInStream->time_base, pOutStream->time_base, AV_ROUND_NEAR_INF);
            m_pktFilterAudio->dts = av_rescale_q_rnd(m_pktFilterAudio->dts, pInStream->time_base, pOutStream->time_base, AV_ROUND_NEAR_INF);
            m_pktFilterAudio->duration = av_rescale_q_rnd(m_pktFilterAudio->duration, pInStream->time_base, pOutStream->time_base, AV_ROUND_NEAR_INF);
            m_pktFilterAudio->stream_index = pOutStream->index;

            //用差值作时间戳
            if(m_nFirstAudioPts == 0)
            {
                m_nFirstAudioPts = m_pktFilterAudio->pts;
                m_pktFilterAudio->pts = 0;
                m_pktFilterAudio->dts = 0;
            }
            else
            {
                m_pktFilterAudio->pts = m_pktFilterAudio->pts - m_nFirstAudioPts;
                m_pktFilterAudio->dts = m_pktFilterAudio->dts - m_nFirstAudioPts;
            }

            m_pktFilterAudio->pos = -1;
            m_pktFilterAudio->flags |= AV_PKT_FLAG_KEY;

            ret = av_interleaved_write_frame(m_pOfmtCtx, m_pktFilterAudio);
        }
        else
        {
            pkt.pts = av_rescale_q_rnd(pkt.pts, pInStream->time_base, pOutStream->time_base, AV_ROUND_NEAR_INF);
            pkt.dts = av_rescale_q_rnd(pkt.dts, pInStream->time_base, pOutStream->time_base, AV_ROUND_NEAR_INF);
            pkt.duration = av_rescale_q_rnd(pkt.duration, pInStream->time_base, pOutStream->time_base, AV_ROUND_NEAR_INF);
            pkt.stream_index = pOutStream->index;

            //用差值作时间戳
            if(m_nFirstAudioPts == 0)
            {
                m_nFirstAudioPts = pkt.pts;
                pkt.pts = 0;
                pkt.dts = 0;
            }
            else
            {
                pkt.pts = pkt.pts - m_nFirstAudioPts;
                pkt.dts = pkt.dts - m_nFirstAudioPts;
            }

            pkt.pos = -1;
            pkt.flags |= AV_PKT_FLAG_KEY;

            ret = av_interleaved_write_frame(m_pOfmtCtx, &pkt);
        }
        av_packet_unref(&pkt);
        if (ret < 0)
        {
            qDebug() << "Audio Error muxing packet";
        }
    }
    return (ret == 0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436

四、ffmpeg库下载

链接地址:https://download.csdn.net/download/linyibin_123/87542123

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/901710
推荐阅读
相关标签
  

闽ICP备14008679号