您的位置:首页 > 其它

简化ffplay的分析

2017-01-23 15:46 357 查看
这份代码来自:http://www.cnblogs.com/mcodec/articles/1933754.htm

感谢这位作者. ffplay简化版,剪了百分之80的代码.入门还是不错的,并且可以在vc 6.0编译.但是这个版本很老了,应该是0版本,现在都是3版本了.代码基本都变动了.但是万变不离其宗.

总结写在前面,这一部分是在本篇结束后写的:

音视频的真正输出是SDL,SDL是Simple DirectMedia Layer(简易直控媒体层)的缩写。它是一个跨平台的多媒体库,以用于直接控制底层的多媒体硬件的接口。所以弄清楚的是怎么使用SDL即可.

ffplay是多线程模式:主线程wait,解析数据线程,audio线程,vedio线程

ffmpeg好多协议,主要有这三个: proto:数据来源方式,主要是文件的,网络的. format:封装协议, decode:编解码.

从初始化开始.

void av_register_all(void)
{
static int inited = 0;
if (inited != 0)
return ;
inited = 1;

//数字信号处理(Digital Signal Processing,简称DSP)
avcodec_init();

//decoder
avcodec_register_all();

//format
avidec_init();

//poto
register_protocol(&file_protocol);
}


出现了一个问题:由于有很多编解码,很多容器,很多数据来源方式, 靠什么来确定他们的呢?一个一个的来,数据来源方式如何选择:

如果确定IO呢?

获取数据有很多方式,最常用的网络和文件,不知道ffmpeg考虑串口没?

由于看的是简化版,所以直接读的文件,没有区分file proto和net proto.

有一个protocol列表.

typedef struct URLProtocol
{
const char *name;
int(*url_open)(URLContext *h, const char *filename, int flags);
int(*url_read)(URLContext *h, unsigned char *buf, int size);
int(*url_write)(URLContext *h, unsigned char *buf, int size);
offset_t(*url_seek)(URLContext *h, offset_t pos, int whence);
int(*url_close)(URLContext *h);
struct URLProtocol *next;
} URLProtocol;

URLProtocol file_protocol =
{
"file",
file_open,
file_read,
file_write,
file_seek,
file_close,
};

register_protocol(&file_protocol);


在av_open_input_file函数中使用到了 url_fopen,url_fread,url_fclose,url_fseek. 分析一下open函数,找open中找到了proto.

url_fopen主要逻辑在于url_open,其他都在初始化.

int url_fopen(ByteIOContext *s, const char *filename, int flags)
{
URLContext *h;
uint8_t *buffer;
int buffer_size, max_packet_size;
int err;

err = url_open(&h, filename, flags);
...

return 0;
}


url_open函数简化了.只有file_proto.在这个函数中找到了proto.

int url_open(URLContext **puc, const char *filename, int flags)
{
URLContext *uc;
URLProtocol *up;
const char *p;
char proto_str[128],  *q;
int err;

p = filename;
q = proto_str;

file_proto:
strcpy(proto_str, "file");

//在protocol列表中找到proto
up = first_protocol;
while (up != NULL)
{
if (!strcmp(proto_str, up->name))
goto found;
up = up->next;
}
err =  - ENOENT;
goto fail;
found:
uc = av_malloc(sizeof(URLContext) + strlen(filename));
strcpy(uc->filename, filename);
uc->prot = up;
uc->flags = flags;
uc->max_packet_size = 0; // default: stream file

err = up->url_open(uc, filename, flags); //真正的open函数,具体逻辑简单,不用跟了.

*puc = uc;
return 0;
}


上述逻辑不是很清晰,可能是看的是简化版,基本指定了file.

net和file怎么来区分呢?需要进一步分析.

如果确定format呢?

有很多封装,flv的,avi等等.怎么来确定一个封装格式的.

有一个format列表,通过遍历列表,可以获取到format.

void av_register_input_format(AVInputFormat *format)
{
AVInputFormat **p;
p = &first_iformat;
while (*p != NULL)
p = &(*p)->next;
*p = format;
format->next = NULL;
}


定义AVInputFormat,之后加入到format列表中,通过name来区分各个format.

