视音频技术零基础学习笔记(三)及解码程序添加注释

FFmpeg视频解码器

视频解码知识

纯洁的视频解码流程

  • 压缩编码数据->像素数据。
  • 例如解码H.264,就是“H.264码流->YUV”

一般的视频解码流程

Created with Raphaël 2.1.0 开始解码 从封装格式中 提取视频码流 对码流解码 结束

VC下FFmpeg开发环境搭建

新建工程

  • 建Win32控制台程序即可

拷贝FFmpeg开发文件

  • 头文件(*.h)拷贝至项目文件夹的include子文件夹下
  • 导入库文件(*.lib)拷贝至项目文件夹的lib子文件夹下
  • 动态库文件(*.dll)拷贝至项目文件夹下

配置开发文件

头文件配置:
  • 配置属性->C/C++->常规->附加包含目录,输入“include”(刚才拷贝头文件的目录)
导入库配置:
  • 配置属性->链接器->常规->附加库目录,输入“lib” (刚才拷贝库文件的目录)
  • 配置属性->链接器->输入->附加依赖项,输入“avcodec.lib; avformat.lib; avutil.lib; avdevice.lib; avfilter.lib; postproc.lib; swresample.lib; swscale.lib”(导入库的文件名)
动态库不用配置

测试

包含头文件

  • 如果是C语言中使用FFmpeg,则直接使用下面代码
#include "libavcodec/avcodec.h"
  • 如果是C++语言中使用FFmpeg,则使用下面代码
#define __STDC_CONSTANT_MACROS
extern "C"
{
    #include "libavcodec/avcodec.h "
}

测试程序

#define __STDC_CONSTANT_MACROS

#include "stdafx.h"

extern "C"
{
#include "libavcodec/avcodec.h "
}

int _tmain(int argc, _TCHAR* argv[])
{
    printf("%s\n", avcodec_configuration());//FFmpeg的配置信息
    getchar();
    return 0;
}

FFmepg库简介(用FFmpeg开发时会经常用到)

  • avcodec:编解码(最重要的库)。
  • avformat:封装格式处理。
  • avfilter:滤镜特效处理。
  • avdevice:各种设备的输入输出。
  • avutil:工具库(大部分库都需要这个库的支持)。
  • postproc:后加工。
  • swresample:音频采样数据格式转换。
  • swscale:视频像素数据格式转换。

FFmpeg解码的函数

解码流程

解码流程图

  • av_register_all(): 所有程序都用这个函数开头初始化,注册所有的组件
  • avformat_open_input(): 打开输入视频文件
  • avformat_find_stream_info(): 获取输入视频文件的信息,比如分辨率、编码方式等等
  • avcodec_find_decoder(): 根据编码方式找到编解码器(codec)
  • avcodec_open2():打开codec
  • av_read_frame():每调用一次,读一帧
  • AVPacket:结构体,装的是H.264
  • avcodec_decode_video2(): 解码一帧压缩数据
  • AVFrame: 结构体,装的是YUV

其它常用解码函数

  • avcodec_close():关闭解码器。
  • avformat_close_input():关闭输入视频文件。

FFmpeg解码的数据结构

数据结构的层次关系

数据结构层次关系

数据结构简介

  • AVFormatContext
    封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息,最外层的信息。
  • AVInputFormat
    每种封装格式(例如FLV, MKV, MP4, AVI)对应一个该结构体。
  • AVStream
    视频文件中每个视频(音频)流对应一个该结构体。变长数组,但一般的视频文件里就两个,分别是视频流(下标0)和音频流(下表1)
  • AVCodecContext
    编码器上下文结构体,保存了视频(音频)编解码相关信息。
  • AVCodec
    每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。
  • AVPacket
    存储一帧压缩编码数据。
  • AVFrame
    存储一帧解码后像素(采样)数据。

FFmpeg数据结构分析

这里只写一些常用的属性,详细的请见源码中给出的定义

