将视频 YUV 格式编码成 H264


首先开始的时候我们插入一张雷神大大的图帮助大家理解一下我们今天的操作究竟属于那一步。


音视频格式封装层次

从上图可以看出我们要做的,就是将像素层的 YUV 格式,编码出编码层的 h264数据。


前面讲到我们已经成功编译出 iOS 中可用的 ffmpeg 的库了,那么我们首先熟悉一下今天我们要用到的 ffmpeg 中的函数和结构体

AVFormatContext: 数据文件操作者,主要是用于存储音视频封装格式中包含的信息, 在工程当中占着具足轻重的地位,因为很多函数都要用到它作为参数。同时,它也是我们进行解封装的功能结构体。


AVOutputFormat: 输出的格式,包括音频封装格式、视频装格式、字幕封装格式,所有封装格式都在 AVCodecID 这个枚举类型上面了


AVStream: 一个装载着视频/音频流信息的结构体,包括音视频流的长度,元数据信息,其中 index 属性用于标识视频/音频流。


AVCodecContext: 这个结构体十分庞大,但它的主要是用于编码使用的,结构体中的的 AVCodec *codec 就是编码所采用的编码器器, 当然,这个结构体中要存入视频的基本参数,例如宽高等,存入音频的基本参数,声道,采样率等。


AVCodec:编码器,设置编码类型,像素格式,视频宽高,fps(每秒帧数), 用于编解码音视频编码层使用。


AVIOContext:用于管理输入输出结构体。例如解码的情况下,将一个视频文件中的数据先从硬盘中读入到结构体中的 buffer 中,然后送给解码器用于解码,后面我们会用到。


AVFrame: 结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),此外还包含了一些相关的信息。比如说,解码的时候存储了宏块类型表,QP表,运动矢量表等数据。编码的时候也存储了相关的数据。因此在使用FFMPEG进行码流分析的时候,AVFrame是一个很重要的结构体。

好了,上面就是我们这次解封装用到的结构体的大概解析,那么我们就上代码,好好分析一番。


1、先取个霸气点的函数名,通过输入一个 yuv 文件路径,然后将文件数据进行编码,输出 H264文件。

yuvCodecToVideoH264(const char *input_file_name)

2、打开输入的 yuv 文件, 并设置我们 h264 文件的输出路径,

FILE *in_file = fopen(input_file, "rb");  
// 因为我们在 iOS 工程当中,所以输出路径当然要设置本机的路径了
const char* out_file = [[NSTemporaryDirectory() stringByAppendingPathComponent:@"dash.h264"] cStringUsingEncoding:NSUTF8StringEncoding];

3、获取 yuv 视频中的信息

// 注册 ffmpeg 中的所有的封装、解封装 和 协议等,当然,你也可用以下两个函数代替  
// * @see av_register_input_format()
// * @see av_register_output_format()
 av_register_all();

//  用作之后写入视频帧并编码成 h264,贯穿整个工程当中
AVFormatContext* pFormatCtx;
pFormatCtx = avformat_alloc_context();

// 通过这个函数可以获取输出文件的编码格式, 那么这里我们的 fmt 为 h264 格式(AVOutputFormat *)
fmt = av_guess_format(NULL, out_file, NULL);
pFormatCtx->oformat = fmt;

4、将输出文件中的数据读入到程序的 buffer 当中,方便之后的数据写入,也可以说缓存数据写入

// 打开文件的缓冲区输入输出,flags 标识为  AVIO_FLAG_READ_WRITE ,可读写
if (avio_open(&pFormatCtx->pb,out_file, AVIO_FLAG_READ_WRITE) < 0){
  printf("Failed to open output file! \n");
  return;
}

5、创建流媒体数据,规范流媒体的编码格式,设置视频流的 fps

AVStream* video_st;
// 通过媒体文件控制者获取输出文件的流媒体数据,这里 AVCodec * 写 0 , 默认会为我们计算出合适的编码格式
video_st = avformat_new_stream(pFormatCtx, 0);

// 设置 25 帧每秒 ,也就是 fps 为 25
video_st->time_base.num = 1;
video_st->time_base.den = 25;

if (video_st==NULL){
  return ;
}

6、为输出文件设置编码所需要的参数和格式

// 用户存储编码所需的参数格式等等
AVCodecContext* pCodecCtx;

// 从媒体流中获取到编码结构体,他们是一一对应的关系,一个 AVStream 对应一个  AVCodecContext
 pCodecCtx = video_st->codec;

