FFmpeg小白學(xué)習(xí)記錄(五)組合視頻流和音頻流

組合視頻流和音頻流

通過之前視頻流與音頻流編解碼的學(xué)習(xí),我們可以做到將視頻流與音頻流數(shù)據(jù)抽離出來,并將這些數(shù)據(jù)編碼為對(duì)應(yīng)的視頻或音頻。但往往一個(gè)多媒體文件中既包含音頻也包含視頻,所以本次我們學(xué)習(xí)如何通過 FFmpeg 將 圖片+PCM -> mp4 文件

圖片+PCM -> mp4 文件,就是將之前 圖片->MP4 和 PCM->MP3 的流程組合起來,只不過要注意視頻與音頻的pts值設(shè)置

因?yàn)榇a量較多,這里選擇劃分為各個(gè)模塊進(jìn)行講解:基礎(chǔ)模塊、視頻模塊、音頻模塊

基礎(chǔ)模塊

在基礎(chǔ)模塊中主要實(shí)現(xiàn)編碼流程中通用的部分,其余與音頻、視頻有關(guān)的函數(shù)均由視頻模塊、音頻模塊進(jìn)行調(diào)用

extern"C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include"libswresample/swresample.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;

//通過位運(yùn)算實(shí)現(xiàn)選擇測(cè)試功能模塊 1-測(cè)試視頻 2-測(cè)試音頻 3-音視頻
#define mode 3
#define VideoMode 0x01
#define AudioMode 0x02

void rgbPcmtoMp4() {
    int ret;

    //聲明所需的變量名
    AVFormatContext* dstFmtCtx = NULL;
    AVCodec* videoCodec = NULL, * audioCodec = NULL;
    AVCodecContext* vCodecCtx = NULL, * aCodecCtx = NULL;
    AVStream* vStream = NULL, * aStream = NULL;
    //PCM文件
    const char* audioFile = "result.pcm";
    //最終輸出的文件名
    const char* dstFile = "result.mp4";
    FILE* file=NULL;

    do {
        //創(chuàng)建輸出結(jié)構(gòu)上下文 AVFormatContext,會(huì)根據(jù)文件后綴創(chuàng)建相應(yīng)的初始化參數(shù)
        ret = avformat_alloc_output_context2(&dstFmtCtx, NULL, NULL, dstFile);
        if (ret < 0) {
            cout << "Could not create output context" << endl;
            break;
        }

        //打開文件
        ret = avio_open(&dstFmtCtx->pb, dstFile, AVIO_FLAG_READ_WRITE);
        if (ret < 0) {
            cout << "Could not open output file" << endl;
            break;
        }
        
#if mode & VideoMode
        //添加一個(gè)視頻流
        vCodecCtx = addVideoStream(dstFmtCtx, &vStream);
        if (vCodecCtx == NULL) {
            cout << "Could not add video stream" << endl;
            break;
        }
#endif

#if mode & AudioMode
        //添加一個(gè)音頻流
        aCodecCtx = addAudioStream(dstFmtCtx, &aStream);
        if (aCodecCtx == NULL) {
            cout << "Could not add audio stream" << endl;
            break;
        }
#endif
        
        //寫入文件頭信息
        ret = avformat_write_header(dstFmtCtx, NULL);
        if (ret != AVSTREAM_INIT_IN_WRITE_HEADER) {
            cout << "Write file header fail" << endl;
            break;
        }
        
        //打印流信息
        av_dump_format(dstFmtCtx, 0, dstFile, 1);

#if mode & VideoMode
        //編碼視頻流
        ret=encodeVideo(dstFmtCtx,vCodecCtx,vStream);
        if (ret<0) {
            cout << "encodeVideo fail" << endl;
            break;
        }
#endif

#if mode & AudioMode
        fopen_s(&file,audioFile,"rb");
        if (file==NULL) {
            cout << "open audio file fail" << endl;
            break;
        }
        //編碼音頻流
        ret=encodeAudio(dstFmtCtx,aCodecCtx,aStream, file);
        if (ret < 0) {
            cout << "encodeAudio fail" << endl;
            break;
        }
#endif
        //向文件中寫入文件尾部標(biāo)識(shí),并釋放該文件
        av_write_trailer(dstFmtCtx);

    } while (0);

    //釋放資源
    if (dstFmtCtx) {
        avformat_free_context(dstFmtCtx);
        avio_closep(&dstFmtCtx->pb);
    }
    if (vCodecCtx) avcodec_free_context(&vCodecCtx);
    if (aCodecCtx) avcodec_free_context(&aCodecCtx);
    if (file) fclose(file);
}

