阅读 1274

Android 基于ffmpeg开发简易播放器 - ffmpeg解封装

ffmpeg解封装

需要调用ffmpeg的API首先需要引入对应的头文件:

extern "C"{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
复制代码

1.初始化解封装

//初始化解封装
av_register_all();
//初始化网络,可以直接从服务器拉流
avformat_network_init();
复制代码

av_register_all()用于注册所有复用器,编码器和协议处理器。如果要指定注册某种编码器可以使用:av_register_input_format() ,av_register_output_format(),ffurl_register_protocol()。av_register_all()调用了avcodec_register_all()。avcodec_register_all()注册了和编解码器有关的组件:硬件加速器,解码器,编码器,Parser,Bitstream Filter。av_register_all()除了调用avcodec_register_all()之外,还注册了复用器,解复用器,协议处理器。

avformat_network_init()用于网络组件的全局初始化。这是可选的,但建议使用,因为它避免了隐式地为每个会话进行安装的开销。如果在某些主要版本中使用网络协议,调用此函数将成为强制性要求。加载socket库以及网络加密协议相关的库,为后续使用网络相关提供支持。

2.打开媒体文件

//打开文件
AVFormatContext *ic = NULL;
char path[] = "/sdcard/1080.mp4";
int re = avformat_open_input(&ic,path,0,0);
if(re == 0)
{
   LOGW("avformat_open_input %s success!",path);
}
else
{
    LOGW("avformat_open_input failed!:%s",av_err2str(re));
}
复制代码

ffmpeg打开媒体的的过程开始于avformat_open_input()。在该方法调用之前确保av_register_all(),avformat_network_init()已经被调用。该函数用于打开多媒体数据(输入流)并且获得一些相关的信息(头数据)。对应的关闭流的函数为avformat_close_input()。

该方法中主要完成了:

  • 输入输出结构体AVIOContext的初始化;

  • 输入数据的协议(例如RTMP,或者file)的识别(通过一套评分机制):1.判断文件名的后缀 2.读取文件头的数据进行比对;

  • 使用获得最高分的文件协议对应的URLProtocol,通过函数指针的方式,与FFMPEG连接(非专业用词);

  • 剩下的就是调用该URLProtocol的函数进行open,read等操作了。

URLProtocol结构如下,是一大堆函数指针的集合(avio.h文件)

typedef struct URLProtocol {  
    const char *name;  
    int (*url_open)(URLContext *h, const char *url, int flags);  
    int (*url_read)(URLContext *h, unsigned char *buf, int size);  
    int (*url_write)(URLContext *h, const unsigned char *buf, int size);  
    int64_t (*url_seek)(URLContext *h, int64_t pos, int whence);  
    int (*url_close)(URLContext *h);  
    struct URLProtocol *next;  
    int (*url_read_pause)(URLContext *h, int pause);  
    int64_t (*url_read_seek)(URLContext *h, int stream_index,  
                             int64_t timestamp, int flags);  
    int (*url_get_file_handle)(URLContext *h);  
    int priv_data_size;  
    const AVClass *priv_data_class;  
    int flags;  
    int (*url_check)(URLContext *h, int mask);  
} URLProtocol;  
复制代码

URLProtocol功能就是完成各种输入协议的读写等操作。

该方法的签名为:

int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);  
复制代码
  • ps:函数调用成功之后处理过的AVFormatContext结构体。
  • file:打开的视音频流的URL。
  • fmt:强制指定AVFormatContext中AVInputFormat的。这个参数一般情况下可以设置为NULL,这样ffmpeg可以自动检测AVInputFormat。
  • dictionay:附加的一些选项,一般情况下可以设置为NULL。

函数执行成功的话,其返回值大于等于0。

AVFormatContext:输入数据的封装格式

  • AVIOContext *pb:输入数据的缓存

  • unsigned int nb_streams:视音频流的个数

  • AVStream **streams:视音频流

  • char filename[1024]:文件名

  • int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000)

  • int bit_rate:比特率(单位bps,转换为kbps需要除以1000)

  • AVDictionary *metadata:元数据

  • char filename[1024]:输入或输出文件名

  • void avformat_close_input(AVFormatContext **s);:该函数用于关闭一个AVFormatContext,一般情况下是和avformat_open_input()成对使用的。

3.获取流信息

avformat_find_stream_info()。该函数可以读取一部分视音频数据并且获得一些相关的信息(适用于没有头部信息的文件):

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);  
复制代码
  • ic:输入的AVFormatContext。
  • options:额外的选项。

函数正常执行后返回值大于等于0。

//获取流信息
re = avformat_find_stream_info(ic,0);
if(re != 0)
{
    LOGW("avformat_find_stream_info failed!");
}
LOGW("duration = %lld nb_streams = %d",ic->duration,ic->nb_streams);
复制代码

获取音视频信息:

static double r2d(AVRational r)
{
    return r.num==0||r.den == 0 ? 0 :(double)r.num/(double)r.den;
}

int fps = 0;
int videoStream = 0;
int audioStream = 1;

