公司最近在做新款产品的时候,需要同时用到四路镜头,但是目前高通的开发板上只有三个口,故打算再加上一个USB镜头。于是翻看了一下android的USB镜头的使用,发现得自己写JNI代码的,特此记录一下。
大概流程如下:
打开USB镜头-> 获取USB镜头的信息并设置相应的属性->申请一个图像数据的缓冲区-> 开始捕获数据(让USB往缓冲区写数据)-> 循环从缓冲区获取一帧数据-> 关闭USB镜头
根据这个流程,为了方便理解,我们先定义调用jni层的class文件 然后按照顺序一步一步去实现
import android.util.Log;
/**
* Created by 601042 on 2018/2/28.
*/
public class NativeTest {
static {
System.loadLibrary("native-lib");
}
/**
* 打开USB镜头
*
* @return 0:成功
* */
public native int openCamera();
/**
* 获取USB镜头的信息并设置相应的属性
*
* @return 0:成功
* */
public native String getDevicInfo();
/**
* 申请一个图像数据的缓冲区
*
* @return 0:成功
* */
public native int getCache();
/**
* 开始捕获数据
*
* @return 0:成功
* */
public native int startCapture();
/**
* 获取一帧数据
*
* @return 0:成功
* */
public native int getOneFrame();
/**
* 关闭USB镜头
*
* @return 0:成功
* */
public native int closeCamera();
/**
* 开启图像callback
*
* @return 0:成功
* */
public native void start();
/**
* 图像callback
*
* @param data:一帧图像数据
* @param length:一帧图像数据的长度
* */
public void myCallback(byte[] data,int length) {
Log.e("Test", "Callback: " + data.length + " "+length);
}
}
现在先来实现第一步:打开USB镜头
linux是一个文件系统,外接设备也是以一个文件的形式存在,在/dev/目录下可以找到这些,我现在板上有三个镜头的插口,/dev/目录下则有video1、video2、video33、video34四个文件存在,我猜应该是支持四路相机的,只是系统层做了限制,只能同时开三路。现在将USB镜头接到板上,发现/dev/目录下多个video3,拔掉USB镜头又没了,故这个video3就是USB镜头,要打开这个镜头就跟打开一个文件是一样。
static int fd = -1; //镜头ID
static char *dev_name = "/dev/video3"; //镜头名
/*************************************************
Function: openUSBCamera
Description: 打开USB摄像头
*************************************************/
int openUSBCamera() {
struct stat st;
//先判断该文件的状态
if (-1 == stat(dev_name, &st)) {
LOGE("Cannot identify '%s': %d, %s\n", dev_name, errno, strerror(errno));
return -1;
}
//再判断该文件是不是设备
if (!S_ISCHR(st.st_mode)) {
LOGE("%s is not device/n", dev_name);
return -1;
}
//打开设备
fd = open(dev_name, O_RDWR /* required */| O_NONBLOCK, 0);
//判断是否打开成功
if (-1 == fd) {
LOGI("Cannot open '%s': %d, %s\n", dev_name, errno, strerror(errno));
return -1;
}
canCallback = true;
//返回该设备的ID
return fd;
};
extern "C"
JNIEXPORT jint JNICALL
Java_demo_xu_usbcanerademo_NativeTest_openCamera(JNIEnv *env, jobject instance) {
// TODO
return openUSBCamera();
}
好了,到这里我们就打开USB镜头了。
接下来我们来获取USB镜头的信息并设置
/*************************************************
Function: getInfo
Description: 获取USB摄像头的信息
Return: 设备信息
*************************************************/
string getInfo() {
//查看设备名称等信息
struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap);
//把结果封装一下
std::stringstream ss;
ss << "DriverName:" << cap.driver << " Card Name:" << cap.card << " Bus info:"
<< cap.bus_info << " DriverVersion:" << ((cap.version >> 16) & 0XFF)
<< ((cap.version >> 8) & 0XFF) << (cap.version & 0xff);
//查看支持格式
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//获取支持的格式列表 并把结果封装一下
ss << " Supportformat:\n";
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) != -1) {
//把结果封装一下
ss << (fmtdesc.index + 1) << ":" << (fmtdesc.description) << "\n";
fmtdesc.index++;
}
//如果需要设置的话 使用 ioctl(fd, VIDIOC_S_FMT, &fmtdesc);
//查看当前帧的相关信息(长宽)
struct v4l2_format fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_G_FMT, &fmt);
//把结果封装一下
ss << "Currentdata format information:\ntwidth:" << fmt.fmt.pix.width << " height:"
<< fmt.fmt.pix.height;
//如果需要设置的话 使用 ioctl(fd, VIDIOC_S_FMT, &fmt);
return ss.str();
}
extern "C"
JNIEXPORT jstring JNICALL
Java_demo_xu_usbcanerademo_NativeTest_getDevicInfo(JNIEnv *env, jobject instance) {
// TODO
string resule = getInfo();
return env->NewStringUTF(resule.c_str());
}
由于我的镜头没有支持多种格式或者属性,所以我就只获取一下信息,不进行设置。如果需要设置的话需要记住,一定要先获取设备支持的属性或格式列表,再从中获取到满足要求的设置进去,不然设置设备不支持的属性或者格式会无法正常工作的。
接下来我们申请一个图形数据缓冲区
应用程序和设备有三种交换数据的方法,直接read/write ,内存映射(memorymapping) ,用户指针。我使用的是内存映射的方式。
/*************************************************
Function: getCache
Description: 申请一个缓冲区(4帧) 并用buffers把指针存起来
Return: 结果
*************************************************/
int getCache(){
//定义buffer(缓冲区)的属性
struct v4l2_requestbuffers req;
//缓存多少帧
req.count=4;
//buffer的类型
req.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
//采用内存映射的方式
req.memory=V4L2_MEMORY_MMAP;
//申请缓冲区
int result_getcache = ioctl(fd,VIDIOC_REQBUFS,&req);
//如果为-1则说明申请失败
if(result_getcache == -1){
return -1;
}
//用buffer存储缓冲区的指针
buffers =(buffer*)calloc (req.count, sizeof (*buffers));
if (!buffers) {
fprintf (stderr,"Out of memory/n");
return -2;
}
//开始映射 四帧图像的区域都要
for (n_buffers = 0; n_buffers < req.count; ++n_buffers) {
struct v4l2_buffer buf;
memset(&buf,0,sizeof(buf));
buf.type =V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory =V4L2_MEMORY_MMAP;
buf.index =n_buffers;
// 查询序号为n_buffers 的缓冲区,得到其起始物理地址和大小 -1则说明失败
if (-1 == ioctl(fd, VIDIOC_QUERYBUF, &buf)){
return -3;
}
buffers[n_buffers].length= buf.length;
// 映射内存
buffers[n_buffers].start=mmap (NULL,buf.length,PROT_READ | PROT_WRITE ,MAP_SHARED,fd, buf.m.offset);
//如果映射失败则直接返回
if (MAP_FAILED== buffers[n_buffers].start){
return -4;
}
}
return 0;
}
extern "C"
JNIEXPORT jint JNICALL
Java_demo_xu_usbcanerademo_NativeTest_getCache(JNIEnv *env, jobject instance) {
// TODO
return getCache();
}
缓冲区申请好了我们开始捕获数据(让设备往缓冲区里写数据)
/*************************************************
Function: startCapture
Description: 开始捕获数据(数据会存入缓冲区)
Return: 结果
*************************************************/
int startCapture(void) {
unsigned int i;
enum v4l2_buf_type type;
//把四个帧放入队列
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
//把帧放入队列
if (-1 == ioctl(fd, VIDIOC_QBUF, &buf)){
LOGE("VIDIOC_QBUF error %d, %s\n", errno, strerror(errno));
return -1;
}
}
//类型设置为捕获数据
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//启动数据流
if (-1 == ioctl(fd, VIDIOC_STREAMON, &type)){
LOGE("VIDIOC_STREAMON error %d, %s\n", errno, strerror(errno));
return -2;
}
return 0;
}
extern "C"
JNIEXPORT jint JNICALL
Java_demo_xu_usbcanerademo_NativeTest_startCapture(JNIEnv *env, jobject instance) {
// TODO
return startCapture();
}
开启了捕获之后,镜头就会一直往缓冲区里写数据了,数据的格式就是之前你设置的,比如我的就是YUV422(我没设置,因为我的镜头就只有这种格式的)。
现在我们要拿图像数据出来显示,就得从缓存区里拿数据,循环地从缓冲区里拿一帧帧的图像出来显示就是镜头的预览了。
我们先来讲讲如何冲缓冲区获取一帧图像数据出来
struct buffer *buffers = NULL; //图像数据
void *framebuf = NULL; //一帧图像数据
/*************************************************
Function: getOneFrame
Description: 从缓冲区获取一帧数据
Return: 结果
*************************************************/
static int getOneFrame() {
struct v4l2_buffer buf;
unsigned int i;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
//从队列中取数据到缓冲区
if (-1 == ioctl(fd, VIDIOC_DQBUF, &buf)) {
LOGE("VIDIOC_DQBUF error %d , %s", errno, strerror(errno));
}
//
assert(buf.index < n_buffers);
if (buf.bytesused <= 0xaf) {
/* Prevent crash on empty image */
LOGI("Ignoring empty buffer ...\n");
return -1;
}
oneFrameLength = buf.bytesused;
framebuf = (void *) malloc(oneFrameLength);
pthread_mutex_lock(&lock);
//从视频数据copy到framebuf中
memcpy(framebuf, buffers[buf.index].start, oneFrameLength);
pthread_mutex_unlock(&lock);
//填充队列
if (-1 == ioctl(fd, VIDIOC_QBUF, &buf))
LOGE("VIDIOC_QBUF error %d, %s", errno, strerror(errno));
return 0;
}
extern "C"
JNIEXPORT jint JNICALL
Java_demo_xu_usbcanerademo_NativeTest_getOneFrame(JNIEnv *env, jobject instance) {
// TODO
return getOneFrame();
}
这里使用到了一个自己定义结构体buffer
struct buffer {
void *start;
size_t length;
};
到这里我们就可以获取带USB镜头的一帧图像了,在demo,我使用了jni层回调java代码的方式,通过一个线程把循环从缓冲区里拿数据,然后调用java层的callback给传递出去。
/*************************************************
Function: thread_entry
Description: 获取图像并callback到java层的线程
*************************************************/
void *thread_entry(void* data)
{
//获取全局的*env;
JNIEnv *env;
mjavaVM->AttachCurrentThread(&env,NULL);
jclass clazz = env->GetObjectClass(mjavaobject);
//获取到java的方法ID
jmethodID mID = env->GetMethodID(clazz, "myCallback", "([BI)V");
//开始while循环获取图像并callback出去,用canCallback这个变量控制是否停止
while(canCallback) {
if(env != NULL && mID != NULL && mjavaobject != NULL){
struct v4l2_buffer buf;
unsigned int i;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
//从队列中取数据到缓冲区
if (-1 == ioctl(fd, VIDIOC_DQBUF, &buf)) {
LOGE("VIDIOC_DQBUF error %d , %s", errno, strerror(errno));
usleep(50000);
continue;
}
//判断获取到的数据是否有效
assert(buf.index < n_buffers);
if (buf.bytesused <= 0xaf) {
LOGI("Ignoring empty buffer ...\n");
usleep(50000);
continue;
}
//记录下获取到数据的长度
oneFrameLength = buf.bytesused;
pthread_mutex_lock(&lock);
//实例化一个数组
jbyteArray temp_result = env->NewByteArray(oneFrameLength);
//把获取到的图像数据赋值给数组
env->SetByteArrayRegion(temp_result, 0, oneFrameLength,(jbyte *)buffers[buf.index].start);
//调用java层的代码
env->CallVoidMethod(mjavaobject, mID, temp_result,oneFrameLength);
//销毁掉创建出来的两个临时变量
env->DeleteLocalRef(temp_result);
pthread_mutex_unlock(&lock);
//填充队列
if (-1 == ioctl(fd, VIDIOC_QBUF, &buf)){
LOGE("VIDIOC_QBUF error %d, %s", errno, strerror(errno));
usleep(50000);
continue;
}
}else{
LOGD("menv == NULL || obj == NULL || mID == NULL...\n");
}
//间隔50毫秒
//单位:微秒 1000微秒=1毫秒
usleep(50000);
}
//销毁掉
mjavaVM->DetachCurrentThread();
}
extern "C"
JNIEXPORT void JNICALL
Java_demo_xu_usbcanerademo_NativeTest_start(JNIEnv *env, jobject instance) {
// TODO
env->GetJavaVM(&mjavaVM);
mjavaobject = env->NewGlobalRef(instance);
pthread_t trecv;
int result = pthread_create(&trecv,NULL,thread_entry,NULL);
LOGD("result%d",result);
}
拿到了数据,大家想用什么方法显示都可以,保存成文件也可以,这里就不多讲了。
最后一步就是你用完了就得关闭镜头了
/*************************************************
Function: closeUSBCamera
Description: 关闭USB摄像头
Return: 结果
*************************************************/
int closeUSBCamera() {
if(fd == -1){
return 0;
}
//释放申请的缓冲
unsigned int i;
for (i = 0; i < n_buffers; ++i){
if (-1 == munmap(buffers[i].start, buffers[i].length)){
LOGE("munmap error %d , %s", errno, strerror(errno));
}
free(buffers);
}
canCallback = false;
//关闭镜头
return close(fd);
};
extern "C"
JNIEXPORT jint JNICALL
Java_demo_xu_usbcanerademo_NativeTest_closeCamera(JNIEnv *env, jobject instance) {
// TODO
return closeUSBCamera();
}
好了,到这里基本整个USB相机的使用就讲完了。