ref:http://zzhhui.blog.sohu.com/304810230.html

背景说明

FFmpeg是一个开源,免费,跨平台的视频和音频流方案,它提供了一套完整的录制、转换以及流化音视频的解决方案。而ffplay是有ffmpeg官方提供的一个基于ffmpeg的简单播放器。学习ffplay对于播放器流程、ffmpeg的调用等等是一个非常好的例子。本文就是对ffplay的一个基本的流程剖析,很多细节内容还需要继续钻研。

注:本文师基于ffmpeg-2.0版本进行分析,具体代码行还请对号入座,谢谢!

主框架流程

总的流程图如下:

下图是一个ffplay播放rtsp流的流程图

下图是一个packet传递的示意图:

下图一个ffplay函数基本调用关系图,其中只保留了视频部分,去除了音频处理、字幕处理以及一些细节处理部分。

注:图中的数字表示了播放中的一次基本调用流程,X?序号表示退出流程。

从上图中我们可以了解到以下几种信息:

  • 三个线程:主流程用于视频图像显示和刷新、read_thread用于读取数据、video_thread用于解码处理;
  • 视频数据处理:由read_thread读取原始数据解复用后,按照packet的方式放入到队列中;由video_thread从packet队列中读取packet解码后,按照picture的方式放入到队列中;由主流程从picture队列中依次取picture进行显示;
  • 启动流程:启动流程如上图中的数字部分
  • 退出流程:退出流程如上图中的X?序号部分

下面将对三个线程分别加以详细描述。

read_thread线程

从read_thread开始说起而不是从main线程,主要原因是考虑按照视频数据转换的方式比较好理解。

read_thread的创建是在main-->stream_open函数中:

is->read_tid     = SDL_CreateThread(read_thread, is);

read_thread线程主要分为三部分:

  • 初始化部分:主要包括SDL_mutex信号量创建、AVFormatContext创建、打开输入文件、解析码流信息、查找音视频数据流并打开对应的数据流。对应ffplay.c文件中的2693-2810行代码;
  • 循环读取数据部分:主要包括pause和resume操作处理、seek操作处理、packet队列写入失败处理、读数据结束处理、然后是读数据并写入到对应的音视频队列中。对应ffplay.c文件中的2812-2946行代码;
  • 反初始化部分:主要包括退出前的等待、关闭音视频流、关闭avformat、给主线程发送FF_QUIT_EVENT消息以及销毁SDL_mutex信号量。对应ffplay.c文件中的2947-2972行代码;

初始化部分

主要包括SDL_mutex信号量创建、创建avformat上下文、打开输入文件、解析码流信息、查找音视频数据流并打开对应的数据流。

创建wait_mutex互斥量

SDL_mutex *wait_mutex = SDL_CreateMutex();

该互斥量主要用于在对(VideoState *)is->continue_read_thread操作时加保护,如2887行和2925行:

//代码段一

/* if the queue are full, no need to read more */

if (infinite_buffer<1 &&

……) {

/* wait 10 ms */

SDL_LockMutex(wait_mutex);

SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);  <-- line 2887

SDL_UnlockMutex(wait_mutex);

continue;

}

//代码段二

ret = av_read_frame(ic, pkt);

if (ret < 0) {

if (ret == AVERROR_EOF || url_feof(ic->pb))

eof = 1;

if (ic->pb && ic->pb->error)

break;

SDL_LockMutex(wait_mutex);

SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);  <-- line 2925

SDL_UnlockMutex(wait_mutex);

continue;

}

而continue_read_thread从其名字上来看,是一个控制read_thread线程是否继续阻塞的信号量,上面两次阻塞的地方分别是:packet队列已满,需要等待一会(即超时10ms)或者收到信号重新循环;读数据失败,但是并不是IO错误(ic->pb->error),如读取网络实时数据时取不到数据,此时也需要等待或者收到信号重新循环。

注:seek操作时(L1216)和音频队列为空(L2327)时,会发送continue_read_thread信号。

AVFormatContext创建

(AVFormatContext *)ic = avformat_alloc_context();