代碼中的方法在之前都有介紹過,這里不再闡述

視頻模塊

視頻模塊中又分為 初始化編碼 兩個(gè)部分,視頻模塊中與之前的視頻流編碼沒有區(qū)別,可以當(dāng)作復(fù)習(xí)再看一遍

初始化

初始化中主要工作為為文件添加一個(gè)視頻流,并設(shè)置相關(guān)的參數(shù),為之后的編碼作準(zhǔn)備

//寬、高、每幅圖像顯示的幀數(shù)
int w = 600, h = 900, perFrameCnt = 25;

/**
* 添加一個(gè)視頻流,并初始化AVCodecContext和AVStream
* @param fmtCtx AVFormatContext結(jié)構(gòu)體指針
* @param stream Straem指針的指針,用于在函數(shù)中進(jìn)行賦值
* @return 成功返回創(chuàng)建的AVCodecContext指針,若失敗則返回NULL
*/
AVCodecContext* addVideoStream(AVFormatContext* fmtCtx, AVStream** stream) {
    int ret = 0;
    AVCodecContext* codecCtx = NULL;

    do {
        //查找編碼器
        AVCodec* codec = avcodec_find_encoder(fmtCtx->oformat->video_codec);
        if (codec == NULL) {
            ret = -1;
            cout << "Cannot find any endcoder" << endl;
            break;
        }

        //申請(qǐng)編碼器上下文結(jié)構(gòu)體
        codecCtx = avcodec_alloc_context3(codec);
        if (codecCtx == NULL) {
            ret = -1;
            cout << "Cannot alloc AVCodecContext" << endl;
            break;
        }

        //創(chuàng)建視頻流
        *stream = avformat_new_stream(fmtCtx, codec);
        if (*stream == NULL) {
            ret = -1;
            cout << "failed create new video stream" << endl;
            break;
        }

        //為視頻流設(shè)置參數(shù)
        AVCodecParameters* param = (*stream)->codecpar;
        param->width = w;
        param->height = h;
        param->codec_type = AVMEDIA_TYPE_VIDEO;
        param->format = AV_PIX_FMT_YUV420P;     //視頻幀格式:YUV420P
        param->bit_rate = 400000;               //碼率

        //將參數(shù)傳給解碼器上下文
        ret = avcodec_parameters_to_context(codecCtx, param);
        if (ret < 0) {
            cout << "Cannot copy para to context" << endl;
            break;
        }

        // 某些封裝格式必須要設(shè)置該標(biāo)志,否則會(huì)造成封裝后文件中信息的缺失,如:mp4
        if (fmtCtx->oformat->flags & AVFMT_GLOBALHEADER) {
            codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
        }

        //gop表示多少個(gè)幀中存在一個(gè)關(guān)鍵幀
        codecCtx->gop_size = 12;
        codecCtx->qmin = 10;
        codecCtx->qmax = 51;
        codecCtx->qcompress = (float)0.6;
        //設(shè)置時(shí)間基
        codecCtx->time_base = AVRational{ 1,25 };

        //打開解碼器
        ret = avcodec_open2(codecCtx, codec, NULL);
        if (ret < 0) {
            cout << "Open encoder failed" << endl;
            break;
        }

        //再將codecCtx設(shè)置的參數(shù)傳給param,用于寫入頭文件信息
        ret = avcodec_parameters_from_context(param, codecCtx);
        if (ret < 0) {
            cout << "Cannot copy para from context" << endl;
            break;
        }
    } while (0);

    //出錯(cuò)則釋放資源
    if (ret < 0) {
        if (codecCtx) avcodec_free_context(&codecCtx);
        return NULL;
    }

    return codecCtx;
}
編碼
//執(zhí)行具體的編碼操作
int encodeVideoFrame(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVFrame* frame, AVPacket* pkt, AVStream* vStream) {
    int ret = 0;
    //將frame發(fā)送至編碼器進(jìn)行編碼,codecCtx中保存了codec
    //當(dāng)frame為NULL時(shí),表示將緩沖區(qū)中的數(shù)據(jù)讀取出來
    if (avcodec_send_frame(codecCtx, frame) >= 0) {
        //接收編碼后形成的packet
        while (avcodec_receive_packet(codecCtx, pkt) >= 0) {
            //設(shè)置對(duì)應(yīng)的流索引
            pkt->stream_index = vStream->index;
            pkt->pos = -1;
            //轉(zhuǎn)換pts至基于時(shí)間基的pts,可以理解為視頻幀顯示的時(shí)間戳
            av_packet_rescale_ts(pkt, codecCtx->time_base, vStream->time_base);
            cout << "encoder success pts:" << pkt->pts<< endl;
            
            //將包數(shù)據(jù)寫入文件中,該方法不用使用 av_packet_unref
            ret = av_interleaved_write_frame(fmtCtx, pkt);
            if (ret < 0) {
                char errStr[256];
                av_strerror(ret, errStr, 256);
                cout << "error is:" << errStr << endl;
                break;
            }
        }
    }
    return ret;
}

