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;
    
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容