// 设置编码器的 id,每一个编码器都对应着自己的 id,例如 h264 的编码 id 就是 AV_CODEC_ID_H264
pCodecCtx->codec_id = fmt->video_codec;

// 设置编码类型为 视频编码
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;

// 设置像素格式为 yuv 格式
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;

// 设置视频的宽高
pCodecCtx->width = 480;
pCodecCtx->height = 720;

// 设置比特率,每秒传输多少比特数 bit,比特率越高,传送速度越快,也可以称作码率,
// 视频中的比特是指由模拟信号转换为数字信号后,单位时间内的二进制数据量。
pCodecCtx->bit_rate = 400000;

// 设置图像组层的大小。
// 图像组层是在 MPEG 编码器中存在的概念,图像组包 若干幅图像, 组头包 起始码、GOP 标志等,如视频磁带记录器时间、控制码、B 帧处理码等;
pCodecCtx->gop_size=250;

// 设置 25 帧每秒 ,也就是 fps 为 25
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 25;

//设置 H264 中相关的参数
//pCodecCtx->me_range = 16;
//pCodecCtx->max_qdiff = 4;
//pCodecCtx->qcompress = 0.6;
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;

// 设置 B 帧最大的数量,B帧为视频图片空间的前后预测帧, B 帧相对于 I、P 帧来说,压缩率比较大,也就是说相同码率的情况下,
// 越多 B 帧的视频,越清晰,现在很多打视频网站的高清视频,就是采用多编码 B 帧去提高清晰度,
// 但同时对于编解码的复杂度比较高,比较消耗性能与时间
pCodecCtx->max_b_frames=3;

// 可选设置
AVDictionary *param = 0;
//H.264
if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
// 通过--preset的参数调节编码速度和质量的平衡。
av_dict_set(&param, "preset", "slow", 0);

// 通过--tune的参数值指定片子的类型,是和视觉优化的参数,或有特别的情况。
// zerolatency: 零延迟,用在需要非常低的延迟的情况下,比如电视电话会议的编码
av_dict_set(&param, "tune", "zerolatency", 0);

顺便说一下h264 当中有片组的概念,其中编码片分为5种,I 片、P 片、B 片、SP 片和 SI 片。

ES 码流是 MPEG 码流中的基本流,由视频压缩编码后的视频基 码流(Video ES)和音频压缩编码后的音频基 码流(Audio ES)组成。
以下顺带一张 ES 码流的结构图片,作为记录学习之用


ES 码流结构

ES 码流采用图像序列(PS)、图像组(GOP)、图像(P)、片(slice)、宏块(MB)、块(B)六层结构。
(1)图像序列层,图像序列包括若干 GOP,序列头包 起始码和序列参数,如档次、级别、彩色图像格式、帧场选择等等;
(2)图像组层,图像组包 若干幅图像,组头包 起始码、GOP 标志等,如视频磁带记录器时间、控制码、B 帧处理码等;
(3)图像层,一幅图像包 若干片,头信息中有起始码、P 标志,如时间、参考帧号、图像类型、MV、分级等;
(4)片层,片是最小的同步单位,包 若干宏块,片头中有起始码、片地址、量化步长等;
(5)宏块层,宏块由 4 个 8×8 亮度块和 2 个色度块组成,宏块头包括宏块地址、宏块类型、运动矢量等。

7、printf(输出) 一些关于输出格式的详细数据,例如时间,比特率,数据流,容器,元数据,辅助数据,编码,时间戳等等

av_dump_format(pFormatCtx, 0, out_file, 1);

8、设置编码器

// 通过 codec_id 找到对应的编码器
pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
if (!pCodec){
  printf("Can not find encoder! \n");
  return;
}

// 打开编码器,并设置参数 param
if (avcodec_open2(pCodecCtx, pCodec,&param) < 0){
  printf("Failed to open encoder! \n");
  return;
}

9、设置原始数据 AVFrame

AVFrame *pFrame = av_frame_alloc();

// 通过像素格式(这里为 YUV)获取图片的真实大小,例如将 480 * 720 转换成 int 类型
int picture_size = avpicture_get_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);

// 将 picture_size 转换成字节数据,byte
unsigned char *picture_buf = (uint8_t *)av_malloc(picture_size);

// 设置原始数据 AVFrame 的每一个frame 的图片大小,AVFrame 这里存储着 YUV 非压缩数据
avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);

10、准备写入数据之前,当然要先写编码的头部了

// 编写 h264 封装格式的文件头部,基本上每种编码都有着自己的格式的头部,想看具体实现的同学可以看看 h264 的具体实现
int ret = avformat_write_header(pFormatCtx,NULL);
if (ret < 0) {
  printf("write header is failed");
  return;
}

