基於V4L2驅動程序的USB攝像頭Android(JNI)的編寫(一)

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/JansonZhe/article/details/47319727

video4 linux2(V4L2)是Linux內核中關於視頻設備的內核驅動,它爲Linux中視頻設備訪問提供了通用接口,在Linux系統中,本文主要介紹如何使用V4L2驅動程序打開我們的USB攝像頭。同時着重介紹如何編寫相應JNI(NDK)使Android應用程序能夠打開我們外接的USB攝像頭,並獲取視頻流信息。

一、 確定USB攝像頭的設備文件
V4L2驅動的Video設備節點路徑通常/dev/video/中的videoX,V4L2驅動對用戶空間提供字符設備,主設備號爲81,對於視頻設備,其次設備號爲0-63。除此之外,次設備號爲64-127的Radio設備,次設備號爲192-223的是Teletext設備,次設備號爲224-255的是VBI設備。V4L2驅動的Video設備在用戶空間通過各種ioctl調用進行控制,並且可以使用mmap進行內存映射。
這裏首先第一步是要確定免驅的USB攝像頭的設備文件,否則便無法進入下一步工作,要確定USB攝像頭的設備文件也比較簡單。

  1. 將USB攝像頭通過USB線連接進開發板,使用adb shell進入dev目錄,這時可以查看一下當下的包含video的目錄,具體查看命令如下:ls –l video*,這樣就可以看到目前開發板上所包含有的video設備,如下圖所示:
    dev目錄下的video設備
    由上圖我們可以看到video設備的主設備號都爲81,同時次設備號介於0到63之間。但是這上面有這麼多的video設備,我們並不清楚我們的USB攝像頭的設備文件是哪一個。所以下一步就是要確定我們的設備文件。

  2. 將USB攝像頭拔出,同樣的按照上一步的方式,查看dev目錄下的video設備文件。得到的結果如下圖所示
    去掉USB攝像頭的設備文件
    由上面我們可以看到,video4已經隨着我們的USB攝像頭拔出,已經消失不見了,說明我們USB攝像頭的設備文件就是video4

二、 編寫JNI文件
我們確定好USB的設備文件之後,就可以開始編寫JNI程序啦,不過在編寫之前,必須要弄清楚基於V4L2驅動的上層調用程序的一般流程。在後面會介紹許多V4L2驅動的API。

1. 結構流程

V4L2採集結構示意圖
1) 打開設備文件。 int fd=open(”/dev/video4″,O_RDWR);
2) 取得設備的capability,看看設備具有什麼功能,比如是否具有視頻輸入,或者音頻輸入輸出等。VIDIOC_QUERYCAP,struct v4l2_capability
3) 選擇視頻輸入,一個視頻設備可以有多個視頻輸入。VIDIOC_S_INPUT,struct v4l2_input
4) 設置視頻的制式和幀格式,制式包括PAL,NTSC,幀的格式個包括寬度和高度等。VIDIOC_S_STD,VIDIOC_S_FMT,struct v4l2_std_id,struct v4l2_format
5) 向驅動申請幀緩衝,一般不超過5個。struct v4l2_requestbuffers
6) 將申請到的幀緩衝映射到用戶空間,這樣就可以直接操作採集到的幀了,而不必去複製。Mmap
7) 將申請到的幀緩衝全部入隊列,以便存放採集到的數據.VIDIOC_QBUF,struct v4l2_buffer
8) 開始視頻的採集。VIDIOC_STREAMON
9) 出隊列以取得已採集數據的幀緩衝,取得原始採集數據。VIDIOC_DQBUF
10) 將緩衝重新入隊列尾,這樣可以循環採集。VIDIOC_QBUF
11) 停止視頻的採集。VIDIOC_STREAMOFF
12) 關閉視頻設備。close(fd);
下面將具體介紹如何具體編寫JNI的方式

2. 編寫流程

1、打開設備文件
首先第一步當然是要打開設備文件,通過上一步,我們已經知道了設備文件的名稱了,所以這一步我們就可以知道應該打開哪一個設備文件了。打開設備文件一共有兩種方式。

阻塞式
使用阻塞式的方式打開設備文件,如果後面沒有捕獲到視頻信息,驅動程序便會停止不動,直至有視頻信息到來。方式如下:

int fd = open(”/dev/video4″, O_RDWR | O_NONBLOCK, 0);

非阻塞式
使用非阻塞式打開設備文件,即使尚未捕獲到信息,驅動依舊會把緩存裏面的東西返回給應用程序。方式如下:

int fd = open(”/dev/video4″, O_RDWR, 0);
//或者下面這樣
int fd = open(”/dev/video4″, O_RDWR);

在這裏我們目前使用的是非阻塞式。

2、設定採集方式和屬性

打開完設備文件之後,我們下一步需要做的工作就是去給驅動程序設置我們需要採集的視頻信息的格式、大小、緩衝數等等。設置方式是通過我們非常熟悉的ioctl函數進行的,一般的格式爲:
extern int ioctl(fd,request,…..)
這裏的fd便是我們之前打開設備文件時所返回的文件句柄了,而request是V4L2驅動程序所特有的一些命令,用unsigned long表示,通過這些request給底層驅動,驅動程序便會映射相應的處理函數,所以這也是V4L2驅動程序的強大之處。常見的request有:

