FFmpeg入门(一):解码音视频过程,附Demo

under 默认分类  tag FFmpeg    Published on May 14th , 2020 at 05:51 am

一、前言

项目需要,接触到了FFmpeg这个神级工具,本文初步介绍了使用FFmpeg解码音视频的过程。不包含FFmpeg的部署配置。

二、正文

0.引言

ffmpeg解码过程中用到了两个很重要的结构体, 这两个结构体比较复杂, 用到的次数也非常多.

AVPacket 保存未解码的数据.
AVFrame 保存解码后的数据.

1.解码流程一览

2.示例(复制粘贴完事儿)


#include <stdio.h>

#ifdef __cplusplus
extern "C"
{
#endif
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#ifdef __cplusplus
};
#endif

int openCodecContext(const AVFormatContext *pFormatCtx, int *pStreamIndex, enum AVMediaType type, AVCodecContext **ppCodecCtx)
{
    int streamIdx = -1;
    // 获取流下标
    for (int i = 0; i < pFormatCtx->nb_streams; i++) 
    {
        if (pFormatCtx->streams[i]->codec->codec_type == type) 
        {
            streamIdx = i;
            break;
        }
    }
    if (streamIdx == -1) 
    {
        printf("find video stream failed!\n");
        exit(-2);
    }
    // 寻找解码器
    AVCodecContext  *pCodecCtx = pFormatCtx->streams[streamIdx]->codec;
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (NULL == pCodec) 
    {
        printf("avcode find decoder failed!\n");
        exit(-2);
    }

    //打开解码器
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) 
    {
        printf("avcode open failed!\n");
        exit(-2);
    }
    *ppCodecCtx        = pCodecCtx;
    *pStreamIndex    = streamIdx;

    return 0;
}

int main(void)
{
    AVFormatContext    *pInFormatCtx    = NULL;
    AVCodecContext  *pVideoCodecCtx    = NULL;
    AVCodecContext  *pAudioCodecCtx    = NULL;
    AVPacket *pPacket    = NULL;
    AVFrame *pFrame        = NULL; 
    int ret;
    /* 支持本地文件和网络url */
    const char streamUrl[] = "./test.flv";

    /* 1. 注册 */
    av_register_all();
    
    pInFormatCtx = avformat_alloc_context();

    /* 2. 打开流 */
    if(avformat_open_input(&pInFormatCtx, streamUrl, NULL, NULL) != 0)
    {
        printf("Couldn't open input stream.\n");
        return -1;
    }

    /* 3. 获取流的信息 */
    if(avformat_find_stream_info(pInFormatCtx, NULL) < 0)
    {
        printf("Couldn't find stream information.\n");
        return -1;
    }

    int videoStreamIdx = -1;
    int audioStreamIdx = -1;
    /* 4. 寻找并打开解码器 */
    openCodecContext(pInFormatCtx, &videoStreamIdx, AVMEDIA_TYPE_VIDEO, &pVideoCodecCtx);
    openCodecContext(pInFormatCtx, &audioStreamIdx, AVMEDIA_TYPE_AUDIO, &pAudioCodecCtx);

    pPacket    = av_packet_alloc();
    pFrame    = av_frame_alloc();

    int cnt = 30;
    while (cnt--)
    {
        /* 5. 读流数据, 未解码的数据存放于pPacket */
        ret = av_read_frame(pInFormatCtx, pPacket);
        if (ret < 0)
        {
            printf("av_read_frame error\n");
            break;
        }

        /* 6. 解码, 解码后的数据存放于pFrame */
        /* 视频解码 */
        if (pPacket->stream_index == videoStreamIdx)
        {
            avcodec_decode_video2(pVideoCodecCtx, pFrame, &ret, pPacket);
            if (ret == 0)
            {
                printf("video decodec error!\n");
                continue;
            }
            printf("* * * * * * video * * * * * * * * *\n");
            printf("___height: [%d]\n", pFrame->height);
            printf("____width: [%d]\n", pFrame->width);
            printf("pict_type: [%d]\n", pFrame->pict_type);
            printf("___format: [%d]\n", pFrame->format);
            printf("* * * * * * * * * * * * * * * * * * *\n\n");
        }

        /* 音频解码 */
        if (pPacket->stream_index == audioStreamIdx)
        {
            avcodec_decode_audio4(pAudioCodecCtx, pFrame, &ret, pPacket);
            if (ret < 0)
            {
                printf("audio decodec error!\n");
                continue;
            }
            printf("* * * * * * audio * * * * * * * * * *\n");
            printf("____nb_samples: [%d]\n", pFrame->nb_samples);
            printf("__samples_rate: [%d]\n", pFrame->sample_rate);
            printf("channel_layout: [%lu]\n", pFrame->channel_layout);
            printf("________format: [%d]\n", pFrame->format);
            printf("* * * * * * * * * * * * * * * * * * *\n\n");
        }
        av_packet_unref(pPacket);
    }

    av_frame_free(&pFrame);
    av_packet_free(&pPacket);
    avcodec_close(pVideoCodecCtx);
    avcodec_close(pAudioCodecCtx);
    avformat_close_input(&pInFormatCtx);

    return 0;
}