AVFormatContext
  • iformat:输入视频的AVInputFormat
  • nb_streams :输入视频的AVStream 个数
  • streams :输入视频的AVStream []数组
  • duration :输入视频的时长(以微秒为单位)
  • bit_rate :输入视频的码率
AVInputFormat
  • name:封装格式名称
  • long_name:封装格式的长名称
  • extensions:封装格式的扩展名
  • id:封装格式ID
  • 一些封装格式处理的接口函数
AVStream
  • id:序号
  • codec:该流对应的AVCodecContext
  • time_base:该流的时基,是一个分数(结构中的存的是分子和分母这两个整数)。由于每一帧出现的时间需要被记录,而这个数是非整数的,不好记录,因此用时基作为单位,这样存每一帧的时间就可以用整数了,具体用的时候和time_base这个分数一乘即可。
  • r_frame_rate:该流的帧率,一秒有多少帧
AVCodecContext
  • codec:编解码器的AVCodec
  • width, height:图像的宽高(只针对视频)
  • pix_fmt:像素格式(只针对视频)
  • sample_rate:采样率(只针对音频)
  • channels:声道数(只针对音频)
  • sample_fmt:采样格式(只针对音频)
AVCodec
  • name:编解码器名称
  • long_name:编解码器长名称
  • type:编解码器类型
  • id:编解码器ID
  • 一些编解码的接口函数
AVPacket

装H.264

  • pts:显示时间戳,这是显示的时间,需要乘上时基
  • dts :解码时间戳,这是解码的时间,顺序和显示的顺序不一定一样
  • data :压缩编码数据
  • size :压缩编码数据大小
  • stream_index :所属的AVStream,保存索引
AVFrame

装YUV

  • data:解码后的图像像素数据(音频采样数据)。
  • linesize:对视频来说是图像中一行像素的大小;对音频来说是整个音频帧的大小。
  • width, height:图像的宽高(只针对视频)。
  • key_frame:是否为关键帧(只针对视频) 。
  • pict_type:帧类型(只针对视频) 。例如I,P,B。

程序练习

  • 打开视频文件
  • 显示一些信息
  • 输出.h264文件
  • 输出解码后的.YUV文件

我这里查阅了一些资料完成了全篇代码的注释,如果有理解有问题的地方,也希望大家指出
另外我把雷老师的课上作业完成在代码中了,比较简单的写文件

#include <stdio.h>
#include <iostream>
using namespace std;

#define __STDC_CONSTANT_MACROS

extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
};

//个人定义的宏,方便设置文件路径
#define PATH "Titanic.ts"
#define PATH_TMP "cuc_ieschool.flv"