VIDIOC_REQBUFS:分配內存
VIDIOC_QUERYBUF:把VIDIOC_REQBUFS中分配的數據緩存轉換成物理地址
VIDIOC_QUERYCAP:查詢驅動功能
VIDIOC_ENUM_FMT:獲取當前驅動支持的視頻格式
VIDIOC_S_FMT:設置當前驅動的頻捕獲格式
VIDIOC_G_FMT:讀取當前驅動的頻捕獲格式
VIDIOC_TRY_FMT:驗證當前驅動的顯示格式
VIDIOC_CROPCAP:查詢驅動的修剪能力
VIDIOC_S_CROP:設置視頻信號的邊框
VIDIOC_G_CROP:讀取視頻信號的邊框
VIDIOC_QBUF: 把數據放回緩存隊列
VIDIOC_DQBUF:把數據從緩存中讀取出來
VIDIOC_STREAMON:開始視頻顯示函數
VIDIOC_STREAMOFF:結束視頻顯示函數
VIDIOC_QUERYSTD:檢查當前視頻設備支持的標準,例如PAL或NTSC。

當然我們也可以去官網查閱更加詳細的API介紹,現在我們還是按照上面操作V4L2的結構流程圖進行逐一分析。

(1) 檢查當前USB攝像頭設備的功能集
查詢其功能集,使用的request爲:VIDIOC_QUERYCAP

struct v4l2_capability cap;
int rel = 0;
ioctl(Handle, VIDIOC_QUERYCAP, &cap);

使用ioctl這個函數一共有兩個目的,第一點事可以檢查一下當前的這個設備文件是否符合4VL2的規範,因爲如果該設備文件不符合4VL2規範的話,其返回值爲-1,也就是說裏面要麼沒有定義iotcl這個函數,要麼就是沒有request這個命令;第二點就是通過將v4l2_capability這個結構體的指針傳遞給驅動設備,以此來獲取這個設備的功能集,其中這裏的結構體v4l2_capability是Linux特有的一個用來保存攝像頭功能集的一個結構體。定義在linux\videodev2.h文件中,該結構體的結構定義如下:

struct v4l2_capability
{
 __u8 driver[16];   //驅動名。
 __u8 card[32];     // Device名
 __u8 bus_info[32];  //在Bus系統中存放位置
 __u32 version;      //driver 版本
 __u32 capabilities;  //能力集
 __u32 reserved[4];
};

從上面的結構體可以看到,其裏面定義了一個u32(32位)的capabilities(能力集)
其能力集包括:

V4L2_CAP_VIDEO_CAPTURE 0x00000001     The device supports the Video    Capture interface.
V4L2_CAP_VIDEO_OUTPUT   0x00000002     The device supports the Video    Output interface.
V4L2_CAP_VIDEO_OVERLAY 0x00000004     The device supports the Video    Overlay interface.
A video overlay device typically stores captured images directly in the video memory   of a graphics card,with hardware clipping and scaling.
V4L2_CAP_VBI_CAPTURE     0x00000010 The device supports the Raw  VBI Capture interface, providing Teletext and Closed Caption   data.
V4L2_CAP_VBI_OUTPUT     0x00000020      The device supports the Raw  VBI Output interface.
V4L2_CAP_SLICED_VBI_CAPTURE  0x00000040 The device supports the Sliced VBI Capture interface.
V4L2_CAP_SLICED_VBI_OUTPUT   0x00000080 The device supports the Sliced VBI Output interface.
V4L2_CAP_RDS_CAPTURE    0x00000100          [to be defined]
#define V4L2_CAP_TUNER 0x00010000  
#define V4L2_CAP_AUDIO 0x00020000  
#define V4L2_CAP_RADIO 0x00040000  
#define V4L2_CAP_READWRITE 0x01000000  
#define V4L2_CAP_ASYNCIO 0x02000000  
#define V4L2_CAP_STREAMING 0x04000000

因此我們要判斷其是否有某種能力集,只要將獲取的capabilities與某一個功能相“與”看其是否爲1就可以了。通常如果是USB攝像頭設備的話,其獲得的v4l2_capability結構體一般是這樣的:

__u8 driver[16]; //driver名,通常爲:uvcvideo
 __u8 card[32];  //設備名:廠商會填寫。
 __u8 bus_info[32];  //bus,通常爲:usb-hiusb-ehci-2.4
 __u32 version;
 __u32 capabilities;  //通常爲:V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING
 __u32 reserved[4];

(2) 查詢並修改USB攝像頭設備的剪裁能力

首先要解釋一下,爲什麼要做這一步工作,在解釋之前,我想打一個比方:比如我們的USB攝像頭可以捕獲到640*480格式大小的圖片信息,但是我們給它顯示的屏幕只有4.3’’(482*280),那麼這個時候問題就來了,如果我們不修改USB攝像頭捕獲到的圖片大小,直接將640*480給只支持482*280的顯示屏顯示,肯定會有問題的,所以這個時候,我們便要命令底層驅動去剪裁USB捕獲到的圖片大小,使之成爲482*280大小的。這也是V4L2一個強大的地方。
首先第一步便是查詢驅動設備採集區域的一些信息,比如缺省值(defrect)、最大值(bounds)等等這些信息。
這裏需要的命令(request)是VIDIOC_CROPCAP,其iotcl命令是:

