很多人想做基于live555的webcam实时视频传输.在linux下操作webcam有V4L架构.在windows下有两种选择:vfw和directshow.但directshow的架构很独立,你如果想用它,就得做一个能打包rtp发送的render filter,想利用live555架构不是不可以,但是麻烦.所以vfw是最好的选择.
本文分析ffmpeg的vfwcap的实现,同时也帮助了解ffmpeg的架构和inputdevice的写法.
要实现一个input device,首先要实作一个结构:
AVInputFormat ff_vfwcap_demuxer = {
.name = "vfwcap",
.long_name = NULL_IF_CONFIG_SMALL("VfW video capture"),
.priv_data_size = sizeof(struct vfw_ctx),
.read_header = vfw_read_header,
.read_packet = vfw_read_packet,
.read_close = vfw_read_close,
.flags = AVFMT_NOFILE,
.priv_class = &vfw_class,
};
可以看到,input device实际上是一个Input format.这里实现了read header和,read packet和read close函数,分别在input device初始化,读和关闭时被调用.
那么先看一下read header函数:
本文分析ffmpeg的vfwcap的实现,同时也帮助了解ffmpeg的架构和inputdevice的写法.
要实现一个input device,首先要实作一个结构:
AVInputFormat ff_vfwcap_demuxer = {
.name = "vfwcap",
.long_name = NULL_IF_CONFIG_SMALL("VfW video capture"),
.priv_data_size = sizeof(struct vfw_ctx),
.read_header = vfw_read_header,
.read_packet = vfw_read_packet,
.read_close = vfw_read_close,
.flags = AVFMT_NOFILE,
.priv_class = &vfw_class,
};
可以看到,input device实际上是一个Input format.这里实现了read header和,read packet和read close函数,分别在input device初始化,读和关闭时被调用.
那么先看一下read header函数:
看注释吧,很全哦。
static int vfw_read_header(AVFormatContext *s, AVFormatParameters *ap)
{
//vfw_ctx是使用VFW API时需要的一些对象和参数,是专用数据,保存在priv_data中
struct vfw_ctx *ctx = s->priv_data;
AVCodecContext *codec;
AVStream *st;
int devnum;
int bisize;
BITMAPINFO *bi = NULL;
CAPTUREPARMS cparms;
DWORD biCompression;
WORD biBitCount;
int ret;
AVRational framerate_q;
if (!strcmp(s->filename, "list")) {
for (devnum = 0; devnum <= 9; devnum++) {
char driver_name[256];
char driver_ver[256];
//获取VFW驱动信息
ret = capGetDriverDescription(devnum, driver_name,
sizeof(driver_name), driver_ver, sizeof(driver_ver));
if (ret) {
av_log(s, AV_LOG_INFO, "Driver %d\n", devnum);
av_log(s, AV_LOG_INFO, " %s\n", driver_name);
av_log(s, AV_LOG_INFO, " %s\n", driver_ver);
}
}
return AVERROR(EIO);
}
//调用Win API函数,创建与VFW Capture设备相关的窗口
//HWND_MESSAGE是自定义窗口消息,用于响应VFW设备发来的消息。
ctx->hwnd = capCreateCaptureWindow(NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, 0);
if (!ctx->hwnd) {
av_log(s, AV_LOG_ERROR, "Could not create capture window.\n");
return AVERROR(EIO);
}
//s->filename是从命令行接收的,代表vfw设备的序号,默认是0.
/* If atoi fails, devnum==0 and the default device is used */
devnum = atoi(s->filename);
//连接设备
ret = SendMessage(ctx->hwnd, WM_CAP_DRIVER_CONNECT, devnum, 0);
if (!ret) {
av_log(s, AV_LOG_ERROR, "Could not connect to device.\n");
DestroyWindow(ctx->hwnd);
return AVERROR(ENODEV);
}
//设置参数:是不是不要overlay,不要preview?我猜的,没查msdn。
SendMessage(ctx->hwnd, WM_CAP_SET_OVERLAY, 0, 0);
SendMessage(ctx->hwnd, WM_CAP_SET_PREVIEW, 0, 0);
//设置视频数据接收函数。VFW获取视频是在它的窗口线程中,videostream_cb
//在每次获取一帧数据时被调用,所以它也在那个线程中执行
ret = SendMessage(ctx->hwnd, WM_CAP_SET_CALLBACK_VIDEOSTREAM, 0,
(LPARAM) videostream_cb);
if (!ret) {
av_log(s, AV_LOG_ERROR, "Could not set video stream callback.\n");
goto fail;
}
//把AVFormatContext保存在窗口的USERDATA区,为的是以后响应
//VFW的窗口消息时可以获取到AVFormatContext对象
SetWindowLongPtr(ctx->hwnd, GWLP_USERDATA, (LONG_PTR) s);
//创建一个流对象并加入AVFormatContext对象中。
st = av_new_stream(s, 0);
if (!st) {
vfw_read_close(s);
return AVERROR(ENOMEM);
}
/* Set video format */
//先取得video format结构的大小
bisize = SendMessage(ctx->hwnd, WM_CAP_GET_VIDEOFORMAT, 0, 0);
if (!bisize)
goto fail;
//分配存放video format结构的内存
bi = av_malloc(bisize);
if (!bi) {
vfw_read_close(s);
return AVERROR(ENOMEM);
}
//这次才是真正的获取video format结构
ret = SendMessage(ctx->hwnd, WM_CAP_GET_VIDEOFORMAT, bisize, (LPARAM) bi);
if (!ret)
goto fail;
//显示给用户看看
dump_bih(s, &bi->bmiHeader);
//分析帧率,ctx->framerate是从命令行得到的
ret = av_parse_video_rate(&framerate_q, ctx->framerate);
if (ret < 0) {
av_log(s, AV_LOG_ERROR, "Could not parse framerate '%s'.\n",
ctx->framerate);
goto fail;
}
//分析视频的size,也是从命令行得到的,是用户要求的
if (ctx->video_size) {
ret = av_parse_video_size(&bi->bmiHeader.biWidth,
&bi->bmiHeader.biHeight, ctx->video_size);
if (ret < 0) {
av_log(s, AV_LOG_ERROR, "Couldn't parse video size.\n");
goto fail;
}
}
//video foramt中的参数被用户输入所替代,设置给VFW设备
ret = SendMessage(ctx->hwnd, WM_CAP_SET_VIDEOFORMAT, bisize, (LPARAM) bi);
if (!ret) {
av_log(s, AV_LOG_ERROR, "Could not set Video Format.\n");
goto fail;
}
//是否是压缩数据,vfw支持mjpeg编码。
biCompression = bi->bmiHeader.biCompression;
biBitCount = bi->bmiHeader.biBitCount;
/* Set sequence setup */
//获取流参数
ret = SendMessage(ctx->hwnd, WM_CAP_GET_SEQUENCE_SETUP, sizeof(cparms),
(LPARAM) &cparms);
if (!ret)
goto fail;
//显示了用户看看
dump_captureparms(s, &cparms);
//设置一些我们想设置的流数
cparms.fYield = 1; // Spawn a background thread
cparms.dwRequestMicroSecPerFrame = (framerate_q.den * 1000000)
/ framerate_q.num;//一帧持续的时间?是吧?
cparms.fAbortLeftMouse = 0;
cparms.fAbortRightMouse = 0;
cparms.fCaptureAudio = 0;
cparms.vKeyAbort = 0;
//设置回去
ret = SendMessage(ctx->hwnd, WM_CAP_SET_SEQUENCE_SETUP, sizeof(cparms),
(LPARAM) &cparms);
if (!ret)
goto fail;
//设置libav中相应的参数,要与VFW中的设置一致哦。
codec = st->codec;
codec->time_base = (AVRational) {framerate_q.den, framerate_q.num};
codec->codec_type = AVMEDIA_TYPE_VIDEO;
codec->width = bi->bmiHeader.biWidth;
codec->height = bi->bmiHeader.biHeight;
codec->pix_fmt = vfw_pixfmt(biCompression, biBitCount);
if (codec->pix_fmt == PIX_FMT_NONE) {
//如果没有找到pix fmt,说明是种压缩格式,分析出编码器的id。
codec->codec_id = vfw_codecid(biCompression);
if (codec->codec_id == CODEC_ID_NONE) {
//获取不到这种编码格式的编码器,搞不定它,只能退出了。
av_log(s, AV_LOG_ERROR, "Unknown compression type. "
"Please report verbose (-v 9) debug information.\n");
vfw_read_close(s);
return AVERROR_PATCHWELCOME;
}
codec->bits_per_coded_sample = biBitCount;
} else {
//如果找到了pix fmt,说明是一种未编码格式,那就是RAWVIDEO。
codec->codec_id = CODEC_ID_RAWVIDEO;
if (biCompression == BI_RGB) {
codec->bits_per_coded_sample = biBitCount;
codec->extradata = av_malloc(9 + FF_INPUT_BUFFER_PADDING_SIZE);
if (codec->extradata) {
codec->extradata_size = 9;
memcpy(codec->extradata, "BottomUp", 9);
}
}
}
av_freep(&bi);
//设置为每要个帧打时间戳时的参数,32表示时间戳是32位的,1和1000表示计时
//单位为1/1000秒。
av_set_pts_info(st, 32, 1, 1000);
//创建互斥量,videostream_cb运行在VFW的窗线程中,而read_packet运行于ffmpeg的线程中,
//因而需要对一些东西进行同步保护。
ctx->mutex = CreateMutex(NULL, 0, NULL);
if (!ctx->mutex) {
av_log(s, AV_LOG_ERROR, "Could not create Mutex.\n");
goto fail;
}
//创建事件,用于写线程通知读线程,数据到了,没有它读线程会空转。
ctx->event = CreateEvent(NULL, 1, 0, NULL);
if (!ctx->event) {
av_log(s, AV_LOG_ERROR, "Could not create Event.\n");
goto fail;
}
//开始抓取视频,NOFILE设置不保存文件?是吧?
ret = SendMessage(ctx->hwnd, WM_CAP_SEQUENCE_NOFILE, 0, 0);
if (!ret) {
av_log(s, AV_LOG_ERROR, "Could not start capture sequence.\n");
goto fail;
}
return 0;
fail: av_freep(&bi);
vfw_read_close(s);
return AVERROR(EIO);
}
再看一下读取一帧的函数
static int vfw_read_packet(AVFormatContext *s, AVPacket *pkt)
{
struct vfw_ctx *ctx = s->priv_data;
AVPacketList *pktl = NULL;
while (!pktl) {
//等待VFW线程中向pkt1这个队例中插入pkg,这儿的pkt是一帧。
WaitForSingleObject(ctx->mutex, INFINITE);
//该我们了,从队列中取出一帧
pktl = ctx->pktl;
if (ctx->pktl) {
*pkt = ctx->pktl->pkt;
ctx->pktl = ctx->pktl->next;
av_free(pktl);
}
ResetEvent(ctx->event);
ReleaseMutex(ctx->mutex);
if (!pktl) {
if (s->flags & AVFMT_FLAG_NONBLOCK) {
return AVERROR(EAGAIN);
} else {
//如果帧队列为空说明没数据了,等待VFW线程(videostream_cb)向队列中插入新的帧
WaitForSingleObject(ctx->event, INFINITE);
}
}
}
//取得了一帧,返回给调用者。
ctx->curbufsize -= pkt->size;
return pkt->size;
}
VFW回调函数:
//此函数在窗口线程中执行,VFW抓到一帧调用一次
static LRESULT CALLBACK videostream_cb(HWND hwnd, LPVIDEOHDR vdhdr)
{
AVFormatContext *s;
struct vfw_ctx *ctx;
AVPacketList **ppktl, *pktl_next;
//取得窗口中保存的AVFormatContext
s = (AVFormatContext *) GetWindowLongPtr(hwnd, GWLP_USERDATA);
//从而取得私有数据
ctx = s->priv_data;
dump_videohdr(s, vdhdr);
//我们可以丢掉这一帧吗?
if (shall_we_drop(s))
return FALSE;
//等待ffmpeg线程读取帧队列
WaitForSingleObject(ctx->mutex, INFINITE);
//分配一个pkg,用于存放这一帧
pktl_next = av_mallocz(sizeof(AVPacketList));
if (!pktl_next)
goto fail;
if (av_new_packet(&pktl_next->pkt, vdhdr->dwBytesUsed) < 0) {
av_free(pktl_next);
goto fail;
}
//时间戳
pktl_next->pkt.pts = vdhdr->dwTimeCaptured;
memcpy(pktl_next->pkt.data, vdhdr->lpData, vdhdr->dwBytesUsed);
//将包插入队列
for (ppktl = &ctx->pktl; *ppktl; ppktl = &(*ppktl)->next)
;
*ppktl = pktl_next;
ctx->curbufsize += vdhdr->dwBytesUsed;
SetEvent(ctx->event);
//设置事件,使read_packet可以操作队列。因为队列不为空了
ReleaseMutex(ctx->mutex);
return TRUE;
fail: ReleaseMutex(ctx->mutex);
return FALSE;
}