int encodeVideo(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVStream* stream) {
    int ret = 0;
    AVFrame* rgbFrame = NULL, * yuvFrame = NULL;
    AVPacket* pkt = av_packet_alloc();

    do {
        //申請(qǐng)F(tuán)rame
        rgbFrame = av_frame_alloc();
        yuvFrame = av_frame_alloc();

        //獲取相關(guān)參數(shù),為之后調(diào)用av_frame_get_buffer,需要給Frame賦值
        //視頻幀需要設(shè)置格式、寬、高
        int w = codecCtx->width, h = codecCtx->height;
        AVPixelFormat format = codecCtx->pix_fmt;

        yuvFrame->width = w;
        yuvFrame->height = h;
        yuvFrame->format = format;

        rgbFrame->width = w;
        rgbFrame->height = h;
        //這里使用BGR類型是因?yàn)镺penCV讀取圖片其格式為BGR
        rgbFrame->format = AV_PIX_FMT_BGR24;

        //為視頻數(shù)據(jù)分配新的緩沖區(qū),但有幾個(gè)注意點(diǎn)
        ret = av_frame_get_buffer(rgbFrame, 0);
        //bgr格式數(shù)據(jù)是連續(xù)的,所以linesize[0]應(yīng)等于格式所占字節(jié)數(shù)*圖像寬
        //但av_frame_get_buffer方法所得到的linesize[0]會(huì)大于字節(jié)數(shù)*圖像寬
        //需要我們手動(dòng)設(shè)置linesize[0]
        rgbFrame->linesize[0] = w*3;

        if (ret < 0) {
            cout << "rgbFrame get buffer fail" << endl;;
            break;
        }
        
        //為視頻數(shù)據(jù)分配新的緩沖區(qū)
        ret = av_frame_get_buffer(yuvFrame, 0);
        if (ret < 0) {
            cout << "yuvFrame get buffer fail" << endl;;
            break;
        }

        //設(shè)置BGR數(shù)據(jù)轉(zhuǎn)換為YUV的SwsContext
        SwsContext* imgCtx = sws_getContext(w, h, (AVPixelFormat)rgbFrame->format,
            w, h, format, 0, NULL, NULL, NULL);

        //FFmpeg讀取圖片流程過于復(fù)雜,所以使用OpenCV讀取圖像
        //想了解FFmpeg如何讀取圖像的,可以看看 視頻流編碼流程(三)
        Mat img;
        //用于編碼為視頻幀的圖像
        char imgPath[] = "img/p0.jpg";
        //獲取對(duì)應(yīng)一幀BGR圖像所需的字節(jié)數(shù)
        int size = av_image_get_buffer_size((AVPixelFormat)rgbFrame->format,w,h,1);

        for (int i = 0; i < 7; i++) {
            imgPath[5] = '0' + i;
            img = imread(imgPath);
            //BGR數(shù)據(jù)填充至圖像幀
            memcpy(rgbFrame->data[0], img.data, size);

            //進(jìn)行圖像格式轉(zhuǎn)換
            sws_scale(imgCtx,
                rgbFrame->data,
                rgbFrame->linesize,
                0,
                h,
                yuvFrame->data,
                yuvFrame->linesize);

            for (int j = 0; j < perFrameCnt; j++) {
                //設(shè)置 pts 值,用于度量解碼后視頻幀位置
                yuvFrame->pts = i * perFrameCnt + j;
                //將編碼過程抽離為一個(gè)函數(shù)
                ret = encodeVideoFrame(fmtCtx, codecCtx, yuvFrame, pkt, stream);
                if (ret < 0) {
                    cout << "Do encodeVideoFrame function Fail" << endl;
                    break;
                }
            }
            if (ret < 0) break;
        }
        if (ret < 0) break;
        
        //刷新解碼緩存區(qū)
        ret = encodeVideoFrame(fmtCtx, codecCtx, NULL, pkt, stream);
        if (ret < 0) {
            cout << "Do encodeVideoFrame function Fail" << endl;
            break;
        }
    } while (0);
    
    //釋放使用的資源
    if (rgbFrame) av_frame_free(&rgbFrame);
    if (yuvFrame) av_frame_free(&yuvFrame);
    av_packet_free(&pkt);

    return ret;
}