这里顺便记录一下, h264 原始码流,又称为原始码流,都是由一个一个的 NALU 组成的,结构体如下

enum nal_unit_type_e
{
    NAL_UNKNOWN     = 0,    // 未使用
    NAL_SLICE       = 1,    // 不分区、非 IDR 图像的片
    NAL_SLICE_DPA   = 2,    // 片分区 A
    NAL_SLICE_DPB   = 3,    // 片分区 B
    NAL_SLICE_DPC   = 4,    // 片分区 C
    NAL_SLICE_IDR   = 5,    /* ref_idc != 0 */  // 序列参数集
    NAL_SEI         = 6,    /* ref_idc == 0 */  // 图像参数集
    NAL_SPS         = 7,    // 分界符
    NAL_PPS         = 8,    // 序列结束
    NAL_AUD         = 9,    // 码流结束
    NAL_FILLER      = 12,   // 填充
    /* ref_idc == 0 for 6,9,10,11,12 */
};
enum nal_priority_e // 优先级
{
    NAL_PRIORITY_DISPOSABLE = 0,
    NAL_PRIORITY_LOW        = 1,
    NAL_PRIORITY_HIGH       = 2,
    NAL_PRIORITY_HIGHEST    = 3,
};

typedef struct
{
    int startcodeprefix_len;      //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)
    unsigned len;                 //! Length of the NAL unit (Excluding the start code, which does not belong to the NALU)
    unsigned max_size;            //! Nal Unit Buffer size
    int forbidden_bit;            //! should be always FALSE
    int nal_reference_idc;        //! NALU_PRIORITY_xxxx
    int nal_unit_type;            //! NALU_TYPE_xxxx
    char *buf;                    //! contains the first byte followed by the EBSP
} NALU_t;

11、创建编码后的数据 AVPacket 结构体来存储 AVFrame 编码后生成的数据

AVCodec* pCodec;
av_new_packet(&pkt,picture_size);

其实从这里看出 AVPacket 跟 AVFrame 的关系如下
编码前:AVFrame
编码后:AVPacket

12、写入 yuv 数据到 AVFrame 结构体中

// 设置 yuv 数据中 y 图的宽高
int y_size = pCodecCtx->width * pCodecCtx->height;

for (int i=0; i<framenum; i++){
//Read raw YUV data
if (fread(picture_buf, 1, y_size*3/2, in_file) <= 0){
  printf("Failed to read raw data! \n");
  return ;
}else if(feof(in_file)){
  break;
}
pFrame->data[0] = picture_buf;              // Y
pFrame->data[1] = picture_buf+ y_size;      // U
pFrame->data[2] = picture_buf+ y_size*5/4;  // V
//PTS
//pFrame->pts=i;
// 设置这一帧的显示时间
pFrame->pts=i*(video_st->time_base.den)/((video_st->time_base.num)*25);
int got_picture=0;
// 利用编码器进行编码,将  pFrame 编码后的数据传入  pkt 中
int ret = avcodec_encode_video2(pCodecCtx, &pkt,pFrame, &got_picture);
if(ret < 0){
  printf("Failed to encode! \n");
  return ;
}

// 编码成功后写入 AVPacket 到 输入输出数据操作着 pFormatCtx 中,当然,记得释放内存
if (got_picture==1){
  printf("Succeed to encode frame: %5d\tsize:%5d\n",framecnt,pkt.size);
  framecnt++;
  pkt.stream_index = video_st->index;
  ret = av_write_frame(pFormatCtx, &pkt);
  av_free_packet(&pkt);
  }
}

13、flush 编码

int 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) {
        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("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n",enc_pkt.size);
        /* mux encoded frame */
        ret = av_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
    }
    return ret;
}


int ret2 = flush_encoder(pFormatCtx,0);
if (ret2 < 0) {
  printf("Flushing encoder failed\n");
  return;
}

14、我们上面写完了编码头、编码数据,当然也要写入编码的尾部表示结束了啦,这样才是一个完整的编码格式嘛

// 写入数据流尾部到输出文件当中,并释放文件的私有数据
av_write_trailer(pFormatCtx);

15、释放我们之前创建的内存

if (video_st){  
        // 关闭编码器
        avcodec_close(video_st->codec);
        // 释放 AVFrame
        av_free(pFrame);
        // 释放图片 buf,就是 free() 函数,硬要改名字,当然这是跟适应编译环境有关系的
        av_free(picture_buf);
 }

