前段时间,公司的一个项目需要一个rtsp的播放库,原本打算直接用vlc播放的,但我觉得vlc太庞大了,很多功能没必要,还不如用ffmpeg+d3d简单的实现一个库,因此就有了今天讲的这个东西。一个解码库,分为三个部分:网络,解码,显示。网络和解码在ffmpeg里带了,直接用就好,显示,用d3d直接显示yuv是最佳方案了。整个库采用多线程模型,播放一路就创建一个播放线程。库的接口如下:
struct hvplayer; typedef struct hvplayer hvplayer; /** *播放退出回调 *@param h 播放器指针 *@param state 退出状态 */ typedef void (*playend_cb)(hvplayer *h); enum player_state { PLAYER_CONNECTING=0, PLAYER_PLAYING, PLAYER_OFF }; /** *初始化一个播放器 *@param hwnd 一个表示画面显示区域的id,windows上为窗口句柄 *@param url rtsp地址 *@return NULL失败,>0为一个播放器指针 */ HVEXP hvplayer *hvplayer_new(int32_t hwnd, const char *url); /** *播放一个rtsp视频,该方法是一个非阻塞函数,内部会创建一个线程去执行播放 *任务,自动无限重连,直到调用hvplayer_close * *@param h 一个指向播放器的指针 *@return 成功为0,失败-1 */ HVEXP int hvplayer_play(hvplayer *h); /** *获取播放器的状态,成功会设置state * *@param h 一个指向播放器的指针 *@return player_state枚举的值 */ HVEXP int hvplayer_getstate(hvplayer *h); /** *停止播放,并结束播放线程,该方法会阻塞至播放线程结束,同时释放hvplayer句柄 * *@param h 一个指向播放器的指针 */ HVEXP void hvplayer_close(hvplayer *h); /** *注册播放结束回调,hvplayer_close后回调会被调用 * *@param h 一个指向播放器的指针 *@param cb 具体见回调定义 *@waring 多次注册会覆盖 *@waring 不要阻塞该调用线程 */ HVEXP void hvplayer_set_endcb(hvplayer *h, playend_cb cb); /************************************************************************/
/*SDK初始化,必须在使用SDK之前初始化 *@return 0成功 /************************************************************************/ HVEXP int hvdevicevideo_init(void);
使用起来很简单,一个hvplayer对象对应一路视频,先hvplayer_new(),在hvplayer_play(),最后hvplayer_close()就好了。库使用前要调用hvdevicevideo_init进行初始化.
hvplayer的定义如下:
1 struct hvplayer 2 { 3 playend_cb end; 4 int32_t hwnd; 5 int32_t flage; 6 char *url; 7 enum player_state ste; 8 HANDLE thread; 9 clock_t pretm; 10 int play; 11 };
flage是控制重连循环的,play是控制帧数据读取循环的,pretm是控制rtsp服务器连接超时的.
play_loop是播放线程的函数,外层是重连循环,_do是实际播放循环.
1 static int play_loop(void* p) 2 { 3 hvplayer *h=(hvplayer*)p; 4 while(h->flage) 5 { 6 h->ste=PLAYER_CONNECTING; 7 h->pretm=clock(); 8 _do(h); 9 int s=TIMEOUT_S-(clock()-h->pretm)/CLOCKS_PER_SEC; 10 if(s>0) Sleep(s*1000); 11 } 12 if(h->end) h->end(h); 13 free(h->url); 14 free(h); 15 return 1; 16 }
1 static void _do(hvplayer*h) 2 { 3 AVCodec *codec=NULL; 4 AVCodecContext *cc=NULL; 5 AVPacket pk={0}; 6 AVFormatContext *afc=NULL; 7 int vindex=-1; 8 afc=avformat_alloc_context(); 9 if(afc==0){ 10 goto err; 11 } 12 afc->interrupt_callback.callback=timeoutcheck; 13 afc->interrupt_callback.opaque=h; 14 AVDictionary *dir=NULL; 15 char *k1="stimeout"; 16 char *v1="10"; 17 char *k2="rtsp_transport"; 18 char *v2="tcp"; 19 char *k4="max_delay"; 20 char *v4="50000"; 21 int r=av_dict_set(&dir,k1,v1,0); 22 r=av_dict_set(&dir,k2,v2,0); 23 av_dict_set(&dir,k4,v4,0); 24 if(avformat_open_input(&afc,h->url,NULL,&dir)) { 25 goto err; 26 } 27 if(avformat_find_stream_info(afc,NULL)<0) { 28 goto err; 29 } 30 for(int i=0;i<(int)afc->nb_streams;i++) 31 { 32 if(afc->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) 33 { 34 vindex=i; 35 break; 36 } 37 } 38 if(vindex==-1){ 39 goto err; 40 } 41 cc=afc->streams[vindex]->codec; 42 codec=avcodec_find_decoder(cc->codec_id); 43
44 if(codec==0) goto end; 45 if(avcodec_open2(cc,codec,NULL)<0) { 46 goto err; 47 } 48 int dr; 49 hvframe frame; 50 render *render=render_create(RENDER_TYPE_D3D,h->hwnd,cc->coded_width,cc->coded_height); 51 if(render==NULL) 52 { 53 render=render_create(RENDER_TYPE_GDI,h->hwnd,cc->coded_width,cc->coded_height); 54 if(render==NULL){ 55 goto end; 56 } 57 } 58 AVFrame *yuv_buf=av_frame_alloc(); 59 if(yuv_buf==0) { 60 goto end; 61 } 62 h->ste=PLAYER_PLAYING; 63 DWORD a,b; 64 while (h->flage&&av_read_frame(afc,&pk)>=0) 65 { 66
67 if(pk.stream_index==vindex) 68 { 69 avcodec_decode_video2(cc,yuv_buf,&dr,&pk); 70 if(dr>0) 71 { 72 frame.h=cc->coded_height; 73 frame.y=yuv_buf->data[0]; 74 frame.u=yuv_buf->data[1]; 75 frame.v=yuv_buf->data[2]; 76 frame.ypitch=yuv_buf->linesize[0]; 77 frame.uvpitch=yuv_buf->linesize[1]; 78 render->draw(render,&frame); 79 } 80 } 81 av_free_packet(&pk); 82 } 83 end: 84 if(yuv_buf) av_frame_free(&yuv_buf); 85 if(render) render->destory(&render); 86 err: 87 if(cc) avcodec_close(cc); 88 if(afc){ 89 avformat_close_input(&afc); 90 avformat_free_context(afc); 91 } 92 }
- avformat_alloc_context()分配AVFormatContext对象。
- avformat_open_input()打开一个流媒体源,可以是文件,rtsp,这是一个阻塞函数,知道解析成功或失败才返回.那如何设置超时时间呢?这个对象提供了两个中断回调,解析时ffmpeg会以一定频率调用这个回调,这个回调的返回值影响avformat_open_input()是否立马返回.这个回调函数指针就是AVFormatContext->interrupt_callback.callback,同时可以用->interrupt_callback.opaque绑定一个用户数据.回调函数返回1时,avformat_open_input()会失败返回,返回0则正常运行.
我是这样处理的(为了6秒超时判定):
1 #define TIMEOUT_S 6
2
3 int timeoutcheck(void *p) 4 { 5 hvplayer *h=(hvplayer*)p; 6 if(h->flage==0) return 1; 7 if(h->ste==PLAYER_CONNECTING) 8 { 9 clock_t ctm=clock(); 10 int s=(ctm-h->pretm)/CLOCKS_PER_SEC; 11 if(s>=TIMEOUT_S) 12 return 1; 13 } 14 return 0; 15 }
3、avformat_find_stream_info()解析流的格式信息,再用以下代码获取AVCodecContext:
1 for(int i=0;i<(int)afc->nb_streams;i++) 2 { 3 if(afc->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) 4 { 5 vindex=i; 6 break; 7 } 8 } 9 if(vindex==-1){ 10 goto err; 11 } 12 cc=afc->streams[vindex]->codec;
4、avcodec_find_decoder(),利用AVCodecContext->codec_id创建AVCodec,再用avcodec_open2()打开解码器.
5、循环调用av_read_frame()获取一帧编码数据,再调用avcodec_decode_video2(AVCodec*,AVFrame*,int*,AVPacket*)解码为yuv,第三个参数标示是否解码成功(>0成功),最后调用显示模块显示即可.
显示上,我将其封装为render对象,分别实现了d3d,gdi.render的结构如下:
1 struct render{ 2 int hwnd; 3 void (*draw)(struct render *self,hvframe *frame); 4 void (*destory)(struct render **self); 5 }; 6 typedef struct render render;
d3d实现如下:
1 #include <stdlib.h>
2 #include "hvtype.h"
3 #include "irender.h"
4 #include "winapi.h"
5 struct render_d3d 6 { 7 render base; 8 IDirect3D9 *d3d; 9 IDirect3DDevice9 *d3d_dev; 10 IDirect3DSurface9 *surface; 11 RECT rec; 12 }; 13
14 void d3d_draw(render *h,hvframe *frame); 15 void render_free(render **h); 16
17 render *d3d_new(int hwnd,int pic_w,int pic_h) 18 { 19 struct render_d3d *result=(struct render_d3d *)malloc(sizeof(*result)); 20 result->base.hwnd=hwnd; 21 result->d3d=Direct3DCreate9(D3D_SDK_VERSION); 22 if(result->d3d==NULL) return NULL; 23 D3DPRESENT_PARAMETERS d3dpp; 24 memset(&d3dpp,0,sizeof(d3dpp)); 25 d3dpp.Windowed=TRUE; 26 d3dpp.SwapEffect=D3DSWAPEFFECT_DISCARD; 27 d3dpp.BackBufferFormat=D3DFMT_UNKNOWN; 28 GetClientRect((HWND)result->base.hwnd,&result->rec); 29 HRESULT re=IDirect3D9_CreateDevice(result->d3d,D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,(HWND)result->base.hwnd, 30 D3DCREATE_SOFTWARE_VERTEXPROCESSING,&d3dpp,&result->d3d_dev); 31 if(FAILED(re)) return NULL; 32 re=IDirect3DDevice9_CreateOffscreenPlainSurface(result->d3d_dev,pic_w,pic_h, 33 (D3DFORMAT)MAKEFOURCC('Y','V','1','2'), 34 D3DPOOL_DEFAULT,&result->surface,NULL); 35 if(FAILED(re)) return NULL; 36 result->base.draw=d3d_draw; 37 result->base.destory=render_free; 38 return (render *)result; 39 } 40
41 void d3d_draw(render *h,hvframe *frame) 42 { 43 struct render_d3d *self=(struct render_d3d*)h; 44 D3DLOCKED_RECT texture; 45 HRESULT re=IDirect3DSurface9_LockRect(self->surface,&texture,NULL,D3DLOCK_DONOTWAIT); 46 if(FAILED(re)) return; 47 int uvstep=texture.Pitch/2; 48 char *dest=(char*)texture.pBits; 49 char *vdest=(char*)texture.pBits+frame->h*texture.Pitch; 50 char *udest=(char*)vdest+frame->h/2*uvstep; 51 int uvn=0; 52 for(int i=0;i<frame->h;i++) 53 { 54 memcpy(dest,frame->y,frame->ypitch); 55 frame->y+=frame->ypitch; 56 dest+=texture.Pitch; 57 } 58 for(int i=0;i<frame->h/2;i++) 59 { 60 memcpy(vdest,frame->v,frame->uvpitch); 61 memcpy(udest,frame->u,frame->uvpitch); 62 vdest+=uvstep; 63 udest+=uvstep; 64 frame->v+=frame->uvpitch; 65 frame->u+=frame->uvpitch; 66 } 67 IDirect3DSurface9_UnlockRect(self->surface); 68 IDirect3DDevice9_BeginScene(self->d3d_dev); 69 IDirect3DSurface9 *back_surface; 70 IDirect3DDevice9_GetBackBuffer(self->d3d_dev,0,0,D3DBACKBUFFER_TYPE_MONO,&back_surface); 71 IDirect3DDevice9_StretchRect(self->d3d_dev,self->surface,NULL,back_surface,&self->rec,D3DTEXF_LINEAR); 72 IDirect3DDevice9_EndScene(self->d3d_dev); 73 IDirect3DDevice9_Present(self->d3d_dev,NULL,NULL,NULL,NULL); 74 } 75 void render_free(render **h) 76 { 77 if((*h)==0)return; 78 struct render_d3d *self=(struct render_d3d*)(*h); 79 IDirect3DDevice9_Clear(self->d3d_dev,0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0 ); 80 IDirect3DDevice9_Present(self->d3d_dev,NULL,NULL,NULL,NULL); 81 IDirect3DSurface9_Release(self->surface); 82 IDirect3DDevice9_Release(self->d3d_dev); 83 IDirect3D9_Release(self->d3d); 84 free(*h); 85 *h=0; 86 }
都是标准的固定管线步骤,就不赘述了,唯一要注意的就是yuv数据填充到surface中时要注意行对齐.
gdi实现如下:
1 #include <stdlib.h>
2 #include <windows.h>
3 #include "hvpfconvert.h"
4 #include "irender.h"
5 #include "winapi.h"
6
7 struct render_gdi 8 { 9 render base; 10 BITMAPINFO bmp; 11 HDC dc; 12 hvpfconvert *convert; 13 uint8_t *buf; 14 RECT rec; 15 }; 16
17
18 void gdi_free(render **h) 19 { 20 if((*h)==0)return; 21 struct render_gdi *self=(struct render_gdi*)(*h); 22 FillRect(self->dc,&self->rec,(HBRUSH)(pGetStockObject(BLACK_BRUSH))); 23 ReleaseDC((HWND)self->base.hwnd,self->dc); 24 hvpfconvert_free(&self->convert); 25 free(*h); 26 *h=0; 27 } 28
29 void gdi_draw(render *h,hvframe *frame) 30 { 31 struct render_gdi *self=(struct render_gdi*)h; 32 hvpfconvert_convert2(self->convert, frame, self->buf); 33 pSetStretchBltMode(self->dc, STRETCH_HALFTONE); 34 pStretchDIBits(self->dc, 0, 0, self->rec.right, self->rec.bottom, 0, 0, 35 self->bmp.bmiHeader.biWidth, frame->h, self->buf, &self->bmp, DIB_RGB_COLORS, SRCCOPY); 36 } 37
38 render *gdi_new(int32_t hwnd,int pic_w,int pic_h) 39 { 40 struct render_gdi *re = (struct render_gdi *)calloc(1,sizeof(*re)); 41 re->convert = hvpfconvert_new(pic_w, pic_h, pic_w, pic_h, PF_YUV420P, PF_BGR24); 42 re->dc = GetDC((HWND)hwnd); 43 re->base.hwnd = hwnd; 44 GetClientRect((HWND)re->base.hwnd, &re->rec); 45 re->buf = (uint8_t*)malloc(hvpfconvert_get_size(pic_w, pic_h, PF_RGB24)); 46 re->bmp.bmiHeader.biBitCount = 24; 47 re->bmp.bmiHeader.biClrImportant = BI_RGB; 48 re->bmp.bmiHeader.biClrUsed = 0; 49 re->bmp.bmiHeader.biCompression = 0; 50 re->bmp.bmiHeader.biHeight = -pic_h; 51 re->bmp.bmiHeader.biWidth = pic_w; 52 re->bmp.bmiHeader.biPlanes = 1; 53 re->bmp.bmiHeader.biSize = sizeof(re->bmp.bmiHeader); 54 re->bmp.bmiHeader.biSizeImage = pic_w*pic_h * 3; 55 re->bmp.bmiHeader.biXPelsPerMeter = 0; 56 re->bmp.bmiHeader.biYPelsPerMeter = 0; 57 re->base.destory=gdi_free; 58 re->base.draw=gdi_draw; 59 return (render *)re; 60 }
gdi是利用StretchDIBits函数显示位图,因此要将yuv转换为rgb数据,图像格式转换,ffmpeg也自带了高效的转换函数:sws_scale().
最后是sdk的初始化,hvdevicevideo_init()我调用了ffmpeg库和d3d所需的初始化函数:
1 int hvdevicevideo_init(void){ 2 if(hv_winapi_init()) return -1; 3 avcodec_register_all(); 4 av_register_all(); 5 avformat_network_init(); 6 sdk_init=1; 7 CoInitializeEx(NULL,COINIT_MULTITHREADED); 8 return 0; 9 }
最后整个库,性能还不错,不必vlc差。在支持d3d的i5机器上解码一路1080p视频占3%-5%左右的cpu.不支持的将启用gdi显示,每路占13%的cpu.可优化点是rtsp,ffmpeg的rtsp效率
一般般,基本有400毫秒左右的延迟,而liv555可以做到200的延迟,不过我的需求400延迟可以接受,因此就没去折腾了。后来公司有一嵌入式的项目,我又做了一版linux的实现,用了英特尔的vaapi驱动进行硬解码(因为项目运行的机器是一款atom的cpu,主频只有1.2g,软解一路要占40%多的cpu),后续再写基于ffmpeg的vaapi硬解码播放吧.