此处创建的avformat上下文,类似于一个句柄,后续所有avformat相关的函数调用第一个参数都是该上下文指针,如avformat_open_input、avformat_find_stream_info以及一些和av相关的函数接口第一个参数也是该指针,如av_find_best_stream、av_read_frame等等。

打开输入文件

err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);

创建好avformat上下文后,就打开is->filename指定的文件(或流),其中第三个和第四个参数可以传NULL,由ffmpeg自动侦测待输入流的文件格式,也可以通过is->iformat手动指定,format_opts参数表示设置的特殊属性。

通过调用avformat_open_input函数,我们可以得到输入流的一个基本信息。我们可以通过调用av_dump_format(ic, 0, is->filename, 0);来输出解析后的码流信息,可以得到如下数据:

Input #0, mpegts, from '/home/nfer/bak/cw880-latency.ts':0B f=0/0

Duration: N/A, bitrate: N/A

Program 1

Stream #0:0[0x68]:Video:h264 ([27][0][0][0] / 0x001B), 90k tbn

Stream #0:1[0x67]:Audio:aac([15][0][0][0] / 0x000F), 0 channels

即,可以解析出

2  封装格式是mpegts,包含两路数据流

2  流1的PID是0x68,类型是视频,编码格式是H264

2  流2的PID是0x67,类型是音频,编码格式是AAC

但是只有这些信息可定无法解码,比如视频的宽高比、图像编码格式(YUV or RGB …)、音频采样率、音频声道数量等等,以及Duration、bitrate等信息。这些信息都需要通过其他函数来解析。

解析码流信息

err = avformat_find_stream_info(ic, opts);

因为avformat_open_input函数只能解析出一些基本的码流信息,不足以满足解码的要求,因此我们调用avformat_find_stream_info函数来尽量的解析出所有的和输入流相关的信息。

解析码流的内部实现我们不在此处讨论,先看一看调用后该函数后解析出来的信息(同样采用av_dump_format来输出):

Input #0, mpegts, from '/home/nfer/bak/cw880-latency.ts':0B f=0/0

Duration: 00:02:53.73, start: 2051.276989, bitrate: 1983 kb/s

Program 1

Stream #0:0[0x68]: Video: h264 (Baseline) ([27][0][0][0] / 0x001B), yuv420p, 1280x720, 30 tbr, 90k tbn,180k tbc

Stream #0:1[0x67]: Audio: aac ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp,72 kb/s

对比上一步获取的信息,我们可以看到新解析出来的信息:

2  码流信息;节目时长00:02:53.73,开始播放时间2051.276989,码率1983 kb/s

2  视频信息:色彩空间YUV420p,分辨率1280x720,帧率30,文件层的时间精度90k,视频层的时间精度180K

2  音频信息:采样率48000,立体声stereo,音频采样格式fltp(float, planar),音频比特率72 kb/s

需要注意的是,该函数是一个阻塞操作,即默认情况下会在该函数中阻塞5s。具体的实现是在avformat_open_input函数中有一个for(;;) 循环,其中的一个break条件如下:

if (t >= ic->max_analyze_duration) {

av_log(ic, AV_LOG_VERBOSE, "max_analyze_duration %d reached at %"PRId64" microseconds\n", ic->max_analyze_duration, t);

break;

}

而ic->max_analyze_duration的默认值定义在options_table.h文件中,即默认的参数表:

{"analyzeduration",
"specify how many microseconds are analyzed to probe the input",
OFFSET(max_analyze_duration), AV_OPT_TYPE_INT, {.i64 = 5*AV_TIME_BASE },
0, INT_MAX, D},

#define AV_TIME_BASE            1000000            <--file: avutil.h, line: 229

如果觉得这个默认的5s阻塞时间太长,或者甚至觉得完全没有必要,即我们可以手动的设置各种解码的参数,那么可以通过下面的方法将ic->max_analyze_duration的值修改为1s:

ic = avformat_alloc_context();

ic->interrupt_callback.callback = decode_interrupt_cb;

ic->interrupt_callback.opaque = is;

//add by Nfer

