视频编码解码(H264编码实战)

基本步骤

  • 打开编码器
    在打开编码器时,要设置一些参数,例如具体使用的哪一个编码器,编H264时要使用libx264,编H265时要使用libx265。
    还需要设置GOP,码流大小、分辨率宽和高也需要设置。

  • 转换NV12到YUV420P
    NV12是mac下采集视频的格式,NV12是一个包裹的格式,分两层,第一层是Y,第二层是UVUV进行交替存储,yuv420P是编码器libx264要求的格式。

    转换时可以采用ffmpeg自身的swscale转换,还可以用google开发的libyuv,还可以自己手工写一个进行转换。

  • 准备编码数据AVFrame

    因为从设备上获取的数据是AVPacket,是一个输出的数据,不能作为编码器的输入,所以要将AVPacket中的数据导入到AVFrame中,因为AVFrame是输入数据。

  • H264编码

    前面的过程都处理完后,最后通过一个循环,就可以将数据进行编码了,拿到编码后的AVPacket存储进文件中,就可以通过播放器进行播放了。

示例代码

//
//  video_encode.c
//  FFmpegDemo
//
//  Created by qqy on 2021/7/20.
//

#include "video_encode.h"
#include "time.h"

#define V_WIDTH 640
#define V_HETGHT 480

static int rec_status = 0;

void set_status(int status){
    rec_status = status;
}


static AVFormatContext *open_dev(void){
    int ret = 0;
    char errors[1024] = {0, };

    //ctx
    AVFormatContext *fmt_ctx = NULL;
    AVDictionary *options = NULL;

    //[[video device]:[audio device]]
    //0: 机器的摄像头
    //1: 桌面
    char *devicename = "0:";

    //register audio device
    avdevice_register_all();

    //get format
    const AVInputFormat *iformat = av_find_input_format("avfoundation");
    
    av_dict_set(&options, "video_size", "640x480", 0);
    av_dict_set(&options, "framerate", "30", 0);
    av_dict_set(&options, "pixel_format", "nv12", 0);

    //open device
    if((ret = avformat_open_input(&fmt_ctx, devicename, iformat, &options)) < 0){
        av_strerror(ret, errors, 1024);
        fprintf(stderr, "Failed to open video device, [%d] %s\n", ret, errors);
        return NULL;
    }
    av_dump_format(fmt_ctx, 0, devicename, 0);
    return fmt_ctx;
}

/**
* @brief
* @param[in] width
* param[in] height
* param[out] enc_ctx
*/
static void open_encoder(int width, int height, AVCodecContext **enc_ctx){

    int ret = 0;
    const AVCodec *codec = NULL;
    codec = avcodec_find_encoder_by_name("libx264");
    
    if(!codec){
        printf("Codec libx264 not found\n");
        exit(1);
    }

    *enc_ctx = avcodec_alloc_context3(codec);//第三个版本
    
    if(!(*enc_ctx)){
        printf("Could not allocate video codec context!\n");
        exit(1);
    }
    
    //SPS/PPS
    (*enc_ctx)->profile = FF_PROFILE_H264_HIGH_444;
    (*enc_ctx)->level = 50; //表示level是5.0

    //设置分辨率
    (*enc_ctx)->width = width;     //640
    (*enc_ctx)->height = height; //480

    //GOP
    (*enc_ctx)->gop_size = 30; //设置很小时,I帧就会很多,码流就会很大,设置很小时,I帧就会很少,一旦丢包就会出现异常,等到下一个I帧时才会修正。
    (*enc_ctx)->keyint_min = 25;//最小I帧间隔,也就是I帧最小25帧就可以插入I帧 可选项,不设置也可以
    
    //设置B帧数量
    (*enc_ctx)->max_b_frames = 3; //可选项,不设置也可以
    (*enc_ctx)->has_b_frames = 1; // 可选项,不设置也可以

    //参考帧数量
    (*enc_ctx)->refs = 3;//设置参考帧数量  可选项,不设置也可以
    
    //设置输入YUV格式 设置像素格式
    (*enc_ctx)->pix_fmt = AV_PIX_FMT_YUV420P;

    //设置码率
    (*enc_ctx)->bit_rate = 1000000; // 1000kbps

    //设置帧率
    (*enc_ctx)->time_base = (AVRational){1, 25}; //帧与帧之间的间隔是time_base
    (*enc_ctx)->framerate = (AVRational){25, 1}; //帧率,每秒25帧

    ret = avcodec_open2((*enc_ctx), codec, NULL);
    if(ret < 0){
        printf("Could not open codec:%s!\n", av_err2str(ret));
        exit(1);
    }
}

static AVFrame *create_frame(int width, int height){
    
    int ret = 0;
    
    AVFrame *frame = NULL;
    frame = av_frame_alloc();
    if(!frame){
        printf("Error, No Memory!\n");
        goto __ERROR;
    }
    
    //设置参数
    frame->width = width;
    frame->height = height;
    frame->format = AV_PIX_FMT_YUV420P;
    
    //alloc inner memory
    ret = av_frame_get_buffer(frame, 32); //按32位对齐
    if(ret < 0){
        printf("Error, Failed to alloc buffer for frame\n");
        goto __ERROR;
    }
    
    return frame;
    
__ERROR:
    if(frame){
        av_frame_free(&frame);
    }
    
    return NULL;
}

