使用ffmpeg接口将YUV编码封装为文件

来源:互联网 发布:数据架构师 编辑:程序博客网 时间:2024/05/16 10:09

目录

  • 1. 第一步: 初始化相关结构体
    • 1.1. 第一步: 手动创造 AVFormatContext(封装输出文件格式)
    • 1.2. 第二步: 设置stream的id和codec等参数
    • 1.3. 第三步: 打开编码音视频数据所需要的编码器
    • 1.4. 第四步: 分配相应的frame对象 为frame分配空间
    • 1.5. 打开音视频,分配frame对象的代码
  • 2. 第二步: 打开输出文件并写入文件头
  • 3. 第三步: 编码和封装循环
    • 3.1. 第一步: 读取原始视频数据
      • 3.1.1. 第一步: 根据时长判断是否需要继续进行处理、读取视频到AVFrame和设置pts。其中时长判断部分根据pts和AVCodecContext的time_base判断。实现如下:
      • 3.1.2. 第二步: 读取视频到AVFrame我们定义一个fill_yuv_image函数实现:
      • 3.1.3. 第三步: 然后进行pts的设置,很简单,就是上一个frame的pts递增1:
      • 3.1.4. 整个获取视频信号的实现如:
    • 3.2. 第二步: 编码
    • 3.3. 第三步: 写出编码后的数据到输出视频文件
    • 3.4. 写入文件尾,并进行收尾工作

第一步: 初始化相关结构体

第一步: 手动创造 AVFormatContext(封装输出文件格式)

第二步: 设置stream的id和codec等参数

AVStream::codec是一个AVCodecContext类型的指针变量成员,设置其中的值可以对编码进行配置。整个添加stream的例子如:

* Add an output stream. *
static void add_stream(OutputStream *ost, AVFormatContext *oc, AVCodec **codec, enum AVCodecID codec_id)
{
AVCodecContext *c;
int i;

* find the encoder *
*codec = avcodec_find_encoder(codec_id);
if (!(*codec))
{
fprintf(stderr, “Could not find encoder for ‘%s’\n”, avcodec_get_name(codec_id));
exit(1);
}

ost->st = avformat_new_stream(oc, *codec);
if (!ost->st)
{
fprintf(stderr, “Could not allocate stream\n”);
exit(1);
}
ost->st->id = oc->nb_streams - 1;
c = ost->st->codec;

switch ((*codec)->type)
{
case AVMEDIA_TYPE_AUDIO:
c->sample_fmt = (*codec)->sample_fmts ? (*codec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
c->bit_rate = 64000;
c->sample_rate = 44100;

if ((*codec)->supported_samplerates)
{
c->sample_rate = (*codec)->supported_samplerates[0];
for (i = 0; (*codec)->supported_samplerates[i]; i++)
{
if ((*codec)->supported_samplerates[i] == 44100)
c->sample_rate = 44100;
}
}

c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
c->channel_layout = AV_CH_LAYOUT_STEREO;
if ((*codec)->channel_layouts)
{
c->channel_layout = (*codec)->channel_layouts[0];
for (i = 0; (*codec)->channel_layouts[i]; i++)
{
if ((*codec)->channel_layouts[i] == AV_CH_LAYOUT_STEREO)
c->channel_layout = AV_CH_LAYOUT_STEREO;
}
}
c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
{
AVRational r = { 1, c->sample_rate };
ost->st->time_base = r;
}
break;

case AVMEDIA_TYPE_VIDEO:
c->codec_id = codec_id;

c->bit_rate = 400000;
* Resolution must be a multiple of two. *
c->width = 352;
c->height = 288;
/* timebase: This is the fundamental unit of time (in seconds) in terms

  • of which frame timestamps are represented. For fixed-fps content,
  • timebase should be 1/framerate and timestamp increments should be
  • identical to 1. */

{
AVRational r = { 1, STREAM_FRAME_RATE };
ost->st->time_base = r;
}
c->time_base = ost->st->time_base;

c->gop_size = 12; * emit one intra frame every twelve frames at most *
c->pix_fmt = AV_PIX_FMT_YUV420P;
if (c->codec_id == AV_CODEC_ID_MPEG2VIDEO)
{
* just for testing, we also add B frames *
c->max_b_frames = 2;
}
if (c->codec_id == AV_CODEC_ID_MPEG1VIDEO)
{
/* Needed to avoid using macroblocks in which some coeffs overflow.

  • This does not happen with normal video, it just happens here as
  • the motion of the chroma plane does not match the luma plane. */

    c->mb_decision = 2;
    }
    break;

default:
break;
}

*\* Some formats want stream headers to be separate. \**  if (oc->oformat->flags & AVFMT\_GLOBALHEADER)      c->flags |= AV\_CODEC\_FLAG\_GLOBAL\_HEADER;  

}

第三步: 打开编码音视频数据所需要的编码器

打开编码器如之前一样,调用avcodec_open函数

第四步: 分配相应的frame对象 为frame分配空间

打开音视频,分配frame对象的代码

void Open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg, IOParam &io)
{
int ret;
AVCodecContext *c = ost->st->codec;
AVDictionary *opt = NULL;

av_dict_copy(&opt, opt_arg, 0);

* open the codec *
ret = avcodec_open2(c, codec, &opt);
av_dict_free(&opt);
if (ret < 0)
{
fprintf(stderr, “Could not open video codec: %d\n”, ret);
exit(1);
}

* allocate and init a re-usable frame *
ost->frame = alloc_picture(c->pix_fmt, c->width, c->height);
if (!ost->frame)
{
fprintf(stderr, “Could not allocate video frame\n”);
exit(1);
}

/* If the output format is not YUV420P, then a temporary YUV420P

  • picture is needed too. It is then converted to the required
  • output format. */

ost->tmp_frame = NULL;
if (c->pix_fmt != AV_PIX_FMT_YUV420P)
{
ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, c->width, c->height);
if (!ost->tmp_frame)
{
fprintf(stderr, “Could not allocate temporary picture\n”);
exit(1);
}
}