ic->max_analyze_duration =1*1000*1000;

av_log(NULL, AV_LOG_ERROR, "ic->max_analyze_duration %d.\n", ic->max_analyze_duration);

err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);

注:红色部分为添加的代码

查找音视频数据流

if (!video_disable)

st_index[AVMEDIA_TYPE_VIDEO] =

av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,

wanted_stream[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);

av_find_best_stream函数主要就做了一件事:找符合条件的数据流。其简单实现可以参考ffmpeg-tutorial项目中tutorial01.c的代码:

// Find the first video stream

videoStream=-1;

for(i=0; inb_streams; i++)

if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {

videoStream=i;

break;

}

if(videoStream==-1)

return -1; // Didn't find a video stream

注:ffmpeg-tutorial项目是对Stephen Dranger写的7个ffmpeg tutorial做的一个update。

打开对应的数据流

if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {

ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);

}

通过最开始的主框架流程图,我们可以大概的看到stream_component_open函数中最主要的动作就是调用packet_queue_start和创建video_thread线程。当然在这之前还有一些处理,其中包括:

查找解码器

avctx = ic->streams[stream_index]->codec;

codec = avcodec_find_decoder(avctx->codec_id);

如果启动ffplay时通过vcodec参数指定了解码器名称,那么在通过codec_id查找到解码器后,再使用forced_codec_name查找解码avcodec_find_decoder_by_name。但是注意,如果通过解码器名称查找后会覆盖之前通过codec_id查找到解码器,即如果在参数中指定了错误的解码器会导致无法正常播放的。

设置解码参数

opts = filter_codec_opts(codec_opts, avctx->codec_id, ic, ic->streams[stream_index], codec);

if (!av_dict_get(opts, "threads", NULL, 0))

av_dict_set(&opts, "threads", "auto", 0);

if (avctx->lowres)

av_dict_set(&opts, "lowres", av_asprintf("%d", avctx->lowres), AV_DICT_DONT_STRDUP_VAL);

if (avctx->codec_type == AVMEDIA_TYPE_VIDEO || avctx->codec_type == AVMEDIA_TYPE_AUDIO)

av_dict_set(&opts, "refcounted_frames", "1", 0);

打开解码器

if (avcodec_open2(avctx, codec, &opts) < 0)

return -1;

启动packet队列

packet_queue_start(&is->videoq);

启动packet队列时,会向队列中先放置一个flush_pkt,其中详细缘由后面再讲。

创建video_thread线程

is->video_stream = stream_index;

is->video_st = ic->streams[stream_index];

is->video_tid = SDL_CreateThread(video_thread, is);

is->queue_attachments_req = 1;

注:上述分析过程中没有考虑音频和字幕处理的部分,后续有机会再详解。

循环读取数据部分

该部分是一个for (;;)循环,循环中主要包括pause和resume操作处理、seek操作处理、packet队列写入失败处理、读数据结束处理、然后是读数据并写入到对应的音视频队列中。

for循环跳出条件

有两处是break处理的:

//代码段一

if (is->abort_request)

break;                                <-- Line 2814

//代码段二

ret = av_read_frame(ic, pkt);

if (ret < 0) {

if (ic->pb && ic->pb->error)

break;                            <-- Line 2923

}

其中条件一是调用do_exit --> stream_close中将is->abort_request置为1的,代码中有多个地方是判断该条件进行exit处理的;条件二很清晰,就是当遇到读数据失败并且是IO错误时,会退出。

pause和resume操作处理

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);

}

在ffplay中暂停和恢复的按键操作时p键(SDLK_p)和space键(SDLK_SPACE),会调用toggle_pause-->

stream_toggle_pause来修改is->paused标记变量,然后在read_thread线程中通过对is->paused标记变量的判断进行pause和resum(play)的处理。

seek操作处理

if (is->seek_req) {

ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);

if (is->video_stream >= 0) {

packet_queue_flush(&is->videoq);

packet_queue_put(&is->videoq, &flush_pkt);

}

is->seek_req = 0;

}

注:上述代码有所删减,只保留了和视频相关的部分

