hello诶喂八滴跟我一起嗨嗨嗨。。。,阿循今天给大家分享一下最近在学习的开源视频播放器vlcplayer的一些心得,我这边是要把这个弄到Unity里去用,因此提取视频帧和自定义读取是很关键的功能点,前者可以拿到数据给unity渲染,后者可以在C#层面去做数据功能模块,开发效率美滋滋。
libvlc是vlc的开发者库,它的播放器也是通过libvlc实现的,我们这里用到了C++,不过也是有C#绑定的版本的,这里就不讨论了。
首先,含SDK目录的程序可以在http://download.videolan.org/pub/videolan/vlc/找到,官网上的版本里并不包含;配置这些按网上教 程来就好,也很多的。ps:看到SDK不得不吐槽,居然有40多M。或许,一些裁剪可以解决这个问题。然则,终究,还是花时间,先这样吧。
关于这几个功能,网上也是语焉不详,我既然花了时间去跑通了,正宜分享给大家。
1、提取帧,可以用来开发自定义渲染的功能,vlcplayer虽然说可在指定窗体播放,但是灵活性不是很高,要做一些定制性的渲染,就需要通过获取它的视频帧的数据,然后再通过一些手段显示,如dx,gdi等等,其它平台也是一样的思路。
下面的例子我每隔30帧抽取一张图,并用opencv保存成jpg:
#pragma region 提取帧回调
// 回调
const int w = 1920;
const int h = 960;
uchar buffer[w*h * 4];
int idx = -1;
Mat* mat = new Mat(h, w, CV_8UC3);
void* VideoLockCallback(void *opaque, void **planes)
{
memset(buffer, 0, w*h * 4);
*planes = buffer;
return buffer;
}
void VideoUnlockCallback(void *opaque, void *picture, void *const*planes) {}
void VideoDisplayCallback(void *opaque,void *picture)
{
idx++;
if (idx % 30 != 0)
{
return;
}
int j = idx;
// 设置数据
int nl = mat->rows;
int nc = mat->cols;
/*if (mat->isContinuous())
{
nc = nc*nl;
nl = 1;
}*/
for (size_t j = 0; j < nl; j++)
{
uchar *data = mat->ptr<uchar>(j);
for (size_t i = 0; i < nc; i++)
{
// buffer->RGBA
// opencv->BGR
*data++ = buffer[j*nc * 4 + i * 4 + 2];
*data++ = buffer[j*nc * 4 + i * 4 + 1];
*data++ = buffer[j*nc * 4 + i * 4];
}
}
//
vector<int>compression_params;
compression_params.push_back(IMWRITE_JPEG_QUALITY);
compression_params.push_back(50);
imwrite("frame/" + to_string(j) + ".jpg", *mat, compression_params);
}
#pragma endregion 提取帧回调
void GetFrameTest(const char* fileName)
{
libvlc_instance_t* vlcInst = libvlc_new(0, 0);
if (!vlcInst)
cout << "libvlc_new error\n";
libvlc_media_t* vlc_media = libvlc_media_new_path(vlcInst, fileName);
if (!vlc_media)
cout << "libvlc_media_new_path error\n";
libvlc_media_player_t* vlc_player = libvlc_media_player_new_from_media(vlc_media);
libvlc_media_release(vlc_media);
if (!vlc_player)
cout << "libvlc_media_player_new_from_media error\n";
libvlc_video_set_callbacks(vlc_player, VideoLockCallback, VideoUnlockCallback, VideoDisplayCallback, 0);
libvlc_video_set_format(vlc_player, "RGBA", w, h, w * 4);
libvlc_media_player_play(vlc_player);
}
提取数据,关键在于libvlc_video_set_callbacks和libvlc_video_set_format,前者通过几个回调,拦截了vlc自己的视频显示组件,数据将传递到回调里,因此我们可以通过它来拿到数据,在上面的例子中,宽度和高度是可以自己设置的,并不需要等于视频的宽高,vlc会自己缩放到你指定的宽高(通过libvlc_video_set_format指定的)。需要注意的是vlc生成的数据是RGBA的排列顺序,当你拿到数据作它用的时候,要考虑下你的目标的颜色分量的顺序。
2、自定义读取,用处是很多的,比如视频数据是加过密的,或者想播放一个网络视频,自己处理网络缓冲部分,因此它自带的播放接口就不一定能满足要求了。在vlc2.x版本,可以通过libvlc_media_new_location来完成,因为这个方法的url参数支持很多协议,通过内存数据协议("imem://")即可达到目标,不过3.x对此做了封装,提供了新的接口libvlc_media_new_callbacks,在这里我们只讲新接口下的实现。
在例子中,通过自定义数据读取,我使用文件流对本地文件进行读取,并通过自定义数据读取的回调把数据传给了vlc。同样的,你可以从http或者其它哪来获取数据并传给它:
#pragma region 自定义读数据回调
ifstream file;
int MediaOpen(void *opaque, void **datap,uint64_t *sizep)
{
file.open((char*)opaque, ios::binary | ios::in);
file.seekg(0, ios::end);
int len = file.tellg();
file.seekg(0);
*sizep = len;
*datap = &file;
return 0;
}
size_t MediaRead(void *opaque, unsigned char *buf, size_t len)
{
ifstream* in = (ifstream*)opaque;
in->read((char*)buf, len);
auto s = in->gcount();
if (s == 0)
{
if (in->eof())
{
return 0;
}
else
{
return -1;
}
}
return s;
}
int MediaSeek(void *opaque, uint64_t offset)
{
ifstream* in = (ifstream*)opaque;
in->clear();
in->seekg(offset);
return 0;
}
void MediaClose(void *opaque)
{
ifstream* in = (ifstream*)opaque;
in->close();
cout << "close\n";
}
#pragma endregion 自定义读数据回调
void CustomReadTest(const char* fileName)
{
libvlc_instance_t* vlcInst = libvlc_new(0, 0);
if (!vlcInst)
cout << "libvlc_new error\n";
libvlc_media_t* vlc_media = libvlc_media_new_callbacks(
vlcInst, MediaOpen, MediaRead, MediaSeek, MediaClose, (void*)fileName);
if (!vlc_media)
cout << "libvlc_media_new_path error\n";
libvlc_media_player_t* vlc_player = libvlc_media_player_new_from_media(vlc_media);
libvlc_media_release(vlc_media);
if (!vlc_player)
cout << "libvlc_media_player_new_from_media error\n";
libvlc_media_player_play(vlc_player);
Sleep(5000);
auto time = libvlc_media_player_get_length(vlc_player);
Sleep(time);
}
有几个关键点要提一下
1、open回调的第一个参数opaque是libvlc_media_new_callbacks的最后一个参数传入的,而read,seek,close回调的opaque参数则是open回调中的datap,名字一样,可不是同一个数据,因为人家是c库嘛,没得类,有些东西就通过方法参数传递了。
2、我在使用ifstream的时候,因为一个坑也花了半天时间,seekg方法在达到文件尾之后再调用不起作用,要先调用clear方法,我之前一直以为是其它地方有问题,花了很多时间去后vlc的代码。。。