//打开输入YUV文件  fopen\_s(&g\_inputYUVFile, io.input\_file\_name, "rb+");  if (g\_inputYUVFile == NULL)  {      fprintf(stderr, "Open input yuv file failed.\n");      exit(1);  }  

}

第二步: 打开输出文件并写入文件头

如果判断需要写出文件的话,则需要打开输出文件。在这里,我们可以不再定义输出文件指针,并使用fopen打开,而是直接使用FFMpeg的API——avio_open来实现输出文件的打开功能。该函数的声明如下:

int avio_open(AVIOContext **s, const char *url, int flags);

该函数的输入参数为:

s:输出参数,返回一个AVIOContext;如果打开失败则返回NULL;
url:输出的url或者文件的完整路径;
flags:控制文件打开方式,如读方式、写方式和读写方式;

实际的代码实现方式如下:

* open the output file, if needed *
if (!(fmt->flags & AVFMT_NOFILE))// 没有打开文件,需要打开文件
{
ret = avio_open(&oc->pb, io.output_file_name, AVIO_FLAG_WRITE);
if (ret < 0)
{
fprintf(stderr, “Could not open ‘%s’: %d\n”, io.output_file_name, ret);
return 1;
}
}

写入文件头操作是生成视频文件中极为重要的一步,而实现过程却非常简单,只需要通过函数avformat_write_header即可,该函数的声明为:

int avformat_write_header(AVFormatContext *s, AVDictionary **options);

其输入参数实际上重要的只有第一个,即标记输出文件的句柄对象指针;options用于保存无法识别的设置项,可以传入一个空指针。其返回值表示写文件头成功与否,成功则返回0,失败则返回负的错误码。

实现方式如:

* Write the stream header, if any. *
ret = avformat_write_header(oc, &opt);
if (ret < 0)
{
fprintf(stderr, “Error occurred when opening output file: %d\n”,ret);
return 1;
}

第三步: 编码和封装循环

以视频流为例。编解码循环的过程实际上可以封装在一个函数Write_video_frame中。该函数从逻辑上可以分为3个部分:获取原始视频信号、视频编码、写入输出文件。

第一步: 读取原始视频数据

第一步: 根据时长判断是否需要继续进行处理、读取视频到AVFrame和设置pts。其中时长判断部分根据pts和AVCodecContext的time_base判断。实现如下:

AVCodecContext *c = ost->st->codec;

* check if we want to generate more frames *
{
AVRational r = { 1, 1 };
if (av_compare_ts(ost->next_pts, ost->st->codec->time_base, STREAM_DURATION, r) >= 0) // 当前帧的时间没有到达最大的时间间隔
{
return NULL;
}
}

第二步: 读取视频到AVFrame我们定义一个fill_yuv_image函数实现:

static void fill_yuv_image(AVFrame *pict, int frame_index, int width, int height)
{
int x, y, i, ret;

/* when we pass a frame to the encoder, it may keep a reference to it

  • internally;
  • make sure we do not overwrite it here

*/
ret = av_frame_make_writable(pict);
if (ret < 0)
{
exit(1);
}

i = frame_index;

* Y *
for (y = 0; y < height; y++)
{
ret = fread_s(&pict->data[0][y * pict->linesize[0]], pict->linesize[0], 1, width, g_inputYUVFile);
if (ret != width)
{
printf(“Error: Read Y data error.\n”);
exit(1);
}
}

* U *
for (y = 0; y < height / 2; y++)
{
ret = fread_s(&pict->data[1][y * pict->linesize[1]], pict->linesize[1], 1, width / 2, g_inputYUVFile);
if (ret != width / 2)
{
printf(“Error: Read U data error.\n”);
exit(1);
}
}

*\* V \**  for (y = 0; y < height / 2; y++)  {      ret = fread\_s(&pict->data[2][y \* pict->linesize[2]], pict->linesize[2], 1, width / 2, g\_inputYUVFile);      if (ret != width / 2)      {          printf("Error: Read V data error.\n");          exit(1);      }  }  

}

