实战小项目之基于嵌入式的图像采集压缩保存

项目简介

  这是之前图像采集显示程序的升级版,首先基础部分的图像v4l2采集、framebuffer显示、IPU转码都进行了c++封装,之后加入了以下新功能:

  1. SDL1.2显示
  2. FFmpeg编码保存版本一(顺序执行)
  3. FFmpeg编码保存版本二(线程方式)  
  4. c++ pthread库简单封装

  这个小工程是一个附属产品,boss的项目中用到了图像编码保存,然后学了一段时间的多媒体技术(主要就是FFmpeg),后来就衍生出了这个版本的程序

  不多说,上代码

  首先是SDL显示部分

/*
 * SDLDisp.cpp
 *
 *  Created on: Oct 11, 2016
 *      Author: tla001
 */

#include "SDLDisp.h"
/*
 * display different data
 * 1.framesize                 420p:screen_w*screen_h*3/2    422:screen_w*screen_h*2
 * 2.SDL_CreateYUVOverlay     420p:SDL_IYUV_OVERLAY        422:SDL_YUY2_OVERLAY
 * 3.dispfuction            420p:DisplayYUV420P            422:DisplayYUV422
 */

SDLDisp::SDLDisp(int screen_w,int screen_h) {
    // TODO Auto-generated constructor stub
    this->screen_w=screen_w;
    this->screen_h=screen_h;
    this->framesize=screen_w*screen_h*3/2;
    this->buffer=NULL;
    this->sdl_running=1;
    this->thread_exit=0;
}