// 关闭输入数据的缓存
avio_close(pFormatCtx->pb);
// 释放 AVFromatContext 结构体
avformat_free_context(pFormatCtx);

// 关闭输入文件
fclose(in_file);

   
   

雷霄骅 (Lei Xiaohua)
leixiaohua1020@126.com
http://blog.csdn.net/leixiaohua1020


版权声明:本文为博主原创文章,未经博主允许不得转载。

  • 13
    点赞
  • 85
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
以下是一个简单的例子,演示如何使用MediaCodec将YUV格式视频编码为MP4格式: ```cpp #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/mman.h> #include <media/NdkMediaCodec.h> #include <media/NdkMediaFormat.h> #define MAX_BUFFER_SIZE 1024 * 1024 int main(int argc, char *argv[]) { if (argc != 4) { printf("Usage: %s <input.yuv> <output.mp4> <fps>\n", argv[0]); return 0; } const char *input_file = argv[1]; const char *output_file = argv[2]; const int fps = atoi(argv[3]); // 打开输入文件 int input_fd = open(input_file, O_RDONLY); if (input_fd < 0) { printf("Failed to open input file: %s\n", input_file); return -1; } // 获取输入文件大小 struct stat input_stat; if (fstat(input_fd, &input_stat) < 0) { printf("Failed to get input file size.\n"); close(input_fd); return -1; } // 映射输入文件 uint8_t *input_data = (uint8_t *)mmap(NULL, input_stat.st_size, PROT_READ, MAP_PRIVATE, input_fd, 0); if (input_data == MAP_FAILED) { printf("Failed to mmap input file.\n"); close(input_fd); return -1; } // 初始化输出文件 int output_fd = open(output_file, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (output_fd < 0) { printf("Failed to open output file: %s\n", output_file); munmap(input_data, input_stat.st_size); close(input_fd); return -1; } // 初始化编码器 AMediaCodec *codec = AMediaCodec_createEncoderByType("video/avc"); AMediaFormat *format = AMediaFormat_new(); AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, "video/avc"); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, 1920); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, 1080); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_FRAME_RATE, fps); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, OMX_COLOR_FormatYUV420SemiPlanar); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_BIT_RATE, 8000000); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, 1); AMediaCodec_configure(codec, format, NULL, NULL, AMEDIACODEC_CONFIGURE_FLAG_ENCODE); AMediaCodec_start(codec); // 编码循环 uint8_t *input_buffer = NULL; uint8_t *output_buffer = NULL; size_t input_buffer_size = 0; size_t output_buffer_size = 0; AMediaCodecBufferInfo buffer_info; ssize_t input_buffer_index = -1; ssize_t output_buffer_index = -1; size_t frame_size = input_stat.st_size / (fps * 10); // 每帧大小 size_t frame_index = 0; size_t pos = 0; while (true) { // 获取输入缓冲区 input_buffer_index = AMediaCodec_dequeueInputBuffer(codec, 2000); if (input_buffer_index >= 0) { input_buffer = AMediaCodec_getInputBuffer(codec, input_buffer_index, &input_buffer_size); if (input_buffer_size >= frame_size) { memcpy(input_buffer, input_data + pos, frame_size); AMediaCodec_queueInputBuffer(codec, input_buffer_index, 0, frame_size, frame_index * 1000000 / fps, 0); pos += frame_size; frame_index++; } } // 获取输出缓冲区 output_buffer_index = AMediaCodec_dequeueOutputBuffer(codec, &buffer_info, 2000); if (output_buffer_index >= 0) { output_buffer = AMediaCodec_getOutputBuffer(codec, output_buffer_index, &output_buffer_size); write(output_fd, output_buffer, buffer_info.size); AMediaCodec_releaseOutputBuffer(codec, output_buffer_index, false); } // 结束条件 if (pos >= input_stat.st_size) { break; } } // 停止编码器 AMediaCodec_stop(codec); AMediaCodec_delete(codec); AMediaFormat_delete(format); // 关闭文件 munmap(input_data, input_stat.st_size); close(input_fd); close(output_fd); return 0; } ``` 在这个例子中,我们使用了以下的步骤: 1. 打开输入文件并获取文件大小。 2. 将输入文件映射到内存中。 3. 初始化输出文件。 4. 初始化编码器,并设置编码器的格式。 5. 进行编码循环,将YUV数据送给编码器。 6. 将编码输出写入到输出文件中。 7. 停止编码器并关闭文件。 需要注意的是,这个例子中的编码器设置了固定的宽度、高度、帧率、颜色格式、比特率和关键帧间隔。在实际应用中,这些参数可能需要根据具体的视频进行调整。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值