typedef struct AVInputFormat
{
const char *name;
int priv_data_size;
int(*read_probe)(AVProbeData*);
int(*read_header)(struct AVFormatContext *, AVFormatParameters *ap);
int(*read_packet)(struct AVFormatContext *, AVPacket *pkt);
int(*read_close)(struct AVFormatContext*);
const char *extensions;
struct AVInputFormat *next;
} AVInputFormat;

AVInputFormat avi_iformat =
{
"avi",
sizeof(AVIContext),
avi_probe,
avi_read_header,
avi_read_packet,
avi_read_close,
};

av_register_input_format(&avi_iformat);


列表有了,怎么来选出来呢?从一个函数开始 av_open_input_file ,简化并去掉错误处理,就是如下.

int av_open_input_file(AVFormatContext **ic_ptr,
const char *filename,  //文件名
AVInputFormat *fmt,
int buf_size,
AVFormatParameters *ap)
{
int err, must_open_file, file_opened, probe_size;
AVProbeData probe_data,  *pd = &probe_data;
ByteIOContext pb1,  *pb = &pb1;

file_opened = 0;
pd->filename = "";
if (filename)
pd->filename = filename;
pd->buf = NULL;
pd->buf_size = 0;

must_open_file = 1;

url_fopen(pb, filename, URL_RDONLY) //打开文件

file_opened = 1;

//开始探测
for (probe_size = PROBE_BUF_MIN; probe_size <= PROBE_BUF_MAX && !fmt; probe_size <<= 1)
{
pd->buf = av_realloc(pd->buf, probe_size);
pd->buf_size = url_fread(pb, pd->buf, probe_size);
fmt = av_probe_input_format(pd, 1); //开始探测
}
av_freep(&pd->buf); //探测结束后,得free掉.

err = av_open_input_stream(ic_ptr, pb, filename, fmt, ap); //解析文件:解析文件头部
return 0;
}


探测函数:遍历formt列表,每一个进行探测,并比较socre,得到最高的fmt.

AVInputFormat *av_probe_input_format(AVProbeData *pd, int is_opened)
{
AVInputFormat *fmt1,  *fmt;
int score, score_max;

fmt = NULL;
score_max = 0;
//遍历format列表,每个都探测,并打分
for (fmt1 = first_iformat; fmt1 != NULL; fmt1 = fmt1->next)
{
score = 0;
if (fmt1->read_probe)
{
score = fmt1->read_probe(pd);  //打分,很调皮嘛
}
else if (fmt1->extensions) //后缀名
{
if (match_ext(pd->filename, fmt1->extensions))
score = 50;
}

if (score > score_max)
{
score_max = score;
fmt = fmt1;
}
}
return fmt; //返回的是打分最高的fmt.
}


avi的read_probe函数:这个就简单了,直接判断前几个字符.

static int avi_probe(AVProbeData *p)
{
if (p->buf_size <= 32) // check file header
return 0;
if (p->buf[0] == 'R' && p->buf[1] == 'I' && p->buf[2] == 'F' && p->buf[3] == 'F'
&& p->buf[8] == 'A' && p->buf[9] == 'V' && p->buf[10] == 'I'&& p->buf[11] == ' ')
return AVPROBE_SCORE_MAX; //这个可是100分.
else
return 0;
}


format出来后,还直接解析了头部.av_open_input_stream,简化后,设置priv_data,执行fmt.read_header

int av_open_input_stream(AVFormatContext **ic_ptr,
ByteIOContext *pb,
const char *filename,
AVInputFormat *fmt,  //这个就是探测出的format
AVFormatParameters *ap)
{
int err;
AVFormatContext *ic;

ic = av_mallocz(sizeof(AVFormatContext));
ic->iformat = fmt;
if (pb)
ic->pb =  *pb;

//priv data是什么呢? 在avi中就是:AVIContext
ic->priv_data = av_mallocz(fmt->priv_data_size);

err = ic->iformat->read_header(ic, ap);
*ic_ptr = ic;
return 0;

}


fmt的read_header函数,这个就比较复杂啦!很长,都无法简化.

不用列出代码了,解析header,获取到了vedio和audio的一些配置信息.

如何确定编解码呢?

codecer有一个列表,通过遍历列表,可以获取到codecer.

void register_avcodec(AVCodec *format)
{
AVCodec **p;
p = &first_avcodec;
while (*p != NULL)
p = &(*p)->next;
*p = format;
format->next = NULL;
}