SDLDisp::~SDLDisp() {
    // TODO Auto-generated destructor stub
    this->sdl_running=0;
    closeSDL();
}
int SDLDisp::initSDL(char *winName)
{
    buffer=(uint8_t*)malloc(sizeof(uint8_t)*framesize);
    SDL_Init(SDL_INIT_EVERYTHING);
    screen = SDL_SetVideoMode(screen_w, screen_h, 0, SDL_SWSURFACE|SDL_ANYFORMAT);
    if(!screen) {
        printf("SDL: could not set video mode - exiting:%s
",SDL_GetError());
        return -1;
    }
    /* Set the window name */
     SDL_WM_SetCaption(winName, NULL);
     //yuv420P    run with DisplayYUV420P
    // overlay = SDL_CreateYUVOverlay(screen_w, screen_h,SDL_IYUV_OVERLAY, screen);
     //yuv422 run with DisplayYUV422
     overlay = SDL_CreateYUVOverlay(screen_w, screen_h,SDL_IYUV_OVERLAY, screen);

    rect.x=0;
    rect.y = 0;
    rect.w = screen_w;
    rect.h = screen_h;
    //refresh_thread = SDL_CreateThread(sdlcontol,NULL);
    //pthread_create(&controlTid,NULL,sdlcontol, static_cast<void*>(this));
    //初始化读写所
//    int ret=pthread_rwlock_init(&rwlock, NULL);
//    if(ret<0){
//        printf("init wrlock failed
");
//        return -1;
//    }
    //pthread_create(&dispTid,NULL,&sdlDisp,NULL);

    return 0;
}
int SDLDisp::closeSDL()
{
    if(buffer!=NULL)
        free(buffer);
    thread_exit=1;
    SDL_FreeYUVOverlay(overlay);
    SDL_FreeSurface(screen);
    SDL_Quit();
    printf("sdl end
");
    return 0;
}
int SDLDisp::sdlDisp()
{
    printf("sdl disp start
");
    while(sdl_running){
        SDL_WaitEvent(&event);
        if(event.type==REFRESH_EVENT){
            SDL_LockSurface(screen);
            pthread_rwlock_rdlock(&rwlock);
            DisplayYUV420P(buffer, screen_w, screen_h, overlay);
            pthread_rwlock_unlock(&rwlock);
            SDL_UnlockSurface(screen);
            SDL_DisplayYUVOverlay(overlay, &rect);
            //Update Screen
            SDL_Flip(screen);
        }else if(event.type==SDL_QUIT){
            thread_exit=1;
            sdl_running=0;
        }else if(event.type==BREAK_EVENT){
            break;
        }
    }
    thread_exit=1;
    printf("sdl disp end
");
    return 0;
}
int SDLDisp::refresh_video(void *opaque){
    thread_exit=0;
    while (thread_exit==0) {
        SDL_Event event;
        event.type = REFRESH_EVENT;
        SDL_PushEvent(&event);
        SDL_Delay(40);
    }
    thread_exit=0;
    //Break
    SDL_Event event;
    event.type = BREAK_EVENT;
    SDL_PushEvent(&event);
    return 0;
}
int SDLDisp::doSDLDisp(uint8_t *buf)
{
    pthread_rwlock_wrlock(&rwlock);
    memcpy(buffer,buf,framesize);
    pthread_rwlock_unlock(&rwlock);
    return 0;
}
void SDLDisp::normalSDLDisp(uint8_t *buf)
{
    memcpy(buffer,buf,framesize);
    SDL_LockSurface(screen);
    DisplayYUV420P(buffer, screen_w, screen_h, overlay);//420p
    //DisplayYUV422(buffer, screen_w, screen_h, overlay);//422
    SDL_UnlockSurface(screen);
    SDL_DisplayYUVOverlay(overlay, &rect);
    //Update Screen
    SDL_Flip(screen);
}
void SDLDisp::DisplayYUV420P(uint8_t *buf, uint32_t w, uint32_t h, SDL_Overlay *overlay)
{
    /* Fill in video data */
    //uint32_t yuv_size = (w*h) * 3/2;
    uint8_t * y_video_data = (uint8_t*)buf;
    uint8_t * u_video_data = (uint8_t*)(buf + w * h);
    uint8_t * v_video_data = (uint8_t*)(u_video_data + (w * h / 4));

    /* Fill in pixel data - the pitches array contains the length of a line in each plane */
    SDL_LockYUVOverlay(overlay);
    memcpy(overlay->pixels[0], y_video_data, w * h);
    memcpy(overlay->pixels[1], u_video_data, w * h / 4);
    memcpy(overlay->pixels[2], v_video_data, w * h / 4);

    SDL_UnlockYUVOverlay(overlay);
}
void SDLDisp::DisplayYUV420(uint8_t *buf, uint32_t w, uint32_t h, SDL_Overlay *overlay)
{
    /* Fill in video data */
    //uint32_t yuv_size = (w*h) * 3/2;
    uint8_t * y_video_data = (uint8_t*)buf;
    uint8_t * v_video_data = (uint8_t*)(buf + w * h);
    uint8_t * u_video_data = (uint8_t*)(v_video_data + (w * h / 4));

    /* Fill in pixel data - the pitches array contains the length of a line in each plane */
    SDL_LockYUVOverlay(overlay);
    memcpy(overlay->pixels[0], y_video_data, w * h);
    memcpy(overlay->pixels[1], u_video_data, w * h / 4);
    memcpy(overlay->pixels[2], v_video_data, w * h / 4);

    SDL_UnlockYUVOverlay(overlay);
}
void SDLDisp::DisplayYUV422(uint8_t *buf, uint32_t w, uint32_t h, SDL_Overlay *overlay)
{
    /* Fill in pixel data - the pitches array contains the length of a line in each plane */
    SDL_LockYUVOverlay(overlay);
    memcpy(overlay->pixels[0], buf, w * h*2);
    SDL_UnlockYUVOverlay(overlay);
}
View Code

  然后是encodesaver1 

/*
 * EncodeSaver.cpp
 *
 *  Created on: Oct 11, 2016
 *      Author: tla001
 */

#include "EncodeSaver.h"

EncodeSaver::EncodeSaver(int in_w,int in_h,int out_w,int out_h,char* out_file) {
    // TODO Auto-generated constructor stub
    this->in_w=in_w;
    this->in_h=in_h;
    this->out_w=out_w;
    this->out_h=out_h;
    this->framesize=0;
    this->out_file=out_file;
    this->basicsize=in_w*in_h;
    this->frameNum=0;
}

EncodeSaver::~EncodeSaver() {
    // TODO Auto-generated destructor stub
}
int EncodeSaver::initDevice()
{
    av_register_all();
    //方法1.组合使用几个函数
    pFormatCtx = avformat_alloc_context();
    //猜格式
    fmt = av_guess_format(NULL, out_file, NULL);
    pFormatCtx->oformat = fmt;
    if (avio_open(&pFormatCtx->pb,out_file, AVIO_FLAG_READ_WRITE) < 0)
    {
        printf("输出文件打开失败");
        return -1;
    }

    video_st = av_new_stream(pFormatCtx, 0);
    if (video_st==NULL)
    {
        return -1;
    }
    pCodecCtx = video_st->codec;
    pCodecCtx->codec_id = fmt->video_codec;
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    pCodecCtx->pix_fmt = PIX_FMT_YUV420P;
    pCodecCtx->width = in_w;
    pCodecCtx->height = in_h;
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 18;
    pCodecCtx->bit_rate = 400000;
    pCodecCtx->gop_size=250;
    //设置
    video_st->time_base.num=1;
    video_st->time_base.den=18;
    //H264
    pCodecCtx->me_range = 16;
    pCodecCtx->max_qdiff = 4;
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    pCodecCtx->qcompress = 0.6;
    if (pCodecCtx->codec_id == CODEC_ID_MPEG2VIDEO)
    {
        pCodecCtx->max_b_frames = 2;
    }
    if (pCodecCtx->codec_id == CODEC_ID_MPEG1VIDEO)
    {
        pCodecCtx->mb_decision = 2;
    }
    if (!strcmp(pFormatCtx->oformat->name, "mp4") || !strcmp(pFormatCtx->oformat->name, "mov") || !strcmp(pFormatCtx->oformat->name, "3gp"))
    {
        pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
    }
    //输出格式信息
    av_dump_format(pFormatCtx, 0, out_file, 1);

    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec)
    {
        printf("没有找到合适的编码器!
");
        return -1;
    }
    if (avcodec_open2(pCodecCtx, pCodec,NULL) < 0)
    {
        printf("编码器打开失败!
");
        return -1;
    }
    //输出420

    picture = avcodec_alloc_frame();
    framesize = avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
    picture_buf = (uint8_t *)av_malloc(framesize);
    avpicture_fill((AVPicture *)picture, picture_buf, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
    //输入422
    picture422 = avcodec_alloc_frame();
    framesize422 = avpicture_get_size(PIX_FMT_YUYV422, pCodecCtx->width, pCodecCtx->height);
    picture422_buf = (uint8_t *)av_malloc(framesize422);
    avpicture_fill((AVPicture *)picture422, picture422_buf, PIX_FMT_YUYV422, pCodecCtx->width, pCodecCtx->height);
    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUYV422, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
    //写文件头
    avformat_write_header(pFormatCtx,NULL);
    av_new_packet(&pkt,basicsize*3);
}

int EncodeSaver::closeDevice()
{
    int ret = flush_encoder(pFormatCtx,0);
    if (ret < 0) {
        printf("Flushing encoder failed
");
        return -1;
    }
    //写文件尾
    av_write_trailer(pFormatCtx);

    //清理
    if (video_st)
    {
        avcodec_close(video_st->codec);
        av_free(picture);
        av_free(picture_buf);
        av_free(picture422);
        av_free(picture422_buf);
    }
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
    return 0;
}
int EncodeSaver::doEncode(unsigned char *buf)
{
    int ret;
    //420P
    memcpy(picture_buf,buf,basicsize*3/2);
    picture->data[0] = picture_buf;  // 亮度Y
    picture->data[1] = picture_buf+ basicsize;  // U
    picture->data[2] = picture_buf+ basicsize*5/4; // V


    //PTS
    if (pFormatCtx->oformat->flags & AVFMT_RAWPICTURE)
    {
        AVPacket pkt;
        av_init_packet(&pkt);
        pkt.flags |= AV_PKT_FLAG_KEY;
        pkt.stream_index = video_st->index;
        pkt.data = (uint8_t*)picture;
        pkt.size = sizeof(AVPicture);
        ret = av_write_frame(pFormatCtx, &pkt);
    }
    else{
        int out_size = avcodec_encode_video(pCodecCtx, picture_buf, framesize, picture);
        if (out_size > 0)
        {
            AVPacket pkt;
            av_init_packet(&pkt);
            pkt.pts = av_rescale_q(pCodecCtx->coded_frame->pts, pCodecCtx->time_base, video_st->time_base);
            if (pCodecCtx->coded_frame->key_frame)
            {
                pkt.flags |= AV_PKT_FLAG_KEY;
                printf("here 1
");
            }
            pkt.stream_index = video_st->index;
            pkt.data = picture_buf;
            pkt.size = out_size;
            ret = av_write_frame(pFormatCtx, &pkt);
        }
    }
    return 0;
}
int EncodeSaver::doEncode2(unsigned char *buf)
{
    int ret;
    //422
    memcpy(picture422_buf,buf,basicsize*2);
    picture422->data[0] = picture422_buf;
    sws_scale(img_convert_ctx, picture422->data, picture422->linesize, 0, pCodecCtx->height, picture->data, picture->linesize);


    //PTS
    if (pFormatCtx->oformat->flags & AVFMT_RAWPICTURE)
    {
        AVPacket pkt;
        av_init_packet(&pkt);
        pkt.flags |= AV_PKT_FLAG_KEY;
        pkt.stream_index = video_st->index;
        pkt.data = (uint8_t*)picture;
        pkt.size = sizeof(AVPicture);
        ret = av_write_frame(pFormatCtx, &pkt);
    }
    else{
        int out_size = avcodec_encode_video(pCodecCtx, picture_buf, framesize, picture);
        if (out_size > 0)
        {
            AVPacket pkt;
            av_init_packet(&pkt);
            pkt.pts = av_rescale_q(pCodecCtx->coded_frame->pts, pCodecCtx->time_base, video_st->time_base);
            if (pCodecCtx->coded_frame->key_frame)
            {
                pkt.flags |= AV_PKT_FLAG_KEY;
            }
            pkt.stream_index = video_st->index;
            pkt.data = picture_buf;
            pkt.size = out_size;
            ret = av_write_frame(pFormatCtx, &pkt);
        }
    }

    return ret;
}
int EncodeSaver::flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
        CODEC_CAP_DELAY))
        return 0;
    while (1) {
        printf("Flushing stream #%u encoder
", stream_index);
        //ret = encode_write_frame(NULL, stream_index, &got_frame);
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
            NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame)
        {ret=0;break;}
        printf("successed frame!
");
        /* mux encoded frame */
        ret = av_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
    }
    return ret;
}
View Code

  接着是encodesaver2

/*
 * EncodeSaver2.cpp
 *
 *  Created on: Oct 12, 2016
 *      Author: tla001
 */

#include "EncodeSaver2.h"

EncodeSaver2::EncodeSaver2(int in_w,int in_h,int out_w,int out_h,char* out_file) {
    // TODO Auto-generated constructor stub
    this->in_w=in_w;
    this->in_h=in_h;
    this->out_w=out_w;
    this->out_h=out_h;
    this->framesize=0;
    this->out_file=out_file;
    this->basicsize=in_w*in_h;
    this->frameNum=0;
    this->ready=0;
}


EncodeSaver2::~EncodeSaver2() {
    // TODO Auto-generated destructor stub
    closeDevice();
}

int EncodeSaver2::initDevice()
{
    av_register_all();
    //方法1.组合使用几个函数
    pFormatCtx = avformat_alloc_context();
    //猜格式
    fmt = av_guess_format(NULL, out_file, NULL);
    pFormatCtx->oformat = fmt;
    if (avio_open(&pFormatCtx->pb,out_file, AVIO_FLAG_READ_WRITE) < 0)
    {
        printf("输出文件打开失败");
        return -1;
    }

    video_st = av_new_stream(pFormatCtx, 0);
    if (video_st==NULL)
    {
        return -1;
    }
    pCodecCtx = video_st->codec;
    pCodecCtx->codec_id = fmt->video_codec;
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    pCodecCtx->pix_fmt = PIX_FMT_YUV420P;
    pCodecCtx->width = in_w;
    pCodecCtx->height = in_h;
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;
    pCodecCtx->bit_rate = 400000;
    pCodecCtx->gop_size=250;
    //设置
    video_st->time_base.num=1;
    video_st->time_base.den=25;
    //H264
    pCodecCtx->me_range = 16;
    pCodecCtx->max_qdiff = 4;
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    pCodecCtx->qcompress = 0.6;
    if (pCodecCtx->codec_id == CODEC_ID_MPEG2VIDEO)
    {
        pCodecCtx->max_b_frames = 2;
    }
    if (pCodecCtx->codec_id == CODEC_ID_MPEG1VIDEO)
    {
        pCodecCtx->mb_decision = 2;
    }
    if (!strcmp(pFormatCtx->oformat->name, "mp4") || !strcmp(pFormatCtx->oformat->name, "mov") || !strcmp(pFormatCtx->oformat->name, "3gp"))
    {
        pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
    }
    //输出格式信息
    av_dump_format(pFormatCtx, 0, out_file, 1);

    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec)
    {
        printf("没有找到合适的编码器!
");
        return -1;
    }
    if (avcodec_open2(pCodecCtx, pCodec,NULL) < 0)
    {
        printf("编码器打开失败!
");
        return -1;
    }
    //输出420

    picture = avcodec_alloc_frame();
    framesize = avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
    picture_buf = (uint8_t *)av_malloc(framesize);
    avpicture_fill((AVPicture *)picture, picture_buf, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
    //输入422
    picture422 = avcodec_alloc_frame();
    framesize422 = avpicture_get_size(PIX_FMT_YUYV422, pCodecCtx->width, pCodecCtx->height);
    picture422_buf = (uint8_t *)av_malloc(framesize422);
    avpicture_fill((AVPicture *)picture422, picture422_buf, PIX_FMT_YUYV422, pCodecCtx->width, pCodecCtx->height);
    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUYV422, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
    //写文件头
    avformat_write_header(pFormatCtx,NULL);
    av_new_packet(&pkt,basicsize*3);

    this->start();
    cout<<"thread start"<<endl;
}
int EncodeSaver2::closeDevice()
{
    this->stop();
    int ret = flush_encoder(pFormatCtx,0);
    if (ret < 0) {
        printf("Flushing encoder failed
");
        return -1;
    }

    //写文件尾
    av_write_trailer(pFormatCtx);

    //清理
    if (video_st)
    {
        avcodec_close(video_st->codec);
        av_free(picture);
        av_free(picture_buf);
        av_free(picture422);
        av_free(picture422_buf);
    }
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
    return 0;
}
int EncodeSaver2::doEncode(unsigned char *buf)
{
    int ret=0;
    //422
    memcpy(picture422_buft.a,buf,basicsize*2);
    //picture422_bufs.push(picture422_buft);
    this->ready=1;
    return ret;
}
int EncodeSaver2::isReady(){
    return ready;
}
void EncodeSaver2::run(){
    int ret;
    struct timeval temp;
    temp.tv_sec = 0;
    temp.tv_usec = 40000000;//40ms
    printf("thread is up!
");
    while(1){
    if(isStop())
        pthread_exit(0);
    if(isStart()&&isReady()){
//        printf("here 1
");
//        sleep(1);

        //select(0, NULL, NULL, NULL, &temp);
        //usleep(40000000);
        printf("**********time up********ttkf*** 
");
    //    if(picture422_bufs.empty()){
    //        cout<<" queue empty"<<endl;
    //        continue;
    //    }

        //picture422_buf=picture422_bufs.front().a;
        //picture422_bufs.pop();
        memcpy(picture422_buf,picture422_buft.a,basicsize*2);
        picture422->data[0] = picture422_buf;
        sws_scale(img_convert_ctx, picture422->data, picture422->linesize, 0, pCodecCtx->height, picture->data, picture->linesize);
            //PTS
        if (pFormatCtx->oformat->flags & AVFMT_RAWPICTURE)
        {
            AVPacket pkt;
            av_init_packet(&pkt);
            pkt.flags |= AV_PKT_FLAG_KEY;
            pkt.stream_index = video_st->index;
            pkt.data = (uint8_t*)picture;
            pkt.size = sizeof(AVPicture);
            ret = av_write_frame(pFormatCtx, &pkt);
        }
        else{
            int out_size = avcodec_encode_video(pCodecCtx, picture_buf, framesize, picture);
            if (out_size > 0)
            {
                AVPacket pkt;
                av_init_packet(&pkt);
                pkt.pts = av_rescale_q(pCodecCtx->coded_frame->pts, pCodecCtx->time_base, video_st->time_base);
                if (pCodecCtx->coded_frame->key_frame)
                {
                    pkt.flags |= AV_PKT_FLAG_KEY;
                }
                pkt.stream_index = video_st->index;
                pkt.data = picture_buf;
                pkt.size = out_size;
                ret = av_write_frame(pFormatCtx, &pkt);
            }
        }
    }else{
        printf("ready for data
");
        usleep(50000);//线程起来之后,由于参数没准备好,可能会空转
    }
}
}
int EncodeSaver2::flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
        CODEC_CAP_DELAY))
        return 0;
    while (1) {
        printf("Flushing stream #%u encoder
", stream_index);
        //ret = encode_write_frame(NULL, stream_index, &got_frame);
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
            NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame)
        {ret=0;break;}
        printf("successed frame!
");
        /* mux encoded frame */
        ret = av_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
    }
    return ret;
}
View Code

完整工程

https://github.com/tla001/CapTransV2


相关链接  

ffmpeg移植

  http://www.cnblogs.com/tla001/p/5906220.html

音视频处理(FFmpeg)基础

  http://blog.csdn.net/leixiaohua1020/article/details/15811977

  学习的资料,基本都是从雷神的博客中来的

相关命令

ffmpeg -f video4linux2 -i /dev/video0 -vcodec libx264 -s 320*240 -r 10 video0.mkv
ffmpeg -f video4linux2 -s 640*480 -r 25 -i /dev/video0 test.avi
原文地址:https://www.cnblogs.com/tla001/p/6322926.html