第三步: 然后进行pts的设置,很简单,就是上一个frame的pts递增1:

ost->frame->pts = ost->next_pts++;

整个获取视频信号的实现如:

static AVFrame *get_video_frame(OutputStream *ost)
{
AVCodecContext *c = ost->st->codec;

* check if we want to generate more frames *
{
AVRational r = { 1, 1 };
if (av_compare_ts(ost->next_pts, ost->st->codec->time_base, STREAM_DURATION, r) >= 0)
{
return NULL;
}
}

fill_yuv_image(ost->frame, ost->next_pts, c->width, c->height);

ost->frame->pts = ost->next_pts++;

return ost->frame;  

}

第二步: 编码

视频编码的方式同之前几次使用的方式相同,即调用avcodec_encode_video2,实现方法如:

* encode the image *
ret = avcodec_encode_video2(c, &pkt, frame, &got_packet);
if (ret < 0)
{
fprintf(stderr, “Error encoding video frame: %d\n”, ret);
exit(1);
}

第三步: 写出编码后的数据到输出视频文件

这部分的实现过程很简单,方式如下:

* rescale output packet timestamp values from codec to stream timebase *
av_packet_rescale_ts(pkt, *time_base, st->time_base);
pkt->stream_index = st->index;

* Write the compressed frame to the media file. *
// log_packet(fmt_ctx, pkt);
return av_interleaved_write_frame(fmt_ctx, pkt);

av_packet_rescale_ts函数的作用为不同time_base度量之间的转换,在这里起到的作用是将AVCodecContext的time_base转换为AVStream中的time_base。av_interleaved_write_frame函数的作用是写出AVPacket到输出文件。该函数的声明为:

int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);

该函数的声明也很简单,第一个参数是之前打开并写入文件头的文件句柄,第二个参数是写入文件的packet。返回值为错误码,成功返回0,失败则返回一个负值。

Write_video_frame函数的整体实现如:

int Write_video_frame(AVFormatContext *oc, OutputStream *ost)
{
int ret;
AVCodecContext *c;
AVFrame *frame;
int got_packet = 0;
AVPacket pkt = { 0 };

c = ost->st->codec;

frame = get_video_frame(ost);

av_init_packet(&pkt);

* encode the image *
ret = avcodec_encode_video2(c, &pkt, frame, &got_packet);
if (ret < 0)
{
fprintf(stderr, “Error encoding video frame: %d\n”, ret);
exit(1);
}

if (got_packet)
{
ret = write_frame(oc, &c->time_base, ost->st, &pkt);
}
else
{
ret = 0;
}

if (ret < 0)
{
fprintf(stderr, “Error while writing video frame: %d\n”, ret);
exit(1);
}

return (frame || got\_packet) ? 0 : 1;  

}

以上是写入一帧视频数据的方法,写入音频的方法于此大同小异。整个编码封装的循环上层实现如:

while (encode_video || encode_audio)
{
* select the stream to encode *
if (encode_video && (!encode_audio || av_compare_ts(video_st.next_pts, video_st.st->codec->time_base, audio_st.next_pts, audio_st.st->codec->time_base) <= 0))
{// 允许写视频,&&不需要写音频||视频的进度落后于音频
encode_video = !Write_video_frame(oc, &video_st);
if (encode_video)
{
printf(“Write %d video frame.\n”, videoFrameIdx++);
}
else
{
printf(“Video ended, exit.\n”);
}
}
else
{
encode_audio = !Write_audio_frame(oc, &audio_st);
if (encode_audio)
{
printf(“Write %d audio frame.\n”, audioFrameIdx++);
}
else
{
printf(“Audio ended, exit.\n”);
}
}
}

写入文件尾,并进行收尾工作

写入文件尾的数据同写文件头一样简单,只需要调用函数av_write_trailer即可实现:

int av_write_trailer(AVFormatContext *s);

该函数只有一个参数即视频文件的句柄,当返回值为0时表示函数执行成功。

整个流程的收尾工作包括关闭文件中的数据流、关闭输出文件和释放AVCodecContext对象。其中关闭数据流的实现方式如:

void Close_stream(AVFormatContext *oc, OutputStream *ost)
{
avcodec_close(ost->st->codec);
av_frame_free(&ost->frame);
av_frame_free(&ost->tmp_frame);
sws_freeContext(ost->sws_ctx);
swr_free(&ost->swr_ctx);
}

关闭输出文件和释放AVCodecContext对象:

if (!(fmt->flags & AVFMT_NOFILE))
* Close the output file. *
avio_closep(&oc->pb);

* free the stream *
avformat_free_context(oc);

至此,整个处理流程便结束了。正确设置输入的YUV文件就可以获取封装好的音视频文件

原创粉丝点击