FFMpeg视频开发与应用基础五调用FFMpeg SDK封装音频和视频为视频文件

Posted 取次花丛懒回顾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FFMpeg视频开发与应用基础五调用FFMpeg SDK封装音频和视频为视频文件相关的知识,希望对你有一定的参考价值。

《FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK》视频教程已经在“CSDN学院”上线,视频中包含了从0开始逐行代码实现FFMpeg视频开发的过程,欢迎观看!链接地址:FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK

工程代码地址:FFmpeg_Tutorial


音频和视频的封装过程为解封装的逆过程,即将独立的音频数据和视频数据按照容器文件所规定的格式封装为一个完整的视频文件的过程。对于大多数消费者来说,视频封装的容器是大家最为熟悉的,因为它直接体现在了我们使用的音视频文件扩展名上,比较常见的有mp4、avi、mkv、flv等等。

在进行音频和视频封装时,我们将实际操作一系列音频或视频流数据的生成和写入。所谓流,指的是一系列相关联的包的集合,这些包一般同属于一组按照时间先后顺序进行解码/渲染等处理的数据。在一个比较典型的视频文件中,我们通常至少会包含一个视频流和一个音频流。

在FFMpeg中,表示音频流或视频流有一个专门的结构,即”AVStream”实现。该结构主要对音频和视频数据的处理进行管理和控制。另外,”AVFormatContext”结构也是必须的,因为它包含了控制输入和输出的信息。

音频和视频数据封装为视频文件的主要步骤为:


1. 相关数据结构的准备

首先,根据输出文件的格式获取AVFormatContext结构,获取AVFormatContext结构使用函数avformat_alloc_output_context2实现。该函数的声明为:

int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename);

其中:

  • ctx:输出到AVFormatContext结构的指针,如果函数失败则返回给该指针为NULL;
  • oformat:指定输出的AVOutputFormat类型,如果设为NULL则使用format_name和filename生成;
  • format_name:输出格式的名称,如果设为NULL则使用filename默认格式;
  • filename:目标文件名,如果不使用,可以设为NULL;

分配AVFormatContext成功后,我们需要添加希望封装的数据流,一般是一路视频流+一路音频流(可能还有其他音频流和字幕流等)。添加流首先需要查找流所包含的媒体的编码器,这需要传入codec_id后使用avcodec_find_encoder函数实现,将查找到的编码器保存在AVCodec指针中。

之后,调用avformat_new_stream函数向AVFormatContext结构中所代表的媒体文件中添加数据流。该函数的声明如下:

AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);

其中各个参数的含义:

  • s:AVFormatContext结构,表示要封装生成的视频文件;
  • c:上一步根据codec_id产生的编码器指针;
  • 返回值:指向生成的stream对象的指针;如果失败则返回NULL指针。

此时,一个新的AVStream便已经加入到输出文件中,下面就可以设置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;
}   

2. 打开音视频

打开音视频主要涉及到打开编码音视频数据所需要的编码器,以及分配相应的frame对象。其中打开编码器如之前一样,调用avcodec_open函数,分配frame对象调用av_frame_alloc以及av_frame_get_buffer。分配frame对象的实现如下:

static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
{
    AVFrame *picture;
    int ret;

    picture = av_frame_alloc();
    if (!picture)
    {
        return NULL;
    }

    picture->format = pix_fmt;
    picture->width = width;
    picture->height = height;

    /* allocate the buffers for the frame data */
    ret = av_frame_get_buffer(picture, 32);
    if (ret < 0)
    {
        fprintf(stderr, "Could not allocate frame data.\\n");
        exit(1);
    }

    return picture;
}

而上层打开音视频部分的实现如:

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);
    }
}

3. 打开输出文件并写入文件头

如果判断需要写出文件的话,则需要打开输出文件。在这里,我们可以不再定义输出文件指针,并使用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;
}

4. 编码和封装循环

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

(1) 读取原始视频数据

这一部分主要实现根据时长判断是否需要继续进行处理、读取视频到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;
}

(2) 视频编码

视频编码的方式同之前几次使用的方式相同,即调用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);
}

(3) 写出编码后的数据到输出视频文件

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

/* 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");
        }
    }
}

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

写入文件尾的数据同写文件头一样简单,只需要调用函数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文件就可以获取封装好的音视频文件。

以上是关于FFMpeg视频开发与应用基础五调用FFMpeg SDK封装音频和视频为视频文件的主要内容,如果未能解决你的问题,请参考以下文章

我的第一本书《FFmpeg音视频开发基础与实战》已正式出版

我的第一本书《FFmpeg音视频开发基础与实战》已正式出版

我的第一本书《FFmpeg音视频开发基础与实战》已正式出版

我的第一本书《FFmpeg音视频开发基础与实战》已正式出版

一FFmpeg 的初尝试《FFmpeg 音视频开发基础入门到实战》

一FFmpeg 的初尝试《FFmpeg 音视频开发基础入门到实战》