超详细解析FFplay之数据读取线程(ffplay udp)
cac55 2024-10-11 10:51 13 浏览 0 评论
阅读本文前,可以看看前面的文章。
构建debug环境准备,需要被准备好的SDL2.dll拷贝到如下目录,以及FFmpeg常用动态库。
qt命令行设置,如下:
本文还是着重分析数据读取线程。还是先上ffplay"藏宝图"。
从ffplay框架分析我们可以看到,ffplay有专?的线程read_thread()读取数据,且在调?av_read_frame读取数据包之前需要做例如打开?件,查找配置解码器,初始化?视频输出等准备阶段,主要包括三?步骤:
(1)初始化?作
avformat_alloc_context:创建上下?。
ic->interrupt_callback.callback = decode_interrupt_cb; 设置超时时间回调。
avformat_open_input:打开媒体文件,包括网络流,文件,内存流。
avformat_find_stream_info:读取媒体?件的包获取更多的stream信息。
avformat_seek_file:检测是否指定播放起始时间,如果指定时间则seek到指定位置。
查找查找AVStream,将对应的index值记录到st_index[AVMEDIA_TYPE_NB],有如下2种方案。
a.avformat_match_stream_specifier:根据?户指定来查找流。
b.av_find_best_stream:使用这个接口查找流。
从待处理流中获取相关参数,设置显示窗?的宽度、?度及宽??。
stream_component_open:打开?频、视频、字幕解码器,并创建相应的解码线程以及进?对应输出参数的初始化。
(2)For循环读取数据
检测是否退出。
检测是否暂停/继续。
检测是否需要seek。
检测video是否为attached_pic。
检测队列是否已经有?够数据。
检测码流是否已经播放结束。
是否循环播放。
是否?动退出。
使?av_read_frame读取数据包。
检测数据是否读取完毕。
检测是否在播放范围内。
到这步才将数据插?对应的队列。
(3)退出线程处理
如果解复?器有打开则关闭avformat_close_input。
调?SDL_PushEvent发送退出事件FF_QUIT_EVENT。
释放互斥量wait_mutex。
具体准备工作,调用了如下函数:
调?avformat_alloc_context创建解复?器上下?。
// 1. 创建上下?结构体,这个结构体是最上层的结构体,表示输?上下?
ic = avformat_alloc_context();
最终该ic 赋值给VideoState的ic变量。
// videoState的ic指向分配的ic
is->ic = ic;
ic->interrupt_callback
/* .设置中断回调函数,如果出错或者退出,就根据?前程序设置的状态选择继续check或者直接退出。
当执?耗时操作时(?般是在执?while或者for循环的数据读取时),会调?interrupt_ callback.callback.
回调函数中返回1则代表ffmpeg结束耗时操作退出当前函数的调?。
回调函数中返回0则代表ffmpeg内部继续执?耗时操作,直到完成既定的任务(?如读取到既定 的数据包)。
*/
ic->interrupt_callback.callback = decode_interrupt_cb;
ic->interrupt_callback.opaque = is;
interrupt_callback?于ffmpeg内部在执?耗时操作时,检查调?者是否有退出请求,避免?户退出请求,而没有及时响应。
可以在ubuntu环境下,通过gdb ./ffplay_g来播放视频,然后在decode_interrupt_cb打断点。
从调用栈关系看,这个回调函数的触发是在avformat_open_input这个节点,正真的触发是在retry_transfer_wrapper函数ff_check_interrupt。
调用栈如下,顺序应该是从下到上:
decode_interrupt_cb。
ff_check_interrupt。
retry_transfer_wrapper。
ffurl_read。
read_packet_wrapper。
fill_buffer。
avio_read。
av_probe_input_buffer2。
init_input。
avformat_open_input。
#0 decode_interrupt_cb (ctx=0x7ffff7e36040) at fftools/ffplay.c:271 5
#1 0x00000000007d99b7 in ff_check_interrupt (cb=0x7fffd00014b0) at libavformat/avio.c:667
#2 retry_transfer_wrapper (transfer_func=0x7dd950 <file_read>, size _min=1, 5 size=32768, buf=0x7fffd0001700 "", h=0x7fffd0001480) at libavformat/avio.c:374
#3 ffurl_read (h=0x7fffd0001480, buf=0x7fffd0001700 "", size=32768) at libavformat/avio.c:411
#4 0x000000000068cd9c in read_packet_wrapper (size=<optimized out>, 10 buf=<optimized out>, s=0x7fffd00011c0) at libavformat/aviobuf.c: 535
#5 fill_buffer (s=0x7fffd00011c0) at libavformat/aviobuf.c:584
#6 avio_read (s=s@entry=0x7fffd00011c0, buf=0x7fffd0009710 "", size=size@entry=2048) at libavformat/aviobuf.c:677
#7 0x00000000006b7780 in av_probe_input_buffer2 (pb=0x7fffd00011c0, 15 fmt=0x7fffd0000948,filename=filename@entry=0x31d50e0 "source.200kbps.768x320.flv",
#logctx=logctx@entry=0x7fffd0000940, offset=offset@entry=0, 18 max_probe_size=1048576) at libavformat/format.c:262
#8 0x00000000007b631d in init_input (options=0x7fffdd9bcb50, 20 filename=0x31d50e0 "source.200kbps.768x320.flv", s=0x7fffd000094 0)
#at libavformat/utils.c:443
#9 avformat_open_input (ps=ps@entry=0x7fffdd9bcbf8, 23 filename=0x31d50e0 "source.200kbps.768x320.flv", fmt=<optimized out>,
可以看到是在libavformat/avio.c:374?有正真触发到。
avformat_find_stream_info的触发。
read_thread
avformat_find_stream_info
decode_interrupt_cb
详细步骤如下:
#0 decode_interrupt_cb (ctx=0x7ffff7e36040) at fftools/ffplay.c:2715
#1 0x00000000007b25bc in avformat_find_stream_info (ic=0x7fffd000094 0,options=0x0) at libavformat/utils.c:3693
#2 0x00000000004a6ea9 in read_thread (arg=0x7ffff7e36040)
从该调?栈可以看出来 avformat_find_stream_info也会触发ic->interrupt_callback的调?,具体可以看代码(libavformat/utils.c:3693?)。
在avformat_find_stream_info函数里。
触发的点。
av_read_frame的触发耗时回调函数decode_interrupt_cb。基本顺序流程如下:
read_thread
av_read_frame
read_frame_internal
ff_read_packet
flv_read_packet
av_get_packet
append_packet_chunked
avio_read
fill_buffer
read_packet_wrapper
ffurl_read
retry_transfer_wrapper
ff_check_interrupt
decode_interrupt_cb
详细步骤如下:
#0 decode_interrupt_cb (ctx=0x7ffff7e36040) at fftools/ffplay.c:271 5
#1 0x00000000007d99b7 in ff_check_interrupt (cb=0x7fffd00014b0) at libavformat/avio.c:667
#2 retry_transfer_wrapper (transfer_func=0x7dd950 <file_read>, size _min=1, size=32768, buf=0x7fffd0009710 "FLV\001\005", h=0x7fffd0001480) at libavformat/avio.c:374
#3 ffurl_read (h=0x7fffd0001480, buf=0x7fffd0009710 "FLV\001\005", size=32768) at libavformat/avio.c:411
#4 0x000000000068cd9c in read_packet_wrapper (size=<optimized out>, buf=<optimized out>, s=0x7fffd00011c0) at libavformat/aviobuf.c: 535
#5 fill_buffer (s=0x7fffd00011c0) at libavformat/aviobuf.c:584
#6 avio_read (s=s@entry=0x7fffd00011c0, buf=0x7fffd00dbf6d "\177", size=45, size@entry=90) at libavformat/aviobuf.c:677
#7 0x00000000007a99d5 in append_packet_chunked (s=0x7fffd00011c0, pkt=pkt@entry=0x7fffdd9bca00, size=size@entry=90) at libavformat/utils.c:293
#8 0x00000000007aa969 in av_get_packet (s=<optimized out>, pkt=pkt@entry=0x7fffdd9bca00, size=size@entry=90) at libavformat/utils.c:317
#9 0x00000000006b350a in flv_read_packet (s=0x7fffd0000940, pkt=0x7fffdd9bca00) at libavformat/flvdec.c:1295
#10 0x00000000007aad6d in ff_read_packet (s=s@entry=0x7fffd0000940, pkt=pkt@entry=0x7fffdd9bca00) at libavformat/utils.c:856 ---Type <return> to continue, or q <return> to quit---
#11 0x00000000007ae291 in read_frame_internal (s=0x7fffd0000940, pkt=0x7fffdd9bcc00) at libavformat/utils.c:1582
#12 0x00000000007af422 in av_read_frame (s=0x7fffd0000940, pkt=pkt@entry=0x7fffdd9bcc00) at libavformat/utils.c:1779
#13 0x00000000004a68b1 in read_thread (arg=0x7ffff7e36040) at fftools/ffplay.c:3008
avformat_open_input():打开媒体?件。
int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat*fmt, AVDictionary **options);
avformat_open_input?于打开输??件(对于RTMP/RTSP/HTTP?络流也是?样,在ffmpeg内部都抽象为URLProtocol,这?描述为?件是为了?便与后续提到的AVStream的流作区分),读取视频?件的基本信息。
需要提到的两个参数是fmt和options。通过fmt可以强制指定视频?件的封装,options可以传递额外参数给封装(AVInputFormat)。
//特定选项处理
if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_C ASE))
{ av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVE RWRITE);
scan_all_pmts_set = 1; }
/* 3.打开?件,主要是探测协议类型,如果是?络?件则创建?络链接等 */
err = avformat_open_input(&ic, is->filename, is->iformat, &format_op ts);
if (err < 0) {
print_error(is->filename, err);
ret = -1; goto fail; }
if (scan_all_pmts_set)
av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_C ASE);
if ((t = av_dict_get(format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
ret = AVERROR_OPTION_NOT_FOUND;
goto fail;
}
scan_all_pmts是mpegts的?个选项,表示扫描全部的ts流的"Program Map Table"表。这?在没有设定该选项的时候,强制设为1。最后执?avformat_open_input。
使?gdb跟踪options的设置,在av_opt_set打断点。
(gdb) b av_opt_set
(gdb) r
#0 av_opt_set_dict2 (obj=obj@entry=0x7fffd0000940, options=options@entry=0x7fffdd9bcb50, search_flags=search_flags@entry=0) at libavutil/opt.c:1588
#1 0x00000000011c6837 in av_opt_set_dict (obj=obj@entry=0x7fffd0000940, options=options@entry=0x7fffdd9bcb50) at libavutil/opt.c:1605
#2 0x00000000007b5f8b in avformat_open_input (ps=ps@entry=0x7fffdd9bcbf8, filename=0x31d23d0 "source.200kbps.768x320.flv", fmt=<optimized out>, options=0x2e2d450 <format_opts>) at libavformat/utils.c:560
#3 0x00000000004a70ae in read_thread (arg=0x7ffff7e36040) at fftools/ffplay.c:2780
...... (gdb) l
if (!options)
return 0;
while ((t = av_dict_get(*options, "", t, AV_DICT_IGNORE_SUFFIX)))
{
ret = av_opt_set(obj, t->key, t->value, search_flags);
if (ret == AVERROR_OPTION_NOT_FOUND)
ret = av_dict_set(&tmp, t->key, t->value, 0);
if (ret < 0) {
av_log(obj, AV_LOG_ERROR, "Error setting option %s to value %s.\n", t->key, t->value);
(gdb) print **options
$3 = {count = 1, elems = 0x7fffd0001200}
(gdb) print (*options)->elems
$4 = (AVDictionaryEntry *) 0x7fffd0001200
(gdb) print *((*options)->elems)
$5 = {key = 0x7fffd0001130 "scan_all_pmts", value = 0x7fffd0001150 "1"}
(gdb)
参数的设置最终都是设置到对应的解复?器,?如:
mpegts.c,如下:
flvdec.c,如下:
avformat_find_stream_info()
在打开了?件后,就可以从AVFormatContext中读取流信息了。?般调?avformat_find_stream_info获取完整的流信息。为什么在调?了avformat_open_input后,仍然需要调?avformat_find_stream_info才能获取正确的流信息呢?看下注释:
/** * Read packets of a media file to get stream information.
This * is useful for file formats with no headers such as MPEG.
This * function also computes the real framerate in case of MPEG-2 repeat * frame mode.
* The logical file position is not changed by this function;
* examined packets may be buffered for later processing.
** @param ic media file handle * @param options If non-NULL, an ic.nb_streams long array of pointers to
* dictionaries, where i-th member contains options for
* codec corresponding to i-th stream.
* On return each dictionary will be filled with options that were not found.
* @return >=0 if OK, AVERROR_xxx on error
** @note this function isn't guaranteed to open all the codecs, so
* options being non-empty at return is a perfectly normal behavior.
** @todo Let the user decide somehow what information is needed so that
* we do not waste time getting stuff the user does not need.
*/
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
该函数是通过读取媒体?件的部分数据来分析流信息。在?些缺少头信息的封装下特别有?,?如说MPEG(这?应该说ts更准确)(FLV?件也是需要读取packet 分析流信息)。?被读取?以分析流信息的数据可能被缓存,供av_read_frame时使?,在播放时并不会跳过这部分packet的读取。
检测是否指定播放起始时间
如果指定时间则seek到指定位置avformat_seek_file。可以通过 ffplay -ss 设置起始时间,时间格式hh:mm:ss,?如ffplay -ss 00:00:30 test.flv 则是从30秒的起始位置开始播放。
具体调?流程,可以在av_opt_seek 函数打断点进?测试。
{ "ss", HAS_ARG, { .func_arg = opt_seek }, "seek to a given position in seconds", "pos" },
{ "t", HAS_ARG, { .func_arg = opt_duration }, "play \"duration\" sec onds of audio/video", "duration" },
/* if seeking requested, we execute it */
/* 5. 检测是否指定播放起始时间 */
if (start_time != AV_NOPTS_VALUE) {
int64_t timestamp;
timestamp = start_time;
/* add the stream start time */
if (ic->start_time != AV_NOPTS_VALUE)
timestamp += ic->start_time;
// seek的指定的位置开始播放
ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX , 0);
if (ret < 0) {
av_log(NULL, AV_LOG_WARNING, "%s: could not seek to position %0.3f\n",
is->filename, (double)timestamp / AV_TIME_BASE);
}
}
2路音频,一路国语语音,一路粤语语音。
设置ast,可以选择是哪一路音频播放。在这里去配置。比如这里指定0,就是播放粤语。指定1,就是国语了。
这些是视频和音频参数。
具体现在那个流进?播放我们有两种策略:
在播放起始指定对应的流。
ffplay是通过通过命令可以指定流。使用ast是指定音频流,使用vst是指定视频流,使用sst是指定字幕流。
{ "ast", OPT_STRING | HAS_ARG | OPT_EXPERT, { &wanted_stream_spec[AVMEDIA_TYPE_AUDIO] }, "select desired audio stream", "stream_specifier" },
{ "vst", OPT_STRING | HAS_ARG | OPT_EXPERT, { &wanted_stream_spec[AVMEDIA_TYPE_VIDEO] }, "select desired video stream", "stream_specifier" },
{ "sst", OPT_STRING | HAS_ARG | OPT_EXPERT, { &wanted_stream_spec[AVMEDIA_TYPE_SUBTITLE] }, "select desired subtitle stream", "stream_specifier" },
可以通过如下选择:
-ast n 指定?频流(?如我们在看电影时,有些电影可以?持普通话和英?切换,此时可以?该命令进?选择)。
-vst n 指定视频流。
-vst n 指定字幕流。
把对应的index值记录到st_index[AVMEDIA_TYPE_NB];。
使?缺省的流进?播放。
如果我们没有指定,则ffplay主要是通过 av_find_best_stream 来选择,自动去匹配,其原型为:
/**
* Find the "best" stream in the file.
* The best stream is determined according to various heuristics as the most
* likely to be what the user expects.
* If the decoder parameter is non-NULL, av_find_best_stream will fi nd the
* default decoder for the stream's codec;
streams for which no deco der can
* be found are ignored.
* * @param ic media file handle
* @param type stream type: video, audio, subtitles, et c.
* @param wanted_stream_nb user-requested stream number,
* or -1 for automatic selection
* @param related_stream try to find a stream related (eg. in the same
* program) to this one, or -1 if none
* @param decoder_ret if non-NULL, returns the decoder for the
* selected stream
* @param flags flags; none are currently defined
* @return the non-negative stream number in case of success,
* AVERROR_STREAM_NOT_FOUND if no stream with the requested type
* could be found, 21 * AVERROR_DECODER_NOT_FOUND if streams were found but no d ecoder
* @note If av_find_best_stream returns successfully and decoder_re t is not
* NULL, then *decoder_ret is guaranteed to be set to a valid AVCodec.
*/
int av_find_best_stream(AVFormatContext *ic,
enum AVMediaType type, //要选择的流类型
int wanted_stream_nb, //?标流索引
int related_stream, //相关流索引
AVCodec **decoder_ret,
int flags);
具体代码流程如下:
如果是根据用户指定来查找流。使用了正确的wanted_stream_nb,?般情况都是直接返回该指定流,即?户选择的流。
for (i = 0; i < ic->nb_streams; i++)
{
AVStream *st = ic->streams[i];
enum AVMediaType type = st->codecpar->codec_type;
st->discard = AVDISCARD_ALL;
if (type >= 0 && wanted_stream_spec[type] && st_index[type] == - 1)
if (avformat_match_stream_specifier(ic, st, wanted_stream_sp ec[type]) > 0)
st_index[type] = i; }
for (i = 0; i < AVMEDIA_TYPE_NB; i++) {
if (wanted_stream_spec[i] && st_index[i] == -1) {
av_log(NULL, AV_LOG_ERROR, "Stream specifier %s does not mat ch any %s stream\n", wanted_stream_spec[i], av_get_media_type_string (i));
st_index[i] = INT_MAX;
}
}
使用av_find_best_stream选择流。如果?户没有指定流,或指定部分流,或指定流不存在,则主要由av_find_best_stream发挥作?。
if (!video_disable)
st_index[AVMEDIA_TYPE_VIDEO] =
av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
if (!audio_disable)
st_index[AVMEDIA_TYPE_AUDIO] = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
st_index[AVMEDIA_TYPE_AUDIO],
st_index[AVMEDIA_TYPE_VIDEO],
NULL, 0);
if (!video_disable && !subtitle_disable)
st_index[AVMEDIA_TYPE_SUBTITLE] =
av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
st_index[AVMEDIA_TYPE_SUBTITLE],
(st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
st_index[AVMEDIA_TYPE_AUDIO] :
st_index[AVMEDIA_TYPE_VIDEO]),
NULL, 0);
如果指定了相关流,且未指定?标流的情况,会在相关流的同?个节?中查找所需类型的流,但?般结果,都是返回该类型第1个流。
通过AVCodecParameters和av_guess_sample_aspect_ratio计算出显示窗?的宽、?。从待处理流中获取相关参数,设置显示窗?的宽度、?度及宽??。由于帧宽??由解码器设置,但流宽??由解复?器设置,因此这两者可能不相等。基本逻辑是优先使?流宽??(前提是值是合理的),其次使?帧宽??,这样,流宽??(容器设置,易于修改)可以覆盖帧宽??。
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
AVCodecParameters *codecpar = st->codecpar;
// 根据流和帧宽??猜测帧的样本宽??。
//此函数会尝试返回待显示帧应当使?的宽??值。
AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
if (codecpar->width) {
// 设置显示窗?的??和宽??
set_default_window_size(codecpar->width, codecpar->heigh t, sar);
}
}
具体流程如上所示,这?实质只是设置了default_width、default_height变量的??,没有真正改变窗?的??。真正调整窗???是在视频显示调?video_open()函数进?设置。
stream_component_open()
经过以上步骤,?件打开成功,且获取了流的基本信息,并选择?频流、视频流、字幕流。接下来就可以所选流对应的解码器了。
/* open the streams */
/* 5.打开视频、?频解码器。在此会打开相应解码器,并创建相应的解码线程。 */
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
}
ret = -1;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO] ); }
if (is->show_mode == SHOW_MODE_NONE) {
//选择怎么显示,如果视频打开成功,就显示视频画?,否则,显示?频对应的频谱图
is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;
}
if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
}
?频、视频、字幕等流都要调?stream_component_open,他们直接有共同的流程,也有差异化的流程,差异化流程使?switch进?区分。
int stream_component_open(VideoState *is, int stream_index);
stream_component_open这个函数比较长,逐步分析。
/* 为解码器分配?个编解码器上下?结构体 */
avctx = avcodec_alloc_context3(NULL);
if (!avctx)
return AVERROR(ENOMEM);
/* 将码流中的编解码器信息拷?到新分配的编解码器上下?结构体 */
ret = avcodec_parameters_to_context(avctx, ic->streams[stream_in dex]->codecpar);
if (ret < 0)
goto fail;
// 设置pkt_timebase
avctx->pkt_timebase = ic->streams[stream_index]->time_base;
总结来说就是,先通过 avcodec_alloc_context3 分配了解码器上下? AVCodecContex ,然后通过avcodec_parameters_to_context 把所选流的解码参数赋给 avctx ,最后设了 time_base。
注意,还有一个函数,有这样一个区别。
avcodec_parameters_to_context 解码时?,avcodec_parameters_from_context则?于编码。
/* 根据codec_id查找解码器 */
codec = avcodec_find_decoder(avctx->codec_id);
// 获取指定的解码器名字,如果没有设置则为NULL
switch(avctx->codec_type){
case AVMEDIA_TYPE_AUDIO :
is->last_audio_stream = strea m_index;
// 获取指 定的解码器名字
forced_codec_name = audio_codec_name;
break;
case AVMEDIA_TYPE_SUBTITLE:
// 获取指 定的解码器名字
is->last_subtitle_stream = strea m_index;
forced_codec_name = subtitle_codec_name;
break;
case AVMEDIA_TYPE_VIDEO :
is->last_video_stream = strea m_index;
// 获取指 定的解码器名字
forced_codec_name = video_codec_name;
break;
}
}
//如果名字找到了,根据名字去找对应的解码器
if (forced_codec_name)
codec = avcodec_find_decoder_by_name(forced_codec_name);
if (!codec) {
if (forced_codec_name)
av_log(NULL, AV_LOG_WARNING, "No codec could be found with name '%s'\n", forced_codec_name);
else
av_log(NULL, AV_LOG_WARNING,"No decoder could be found for codec %s\n", avcodec_get_name(avctx->codec_id));
ret = AVERROR(EINVAL);
goto fail;
}
总结来说,这段主要是通过 avcodec_find_decoder 找到所需解码器(AVCodec)。如果?户有指定解码器,则设置 forced_codec_name ,并通过 avcodec_find_decoder_by_name 查找解码器。找到解码器后(如果用户没有指定解码器,就需要通过寻找,然后匹配,所以一般在代码中,都是要去匹配),就可以通过 avcodec_open2 打开解码器了。forced_codec_name对应到?频、视频、字幕不同的传?的解码器名字,如果有设置,?如ffplay -acodec aac xx.flv, 此时audio_codec_name被设置为"aac"(音频解码器名字),则相应的forced_codec_name为“aac”。
最后,一个大的switch语句,去启动不同的解码线程。
switch (avctx->codec_type) {
case AVMEDIA_TYPE_AUDIO:
sample_rate = avctx->sample_rate;
nb_channels = avctx->channels;
channel_layout = avctx->channel_layout;
/* prepare audio output 准备?频输出*/
if ((ret = audio_open(is, channel_layout, nb_channels, sampl e_rate, &is->audio_tgt)) < 0)
goto fail;
is->audio_hw_buf_size = ret;
is->audio_src = is->audio_tgt;
is->audio_buf_size = 0;
is->audio_buf_index = 0;
/* init averaging filter 初始化averaging滤镜, ?audio master时 使? */
is->audio_diff_avg_coef = exp(log(0.01) / AUDIO_DIFF_AVG_NB );
is->audio_diff_avg_count = 0;
/* 由于我们没有精确的?频数据填充FIFO,故只有在?于该阈值时才进?校正?频 同步*/
is->audio_diff_threshold = (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;
// 获取audio的stream索引
is->audio_stream = stream_index;
// 获取audio的stream指针
is->audio_st = ic->streams[stream_index];
// 初始化ffplay封装的?频解码器
decoder_init(&is->auddec, avctx, &is->audioq, is->continue_r ead_thread);
if ((is->ic->iformat->flags & (AVFMT_NOBINSEARCH | AVFMT_NOG ENSEARCH
| AVFMT_NO_BYTE_SEEK)) && !is->ic->iformat->read_seek)
{ 26 is->auddec.start_pts = is->audio_st->start_time; 27 is->auddec.start_pts_tb = is->audio_st->time_base; 28 }
总结来说,即根据具体的流类型,作特定的初始化。但不论哪种流,基本步骤都包括了ffplay封装的解码器的初始化和启动解码器线程:
decoder_init 初始化解码器:
//绑定对应的解码器上下?
d->avctx = avctx;
//绑定对应的packet队列
d->queue = queue;
//设置条件变量
//绑定VideoState的continue_read_thread,当解码线程没有packet可读时唤醒read_thread赶紧读取数据。
d->empty_queue_cond = empty_queue_cond;
//初始化start_pts
d->start_pts = AV_NOPTS_VALUE;
//初始化pkt_serial
d->pkt_serial = -1;
decoder_start 启动解码器
//启?对应的packet 队列
packet_queue_start
//创建对应的解码线程
SDL_CreateThread
注意:需要注意的是,对应?频??,这?还初始化了输出参数,这块在讲?频输出的时候再重点展开。
For循环读取数据
主要包括以下步骤:
(1)检测是否退出
if (is->abort_request)
break;
当退出事件发?时,调?do_exit() -> stream_close() -> 将is->abort_request置为1。退出该for循环,并最终退出该线程。
(2)检测是否暂停/继续。
这?的暂停、继续只是对?络流有意义。?如rtsp,av_read_pause。
// 2 检测是否暂停/继续
if (is->paused != is->last_paused) {
is->last_paused = is->paused;
if (is->paused)
is->read_pause_return = av_read_pause(ic);
// ?络流的时候有?
else
av_read_play(ic);
}
/* pause the stream */
static int rtsp_read_pause(AVFormatContext *s)
{ RTSPState *rt = s->priv_data; 5 RTSPMessageHeader reply1, *reply = &reply1;
if (rt->state != RTSP_STATE_STREAMING)
return 0;
else if (!(rt->server_type == RTSP_SERVER_REAL && rt->need_subsc ription)) {
//发送给服务器命令
ff_rtsp_send_cmd(s, "PAUSE", rt->control_uri, NULL, reply, N ULL);
if (reply->status_code != RTSP_STATUS_OK) {
return ff_rtsp_averror(reply->status_code, -1);
}
}
rt->state = RTSP_STATE_PAUSED;
return 0;
}
av_read_play。
static int rtsp_read_play(AVFormatContext *s)
{
RTSPState *rt = s->priv_data;
RTSPMessageHeader reply1, *reply = &reply1;
......
//发送给服务器
ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);
....
rt->state = RTSP_STATE_STREAMING;
return 0;
}
(3)检测是否需要seek。
if (is->seek_req) {
// 是否有seek请求
int64_t seek_target = is->seek_pos;
int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_ rel + 2: INT64_MIN;
int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_ rel - 2: INT64_MAX;
// FIXME the +-2 is due to rounding being not done in the correc t direction in generation
// of the seek_pos/seek_rel variables
// 修复由于四舍五?,没有再seek_pos/seek_rel变量的正确?向上进?
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek _max, is->seek_flags);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, 12 "%s: error while seeking\n", is->ic->url);
} else {
/* seek的时候,要把原先的数据情况,并重启解码器(释放缓存),put flush_pkt的?的是 告知解码线程需要
* reset decoder */
// 如果有?频流
if (is->audio_stream >= 0) {
// 清空packet队列数据
packet_queue_flush(&is->audioq);
// 放?flush pkt, ?来开起新的?个播放序列, 解码器读取到flush_pk t也清空解码器
packet_queue_put(&is->audioq, &flush_pkt);
}
// 如果有字幕流
if (is->subtitle_stream >= 0) {
// 同上作用
packet_queue_flush(&is->subtitleq);
packet_queue_put(&is->subtitleq, &flush_pkt);
}
// 如果有视频流
if (is->video_stream >= 0) {
// 同上作用
packet_queue_flush(&is->videoq);
packet_queue_put(&is->videoq, &flush_pkt);
}
//按字节还是按时间,设置时钟。
if (is->seek_flags & AVSEEK_FLAG_BYTE) {
//按字节
set_clock(&is->extclk, NAN, 0);
} else {
//按时钟
set_clock(&is->extclk, seek_target / (double)AV_TIME_BAS E, 0);
}
}
is->seek_req = 0;
is->queue_attachments_req = 1;
is->eof = 0;
if (is->paused)
// 如果本身是pause状态的则显示?帧(seek到那里的帧)继续暂停
step_to_next_frame(is);
}
主要的seek操作通过avformat_seek_file完成(该函数的具体使?在播放控制seek时做详解)。根据avformat_seek_file的返回值,如果seek成功,需要做到以下2点:
清除PacketQueue的缓存,并放??个flush_pkt。放?的flush_pkt可以让PacketQueue的serial增1,以区分seek前后的数据(PacketQueue函数的分析0),该flush_pkt也会触发解码器重新刷新解码器缓存,avcodec_flush_buffers(),以避免解码时使?了原来的buffer作为参考?出现?赛克。
(4)检测video是否为attached_pic。封面也相当于是一个Video Stream,相当于一个AVPacket。AV_DISPOSITION_ATTACHED_PIC 是?个标志。如果?个流中含有这个标志的话,那么就是说这个流是 *.mp3等 ?件中的?个 Video Stream 。并且该流只有?个 AVPacket ,也就是attached_pic 。这个 AVPacket 中所存储的内容就是这个 *.mp3等 ?件的封?图?。因此,也可以很好的解释了?章开头提到的为什么 st->disposition & AV_DISPOSITION_ATTACHED_PIC,这个操作可以决定是否可以继续向缓冲区中添加 AVPacket 。
// 4 检测video是否为attached_pic
if (is->queue_attachments_req) {
// attached_pic 附带的图?。?如说?些MP3,AAC?频?件附带的专辑封?,所以 需要注意的是?频?件不?定只存在?频流本身
if (is->video_st && is->video_st->disposition & AV_DISPOSITION_A TTACHED_PIC) {
AVPacket copy = { 0 };
if ((ret = av_packet_ref(?, &is->video_st->attached_pic) ) < 0)
goto fail;
packet_queue_put(&is->videoq, ?);
packet_queue_put_nullpacket(&is->videoq, is->video_stream);
}
is->queue_attachments_req = 0;
}
(5)检测队列是否已经有?够数据。
?频、视频、字幕队列都不是?限?的,如果不加以限制?直往队列放?packet,那将导致队列占??量的内存空间,影响系统的性能,所以必须对队列的缓存??进?控制。PacketQueue默认情况下会有??限制,达到这个??后,就需要等待10ms,以让消费者——解码线程能有时间消耗。
// 检测队列是否已经有?够数据
/* if the queue are full, no need to read more */
/* 缓存队列有?够的包,不需要继续读取数据 */
// 缓冲区不是?限?
if (infinite_buffer<1 &&
(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QU EUE_SIZE
|| (stream_has_enough_packets(is->audio_st, is->audio_st ream, &is->audioq) &&
stream_has_enough_packets(is->video_st, is->video_st ream, &is->videoq) &&
stream_has_enough_packets(is->subtitle_st, is->subti tle_stream, &is->subtitleq))))
{
/* wait 10 ms */
SDL_LockMutex(wait_mutex);
// 如果没有唤醒则超时10ms退出,?如在seek操作时这?会被唤醒
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
}
缓冲区满有两种可能:
第一,audioq,videoq,subtitleq三个PacketQueue的总字节数达到了MAX_QUEUE_SIZE(15M,为什么是15M?这?只是?个经验计算值,?如4K视频的码率以50Mbps计算,则15MB可以缓存2.4秒,从这么计算实际上如果我们真的是播放4K?源,15MB是偏?的数值,有些?源?较坑 同?个?件位置附近的pts差值超过5秒,此时如果视频要缓存5秒才能做同步,那15MB的缓存??就不够了)。所以要根据实际的情况进行调整。
第二,等待10ms的条件是,?频、视频、字幕流都已有够?的包(stream_has_enough_packets),三者要同时成立才行。
第?种好理解,看下第?种中的stream_has_enough_packets。这是好多种情况的组合。
static int stream_has_enough_packets(AVStream *st, int stream_id, Pac ketQueue *queue)
{
return stream_id < 0 || // 没有该流
queue->abort_request || // 请求退出
(st->disposition & AV_DISPOSITION_ATTACHED_PIC) || // 是ATTACHED_PIC
queue->nb_packets > MIN_FRAMES // packet数>25
&& (!queue->duration || // 满?PacketQueue总时?为0
av_q2d(st->time_base) * queue->duration > 1.0);//或总时?超过1s
}
有这么?种情况包是够?的,也就是说可以足够去用,不用担心空包问题:
第一,流没有打开(stream_id < 0),没有相应的流返回逻辑true。
第二,有退出请求(queue->abort_request)。
第三,配置了AV_DISPOSITION_ATTACHED_PIC。
第四,packet队列内包个数?于MIN_FRAMES(>25),并满?PacketQueue总时?为0或总时?超过1s。
包是否足够思路:
第一,看总数据??。
第二,每个packet队列的情况。
(6)检测码流是否已经播放结束。
是否循环播放。
是否?动退出。
?暂停状态才进?步检测码流是否已经播放完毕,注意:数据播放完毕和码流数据读取完毕是两个概念。只有,PacketQueue和FrameQueue都消耗完毕,才是真正的播放完毕。
// 检测码流是否已经播放结束
if (!is->paused // ?暂停
&& // 这?的执?是因为码流读取完毕后 插?空包所致
(!is->audio_st // 没有?频流
|| (is->auddec.finished == is->audioq.serial // 或者?频播放完毕
&& frame_queue_nb_remaining(&is->sampq) == 0))
&& (!is->video_st // 没有视频流
|| (is->viddec.finished == is->videoq.serial // 或者视频播放完毕
&& frame_queue_nb_remaining(&is->pictq) == 0))) {
if (loop != 1 // a 是否循环播放
&& (!loop || --loop)) {
//循环播放,自动从头开始
stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
} else if (autoexit) { // b 是否?动退出
ret = AVERROR_EOF;
goto fail;
}
}
这?判断播放已完成的条件需要同时满?这几个条件:
第一,不在暂停状态
第二,?频未打开;或者打开了,但是解码已解完所有packet,?定义的解码器(decoder)serial等于PacketQueue的serial,并且FrameQueue中没有数据帧,音频数据已经消耗完毕。
PacketQueue.serial -> packet.serail -> decoder.pkt_serial
decoder.finished = decoder.pkt_serial
is->auddec.finished == is->audioq.serial 最新的播放序列的packet都解码完毕。
frame_queue_nb_remaining(&is->sampq) == 0 对应解码后的数据也播放完毕。
第三,视频未打开;或者打开了,但是解码已解完所有packet,?定义的解码器(decoder)serial等于PacketQueue的serial,并且FrameQueue中没有数据帧。视频数据已经播放完毕。
在确认?前码流已播放结束的情况下,?户有两个变量可以控制播放器?为:
第一,loop: 控制播放次数(当前这次也算在内,也就是最?就是1次了),0表示?限次。
第二,autoexit:?动退出,也就是播放完成后?动退出。就是不用自动循环。
loop条件简化的?常不友好,其意思是:如果loop==1,那么已经播了1次了,?需再seek重新播放。如果loop不是1,==0,随意,?限次循环;减1后还?于0(--loop),也允许循环。也就是每次播放完毕后,循环的次数就要减1。
是否循环播放
如果循环播放,即是将?件seek到起始位置 stream_seek(is, start_time != AV_NOPTS_VALUE ?start_time : 0, 0, 0); 注意,这?讲的的起始位置不?定是从头开始,具体也要看?户是否指定了起始播放位置。
是否?动退出。
如果播放完毕?动退出。
(7)使?av_read_frame读取数据包。
读取数据包很简单,但要注意传?的packet,av_read_frame不会释放其数据,?是每次都重新申请数据。注意,内存泄漏问题。
//读取媒体数据,得到的是?视频分离后、解码前的数据
// 调?不会释放pkt的数据,都是要??去释放
ret = av_read_frame(ic, pkt);
(8)检测数据是否读取完毕。
if (ret < 0) {
if ((ret == AVERROR_EOF || avio_feof(ic->pb))
&& !is->eof)
{
// 插?空包说明码流数据读取完毕了,之前讲解码的时候说过刷空包是为了从解码 器把所有帧都读出来
if (is->video_stream >= 0)
//插入视频空包
packet_queue_put_nullpacket(&is->videoq, is->video_strea m);
if (is->audio_stream >= 0)
//插入音频空包
packet_queue_put_nullpacket(&is->audioq, is->audio_strea m);
if (is->subtitle_stream >= 0)
//插入字幕空包
packet_queue_put_nullpacket(&is->subtitleq, is->subtitle _stream);
is->eof = 1;// ?件读取完毕
}
if (ic->pb && ic->pb->error)
break;
SDL_LockMutex(wait_mutex);
//等待超时时间,运行读取线程。
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
// 继续循环 保证线程的运?,?如要seek到某个位置播放可以继 续响应
continue;
} else {
is->eof = 0;
}
数据读取完毕后,放对应?频、视频、字幕队列插?“空包”,以通知解码器冲刷buffer,将缓存的所有数据都解出来frame并取出来。然后继续在for{}循环,直到收到退出命令,或者loop播放,或者seek等操作。
(9)检测是否在播放范围内。
播放器可以设置:-ss 起始位置,以及 -t 播放时?。
// 检测是否在播放范围内
/* check if packet is in play range specified by user, then queue, o therwise discard */
// 获取流的起始时间
stream_start_time = ic->streams[pkt->stream_index]->start_time;
// 获取pack et的时间戳
pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
// 这?的duration是在命令?时?来指定播放?度
pkt_in_play_range = duration == AV_NOPTS_VALUE ||
(pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_ti me : 0)) *
av_q2d(ic->streams[pkt->stream_index]->time_base) -
(double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 100000 0
<= ((double)duration / 1000000);
从流获取的参数说明,如下:
第一,stream_start_time,是从当前流AVStream->start_time获取到的时间,如果没有定义具体的值则默认为AV_NOPTS_VALUE,即该值是?效的;那stream_start_time有意义的就是0值。也就是说,默认是从0开始。
第二,pkt_ts,当前packet的时间戳,pts有效就?pts的,pts?效就?dts。
第三,duration,使?"-t value"指定的播放时?,默认值AV_NOPTS_VALUE,即该值?效不?参考。
第四,start_time,使?“-ss value”指定播放的起始位置,默认AV_NOPTS_VALUE,即该值?效不?参考。
pkt_in_play_range的值为0或1。
当没有指定duration播放时?时,很显然duration == AV_NOPTS_VALUE的逻辑值为1,所以pkt_in_play_range为1。
当duration被指定(-t value)且有效时,主要判断。
(pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
av_q2d(ic->streams[pkt->stream_index]->time_base) -
(double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
<= ((double)duration / 1000000);
实质就是当前时间戳 pkt_ts - start_time 是否 < duration,这?分为:
stream_start_time是否有效:有效就?实际值,?效就是从0开始。start_time 是否有效,有效就?实际值,?效就是从0开始。即是pkt_ts - stream_start_time - start_time < duration。设置了就有有效值,没设置就默认从0开始。
(10)将数据插?对应的队列。
// 将?视频数据分别送?相应的queue中
if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
//插入到音频packet队列
packet_queue_put(&is->audioq, pkt);
} else if (pkt->stream_index == is->video_stream && pkt_in_play_rang e
&& !(is->video_st->disposition & AV_DISPOSITION_ATTACHED _PIC)) {
//printf("pkt pts:%ld, dts:%ld\n", pkt->pts, pkt->dts);
//插入到视频packet队列
packet_queue_put(&is->videoq, pkt);
} else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_r ange) {
//插入到字幕packet队列
packet_queue_put(&is->subtitleq, pkt);
} else {
av_packet_unref(pkt);// 不?队列则直接释放数据
}
(3)退出线程处理
主要包括以下步骤:
第一,解复?器有打开则关闭avformat_close_input。
第二,调?SDL_PushEvent发送退出事件FF_QUIT_EVENT,发送的FF_QUIT_EVENT退出播放事件由event_loop()函数相应,收到FF_QUIT_EVENT后调?do_exit()做退出操作。
第三,消耗互斥量wait_mutex。
解码如下视频格式和参数。
可以看出来CPU占有率,还是挺低的,大概就在6%左右。效果不错。
本篇文章就分享到这里,欢迎关注,点赞,转发,收藏。也欢迎私信讨论技术问题。
相关推荐
- 无力吐槽的自动续费(你被自动续费困扰过吗?)
-
今天因为工作需要,需要在百度文库上下载一篇文章。没办法,确实需要也有必要,只能老老实实的按要求买了个VIP。过去在百度文库上有过类似经历,当时为了写论文买了一个月的VIP,后面也没有太注意,直到第二个...
- 百度文库推出“文源计划”创作者可一键认领文档
-
11月7日,百度文库发布了旨在保护创作者权益的“文源计划”。所谓“文源计划”,即为每一篇文档找到源头,让创作者享受更多的权益。据百度文库总经理李小婉介绍,文源计划分为三部分,分别是版权认证、版权扶持和...
- 有开放大学学号的同学,百度文库高校版可以用了。
-
还在网上找百度文库的下载方式,只要从身边的朋友在读开放大学的,那他(她)的学号就可以登陆到国家开放大学图书馆,还使用百度文库高校版来下载。与百度文库稍有不同,但足够使用了。现转国图链接如下:htt...
- 搜索资源方法推荐(搜索资源的方法)
-
今天msgbox就要教大家如何又快又准的搜到各类资源,第一点,排除干扰百度搜索出来啊经常前排展示它的产品以及百度文库,如何去除呢?很简单,后面输入空格减号百度文库,比如你搜高等数学百度文库很多,只要后...
- 一行代码搞定百度文库VIP功能(2021百度文库vip账号密码共享)
-
百度文库作为大家常用查资料找文档的平台,大多数文档我们都可以直接在百度文库找到,然而百度文库也有让人头痛的时候。好不容易找到一篇合适的文档,当你准备复制的时候他却提示你需要开通VIP才能复制~~~下载...
- 百度文库文档批量上传工具用户说明书
-
百度文库文档批量上传工具用户说明书1、软件主要功能1、批量上传文档到百度文库,支持上传到收费、VIP专享、优享以及共享。2、支持自动分类和自动获取标签3、支持多用户切换,一个账户传满可以切换到...
- 百度文库现在都看不到文档是否上传成功,要凉了吗?
-
打开知识店铺,百度文库文档里显示都是下载这一按键,上传的文档也看不到是否成功?咋情况,要取消了吗?没通过审核的也不让你删除,是几个意思,想通吃吗?现在百度上传文档也很费劲,有时弄了半天的资料上传审核过...
- 微信推广引流108式:利用百度文库长期分享软文引流
-
百度文库相对于百度知道、百度百科来说,操作上没那么多条条框框,规则上也相对好把握些。做一条百度知道所花费的精力一般都会比做一条百度文库的要多些,老马个人操作下来觉得百度文库更好把握。但见仁见智吧,今天...
- 职场“避雷”指南 百度文库推出标准化劳动合同范本
-
轰轰烈烈的毕业季结束了,众多应届生在经过了“职场海选”后,已正式成为职场生力军的一员。这一阶段,除了熟悉业务,签订劳动合同、了解职场福利也迅速被提上日程。而随着国人法律意识的增强,百度文库内《劳动合同...
- 《百度文库》:素材精选宝库(百度文库官网首页)
-
《百度文库》:独特功能助力选择高质量素材在当今信息爆炸的时代,如何高效地获取并利用有价值的素材成为了许多人面临的挑战。而《百度文库》作为百度公司推出的一款在线文档分享平台,凭借其丰富的资源、强大的功能...
- 深度整合和开放AI能力 百度文库和网盘推出内容操作系统「沧舟OS」
-
【TechWeb】4月25日消息,Create2025百度AI开发者大会上,百度文库和百度网盘推出全球首个内容操作系统——沧舟OS。基于沧舟OS,百度文库APP全新上线「GenFlow超能搭子」...
- 女子发现大二作业被百度文库要求付费下载,律师:平台侵权,应赔偿
-
近日,28岁的黎女士在百度百科搜索家乡的小地名时,发现了自己在大二完成的课题作业。她继续搜索,发现多个平台收录了该文,比如豆丁网和文档之家等,有的还设置了付费或积分下载。2月15日,九派新闻记者以用户...
- 2016杀入百度文库的新捷径,只有少数人才知道的喔
-
百度的产品在SEO优化中的分量真不用多说,其实很多人都像我一样一直在找捷径。但是我经常发现很多人都是在用死方法。比如发贴吧发帖而不知道去申请一个吧主,知道自问自答而不知道去申请一个合作资格。口碑和贴吧...
- 百度文库付费文档搜索方法(百度文库付费文档搜索方法有哪些)
-
一直以来,百度文库中无论是个人中心还是个人主页,都没有像淘宝一样的店内搜索功能,连最近新开的知识店铺也没有设计店内搜索功能,这无论是对上传用户还是下载用户都不方便,上传用户想要搜索自己的文档无法办到...
- 供读者免费使用!泰达图书馆机构版百度文库新年上新啦
-
在泰达图书馆读者使用百度文库数字资源不需要VIP,免-费-用!惊不惊喜?快来了解一下吧……新年伊始,为满足区域企业、高校、科研院所以及居民群众在教学、科研及学习过程中,对各类文献资源的需求,泰达图书馆...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 如何绘制折线图 (52)
- javaabstract (48)
- 新浪微博头像 (53)
- grub4dos (66)
- s扫描器 (51)
- httpfile dll (48)
- ps实例教程 (55)
- taskmgr (51)
- s spline (61)
- vnc远程控制 (47)
- 数据丢失 (47)
- wbem (57)
- flac文件 (72)
- 网页制作基础教程 (53)
- 镜像文件刻录 (61)
- ug5 0软件免费下载 (78)
- debian下载 (53)
- ubuntu10 04 (60)
- web qq登录 (59)
- 笔记本变成无线路由 (52)
- flash player 11 4 (50)
- 右键菜单清理 (78)
- cuteftp 注册码 (57)
- ospf协议 (53)
- ms17 010 下载 (60)