3. 函数说明

av_register_all()        / 使用ffmpeg几乎都要调用这一个函数, 注册ffmpeg各种编解码器, 复用器等. /

avformat_open_input()     / 该函数用于打开本地多媒体文件或者网络流媒体url /

avformat_find_stream_info() / 该函数用于读取一部分音视频数据并且获得一些相关的信息 /

avcodec_find_decoder()    / 由codec_id或者解码器名称来寻找对应的解码器 /

avcodec_open2()        / 初始化解码器 /

av_read_frame()        / 读流数据, 读出来的是压缩数据, 存放于AVPacket /

avcodec_decode_video2()    / 视频解码 解码后数据为原始数据, 存放于AVFrame /

avcodec_decode_audio4()    / 音频解码 解码后数据为原始数据, 存放于AVFrame /

三、真正的正文

1. 首先要初始化一下,使用如下函数:

av_register_all();  //初始化FFMPEG  调用了这个才能正常适用编码器和解码器

使用这个函数完成编码器和解码器的初始化,只有初始化了编码器和解码器才能正常使用,否则会在打开编解码器的时候失败。

2. 接着需要分配一个AVFormatContext,FFMPEG所有的操作都要通过这个AVFormatContext来进行

AVFormatContext *pFormatCtx = avformat_alloc_context();

3.接着调用打开视频文件

char *file_path = "E:in.mp4";
avformat_open_input(&pFormatCtx, file_path, NULL, NULL);

这里文件名先不要使用中文,否则会打开失败.

4.文件打开成功后就是查找文件中的视频流了:

///循环查找视频中包含的流信息,直到找到视频类型的流    
///便将其记录下来 保存到videoStream变量中
///这里我们现在只处理视频流  音频流先不管他
for (i = 0; i < pFormatCtx->nb_streams; i++) {
    if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
        videoStream = i;
    }
}
 
///如果videoStream为-1 说明没有找到视频流
if (videoStream == -1) {
    printf("Didn't find a video stream.");
    return -1;
}

5.现在根据视频流 打开一个解码器来解码:

///查找解码器    
pCodecCtx = pFormatCtx->streams[videoStream]->codec;
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
 
if (pCodec == NULL) {
    printf("Codec not found.");
    return -1;
}
 
///打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
    printf("Could not open codec.");
    return -1;
}

可以看出 我们可以直接根据查找到的视频流信息获取到解码器。

而且我们并不知道他实际用的是什么编码器。

这就是为什么一开始我们使用FFMPEG来操作,因为很多东西我们可以不关系

6.现在开始读取视频了:

int y_size = pCodecCtx->width * pCodecCtx->height;
AVPacket *packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据
 
if (av_read_frame(pFormatCtx, packet) < 0)
{
    break; //这里认为视频读取完了
}

可以看出 av_read_frame读取的是一帧视频,并存入一个AVPacket的结构中。

7.前面我们说过 视频里面的数据是经过编码压缩的,因此这里我们需要将其解码:

if (packet->stream_index == videoStream) 
{        
    ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet); 
    if (ret < 0) {
        printf("decode error.");
        return -1;
    }
}

8.基本上所有解码器解码之后得到的图像数据都是YUV420的格式,而这里我们需要将其保存成图片文件,因此需要将得到的YUV420数据转换成RGB格式,转换格式也是直接使用FFMPEG来完成:

if (got_picture) {        
    sws_scale(img_convert_ctx,
    (uint8_t const * const *) pFrame->data,
    pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
    pFrameRGB->linesize);
}

至于YUV420和RGB图像格式的具体内容,这里不用去了解。这里只需要知道有这么个东西就行了,对我们使用FFMPEG转换没有影响。

9.得到RGB数据之后就是直接写入文件(或者直接显示)

SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, index++); //保存图片     
if (index > 50) return 0; //这里我们就保存50张图片

至此读取视频解码保存成图片就写好了:


本文由simyng创作, 采用知识共享署名4.0 国际许可协议进行许可,转载前请务必署名
  文章最后更新时间为:May 13th , 2020 at 09:51 pm