音頻模塊

音頻模塊中也分為 初始化編碼 兩個(gè)部分,音頻模塊大致流程與之前的音頻流編碼沒有區(qū)別,只是在音頻幀pts設(shè)置上需要做一些改變,因?yàn)橹熬幋a音頻流只需要保證按照順序播放即可(即pts呈遞增趨勢(shì)),但是加入了畫面,就要保證音頻與視頻播放同步

初始化

初始化中主要工作為為文件添加一個(gè)音頻流,并設(shè)置相關(guān)的參數(shù),為之后的編碼作準(zhǔn)備

static int select_bit_rate(AVCodec* codec) {
    // 對(duì)于不同的編碼器最優(yōu)碼率不一樣,單位bit/s;對(duì)于mp3來說,192kbps可以獲得較好的音質(zhì)效果。
    int bit_rate = 64000;
    AVCodecID id = codec->id;
    if (id == AV_CODEC_ID_MP3) {
        bit_rate = 192000;
    }
    else if (id == AV_CODEC_ID_AC3) {
        bit_rate = 192000;
    }

    return bit_rate;
}

/**
* 添加一個(gè)音頻流,并初始化AVCodecContext和AVStream
* @param fmtCtx AVFormatContext結(jié)構(gòu)體指針
* @param stream Straem指針的指針,用于在函數(shù)中進(jìn)行賦值
* @return 成功返回創(chuàng)建的AVCodecContext指針,若失敗則返回NULL
*/
AVCodecContext* addAudioStream(AVFormatContext* fmtCtx, AVStream** stream) {
    int ret = 0;
    AVCodecContext* codecCtx = NULL;

    do {
        //查找編碼器
        AVCodec* codec = avcodec_find_encoder(fmtCtx->oformat->audio_codec);
        if (codec == NULL) {
            ret = -1;
            cout << "Cannot find any audio endcoder" << endl;
            break;
        }

        //申請(qǐng)編碼器上下文結(jié)構(gòu)體
        codecCtx = avcodec_alloc_context3(codec);
        if (codecCtx == NULL) {
            ret = -1;
            cout << "Cannot alloc context" << endl;
            break;
        }
        
        //設(shè)置相關(guān)參數(shù)
        codecCtx->bit_rate = select_bit_rate(codec);    // 碼率
        codecCtx->sample_rate = 48000;                  // 采樣率
        codecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP;      // 采樣格式
        codecCtx->channel_layout = AV_CH_LAYOUT_STEREO; // 聲道格式
        codecCtx->channels = 2;                         // 聲道數(shù)

        //創(chuàng)建音頻流
        *stream = avformat_new_stream(fmtCtx, codec);
        if (*stream == NULL) {
            ret = -1;
            cout << "failed create new audio stream" << endl;
            break;
        }

        //打開解碼器
        ret = avcodec_open2(codecCtx, codec, NULL);
        if (ret < 0) {
            cout << "avcodec_open2 fail" << endl;
            break;
        }

        AVCodecParameters* param = (*stream)->codecpar;
        param->codec_type = AVMEDIA_TYPE_AUDIO;
        
        // 將codecCtx設(shè)置的參數(shù)傳給param,用于寫入頭文件信息
        ret = avcodec_parameters_from_context(param, codecCtx);
        if (ret < 0) {
            cout << "Cannot copy para from context" << endl;
            break;
        }
    } while (0);

    //出錯(cuò)則釋放資源
    if (ret < 0) {
        if (codecCtx) avcodec_free_context(&codecCtx);
        return NULL;
    }

    return codecCtx;
}
編碼
//執(zhí)行具體的編碼操作
int encodeAudioFrame(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVFrame* frame, AVPacket* pkt, AVStream* aStream) {
    int ret = 0;
    //將frame發(fā)送至編碼器進(jìn)行編碼
    if (avcodec_send_frame(codecCtx, frame) >= 0) {
        //接收編碼后形成的packet
        while (avcodec_receive_packet(codecCtx, pkt) >= 0) {
            //設(shè)置對(duì)應(yīng)的流索引
            pkt->stream_index = aStream->index;
            pkt->pos = -1;

            //轉(zhuǎn)換pts至基于時(shí)間基的pts
            av_packet_rescale_ts(pkt, codecCtx->time_base, aStream->time_base);
            cout << "encoder audio success pts:" << pkt->pts << endl;

            //將包數(shù)據(jù)寫入文件中,該方法不用使用 av_packet_unref
            ret = av_interleaved_write_frame(fmtCtx, pkt);
            if (ret < 0) {
                char errStr[256];
                av_strerror(ret, errStr, 256);
                cout << "error is:" << errStr << endl;
                break;
            }
        }
    }
    return ret;
}