int main(int argc, char* argv[])
{
    AVFormatContext *pFormatCtx;
    int             i, videoindex;
    AVCodecContext  *pCodecCtx;
    AVCodec         *pCodec;
    AVFrame *pFrame,*pFrameYUV;
    uint8_t *out_buffer;
    AVPacket *packet;
    int y_size;
    int ret, got_picture;
    //这个结构体主要用于图像的处理,比如完成图像格式转换、拉伸图像等。具体可以参见雷老师Blog
    struct SwsContext *img_convert_ctx;
    //输入文件路径
    char filepath[]= PATH;

    int frame_cnt;

    //初始化
    av_register_all();//注册相关组件
    avformat_network_init();//初始化网络连接
    pFormatCtx = avformat_alloc_context();//为AVFormatContext结构分配内存空间

    //打开输入视频文件
    if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){
        printf("Couldn't open input stream.\n");
        return -1;
    }
    //读取一部分视音频数据并且获得一些相关的信息。
    if(avformat_find_stream_info(pFormatCtx,NULL)<0){
        printf("Couldn't find stream information.\n");
        return -1;
    }

    //这段代码目的是找哪个流是视频流,然后把索引下标赋值给videoindex这个int
    //一般其实index=0是视频流,但并不绝对,所以要做这一步
    videoindex=-1;
    for(i=0; i<pFormatCtx->nb_streams; i++) 
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
            videoindex=i;
            break;
        }
    if(videoindex==-1){
        printf("Didn't find a video stream.\n");
        return -1;
    }
    //这里找到了我们要的视频流的解码器,为引用方便,把它保存入AVCodecContext结构中
    pCodecCtx=pFormatCtx->streams[videoindex]->codec;

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

    /*
    * 在此处添加输出视频信息的代码
    * 取自于pFormatCtx,使用fprintf()
    */
    //这里是课上的小实验,我就懒得开文件了,用cout了。。。
    cout << "时长:= " <<pFormatCtx->duration << "ms" << endl;
    cout << "封装格式:" << pFormatCtx->iformat->name << endl;
    cout << "长封装格式:" << pFormatCtx->iformat->long_name << endl;
    //访问视频流获取信息
    cout << "长:=" << pCodecCtx->height << ",宽:=" << pCodecCtx->width << endl;


    /*以下这段代码主要是分配空间,方便后面遍历每一个数据包时的解压和YUV数据的剪裁*/

    //为AVFrame分配空间
    pFrame=av_frame_alloc();
    pFrameYUV=av_frame_alloc();

    //av_malloc是对malloc的封装,保证地址对齐
    //avpicture_get_size第一个参数指明每一个像素存的数据类型
    out_buffer=(uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));

    //把buffer和pFramYUV关联,让每一帧YUV数据data有内存放
    avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);

    //给压缩的数据包分配空间,方便后续使用
    packet=(AVPacket *)av_malloc(sizeof(AVPacket));

    //Output Info-----------------------------
    printf("--------------- File Information ----------------\n");
    //dump只是个调试函数,输出文件的音、视频流的基本信息了,帧率、分辨率、音频采样等等
    av_dump_format(pFormatCtx,0,filepath,0);
    printf("-------------------------------------------------\n");

    //初始化用于图像转换的结构体,其中倒数第四个参数表示拉伸图像所用的算法
    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, 
        pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); 

    //核心的循环,完成解码
    FILE* fp1 = fopen("out.h264", "wb+");
    FILE* fp2 = fopen("out.yuv", "wb+");
    frame_cnt=0;
    //函数读取下一个包
    while(av_read_frame(pFormatCtx, packet)>=0){
        //如果这个包属于视频
        if(packet->stream_index==videoindex){
                /*
                 * 在此处添加输出H264码流的代码
                 * 取自于packet,使用fwrite()
                 */
            fwrite(packet->data, packet->size, 1, fp1);

            //解码这个包,存入帧内
            ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
            if(ret < 0){
                printf("Decode Error.\n");
                return -1;
            }

            //由于此时未必完成了一帧的解码,判断一下是否完成了
            if(got_picture){
                //解码出来的数据在右面可能会有黑边,这和系统硬件相关,该函数就是为了去除这个黑边
                //原来保存在pFrame中的数据,经过处理存入了pFrameYUV
                sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, 
                    pFrameYUV->data, pFrameYUV->linesize);
                printf("Decoded frame index: %d\n",frame_cnt);

                /*
                 * 在此处添加输出YUV的代码
                 * 取自于pFrameYUV,使用fwrite()
                 */
                //注意这里的大小需要根据YUV420p的知识计算,不能单纯的用sizeof
                //一个像素的Y正好占一个字节
                fwrite(pFrameYUV->data[0], pCodecCtx->width * pCodecCtx->height, 1, fp2);
                fwrite(pFrameYUV->data[1], pCodecCtx->width * pCodecCtx->height / 4, 1, fp2);
                fwrite(pFrameYUV->data[2], pCodecCtx->width * pCodecCtx->height / 4, 1, fp2);

                frame_cnt++;

            }
        }
        //和av_read_frame相对,释放掉packet中data申请来的空间
        av_free_packet(packet);
    }

    sws_freeContext(img_convert_ctx);

    av_frame_free(&pFrameYUV);
    av_frame_free(&pFrame);
    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);

    fclose(fp1);
    fclose(fp2);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值