同上面pause和resume的处理,is->seek_req是在按键操作(SDLK_PAGEUP、SDLK_PAGEDOWN、SDLK_LEFT、SDLK_RIGHT、SDLK_UP和SDLK_DOWN)时,调用stream_seek函数来修改is->seek_req标记变量,然后在read_thread线程中根据is->seek_req标记变量来进行处理。

具体处理除了调用ffmpeg的avformat_seek_file接口外,还向packet队列中放置了一个flush_pkt,这个在video_thread中的处理中会解决seek操作的花屏效果。

packet队列写入失败处理

/* if the queue are full, no need to read more */

if (infinite_buffer<1 &&

(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE

|| (   (is->audioq   .nb_packets > MIN_FRAMES || is->audio_stream < 0 || is->audioq.abort_request)

&& (is->videoq   .nb_packets > MIN_FRAMES || is->video_stream < 0 || is->videoq.abort_request

|| (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC))

&& (is->subtitleq.nb_packets > MIN_FRAMES ||
is->subtitle_stream < 0 || is->subtitleq.abort_request)))) {

/* wait 10 ms */

SDL_LockMutex(wait_mutex);

SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);

SDL_UnlockMutex(wait_mutex);

continue;

}

此处的各种判断条件不详细解释,重点是在播放器处理中,写数据失败时需要wait and continue的处理。

读数据结束处理

if (eof) {

if (is->video_stream >= 0) {

av_init_packet(pkt);

pkt->data = NULL;

pkt->size = 0;

pkt->stream_index = is->video_stream;

packet_queue_put(&is->videoq, pkt);

}

SDL_Delay(10);

if (is->audioq.size + is->videoq.size + is->subtitleq.size == 0) {

if (loop != 1 && (!loop || --loop)) {

stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);

} else if (autoexit) {

ret = AVERROR_EOF;

goto fail;

}

}

eof=0;

continue;

}

当遇到eof,即end of file时,做一下几个步骤:

  • 向packet队列中放置一个null packet,此处用于loop时使用
  • 判断是否是loop操作,如果是就seek到开始位置重新播放
  • 如果是autoexit模式,就goto fail退出

注意,在读数据eof时,读数据部分还有些滞后,即if
(is->audioq.size + is->videoq.size + is->subtitleq.size==
0)判断不一定为true,引起在判断前先delay了10ms(SDL_Delay(10););但是仍然不一定为true,因此需要continue。当然下一步av_read_frame失败也会返回AVERROR_EOF,eof会重新赋值为1。即,eof退出会wait到真正的播放完毕。

读数据并写入到对应的音视频队列

ret = av_read_frame(ic, pkt);

