8. 【视频编解码实战】

码流分析工具

  1. Elecard Stream Eye 最强大 也最贵的一个工具;
    下载地址
  2. CodecVisa比Elecard 稍便宜;
  3. 雷神开发的工具 免费,只支持Windows;
    H264码流分析工具下载地址
    完整功能工具下载地址

x264编码器相关参数介绍
x264参数在ffmpeg中的映射关系

编码步骤

1. 打开编码器

  • avcodec_find_encoder_by_name("libx264") 获取编码器

  • 根据codec获取编码器上下文, av,avcodec_alloc_context3;

  • 设置编码器上下文的相关参数:

    1. SPS的profile = FF_PROFILE_H264_HIGH_444;表示支持最大等级
    2. SPS的level = 50; 相当于5.0,最大支持2560 x 1920分辨率 帧率30
    3. 设置分辨率 width height
    4. 设置采样格式,pix_fmt = AV_PIX_FMT_YUV420P;
    5. 设置码率 bit_rate = 600000B,根据level参数表,level5.0的最大码率支持4000kbps;
    6. 设置gop一组的帧数 gop_size,帧数越小I帧越多数据量就越大
    7. 设置帧率,framerate = (AVRational){25, 1} 表示1秒25帧, time_base = (AVRational){1, 25} 表示帧与帧之间的间隔

    下面的参数设置是可选的:

    1. 设置gop中最小插入I帧的间隔 keyint_min,gop中也会自动根据变化大的帧数前插入I帧;
    2. 设置gop中最大B帧数,可以减少码流的大小,max_b_frames和has_b_frames, 一般设置不超过3帧;
    3. 设置参考帧的数量,refs, 一般是3~5;
  • 打开编码器:av_codec_open2

static AVCodecContext* open_codec(int width, int height) {
    
    AVCodecContext *ctx = NULL;
    AVCodec *codec = NULL;
    
    codec = avcodec_find_encoder_by_name("libx264");
//    codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    
    if (!codec) {
        printf("未找到libx264编码器\n");
        return  NULL;
    }
    
    ctx = avcodec_alloc_context3(codec);
    
    ctx->profile = FF_PROFILE_H264_HIGH_444;
    // 相当于5.0  分辨率最大支持2560 * 1920 最大帧率30  最大码率135000kbps
    ctx->level = 50;
    // 设置分辨率
    ctx->width = width;
    ctx->height = height;
    //设置格式
    ctx->pix_fmt = AV_PIX_FMT_YUV420P;
    // AVRation 第一个是分子 第二个是分母, time_base表示是帧与帧之间的间隔,也就是1/25
    ctx->time_base = (AVRational){1, 25};
    // 设置帧率 1 秒 25帧
    ctx->framerate = (AVRational){25,1};
    // 设置码率为600kbps
    ctx->bit_rate = 600000;
    // 设置每组gop的帧数
    ctx->gop_size = 50;
    
    // 设置一组gop中最多的B帧数
    ctx->max_b_frames = 3;
    // 设置一组中的参考帧的数量
    ctx->refs = 3;
    
    int ret = avcodec_open2(ctx, codec, NULL);
    
    if (ret < 0) {
        printf("编码器打开失败:%s \n", av_err2str(ret));
        avcodec_free_context(&ctx);
        return NULL;
    }
    
    return ctx;
}

2. 准备编码数据AVFrame

  • 跟音频编码差不多,alloc一个frame;
  • 设置数据的分辨率、采样格式;
  • 获取缓冲区,以32位对齐;av_frame_get_buffer
/**
 * 创建编码输入缓冲区
 */
static AVFrame* create_frame(int width, int height) {
    
    AVFrame *frame = NULL;
    frame = av_frame_alloc();
    if (!frame) {
        return NULL;
    }
    
    frame->width = width;
    frame->height = height;
    frame->format = AV_PIX_FMT_YUV420P;
    frame->pts = 0;
    // 缓冲区以32位对齐
    int ret = av_frame_get_buffer(frame, 32);
    if (ret < 0) {
        printf("初始化frame缓冲区失败:%s \n", av_err2str(ret));
        if (frame) {
            av_frame_free(&frame);
        }
        return NULL;
    }
    
    return frame;
}