把msrle放到codec列表中.

register_avcodec(&msrle_decoder);


定义msrle decoder.

AVCodec msrle_decoder =
{
"msrle",
CODEC_TYPE_VIDEO,
CODEC_ID_MSRLE,
sizeof(MsrleContext),
msrle_decode_init,
NULL,
msrle_decode_end,
msrle_decode_frame
};


这里面有一个id = CODEC_ID_MSRLE, 但是这个id是如何确定的呢?

typedef struct
{
int id;
unsigned int tag;
} CodecTag;

const CodecTag codec_bmp_tags[] =
{
{CODEC_ID_MSRLE, MKTAG('m', 'r', 'l', 'e')},
{CODEC_ID_MSRLE, MKTAG(0x1, 0x0, 0x0, 0x0)},
{CODEC_ID_NONE,  0},
};


id获取函数:

enum CodecID codec_get_id(const CodecTag *tags, unsigned int tag)
{
while (tags->id != CODEC_ID_NONE)
{
if (toupper((tag >> 0) &0xFF) == toupper((tags->tag >> 0) &0xFF)
&& toupper((tag >> 8) &0xFF) == toupper((tags->tag >> 8) &0xFF)
&& toupper((tag >> 16)&0xFF) == toupper((tags->tag >> 16)&0xFF)
&& toupper((tag >> 24)&0xFF) == toupper((tags->tag >> 24)&0xFF))
return tags->id;

tags++;
}
return CODEC_ID_NONE;
}


看一下使用方式:

case CODEC_TYPE_VIDEO:    // BITMAPINFOHEADER
get_le32(pb); // size
st->actx->width = get_le32(pb);
st->actx->height = get_le32(pb);
get_le16(pb); // panes
st->actx->bits_per_sample = get_le16(pb); // depth
tag1 = get_le32(pb);
......
st->actx->codec_type = CODEC_TYPE_VIDEO;
st->actx->codec_id = codec_get_id(codec_bmp_tags, tag1);


通过vedio tag 来获取到的.所以可以确定:

每一个封装,都会有自定义来判断方式.

整体架构

简化版的ffplay,直接读取的是file文件,简单清晰明了.分4步:

先注册proto,format,decoder.

初始化SDL

open文件

loop

int main(int argc, char **argv)
{
const char *input_filename = "./clocktxt_320.avi"; //直接指定了文件名

av_register_all();

int flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
SDL_Init(flags)
SDL_EventState(SDL_ACTIVEEVENT, SDL_IGNORE);
SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
SDL_EventState(SDL_SYSWMEVENT,  SDL_IGNORE);
SDL_EventState(SDL_USEREVENT,   SDL_IGNORE);

cur_stream = stream_open(input_filename);

event_loop();

return 0;
}


1,2步不用看了.先看4步,因为第4步简单,没做啥事.主线程等待SDL event,特别是exit.

void event_loop(void)
{
SDL_Event event;

for (;;)
{
SDL_WaitEvent(&event);
switch (event.type)
{
case SDL_KEYDOWN:
switch (event.key.keysym.sym)
{
case SDLK_ESCAPE:
case SDLK_q:
do_exit();
break;
default:
break;
}
break;
case SDL_QUIT:
case FF_QUIT_EVENT:
do_exit();
break;
default:
break;
}
}
}


再来第三步,创建了一个解析线程而已.

static VideoState *stream_open(const char *filename)
{
VideoState *is;

is = av_mallocz(sizeof(VideoState));
pstrcpy(is->filename, sizeof(is->filename), filename);

//create thread
is->parse_tid = SDL_CreateThread(decode_thread, is);

return is;
}


解析线程的入口函数:

解析文件,启动子线程,并把解析出来的音视频放入队列中.