if (pkt->stream_index == is->video_stream && pkt_in_play_range

&& !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {

packet_queue_put(&is->videoq, pkt);

}

注:上述代码有所删减,只保留了和视频相关的部分

此处的处理实际上比较简单,就是av_read_frame和packet_queue_put,不详解。

反初始化部分

主要包括退出前的等待、关闭音视频流、关闭avformat、给主线程发送FF_QUIT_EVENT消息以及销毁SDL_mutex信号量。

退出前的等待

/* wait until the end */

while (!is->abort_request) {

SDL_Delay(100);

}

因为之前for循环跳出条件中说明了只有两种情况下才会break出来,其一就是is->abort_request为true,其二直接就goto到fail了,因此两种情况下该while循环都不会判断为true,直接略过。具体代码原因不明。

关闭音视频流

if (is->video_stream >= 0)

stream_component_close(is, is->video_stream);

注:上述代码有所删减,只保留了和视频相关的部分

其中stream_component_close关闭视频流做了以下处理:

  • 终止packet队列:packet_queue_abort(&is->videoq);
  • 发送信号给video_thread,避免继续解码阻塞:SDL_CondSignal(is->pictq_cond);
  • 等待vide_thread线程退出:SDL_WaitThread(is->video_tid, NULL);
  • 清空packet队列:packet_queue_flush(&is->videoq);

给主线程发送FF_QUIT_EVENT

if (ret != 0) {

SDL_Event event;

event.type = FF_QUIT_EVENT;

event.user.data1 = is;

SDL_PushEvent(&event);

}

在主线程会接收到FF_QUIT_EVENT消息,从而会调用do_exit函数来做退出处理。

销毁SDL_mutex信号量

SDL_DestroyMutex(wait_mutex);

read_thread基本就分析到这里,下面描述以下video_thread。

video_thread线程

从主框架流程中可以看出,video_thread线程是在read_thread-->

stream_component_open中创建的,负责从packet队列中读取packet并解码为picture,然后存储到picture队列中供主线程读取并刷新显示。

video_thread的创建是在read_thread --> stream_component_open函数中:

is->video_tid = SDL_CreateThread(video_thread, is);

read_thread线程同样分为三部分:

  • 初始化部分:主要包括AVFrame创建和AVFilterGraph创建。对应ffplay.c文件中的1881-1895行代码;
  • 循环解码部分:主要包括pause和resume操作处理、读取packet处理、AVFILTER处理、然后是将picture写入视频队列中以及每次解码后的清理动作。对应ffplay.c文件中的1897-1966行代码;
  • 反初始化部分:主要包括刷新codec中的数据、释放AVFilterGraph、释放AVPacket以及释放AVFrame。对应ffplay.c文件中的1972-1978行代码;

初始化部分

该线程的初始化就是创建了AVFrame和AVFilterGraph,其中AVFilterGraph还是和编译宏包含,如果没有打开CONFIG_AVFILTER可以直接省略。

is->video_tid = SDL_CreateThread(video_thread, is);

… …

AVFrame *frame = av_frame_alloc();

#if CONFIG_AVFILTER

AVFilterGraph *graph = avfilter_graph_alloc();

#endif

循环解码部分

主要包括pause和resume操作处理、读取packet处理、AVFILTER处理、然后是将picture写入视频队列中以及每次解码后的清理动作。

pause和resume操作处理

video_thread中的关于pause和resume的处理比较简单,就是如果是pause状态就delay(线程sleep):

while (is->paused && !is->videoq.abort_request)

SDL_Delay(10);

读取packet处理

avcodec_get_frame_defaults(frame);

av_free_packet(&pkt);

ret = get_video_frame(is, frame, &pkt, &serial);

//关于frame的一些处理

av_frame_unref(frame);

从上述代码中可以看出,一个frame(和packet)的完整生命流程。

ffmpeg-tutorial项目中tutorial01.c中的例子是使用avcodec_alloc_frame()来申请并设置default

value的操作,但是在这里就分成了两步:av_frame_alloc()然后avcodec_get_frame_defaults(frame)。

av_free_packet实际上清空上一次get_video_frame中获取的packet数据,函数本身是有异常处理的,所以连续调用两次av_free_packet是没有问题的。

get_video_frame函数中主要部分是packet_queue_get然后avcodec_decode_video2,即从packet队列中读取数据然后进行解码,具体内容有机会另开文章进行讲解。

AVFILTER处理

AVFILTER处理是一个比较模块化很高的处理部分,大致流程包括以下几步:

  1. 释放旧的AVFilterGraph并创建一个新的:avfilter_graph_free()和avfilter_graph_alloc()
  2. 配置video filters:configure_video_filters
  3. 向buffersrc中添加frame:av_buffersrc_add_frame
  4. 情况原有的frame和packet:av_frame_unref、avcodec_get_frame_defaults和av_free_packet
  5. 从buffersink中读取处理后的frame:av_buffersink_get_frame_flags

简单的理解就是:

将picture写入视频队列

如果需要avfilter处理,那么处理完后或者不需要avfilter处理,解码完成后的frame会调用queue_picture写入到picture队列中。具体细节不详解。

解码后的清理动作

使用完packet后,必须从frame中释放出来:av_frame_unref。如api说明:Unreference allthe buffers referenced by frame and reset the frame fields.

for循环跳出条件

有以下几种情况下会break出for循环:

  • get_video_frame读数据失败,并且返回<0:该函数失败条件和read_thread其实是一致的,即当q->abort_request为true时;
  • configure_video_filters配置filter失败:该函数失败的情况下,我遇到的一种就是avfilter_graph_create_filter创建crop

    filter时失败,原因在于在configureffmpeg时没有把filter配置打开,导致只有默认的几个filter,其他一些特性filter都没有添加进行;

  • av_buffersrc_add_frame添加frame失败:该函数属于api,不详解;
  • queue_picture保存picture失败:该函数的失败条件是当is->videoq.abort_request为true时;

即正常情况下,有两种退出模式:

  1. 正常播放完成后退出,此时会通过get_video_frame读数据失败退出
  2. 如果是按ESCAPE和Q键退出,会直接退出,则不会等到,直接在queue_picture函数失败

反初始化部分

反初始化部分比较简单,就是先通知avcodec进行flush数据,然后依次释放AVFilterGraph、AVPacket和AVFrame。

video_thread讲解的比较粗糙,主要原因还是由于个人了解的知识有所欠缺,后续有机会会补上。

主线程

主流程用于视频图像显示和刷新,实际上还主线程是一个事件驱动的,就是一个wait_event然后switch处理,然后继续for循环。

refresh_loop_wait_event处理

该函数会从event队列中读取出event,SDL_PumpEvents、SDL_PeepEvents。同时会调用video_refresh来进行视频刷新和显示。此处会有大量和SDL API相关的操作,由于个人能力有限暂不分析。

event的switch处理

该event的处理分为以下几类:

  • SDL_KEYDOWN键盘按键事件
  • SDL_VIDEOEXPOSE屏幕重画事件
  • SDL_MOUSEBUTTONDOWN鼠标按下事件,如果启动ffplay时有exitonmousedown参数,会相应鼠标按下事件,然后退出播放;
  • SDL_MOUSEMOTION鼠标移动事件,主要seek操作
  • SDL_VIDEORESIZE视频大小变化事件,比如视频中间会出现大小变化,会触发该事件
  • SDL_QUIT、FF_QUIT_EVENT退出事件,如read_thread中出现各种异常会发送该消息
  • FF_ALLOC_EVENT事件比较特殊,如代码中的注释“ifthe
    queue is aborted, we have to pop the pending ALLOC event or wait for
    theallocation to complete”,该消息是video_thread中的发出的消息

总结

由于时间有限,文章有些虎头蛇尾,还请各位谅解。有多个方面没有详细分析,如音频处理和字幕处理部分,音视频同步,SDL显示等等很多很多有关的知识,这些知识对于我来说大部分也还是全新的东西,后续有机会还会继续学习和各位分享。

(转)基于FFPMEG2.0版本的ffplay代码分析的更多相关文章

  1. Hadoop基于Protocol Buffer的RPC实现代码分析-Server端

    http://yanbohappy.sinaapp.com/?p=110 最新版本的Hadoop代码中已经默认了Protocol buffer(以下简称PB,http://code.google.co ...

  2. Hadoop基于Protocol Buffer的RPC实现代码分析-Server端--转载

    原文地址:http://yanbohappy.sinaapp.com/?p=110 最新版本的Hadoop代码中已经默认了Protocol buffer(以下简称PB,http://code.goog ...

  3. DotnetCore下Grpc的简单使用(基于3.0版本)

    目录: 一.简单介绍DotnetCore3.0如何将.proto文件生成对应的服务端和客户端类 二.介绍如何在服务端使用Grpc,以及Grpc需要的条件(HTTP2.TLS) 三.介绍如何创建Grpc ...

  4. vue 2.0版本----》常用代码说明

    ------------------------------------------代码不显示-------------------------------------------- [v-cloak ...

  5. android4.0 的图库Gallery2代码分析(四) 之相册的数据处理以及显示

    最近迫于生存压力,不得不给人兼职打工.故在博文中加了个求点击的链接.麻烦有时间的博友们帮我点击一下.没时间的不用勉强啊.不过请放心,我是做技术的,肯定链接没病毒,就是我打工的淘宝店铺.嘻嘻.http: ...

  6. android4.0 的图库Gallery2代码分析(三) 之Applition的初始化准备

    Applition的初始化准备 图库的一切动作都明显地起源于Application.这是区别与其他那种感觉不到Application存在,仅仅感觉到Activity存在的简单应用的一个特点. 图库的a ...

  7. android4.0 的图库Gallery2代码分析(二)

    最近迫于生存压力,不得不给人兼职打工.故在博文中加了个求点击的链接.麻烦有时间的博友们帮我点击一下.没时间的不用勉强啊.不过请放心,我是做技术的,肯定链接没病毒,就是我打工的淘宝店铺.嘻嘻.http: ...

  8. android4.0 的图库Gallery2代码分析(一)

    最近迫于生存压力,不得不给人兼职打工.故在博文中加了个求点击的链接.麻烦有时间的博友们帮我点击一下.没时间的不用勉强啊.不过请放心,我是做技术的,肯定链接没病毒,就是我打工的淘宝店铺.嘻嘻.http: ...

  9. Spark2.0机器学习系列之6:GBDT(梯度提升决策树)、GBDT与随机森林差异、参数调试及Scikit代码分析

    概念梳理 GBDT的别称 GBDT(Gradient Boost Decision Tree),梯度提升决策树.     GBDT这个算法还有一些其他的名字,比如说MART(Multiple Addi ...

随机推荐

  1. Java TreeSet,Collections使用

    一.创建TreeSet实例 public static void main(String[] args) { TreeSet set = new TreeSet(); set.add("C& ...

  2. C# Area 双重路由

    在WebApi项目里面 一般除了接口, 还有管理端...一些乱七八糟的,你想展示的东西, 一种做法是分开写: 比如管理后台一个项目, 然后接口一个, 然后页面一个, 其实这样做也可以,但是这么做, 无 ...

  3. darknet 的python接口使用

    首先,python接口文件在安装好的darknet目录下的python文件夹,打开就可以看到 这里的darknet.py文件就是python接口 用编辑器打开查看最后部分代码: 使用十分简单,先将网络 ...

  4. 为什么说基于TCP的移动端IM仍然需要心跳保活?(转)

    源:https://segmentfault.com/a/1190000006832547 为什么说基于TCP的移动端IM仍然需要心跳保活?

  5. flutter 权限申请

    添加依赖 permission_handler: ^3.2.2 androidmanifest.xml添加对应的用户权限 在flutter 中app入口申请权限: import 'package:fl ...

  6. Centos7搭建OpenNebula云平台

    OpenNebula概述 OpenNebula是专门为云计算打造的开源系统,用户可以使用Xen.KVM.VMware等虚拟化软件一起打造企业云.利用OpenNebula可以轻松构建私有云.混合云.公开 ...

  7. 004-行为型-11-解析器模式(Interpreter)

    一.概述 提供了评估语言的语法或表达式的方式.这种模式实现了一个表达式接口,该接口解释一个特定的上下文.这种模式被用在 SQL 解析.符号处理引擎等. 意图:给定一个语言,定义它的文法表示,并定义一个 ...

  8. Qt编写自定义控件60-声音波形图

    一.前言 这个控件源自于一个音乐播放器,在写该音乐播放器的时候,需要将音频的数据转换成对应的频谱显示,采用的fmod第三方库来处理(fmod声音系统是为游戏开发者准备的革命性音频引擎,非常强大和牛逼) ...

  9. Linux的桌面虚拟化技术KVM(五)——virsh常用命令

    Linux的桌面虚拟化技术KVM(一)——新建KVM虚拟机 Linux的桌面虚拟化技术KVM(二)——远程桌面管理 Linux的桌面虚拟化技术KVM(三)——KVM虚拟机克隆和快照 Linux的桌面虚 ...

  10. python通过socket实现多个连接并实现ssh功能详解

    python通过socket实现多个连接并实现ssh功能详解 一.前言 上一篇中我们已经知道了客户端通过socket来连接服务端,进行了一次数据传输,那如何实现客户端多次发生数据?而服务端接受多个客户 ...