3. 转换NV12到YUV420p进行存储

  • 可以使用libyuv转,也可以自己写for循环转换;
  • 转换步骤:

    1、先将采集的数据的Y部分拷贝到frame,Y的大小 = 分辨率的宽 * 分辨率的高;
    2、再通过遍历将U和V数据放到缓冲区中的第一组
    3、确定遍历次数:因为420的比例中,U和V的数据各占Y的4分之一,所以遍历次数就是Y数据量的4分之一
    4、U数据放入缓冲区的第二组,V数据放入缓冲区的第三组,再因为NV12的存储格式是YYYYYYYY UVUV,所以U和V数据是在Y数据之后的,而且UV数据的排列方式是线性的相互错开的,所以在获取UV的时候,也要错开;

  • 在存储的时候也要将缓冲区的1组和2组也要写入文件;
/**
    将nv12的数据转换yuv420p,再进行存储
 */
static void convert_nv12_to_yuv420(AVFrame *frame, AVPacket *packet, FILE *file) {
    
    // 手动转换nv12 到 YUV420p
    // 1. 先将Y数据拷贝frame缓冲区的第一组
    int y_count = 640 * 480;
    memcpy(frame->data[0], packet->data, y_count);
    // 2. 将UV数据分别拷贝到frame缓冲区的第二和第三组
    for (int i = 0; i < y_count / 4; i ++) {
        // 拷贝U数据
        frame->data[1][i] = packet->data[y_count + i * 2];
        // 拷贝V数据
        frame->data[2][i] = packet->data[y_count + i * 2 + 1];

    }
    fwrite(frame->data[0], 1, y_count, file);
    fwrite(frame->data[1], 1, y_count / 4, file);
    fwrite(frame->data[2], 1, y_count / 4, file);
    
}

4. H264编码

  • 编码过程跟音频编码差不多
  • 调用av_send_frame,将数据送入编码器
  • 调用av_receive_packet,,读取编码好的数据
  • 需要注意的frame的pts每编码一次都要变化,进行+1操作,不然会造成花屏;
/**
    开始编码
 */
static void encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *packet, FILE *file) {
    
    int ret = 0;
    ret = avcodec_send_frame(ctx, frame);
    
    while (ret == 0) {
        
        ret = avcodec_receive_packet(ctx, packet);
        if (ret == -EAGAIN || ret == AVERROR_EOF) {
            return;
        } else if (ret == 0) {
            // 读取编码好的数据
            fwrite(packet->data, 1, packet->size, file);
            av_packet_unref(packet);
        } else {
            printf("获取视频编码错误: %s",av_err2str(ret));
        }
    }
}

5. 外层代码

void start_video_recorder(void) {
    
    AVFormatContext *fmt_ctx = NULL;
    fmt_ctx = open_device();
    if (!fmt_ctx) {
        goto __ERROR;
    }
    
    AVPacket *packet = NULL;
    packet = av_packet_alloc();
    if (!packet) {
        goto __ERROR;
    }
    
    AVCodecContext *codec_ctx = open_codec(640, 480);
    if (!codec_ctx) {
        goto  __ERROR;
    }
    AVFrame *codec_frame = create_frame(640, 480);
    if (!codec_frame) {
        goto  __ERROR;
    }
    AVPacket *codec_packet = av_packet_alloc();
    if (!codec_packet) {
        goto  __ERROR;
    }
    char *path = "/Users/cunw/Desktop/learning/音视频学习/音视频文件/video_recoder.yuv";
    FILE *file = fopen(path, "wb+");
    FILE *h264_file = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/video_encoder.h264", "wb+");
    
    int rst = 0;
    while (isRecording) {
        rst = av_read_frame(fmt_ctx, packet);
        if (rst == 0 && packet->size > 0) {
            printf("packet size is %d\n", packet->size);
            // 将nv12 转换成 yuv420
            convert_nv12_to_yuv420(codec_frame,packet, file);
            
            codec_frame->pts += 1;
            // 开始编码
            encode(codec_ctx, codec_frame, codec_packet, h264_file);
            // 释放已读取的数据
            av_packet_unref(packet);
        } else if (rst == -EAGAIN) {
            av_usleep(1);
        }
    }
    encode(codec_ctx, NULL, codec_packet, h264_file);
    
__ERROR:
    
    isRecording = 0;
    fflush(file);
    fclose(file);
    fflush(h264_file);
    fclose(h264_file);
    avcodec_free_context(&codec_ctx);
    
    av_frame_free(&codec_frame);
    av_packet_free(&codec_packet);
    
    
    av_packet_free(&packet);
    avformat_close_input(&fmt_ctx);
    printf("video encoder finished!\n");
}

void stop_video_recorder(void) {
    
    isRecording = 0;
    
}

你可能感兴趣的:(8. 【视频编解码实战】)