利用ffmpeg进行视频软解播放
前段时间,公司的一个项目需要一个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=,
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的定义如下:
struct hvplayer
{
playend_cb end;
int32_t hwnd;
int32_t flage;
char *url;
enum player_state ste;
HANDLE thread;
clock_t pretm;
int play;
};
flage是控制重连循环的,play是控制帧数据读取循环的,pretm是控制rtsp服务器连接超时的.
play_loop是播放线程的函数,外层是重连循环,_do是实际播放循环.
static int play_loop(void* p)
{
hvplayer *h=(hvplayer*)p;
while(h->flage)
{
h->ste=PLAYER_CONNECTING;
h->pretm=clock();
_do(h);
int s=TIMEOUT_S-(clock()-h->pretm)/CLOCKS_PER_SEC;
if(s>) Sleep(s*);
}
if(h->end) h->end(h);
free(h->url);
free(h);
return ;
}
static void _do(hvplayer*h)
{
AVCodec *codec=NULL;
AVCodecContext *cc=NULL;
AVPacket pk={};
AVFormatContext *afc=NULL;
int vindex=-;
afc=avformat_alloc_context();
if(afc==){
goto err;
}
afc->interrupt_callback.callback=timeoutcheck;
afc->interrupt_callback.opaque=h;
AVDictionary *dir=NULL;
char *k1="stimeout";
char *v1="";
char *k2="rtsp_transport";
char *v2="tcp";
char *k4="max_delay";
char *v4="";
int r=av_dict_set(&dir,k1,v1,);
r=av_dict_set(&dir,k2,v2,);
av_dict_set(&dir,k4,v4,);
if(avformat_open_input(&afc,h->url,NULL,&dir)) {
goto err;
}
if(avformat_find_stream_info(afc,NULL)<) {
goto err;
}
for(int i=;i<(int)afc->nb_streams;i++)
{
if(afc->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
vindex=i;
break;
}
}
if(vindex==-){
goto err;
}
cc=afc->streams[vindex]->codec;
codec=avcodec_find_decoder(cc->codec_id); if(codec==) goto end;
if(avcodec_open2(cc,codec,NULL)<) {
goto err;
}
int dr;
hvframe frame;
render *render=render_create(RENDER_TYPE_D3D,h->hwnd,cc->coded_width,cc->coded_height);
if(render==NULL)
{
render=render_create(RENDER_TYPE_GDI,h->hwnd,cc->coded_width,cc->coded_height);
if(render==NULL){
goto end;
}
}
AVFrame *yuv_buf=av_frame_alloc();
if(yuv_buf==) {
goto end;
}
h->ste=PLAYER_PLAYING;
DWORD a,b;
while (h->flage&&av_read_frame(afc,&pk)>=)
{ if(pk.stream_index==vindex)
{
avcodec_decode_video2(cc,yuv_buf,&dr,&pk);
if(dr>)
{
frame.h=cc->coded_height;
frame.y=yuv_buf->data[];
frame.u=yuv_buf->data[];
frame.v=yuv_buf->data[];
frame.ypitch=yuv_buf->linesize[];
frame.uvpitch=yuv_buf->linesize[];
render->draw(render,&frame);
}
}
av_free_packet(&pk);
}
end:
if(yuv_buf) av_frame_free(&yuv_buf);
if(render) render->destory(&render);
err:
if(cc) avcodec_close(cc);
if(afc){
avformat_close_input(&afc);
avformat_free_context(afc);
}
}
- 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秒超时判定):
#define TIMEOUT_S 6 int timeoutcheck(void *p)
{
hvplayer *h=(hvplayer*)p;
if(h->flage==) return ;
if(h->ste==PLAYER_CONNECTING)
{
clock_t ctm=clock();
int s=(ctm-h->pretm)/CLOCKS_PER_SEC;
if(s>=TIMEOUT_S)
return ;
}
return ;
}
3、avformat_find_stream_info()解析流的格式信息,再用以下代码获取AVCodecContext:
for(int i=;i<(int)afc->nb_streams;i++)
{
if(afc->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
vindex=i;
break;
}
}
if(vindex==-){
goto err;
}
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的结构如下:
struct render{
int hwnd;
void (*draw)(struct render *self,hvframe *frame);
void (*destory)(struct render **self);
};
typedef struct render render;
d3d实现如下:
#include <stdlib.h>
#include "hvtype.h"
#include "irender.h"
#include "winapi.h"
struct render_d3d
{
render base;
IDirect3D9 *d3d;
IDirect3DDevice9 *d3d_dev;
IDirect3DSurface9 *surface;
RECT rec;
}; void d3d_draw(render *h,hvframe *frame);
void render_free(render **h); render *d3d_new(int hwnd,int pic_w,int pic_h)
{
struct render_d3d *result=(struct render_d3d *)malloc(sizeof(*result));
result->base.hwnd=hwnd;
result->d3d=Direct3DCreate9(D3D_SDK_VERSION);
if(result->d3d==NULL) return NULL;
D3DPRESENT_PARAMETERS d3dpp;
memset(&d3dpp,,sizeof(d3dpp));
d3dpp.Windowed=TRUE;
d3dpp.SwapEffect=D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferFormat=D3DFMT_UNKNOWN;
GetClientRect((HWND)result->base.hwnd,&result->rec);
HRESULT re=IDirect3D9_CreateDevice(result->d3d,D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,(HWND)result->base.hwnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,&d3dpp,&result->d3d_dev);
if(FAILED(re)) return NULL;
re=IDirect3DDevice9_CreateOffscreenPlainSurface(result->d3d_dev,pic_w,pic_h,
(D3DFORMAT)MAKEFOURCC('Y','V','',''),
D3DPOOL_DEFAULT,&result->surface,NULL);
if(FAILED(re)) return NULL;
result->base.draw=d3d_draw;
result->base.destory=render_free;
return (render *)result;
} void d3d_draw(render *h,hvframe *frame)
{
struct render_d3d *self=(struct render_d3d*)h;
D3DLOCKED_RECT texture;
HRESULT re=IDirect3DSurface9_LockRect(self->surface,&texture,NULL,D3DLOCK_DONOTWAIT);
if(FAILED(re)) return;
int uvstep=texture.Pitch/;
char *dest=(char*)texture.pBits;
char *vdest=(char*)texture.pBits+frame->h*texture.Pitch;
char *udest=(char*)vdest+frame->h/*uvstep;
int uvn=;
for(int i=;i<frame->h;i++)
{
memcpy(dest,frame->y,frame->ypitch);
frame->y+=frame->ypitch;
dest+=texture.Pitch;
}
for(int i=;i<frame->h/;i++)
{
memcpy(vdest,frame->v,frame->uvpitch);
memcpy(udest,frame->u,frame->uvpitch);
vdest+=uvstep;
udest+=uvstep;
frame->v+=frame->uvpitch;
frame->u+=frame->uvpitch;
}
IDirect3DSurface9_UnlockRect(self->surface);
IDirect3DDevice9_BeginScene(self->d3d_dev);
IDirect3DSurface9 *back_surface;
IDirect3DDevice9_GetBackBuffer(self->d3d_dev,,,D3DBACKBUFFER_TYPE_MONO,&back_surface);
IDirect3DDevice9_StretchRect(self->d3d_dev,self->surface,NULL,back_surface,&self->rec,D3DTEXF_LINEAR);
IDirect3DDevice9_EndScene(self->d3d_dev);
IDirect3DDevice9_Present(self->d3d_dev,NULL,NULL,NULL,NULL);
}
void render_free(render **h)
{
if((*h)==)return;
struct render_d3d *self=(struct render_d3d*)(*h);
IDirect3DDevice9_Clear(self->d3d_dev,, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(,,), 1.0f, );
IDirect3DDevice9_Present(self->d3d_dev,NULL,NULL,NULL,NULL);
IDirect3DSurface9_Release(self->surface);
IDirect3DDevice9_Release(self->d3d_dev);
IDirect3D9_Release(self->d3d);
free(*h);
*h=;
}
都是标准的固定管线步骤,就不赘述了,唯一要注意的就是yuv数据填充到surface中时要注意行对齐.
gdi实现如下:
#include <stdlib.h>
#include <windows.h>
#include "hvpfconvert.h"
#include "irender.h"
#include "winapi.h" struct render_gdi
{
render base;
BITMAPINFO bmp;
HDC dc;
hvpfconvert *convert;
uint8_t *buf;
RECT rec;
}; void gdi_free(render **h)
{
if((*h)==)return;
struct render_gdi *self=(struct render_gdi*)(*h);
FillRect(self->dc,&self->rec,(HBRUSH)(pGetStockObject(BLACK_BRUSH)));
ReleaseDC((HWND)self->base.hwnd,self->dc);
hvpfconvert_free(&self->convert);
free(*h);
*h=;
} void gdi_draw(render *h,hvframe *frame)
{
struct render_gdi *self=(struct render_gdi*)h;
hvpfconvert_convert2(self->convert, frame, self->buf);
pSetStretchBltMode(self->dc, STRETCH_HALFTONE);
pStretchDIBits(self->dc, , , self->rec.right, self->rec.bottom, , ,
self->bmp.bmiHeader.biWidth, frame->h, self->buf, &self->bmp, DIB_RGB_COLORS, SRCCOPY);
} render *gdi_new(int32_t hwnd,int pic_w,int pic_h)
{
struct render_gdi *re = (struct render_gdi *)calloc(,sizeof(*re));
re->convert = hvpfconvert_new(pic_w, pic_h, pic_w, pic_h, PF_YUV420P, PF_BGR24);
re->dc = GetDC((HWND)hwnd);
re->base.hwnd = hwnd;
GetClientRect((HWND)re->base.hwnd, &re->rec);
re->buf = (uint8_t*)malloc(hvpfconvert_get_size(pic_w, pic_h, PF_RGB24));
re->bmp.bmiHeader.biBitCount = ;
re->bmp.bmiHeader.biClrImportant = BI_RGB;
re->bmp.bmiHeader.biClrUsed = ;
re->bmp.bmiHeader.biCompression = ;
re->bmp.bmiHeader.biHeight = -pic_h;
re->bmp.bmiHeader.biWidth = pic_w;
re->bmp.bmiHeader.biPlanes = ;
re->bmp.bmiHeader.biSize = sizeof(re->bmp.bmiHeader);
re->bmp.bmiHeader.biSizeImage = pic_w*pic_h * ;
re->bmp.bmiHeader.biXPelsPerMeter = ;
re->bmp.bmiHeader.biYPelsPerMeter = ;
re->base.destory=gdi_free;
re->base.draw=gdi_draw;
return (render *)re;
}
gdi是利用StretchDIBits函数显示位图,因此要将yuv转换为rgb数据,图像格式转换,ffmpeg也自带了高效的转换函数:sws_scale().
最后是sdk的初始化,hvdevicevideo_init()我调用了ffmpeg库和d3d所需的初始化函数:
int hvdevicevideo_init(void){
if(hv_winapi_init()) return -;
avcodec_register_all();
av_register_all();
avformat_network_init();
sdk_init=;
CoInitializeEx(NULL,COINIT_MULTITHREADED);
return ;
}
最后整个库,性能还不错,不必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硬解码播放吧.
利用ffmpeg进行视频软解播放的更多相关文章
- 利用FFmpeg生成视频缩略图 2.1.6
利用FFmpeg生成视频缩略图 1.下载FFmpeg文件包,解压包里的\bin\下的文件解压到 D:\ffmpeg\ 目录下. 下载地址 http://ffmpeg.zeranoe.com/build ...
- 利用FFmpeg生成视频的缩略视频 v8.3
目前生成视频缩略图的工具大多数是生成静态的图片,为了解决这样的局限性,这 次春节期间搞了个利用 FFMpeg 能生成缩略动态视频的批处理. 把 Make_NxM_videos.bat LED_font ...
- C# 利用ffmpeg 对视频转换系类操作 (1) 基本分析
最近公司做一个项目,开发一个视频站点.项目需求中有很多视频转换的需求,如:格式转换(flv,Mp4),视频水印,视频截图,视频合成,获取视频的基本信息(时间戳,视频大小等).经过网络的收集资料以及自己 ...
- 基于C#利用ffmpeg提取视频帧
利用ffmepg提取视频帧实际上是利用C#调用ffmepg命令行进行处理对应的视频,然后输出出视频帧 GetPicFromVideo("); static public string Get ...
- 利用FFmpeg生成视频缩略图 2.3.1
1.下载FFmpeg文件包,解压包里的\bin\下的文件解压到 D:\ffmpeg\ 目录下. 下载地址 http://ffmpeg.zeranoe.com/builds/win32/static/ ...
- 利用FFmpeg生成视频缩略图 2.1.8
1.下载FFmpeg文件包,解压包里的\bin\下的文件解压到 D:\ffmpeg\ 目录下. 下载地址 http://ffmpeg.zeranoe.com/builds/win32/static/ ...
- 利用FFmpeg切割视频
关键词:FFmpeg,seek,ss,t,to,搜索,定位 介绍 如果你想要从输入文件中切割一部分,需要用到ss选项. 快速定位 需要将ss放在输入文件的前面(即-i的前面) elesos1.jpg ...
- 史林枫:C#.NET利用ffmpeg操作视频实战(格式转换,加水印 一步到位)
ffmpeg.exe是大名鼎鼎的视频处理软件,以命令行参数形式运行.网上也有很多关于ffmpeg的资料介绍.但是在用C#做实际开发时,却遇到了几个问题及注意事项,比如如何无损处理视频?如何在转换格式的 ...
- 利用ffmpeg获取视频帧
如果要对视频帧进行处理,可以先把视频帧读取出来. sh文件代码如下: #!/usr/bin/env sh VIDEO=/home/xxx/video/ FRAMES=/home/xxx/frame/ ...
随机推荐
- Java中使用File类删除文件夹和文件
删除工具类: import java.io.File; public class DeleteAll{ public static void deleteAll(File file){ if(file ...
- css3两个汤圆亲吻动效
效果图: 模板来源:https://www.17sucai.com/pins/demo-show?id=35132 自己仿写出来的效果图: 笔记: 1.transform:translate(-50% ...
- 使用Hbuilder 报错The keyword 'export' is reserved
右击文件 > 验证本文档语法(V)后报错 解决: 项目右键->”属性”->”语法&框架”界面中配置项目的javaScript版本,将ECMAScript5.1 修改为6.
- myeclipse 报错:Set project compiler compliance settings to '1.5'
myeclipse 报错:Set project compiler compliance settings to '1.5' 解决方法:项目右击-->properties-->java c ...
- zookeeper常用配置详解
#ZK中的一个时间单元.ZK中所有时间都是以这个时间单元为基础,进行整数倍配置的.例如,session的最小超时时间是2*tickTime tickTime=2000 #Follower在启动过程中, ...
- 【Luogu】【关卡2-7】深度优先搜索(2017年10月)【AK】【题解没写完】
任务说明:搜索可以穷举各种情况.很多题目都可以用搜索完成.就算不能,搜索也是骗分神器. P1219 八皇后 直接dfs.对角线怎么判断:同一条对角线的横纵坐标的和或者差相同. #include < ...
- Python改变当前工作目录
import os print(os.getcwd()) # 打印当前工作目录 os.chdir('/Users/<username>/Desktop/') # 将当前工作目录改变为`/U ...
- Android中的Toast重复显示的问题
Toast是Android中用来显示信息的一种机制,和Dialog不一样的是,Toast是没有焦点,而且Toast显示的时间有限,过一定的时间就会自动消失. Toast一般用来提示用户的误操作.但是如 ...
- Delphi 判断某个系统服务是否存在及相关状态
记得use WinSvc; //------------------------------------- // 获取某个系统服务的当前状态 // // return status code if s ...
- Tomcat运行错误示例二
Tomcat运行错误示例二 当遇到这种错误时,一般是构建路径的问题,按步骤来就好.如图: 点击---->库---->Add Library---->下一步---->选择tomc ...