static void encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *newpkt, FILE *outfile){
    int ret = 0;
    if(frame){
        printf("send frame to encoder pts=%lld\n", frame->pts);
    }
    
    //送原始数据给编码器进行编码
    ret = avcodec_send_frame(enc_ctx, frame);
    if(ret < 0){
        printf("Error, Failed to send a frame for encoding\n");
        exit(1);
    }
    
    //从编码器获取编码好的数据
    while(ret >= 0){
        ret = avcodec_receive_packet(enc_ctx, newpkt); //编码器并不是一帧一帧的输出,可能是送入好几个都不输出,然后送入一个出来好几个,所以要用循环进行获取
        
        //如果编码器数据不足时,会返回EAGAIN 或者到数据尾部时会返回AVERROR_EOF
        if(ret == AVERROR(EAGAIN)||ret == AVERROR_EOF){
            return;
        }else if(ret < 0){
            printf("Error, Failled to encode!\n");
            exit(1);
        }
        fwrite(newpkt->data, 1, newpkt->size, outfile);
        av_packet_unref(newpkt);//减少packet的引用计数
    }
    
}

void rec_video(void){
    int ret = 0;
    //packet
    AVPacket pkt;
    AVFormatContext *fmt_ctx = NULL;
    AVCodecContext *enc_ctx = NULL;

    //set log level
    av_log_set_level(AV_LOG_DEBUG);

    //start record
    rec_status = 1000;

    //create file
    char *yuvout = "/Users/qqy/workspace/VedioFiles/video.yuv";
    char *out = "/Users/qqy/workspace/VedioFiles/video.h264";

    FILE *yuvoutfile = fopen(yuvout, "wb+");
    FILE *outfile = fopen(out, "wb+");

    //打开设备
    fmt_ctx = open_dev();
    if(!fmt_ctx){
        printf("Error, Failed to open device!\n");
        goto __ERROR;
    }

    
    //打开编码器
    open_encoder(V_WIDTH, V_HETGHT, &enc_ctx);
    
    //创建 AVFrame
    AVFrame *frame = create_frame(V_WIDTH, V_HETGHT);
    
    //创建编码输出packet
    AVPacket *newpkt = av_packet_alloc();
    if(!newpkt){
        printf("Error, Failed to alloc avpacket!\n");
        goto __ERROR;
    }
    
    int base = 0;
    
    //read data from device
//    while((ret = av_read_frame(fmt_ctx, &pkt)) == 0 && (rec_status > 0)) {
    while ((ret = av_read_frame(fmt_ctx, &pkt)) == 0 || ret == -35) {
            if(!rec_status){
                break;
            }
            if(ret == -35){
                av_usleep(100);
                continue;
            }

        
        av_log(NULL, AV_LOG_INFO, "packet size is %d(%p)\n", pkt.size, pkt.data);
        
        // (宽 x 高) x(yuv420=1.5 / yuv422=2 / yuv444=3)
//        fwrite(pkt.data, 1, 460800, yuvoutfile); //直接将采集的数据格式NV12存储下来
        
        //此时直接从设备获取到的数据格式为 YYYYYYYYUVUV      NV12的数据格式
        //目标要转成的YUV420P数据格式为  YYYYYYYYUUVV      YUV420的数据格式
        memcpy(frame->data[0], pkt.data, V_WIDTH*V_HETGHT); //640x480=307200 Y数据
        
        //307200之后是UV数据
        for(int i = 0; i < V_WIDTH*V_HETGHT/4; i++){
            frame->data[1][i] = pkt.data[V_WIDTH*V_HETGHT+i*2];
            frame->data[2][i] = pkt.data[V_WIDTH*V_HETGHT+i*2+1];
        }//虽然已经实现了NV12转为YUV420,但是效率不算太高,可以尝试使用汇编指令优化来进行优化。这里暂时理解就可以,后面可以使用libyuv实现转换
        
        fwrite(frame->data[0], 1, V_WIDTH*V_HETGHT, yuvoutfile);
        fwrite(frame->data[1], 1, V_WIDTH*V_HETGHT/4, yuvoutfile);
        fwrite(frame->data[2], 1, V_WIDTH*V_HETGHT/4, yuvoutfile);
        printf("pkt, pkt.size=%d!\n", pkt.size);
        fflush(yuvoutfile);
        
        frame->pts = base++;
        encode(enc_ctx, frame, newpkt, outfile);
        
        av_packet_unref(&pkt); //release pkt
        
        rec_status--;
    }
    
    encode(enc_ctx, NULL, newpkt, outfile); //这一步如果不做,可能送进去最后一帧时,编码器没有将全部数据输出,这个时候就有可能发生丢帧的现象
    
__ERROR:
    if(yuvoutfile){
        fclose(yuvoutfile);
    }
    
    if(fmt_ctx){
        avformat_close_input(&fmt_ctx);
    }
    
    av_log(NULL, AV_LOG_DEBUG, "finish!\n");
    return;
    
}

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值