static int decode_thread(void *arg)
{
VideoState *is = arg;
AVFormatContext *ic;
int err, i, ret, video_index, audio_index;
AVPacket pkt1,  *pkt = &pkt1;
AVFormatParameters params,  *ap = ¶ms;
int n;
int flags = SDL_HWSURFACE | SDL_ASYNCBLIT | SDL_HWACCEL | SDL_RESIZABLE;

video_index =  - 1;
audio_index =  - 1;

is->video_stream =  - 1;
is->audio_stream =  - 1;
n = 0;

memset(ap, 0, sizeof(*ap));

//1 这个函数分析过,获取了proto,format和decoder.
err = av_open_input_file(&ic, is->filename, NULL, 0, ap);
is->ic = ic;

//2 启动音视频的线程
if (audio_index >= 0)
stream_component_open(is, audio_index); //启动解析audio的线程
if (video_index >= 0)
stream_component_open(is, video_index); //启动解析vedio的线程

//3 read线程:读文件,并写入到文件中.
for (;;)
{
if (is->audioq.size > MAX_AUDIOQ_SIZE || is->videoq.size > MAX_VIDEOQ_SIZE || url_feof(&ic->pb))
{
SDL_Delay(10); // if the queue are full, no need to read more,wait 10 ms
continue;
}

ret = av_read_packet(ic, pkt); //av_read_frame(ic, pkt);

if (pkt->stream_index == is->audio_stream)
{
packet_queue_put(&is->audioq, pkt,"audio");
}
else if (pkt->stream_index == is->video_stream)
{
packet_queue_put(&is->videoq, pkt,"video");
}
else
{
av_free_packet(pkt);
}
}

return 0;
}


一共有线程:

主线程: 起到一个监听事件的作用

解析线程: 解析文件,获取音视频包,并方式到queue中.

音频线程: 通过queue和解析线程交互.

视频线程: 通过queue和解析线程交互.

线程结构完成了,看一下视频线程的具体逻辑.

static int stream_component_open(VideoState *is, int stream_index)
{
AVFormatContext *ic = is->ic;
AVCodecContext *enc;
AVCodec *codec;
SDL_AudioSpec wanted_spec, spec;

// 1 知道codecoder
enc = ic->streams[stream_index]->actx;
codec = avcodec_find_decoder(enc->codec_id);
if (!codec || avcodec_open(enc, codec) < 0)
return  - 1;

switch (enc->codec_type)
{
case CODEC_TYPE_AUDIO:
...
break;

case CODEC_TYPE_VIDEO:
is->video_stream = stream_index;
is->video_st = ic->streams[stream_index];
is->frame_last_delay = is->video_st->frame_last_delay;
packet_queue_init(&is->videoq);
is->video_tid = SDL_CreateThread(video_thread, is);  //video线程启动
break;
default:
break;
}

return 0;
}


video thread从queue中获取到pkg,开始decode,之后显示.

static int video_thread(void *arg)
{
VideoState *is = arg;
AVPacket pkt1,  *pkt = &pkt1;
int len1, got_picture;
double pts = 0;

AVFrame *frame = av_malloc(sizeof(AVFrame));
memset(frame, 0, sizeof(AVFrame));

alloc_picture(is);

for (;;) {
//获取到vedio pkt
if (packet_queue_get(&is->videoq, pkt, 1,"video") < 0)
break;

SDL_LockMutex(is->video_decoder_mutex);
len1 = avcodec_decode_video(is->video_st->actx, frame, &got_picture, pkt->data, pkt->size);
SDL_UnlockMutex(is->video_decoder_mutex);

if (pkt->dts != AV_NOPTS_VALUE)
pts = av_q2d(is->video_st->time_base) *pkt->dts;
if (got_picture)
{
video_display(is, frame, pts);
}
av_free_packet(pkt);
}

the_end:
av_free(frame);
return 0;
}


display靠的是SDL,这个具体逻辑另开一篇吧.这个比较复杂.主要看decode,调用了decoder的decode.每个编解码都会有自己的decode.

int avcodec_decode_video(AVCodecContext *avctx, AVFrame *picture, int *got_picture_ptr,
uint8_t *buf, int buf_size)
{
int ret;

*got_picture_ptr = 0;

if (buf_size)
{
fprintf(stderr, "avcodec_decode_video buf_size:%d,frame_number:%d\n",buf_size,avctx->frame_number);
ret = avctx->codec->decode(avctx, picture, got_picture_ptr, buf, buf_size);
if (*got_picture_ptr)
avctx->frame_number++;
}
else
ret = 0;

return ret;
}


好了,整体框架应该是理清啦.

其实还没有到重点.重点是编解码逻辑,还是显示逻辑.

下一步分析一下显示逻辑.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ffmpeg
相关文章推荐