for(int i = 0; i < ic->nb_streams; i++)
{
    AVStream *as = ic->streams[i];
    if(as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
    {
        LOGW("视频数据");
        videoStream = i;
        fps = r2d(as->avg_frame_rate);

        LOGW("fps = %d,width=%d height=%d codeid=%d pixformat=%d",fps,
             as->codecpar->width,
             as->codecpar->height,
             as->codecpar->codec_id,
             as->codecpar->format
        );
    }
    else if(as->codecpar->codec_type ==AVMEDIA_TYPE_AUDIO )
    {
        LOGW("音频数据");
        audioStream = i;
        LOGW("sample_rate=%d channels=%d sample_format=%d",
             as->codecpar->sample_rate,
             as->codecpar->channels,
             as->codecpar->format
        );
    }
}
复制代码

AVStream:存储每一个视频/音频流信息的结构体。

int index:标识该视频/音频流

AVCodecContext *codec:指向该视频/音频流的AVCodecContext(它们是一一对应的关系)。codec参数在58版本及之后就不会支持了,需要由codecpar参数所替代。

AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。FFMPEG其他结构体中也有这个字段,但是根据我的经验,只有AVStream中的time_base是可用的。PTS*time_base=真正的时间。

int64_t duration:该视频/音频流长度。

AVDictionary *metadata:元数据信息。

AVRational avg_frame_rate:帧率(注:对视频来说,这个挺重要的)。

AVPacket attached_pic:附带的图片。比如说一些MP3,AAC音频文件附带的专辑封面。

AVCodecParameters *codecpar:codec参数在58版本及之后就不会支持了,需要由codecpar参数所替代。

获取音频流索引:

//获取音频流信息
audioStream = av_find_best_stream(ic,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
复制代码
int av_find_best_stream	(	
    AVFormatContext * 	ic,
    enum AVMediaType 	type,
    int 	wanted_stream_nb,
    int 	related_stream,
    AVCodec ** 	decoder_ret,
    int 	flags 
)	
复制代码

在文件中找到“最佳”流。

ic:媒体文件句柄。

type:流类型:视频,音频,字幕等。

wanted_stream_nb:用户请求的流号码,或-1用于自动选择。

related_stream:尝试查找与此相关的流(例如,在相同的程序中),如果没有,则返回-1。

decoder_ret:如果非NULL,则返回所选流的解码器。

flags:目前没有定义。

4.读取音视频帧数据

//读取帧数据
AVPacket *pkt = av_packet_alloc();
for(;;)
{
    int re = av_read_frame(ic,pkt);
    if(re != 0)
    {
        LOGW("读取到结尾处!");
        int pos = 20 * r2d(ic->streams[videoStream]->time_base);
        av_seek_frame(ic,videoStream,pos,AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME );
        continue;
    }
    LOGW("stream = %d size =%d pts=%lld flag=%d",
         pkt->stream_index,pkt->size,pkt->pts,pkt->flags
    );
    av_packet_unref(pkt);
}
复制代码

AVPacket *av_packet_alloc(void):分配一个AVPacket结构体大小的内存。

void av_packet_unref(AVPacket *pkt):释放对应的AVPacket结构体。

AVPacket是存储压缩编码数据相关信息的结构体。

uint8_t *data:压缩编码的数据。

例如对于H.264来说。1个AVPacket的data通常对应一个NAL。

注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流

因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。

int size:data的大小

int64_t pts:显示时间戳(num/den)

int64_t dts:解码时间戳

int stream_index:标识该AVPacket所属的视频/音频流。

读取帧数据:

int av_read_frame(AVFormatContext * s,AVPacket * pkt)
复制代码

返回流的下一帧。

此函数返回存储在文件中的内容,并且不验证解码器的有效帧是什么。它会将存储在文件中的内容拆分为帧,并为每个调用返回一个。它不会忽略有效帧之间的无效数据,从而为解码器提供解码所需的最大信息。

如果pkt-> buf为NULL,那么数据包在下一个av_read_frame()或avformat_close_input()之前是有效的。否则数据包无限期地有效。在这两种情况下,数据包必须在不再需要时使用av_free_packet释放。对于视频,数据包恰好包含一帧。对于音频,如果每个帧具有已知的固定大小(例如PCM或ADPCM数据),则它包含整数个帧。如果音频帧具有可变大小(例如MPEG音频),则它包含一帧。

pkt-> pts,pkt-> dts和pkt->duration始终设置为以AVStream.time_base单位的正确值。如果视频格式具有B帧,则pkt-> pts可以是AV_NOPTS_VALUE,所以如果不解压缩有效载荷,则最好依赖pkt-> dts。

返回

如果成功返回为0,错误或文件结束时为 < 0。

设置ffmpeg将流偏移到正确的起始位置:

int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);
复制代码

s:为容器内容;

stream_index:流索引

timestamp:将要定位处的时间戳

flags:功能flag

#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward
#define AVSEEK_FLAG_BYTE     2 ///< seeking based on position in bytes
#define AVSEEK_FLAG_ANY      4 ///< seek to any frame, even non-keyframes
#define AVSEEK_FLAG_FRAME    8 ///< seeking based on frame number
复制代码