struct v4l2_cropcap cropcap;
ioctl(Handle, VIDIOC_CROPCAP, &cropcap);

這樣結構體cropcap便可以獲得採集區域的一些信息了。v4l2_cropcap結構體是這樣定義的:

struct v4l2_cropcap {
    enum v4l2_buf_type type; //類型
    struct v4l2_rect bounds;  //最大值
    struct v4l2_rect defrect;  //缺省值
    struct v4l2_fract pixelaspect;//有關縮放比的
};

在查詢到了設備的採集區域的信息之後呢,這個時候我們便可以開始重新修改我們的採集區域了。修改命令我們用VIDIOC_S_CROP。其使用的ioctl命令是:
struct v4l2_crop crop;
ioctl(Handle, VIDIOC_CROPCAP, &crop);
其中crop結構體包含了我們要對採集區域進行修改的一些參數,v4l2_crop結構體定義如下:

struct v4l2_crop
{
enum v4l2_buf_type type;// 應用程序設置
struct v4l2_rect c;
}

可以看到在v4l2_crop中還包含一個結構體v4l2_rect其用c表示,這裏的v4l2_rect(rectangle便是矩形的意思)定義如下:

struct v4l2_rect {
    __s32 left;
    __s32 top;
    __s32 width;
    __s32 height;
};

由於我們現在用的USB攝像頭的缺省值和我們需要顯示的值的大小是一樣的,因此這裏我們就直接設置缺省值爲我們的採集區域,全部代碼如下:

struct v4l2_cropcap cropcap;
struct v4l2_crop crop;
if (0 == xioctl (fd, VIDIOC_CROPCAP, &cropcap)) {
        crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        crop.c = cropcap.defrect; 

        if (-1 == xioctl (fd, VIDIOC_S_CROP, &crop)) {
            switch (errno) {
                case EINVAL:
                    break;
                default:
                    break;
            }
            }
    }

(3) 設置採集視頻幀的格式、寬、高、大小等等

現在我們已經設置好了我們的攝像頭採集區域的大小,那麼這個時候問題就來了,我們要採用怎麼的方式在這個採集區域裏面進行圖片信息的採集呢?採集的大小?採集來的數據的存放格式?等等這些問題都需要我們在這一步進行設定。這裏同樣是通過一個命令進行設置的,這個request是VIDIOC_S_FMT,使用方式是:

struct v4l2_format fmt;
ioctl(Handle, VIDIOC_S_FMT, &fmt);

這裏我們使用到的結構體是v4l2_format,其定義如下:

struct v4l2_format
{
 enum v4l2_buf_type type;   // Camera,則用戶必須填寫:V4L2_BUF_TYPE_VIDEO_CAPTURE
 union
 {
 struct v4l2_pix_format pix;    // used by video capture and output devices
 struct v4l2_window win;
 struct v4l2_vbi_format vbi;
 struct v4l2_sliced_vbi_format sliced;
 __u8 raw_data[200];
 } fmt;
};

我們可以看到v4l2_format這個結構體還比較複雜,裏面還包含一個名爲fmt的union,在union裏面仍然包含有許多結構體,像我們接下來要用到的v4l2_pix_format,v4l2_pix_format這個結構體的定義是這樣的:

struct v4l2_pix_format
{
__u32                   width;         // 寬,必須是16的倍數
__u32                   height;        // 高,必須是16的倍數
__u32                   pixelformat;   // 視頻數據存儲類型,例如是//YUV4:2:2還是RGB
enum v4l2_field         field;
__u32                   bytesperline;
__u32                   sizeimage;
enum v4l2_colorspace    colorspace;
__u32                   priv;
};

所以整個程序就應該是:

    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width       = IMG_WIDTH; 
    fmt.fmt.pix.height      = IMG_HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //數據儲存類型
    fmt.fmt.pix.field       = V4L2_FIELD_INTERLACED;

    if (-1 == xioctl (fd, VIDIOC_S_FMT, &fmt))
        return errnoexit ("VIDIOC_S_FMT");
    min = fmt.fmt.pix.width * 2;
    if (fmt.fmt.pix.bytesperline < min)
        fmt.fmt.pix.bytesperline = min;
    min = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height;
    if (fmt.fmt.pix.sizeimage < min)
    fmt.fmt.pix.sizeimage = min;

這裏需要指出的是bytesperline這個參數,其表示表明緩衝區中有多少字節用於表示圖像中一行像素的所有像素值。由於一個像素可能有多個字節表示,所以 bytesPerLine 可能是字段 width 值的若干倍,在這裏設置爲2倍,即一個像素點用兩個2節表示,同理我們也可以推出sizeimage這個參數所表示的意思,應該是整個圖像需要用多少字節來存儲。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章