int encodeAudio(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVStream* stream, FILE* file) {
    int ret = 0;
    AVFrame* srcFrame = NULL, * dstFrame = NULL;
    AVPacket* pkt = av_packet_alloc();

    do {
        /*設(shè)置音頻幀參數(shù)
        * PCM文件中的參數(shù),PCM文件不攜帶音頻格式信息,需要我們自己記錄相關(guān)的參數(shù)
        * 并為srcFrame的參數(shù)賦值
        * 音頻幀調(diào)用av_frame_get_buffer分配緩沖區(qū),需要format、nb_samples、channel_layout
        */
        
        srcFrame = av_frame_alloc();
        srcFrame->format = AV_SAMPLE_FMT_FLT;
        srcFrame->nb_samples = 1152;
        srcFrame->channel_layout = AV_CH_LAYOUT_STEREO;
        srcFrame->channels = 2;
        srcFrame->sample_rate = 44100;

        dstFrame = av_frame_alloc();
        dstFrame->format = codecCtx->sample_fmt;
        dstFrame->nb_samples = codecCtx->frame_size;
        dstFrame->channel_layout = codecCtx->channel_layout;
        dstFrame->channels = codecCtx->channels;
        dstFrame->sample_rate = codecCtx->sample_rate;

        //為音頻數(shù)據(jù)分配新的緩沖區(qū)
        ret = av_frame_get_buffer(srcFrame, 0);
        if (ret < 0) {
            cout << "frame get buffer fail" << endl;;
            break;
        }
        //使得srcFrame可寫
        av_frame_make_writable(srcFrame);

        ret = av_frame_get_buffer(dstFrame, 0);
        if (ret < 0) {
            cout << "frame get buffer fail" << endl;;
            break;
        }
        av_frame_make_writable(dstFrame);

        // 申請(qǐng)進(jìn)行對(duì)應(yīng)轉(zhuǎn)換所需的SwrContext
        SwrContext* swrCtx = swr_alloc_set_opts(NULL,
            codecCtx->channel_layout, codecCtx->sample_fmt, codecCtx->sample_rate,
            srcFrame->channel_layout, (enum AVSampleFormat)srcFrame->format, srcFrame->sample_rate,
            0, NULL);

        int audioPts = 0;
        // 獲取一幀源數(shù)據(jù)的字節(jié)大小
        int require_size = av_samples_get_buffer_size(NULL, srcFrame->channels, srcFrame->nb_samples, (AVSampleFormat)srcFrame->format, 0);
        while (1) {
            ret = fread(srcFrame->data[0], 1, require_size, file);
            if (ret < 0) {
                cout << "fread error" << endl;
                break;
            }
            // 進(jìn)行格式轉(zhuǎn)換
            ret = swr_convert_frame(swrCtx, dstFrame, srcFrame);
            if (ret < 0) {
                cout << "swr_convert_frame fail " << ret << endl;
                continue;
            }
            /*
             * 音頻:pts = (timebase.den/sample_rate)*[nb_samples*index]
             * index為當(dāng)前第幾個(gè)音頻AVFrame(索引從0開始),nb_samples為每個(gè)AVFrame中的采樣數(shù)
             * 但經(jīng)過swr_convert_frame后,nb_samples會(huì)修改并且大小會(huì)改變
             * 需要一個(gè)數(shù)記錄,而(timebase.den/sample_rate)中的部分會(huì)在
             * av_packet_rescale_ts 函數(shù)中進(jìn)行轉(zhuǎn)換
            */
            dstFrame->pts = audioPts;
            audioPts += dstFrame->nb_samples;
            ret = encodeAudioFrame(fmtCtx, codecCtx, dstFrame, pkt, stream);
            if (ret < 0) {
                cout << "Do encodeVideoFrame function Fail 1" << endl;
                break;
            }
            if (feof(file)) {
                break;
            }
        }
        if (ret < 0) break;

        //刷新轉(zhuǎn)換緩存區(qū)
        while (swr_convert_frame(swrCtx, dstFrame, NULL) == 0) {
            if (dstFrame->nb_samples != 0) {
                dstFrame->pts = audioPts;
                audioPts += dstFrame->nb_samples;
                ret = encodeAudioFrame(fmtCtx, codecCtx, dstFrame, pkt, stream);
                if (ret < 0) {
                    cout << "Do encodeVideoFrame function Fail 2" << endl;
                    break;
                }
            }
            else {
                break;
            }
        }

        //刷新編碼緩沖區(qū)
        ret = encodeAudioFrame(fmtCtx, codecCtx, NULL, pkt, stream);
        if (ret < 0) {
            cout << "Do encodeVideoFrame function Fail 3" << endl;
            break;
        }
    } while (0);

    //釋放資源
    av_packet_free(&pkt);
    av_frame_free(&srcFrame);
    av_frame_free(&dstFrame);

    return ret;
}

實(shí)現(xiàn)效果

通過代碼可以知道,案例中編寫視頻長(zhǎng)度應(yīng)該只有7s,而音頻長(zhǎng)度取決于PCM文件大小,這里使用的PCM是由1分31秒的mp3文件轉(zhuǎn)換成的,所以音頻長(zhǎng)度 > 視頻長(zhǎng)度,最終呈現(xiàn)效果為視頻與音頻同步播放,7s之后畫面呈現(xiàn)視頻最后一幀圖像,mp4文件長(zhǎng)度為音頻長(zhǎng)度——1分31秒

現(xiàn)在我們已經(jīng)可以將原始數(shù)據(jù)轉(zhuǎn)換為mp4文件

加上視頻文件的解碼,我們就可以做到將一個(gè)多媒體文件中的視頻流和另一個(gè)多媒體文件中的音頻流組合在一起形成一個(gè)新的多媒體文件了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容