# 2020q1 Final Project (vcam)
contributed by < `eecheng87` >
> GitHub: [eecheng87/vcam](https://github.com/eecheng87/vcam): Virtual camera device driver for Linux
###### tags: `linux2020`
## Frame buffer
vcam 的開發和 frame buffer 有著高度關聯,故先來了解 frame buffer 到底是什麼。整理自 [Wikipedia](https://en.wikipedia.org/wiki/Framebuffer) 的資料:
* frame : 影片的一格
* buffer : 一段記憶體,用來存影像資料,稍後寫入到輸出裝置
事實上,frame buffer 是 RAM 的一部分。
frame buffer 之於其他周邊裝置的關係:
* Video card (顯示卡) <-> frame buffer(程式對其中的資料進行處理) <-> 顯示器(輸出影像)
[註1] 運作流程是,cpu 上的 memory mapping 到 buffer 中的 RAM,而這塊 RAM 就是存稍後要顯示在裝置上的圖片。
[註2] 現在的顯示卡內部會有 frame buffer 的電路 (integrated circuit),這個電路可以將存在 buffer 內的 bitmap 轉換成真正要輸出 (frame buffer 可以直接 memory mapping 到 cpu 的 memory 上) 的 video signal。(因為是積體電路,所以在 1975 年代後電腦才普遍使用 buffer frame)
frame buffer 可達到的功能:
* 以通過 frame buffer 獲取顯示卡的視訊記憶體中的資料,處理之後,實現更大的解析度的影象,然後將資料直接輸出到顯示器上。
[註1] 早期電腦不會管你寫什麼東西進 buffer,這樣可以會引起安全上的問題。如:打出的顏色把螢幕燒掉。
控制模式:透過 UNIX 機器和作業系統可以輕易的對硬體操作([ioctl](http://man7.org/linux/man-pages/man2/ioctl.2.html)),控制不同的解析度、螢幕更新頻率和顏色深度等等。
[註2] [linux frame buffer](https://en.wikipedia.org/wiki/Linux_framebuffer)
## vcam 運作原理
以下是兩個版本的 v4l2 的框架圖,能加速對後續的理解。
![](https://i.imgur.com/8DQygrA.png)
圖中間的實做在 device.c
![](https://i.imgur.com/WO86Eaw.png)
v4l2_device 是整個輸入設備的總結構體,可以認為它是整個 v4l2 框架的入口。再往下分是輸入子設備,舉例: MIPI 等設備,它們是屬於一個 V4L2 device 之下。子設備可以有很多個,透過 list 串起來,示意圖如下:
![](https://i.imgur.com/CkPXSP8.png)
Video4linux2 驅動主要負責從 sensor (通常是通過DMA)上獲取視頻資料然後把這些視頻幀傳輸到使用者空間,大量資料的傳輸有效能議題要考慮。出於此目的,V4L2 定義了複雜的 API 去處理串流資料。V4L2 子系統為實現這些 API 也加入了不少複雜的程式碼,當然大部分的程式碼沿用了 V4L。為了使驅動工程師更方便實現視頻驅動程式碼,videobuf 子系統提供了一組用於管理串流 IO buffer 的介面 (將於稍後詳細解釋 videobuf)。
簡單來說 v4l2 主要是調用一系列的 `ioctl` 函數去對 v4l2 設備進行打開、關閉、查詢和設置等操作。v4l2 是一個 character driver,其驅動的主要工作就是實現各種各樣的 ioctl。
### 初始化模組
從模組初始檔案開始追蹤,在 `vcam_init` 內的 `create_control_device` 功能如其名,實際做了:
* 分配 `control_device *` 空間,包含其成員 `vcam_devices`,並歸零部份成員。
* 建立裝置資料夾
* class_create 和 class_device_create 用法
```cpp
struct class *myclass = class_create(THIS_MODULE, “my_device_driver”);
class_device_create(myclass, NULL, MKDEV(major_num, 0), NULL, “my_device”);
```
效果:當 module 被載入時,[udev](https://zh.wikipedia.org/wiki/Udev) 會自動在 `/dev` 下建立裝置檔案 (建立字元裝置)。所以可以透過 `ls | grep clt` 發現 `/dev` 底下的確有 `vcamctl`。
[註] 詳細參數用法
<details>
```shell
linux-2.6.22/include/linux/device.h
struct class *class_create(struct module *owner, const char *name)
class_create - create a struct class structure
@owner: pointer to the module that is to "own" this struct class
@name: pointer to a string for the name of this class.
在/sys/class/下創建類目錄
linux-2.6.22/include/ linux/device.h
struct class_device * class_device_create (struct class *cls,
struct class_device *parent,
dev_t devt,
struct device *device,
const char *fmt, ...)
class_device_create - creates a class device and registers it with sysfs
@cls: pointer to the struct class that this device should be registered to.
@parent: pointer to the parent struct class_device of this new device, if any.
@devt: the dev_t for the char device to be added.
@device: a pointer to a struct device that is assiociated with this class device.
@fmt: string for the class device's name
```
</details>
* 登記 major number,可分動態登記和靜態登記(後者過時,不建議。本專案使用前者)。
* 加載步驟:
1、以 `alloc_chrdev_region` 動態取得 major number。
2、以 `cdev_init` 登記系統呼叫 handler。
3、以 `cdev_add` 向 kernel 登記驅動程式。
* 卸載步驟:
1、以 `cdev_del` 向 kernel 釋放驅動程式。
2、以 `unregister_chrdev_region` 釋放 major number。
<details>
`alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)` 是依驅動程式名稱用來取得 major number,並從指定的起點開始預留指定數目的 minor number。
`cdev_init(struct cdev *cdev, const struct file_operations *fops)` 負責初始化 cdev 結構,並登記系統呼叫 handler(fops),另外,其 `cdev` 結果變數在卸除驅動程式時還要用到,需定義為全域變數。
在呼叫 `cdev_init()` 後,還要將 `cdev` 結構變數的 `owner` 成員設為 「THIS_MODULE」。
`cdev_add(struct cdev *p, dev_t dev, unsigned count)` 向 kernel 登記 cdev_init() 設定好的裝置資訊。
</details>
[註] Major number: 驅動程式載入時,可以向 kernel 登記 major number,每個驅動程式都有獨一無二的 major number。
* 執行 `request_vcam_device`,實際上是呼叫 `create_vcam_device`。
* 當中 `v4l2_device_register` 是為了註冊 V4L2 裝置(可以在 `/dev` 底下找到 video0)。
* 初始化 video buffer,透過 ` vcam_out_videobuf2_setup`。詳細實作參考 [videobuf.c 說明](https://hackmd.io/nM7Iu_FnT8y6UOM5XGnhkA#videobufc)。
* `init_framebuffer` 是用來初始化 frame buffer,會在 `/proc` 底下建立對應的檔案(如:`vcamfb`)。所以可以直接對該檔案做寫的動作,接著就會輸出到螢幕。
* 初始化裝置 `vcam`(即 `video_device *vdev`)。通過 `video_register_device`,其中選 `GRABBER` 作為參數。
* `VFL_TYPE_GRABBER` indicates a frame grabber device - including cameras, tuners, and such.
* `VFL_TYPE_VBI` is for devices which pull information transmitted during the video blanking interval.
* `VFL_TYPE_RADIO` for radio devices.
* `VFL_TYPE_VTX` for videotext devices.
* video_device 之於系統的關係圖(參考自[網站製圖](https://work-blog.readthedocs.io/en/latest/v4l2%20framework%20intro.html))![](https://i.imgur.com/3hHQh8Z.png)
* 初始化 queue 透過 `vcam_in_queue_setup`
### device.c 功能
在 `device.c` 中花了很大的篇幅實做 `struct v4l2_ioctl_ops vcam_ioctl_ops` 成員函數。目的是 videobuf2 子系統要把使用者空間的操作和視頻設備驅動關聯起來。在 [網站](https://lwn.net/Articles/213798/) 中有提到,舉 input 來說,要實做三種版本(`.vidioc_enum_input`、`.vidioc_g_input` 和 `.vidioc_s_input`),供使用者選擇。舉例來說,`.vidioc_querycap = vcam_querycap` 代表當應用程式使用 `ioctl` 時,且參數是 `VIDIOC_QUERYCAP`,則會執行自己實做的函數 `vcam_querycap`。
以下簡單說明需要實做的函數
* `vcam_querycap`:用來填寫裝置的能力。比較重要的是 `vcam_querycap` 的成員變數 `capabilities`,這個變數是紀錄支援的操作。舉例來說,在 vcam 中擁有 `V4L2_CAP_VIDEO_CAPTURE` 的能力。在應用程式端該如何檢查,應該如下:
```cpp
struct v4l2_capability cap;
memset(&cap, 0, sizeof(cap));
if(ioctl(dev->fd, VIDIOC_QUERYCAP, &cap) < 0){
// some info will be written into cap
if(EINVAL == errno){
// ERROR
}
// check cap's member which record some capabilities
if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)){
printf(stderr, "%s is no video capture device\n",dev->dev);
return TFAIL;
}
```
* `vcam_enum_input`
... 待補
除了實做 `ioctl` 的操作,另一個重點是實做 `v4l2_file_operations` 的成員(函數)。事實上,上個部份的 `v4l2_ioctl_ops` 和本部份都是操作的集合,之所以要分開是基於設計考量,希望不要那麼複雜,所以分成兩種。(經過觀察,在 `v4l2_file_operations` 指定的函數都是 `/v4l2-core/v4l2-fh.c` 提供的,而非自己實做的)。
總結上述提到的兩大類 operation,把他們填回 `video_device` 的成員變數。
```cpp
static const struct video_device vcam_video_device_template = {
.fops = &vcam_fops,
.ioctl_ops = &vcam_ioctl_ops,
.release = video_device_release_empty,
};
```
device.c 還有處理 [YUV](https://zh.wikipedia.org/wiki/YUV) 和 RGB 兩種顏色編碼的轉換。
### control.c 功能
先來看在檔案的結構 `file_operations control_fops` ,file_operations 內的成員為函數指標,指向 system call 的實作函數。Linux 驅動程式是透過 file_operations 來建構 VFS 層的支援。而 file_operation 裡的函數指標,即是指向每一個 system call 的實作函數。實做的函數包含:
* `control_ioctl` 指向 `unlocked_ioctl`:`unlocked_ioctl` 取代過時的 `ioctl`,想了解詳細原因可參考[此](https://lwn.net/Articles/119652/)。`ioctl` 的原形是 `int ioctl(int fd, unsigned long request, ...)`,和對應的實做 `control_ioctl` 原形一樣(這是必然的,否則無法透過指標函式的方式指給對方)。`control_ioctl` 的內容透過 switch 和對應的 command,來決定呼叫什麼指令。所以在 user space 的呼叫大概像
```cpp
ioctl(fd, VCAM_IOCTL_CREATE_DEVICE, dev)
```
這樣就會執行 case `VCAM_IOCTL_CREATE_DEVICE`,即做 `request_vcam_device`。
* 當 command 是 `VCAM_IOCTL_CREATE_DEVICE`:會執行 `request_vcam_device`,在 `/dev` 底下建立 videox。
* 當 command 是 `VCAM_IOCTL_DESTROY_DEVICE`:刪除裝置。
* 當 command 是 `VCAM_IOCTL_GET_DEVICE`:可以得到裝置的資訊,資訊內容即 `struct vcam_device_spec` 內的成員。
* 當 command 是 `VCAM_IOCTL_MODIFY_SETTING`:根據傳入的參數 `dev_spec` 來填寫規格到 `vcam_device` 中的 `input_format`。
* `control_open` 和 `control_release` 沒有特別實做額外功能。[註]目前觀察起來,專案下種共有 3 種不同的 `open` 方式,分別是 `.open = vcamfb_open`、`.open = control_open` 和 `.open = v4l2_fh_open`。而 vcam-util.c 使用的 open 是 `control_open`。
* `control_read` 和 `control_write` 待補。
最後一個部份是 `create_control_device`,這部份的內容已在初始化模組提及。
### fb.c 功能
本檔案和 frame buffer 有極大關聯。最粗淺的觀察可以發現,本檔案會在 `/proc` 底下建立檔案,此檔案可供寫入,而這塊記憶體被映射到 video。也就是說,可以直接把想顯示在畫面的影像寫入本檔案。這樣的行為的確很像 frame buffer。
接著來仔細研究一下這個檔案。首先,proc 是一個虛擬的檔案系統,我們利用它實現 Linux 核心空間與使用者空間的通訊。在 proc 檔案系統中,我們可以將對虛擬檔案的讀寫作為與核心中實體進行通訊的一種手段,但是與普通檔案不同的是,這些虛擬檔案的內容都是動態建立的。
在 fb.c 中會初始化 frame buffer,即執行 `init_framebuffer`。當中呼叫 `proc_create_data`,會在 `/proc` 底下建立 `vcamfbx`、設定檔案權限、註冊 file operation 和傳入資料( 此資料即 `vcam_device`,可透過 `PDE_DATA` 取得 )。
以下是 `proc_create_data` 的原型:
```cpp
struct proc_dir_entry *proc_create_data(const char *name, umode_t mode,
struct proc_dir_entry *parent,
const struct file_operations *proc_fops,
void *data)
```
接著來看 file operation 實作的函數。
#### vcamfb_open
透過 `PDE_DATA` 取得在 `proc_create_data` 傳入的參數(`void *data`),然後存在 `private_data`。其實 `private_data` 是用來保存自定義設備結構體的地址的。自定義結構體的地址被保存在 `private_data` 後,可以在 read,write 等驅動函數中被傳遞和調用自定義設備結構體中的成員。
#### vcamfb_write
這邊比較關鍵的是 `copy_from_user` 的使用,`copy_from_user` 的原型是:
```cpp
unsigned long copy_from_user (void *to,
const void __user *from,
unsigned long n);
```
函數顧名思義,就是要把 user space 的資料搬到 kernel space。而第一個參數是 destination address,即核心的位址;第二個參數就是 source address,即使用者的位址。
從 source code 的調用 `copy_from_user(data + buf->filled, (void *) buffer, to_be_copyied)` 來觀察,想要將 `buffer` 複製到 `data + buf->filled`,這意味著 `data + buf->filled` 在 kernel space。
接著推測要怎麼使用這些 API 和呼叫的情境。`vcamfb_write` 是實作系統呼叫 `write`,所以我們預期在 user space 的某處會開啟 `/proc/vcambfx` 取得 `fd`,然後我們對這個 `fd` 做 `write` 時,會做 `vcamfb_write`。由 `write` 的原型 `write(int fd, const void *buf, size_t count)` 可以發現傳入的 `buf` 是想要寫的東西,對應到 `vcamfb_write` 的參數是 `buffer`。
所以我們可以推測使用的情境應該如下:
在 user space 或 user application 有人會想寫入 frame buffer,開啟 `/proc` 底下的檔案取得 file descriptor 對其 `write`,而其第二個參數 `buf` 是想寫的字串(此字串是 user space 的位址)。接著呼叫 `vcamfb_write`,在此函數中從 `buffer` 是剛剛傳進來的 `buf`。此時,我們要把這個「想寫」入 frame buffer 的字串真正地寫進,所以透過 `copy_from_user` 達成。將在 user space 的字串 `buffer` 寫進 kernel space 的 `data + offset`。
### videobuf.c
這部分的程式碼內容和 videobuf2 有極大關連,所以先來研究一下 videobuf2 到底是甚麼。videobuf 已經被 videobuf2 取代,相比 videobuf,videobuf2 更加完善,更加實用。雖然 videobuf 工作的很好,但是在一些方面並不是那麼完善,各種不同的 API 相當依賴 buffer 的類型,也沒有提供相關的介面用於視訊緩衝區管理。Videobuf2 提供了一組統一的 API 介面,允許驅動自己有更多的配置。
若從前面的 v4l2 架構圖可以看出,video buffer 是連接 user space 和 v4l2 driver 的橋樑。 在 videobuf.c 中的 `vb2_ops vcam_vb2_ops` 實作了一些 call back function 用來完善對緩衝區的操作建構 videobuf2 底層的基礎設施。實作了包含:
* `queue_setup` (`vcam_out_queue_setup`): 在 user space 執行 `ioctl:VIDIOC_REQBUFS` 操作時被呼叫,作用是用來建立 buffer queue。參數部分,`nbuffers` 是申請的緩衝數,`nplanes` 是一個 frame 中所需要 video plane 的數目,`sizes` 是每個 video plane 的大小(bytes),`alloc_ctxs` 包含了每個 plane 的 allocation context,這個只會在 contiguous DMA 模式使用到。函數內透過 `vb2_get_drv_priv` 取得 queue 上的 driver private data,並取出規格利用。
* `start_streaming` (`vcam_start_streaming`): 告訴 driver 甚麼時候開始獲取影片資料。用來回應 `ioctl:VIDIOC_STREAMON` 操作開始抓取視頻資料,如果驅動程式實現了 `read()` 也會呼叫到`start_streaming()`。實作的內容是開了一條 thread 做 `submitter_thread` ( device.c 內)。在 `submitter_thread` 當中,做 `submit_copy_buffer` 屬最重要,將 rgb (使用者寫入的編碼)編碼轉成 yuvv。
* `stop_streaming` (`vcam_stop_streaming`): 告訴 driver 甚麼時候停止獲取影片資料。用來回應 `ioctl:VIDIOC_STREAMOFF` 操作停止抓取視頻資料,等待 DMA 停止後函數才會返回。這裡值得注意的是,呼叫 `stop_streaming()` 後,videobuf2 子系統會回收掉所有傳遞到驅動程式的 buffers,此時驅動程式不能訪問這些 buffers。
* `wait_prepare` (`vcam_outbuf_unlock`): 根據文件內說明實作,簡單來說就是釋放鎖。
> release any locks taken while calling vb2 functions; it is called before an ioctl needs to wait for a new buffer to arrive; required to avoid a deadlock in blocking access type.
* `wait_finish` (`vcam_outbuf_lock`): 和 `wait_prepare` 對應。
> reacquire all locks released in the previous callback; required to continue operation after sleeping while waiting for a new buffer to arrive.
* `buf_prepare` (`vcam_out_buffer_prepare`)
* `buf_queue` (`vcam_out_buffer_queue`)
完成類似 file operation 的實作之後,填入結構 `vb2_ops vcam_vb2_ops`。接著註冊這些實作好的東西是在 `vcam_out_videobuf2_setup` 裡面達成。拿到 `vb2_queue` 開始填充成員,當然包含告知剛剛實作的 `ops`(`q->ops = &vcam_vb2_ops`)。另外 `io_modes` 表示該用何種方式來訪問 buffer,vcam 是用 `VB2_MMAP | VB2_USERPTR | VB2_READ` 表示:
* 緩衝區由內核空間分配,通過 `mmap` 映射緩衝區到用戶空間。通常 `vmalloc` 和 contiguous DMA buffers 來分配此方式的緩衝區。
* 緩衝區由使用者空間分配,通常需要設備支援集散 IO 才可以分配用戶空間緩衝區。有趣的是,videobuf2 支持分配連續的緩衝區 在使用者空間,但是需要運用一些特別的機制,比如 android 的 pmem 驅動。在使用者空間分配連續大量的頁面是不支援的。
* 使用者空間緩衝區提供 `read()` 方式訪問視頻設備。
最後透過 `vb2_queue_init` 完成初始化。
## 測試
### 功能性測驗
檢查是否能跑 initial code,檢查步驟可參考工水鳥整理的[筆記](https://hackmd.io/@bentu/SkUFns9XI?fbclid=IwAR0dip-LYX8zLmaOWMcCdEu5w8fCWPtjBLh9TV7vH0FRQG1jmfNboDlIvI4)。
## 實際使用 vcam driver
### 初探
**本段落以後均不仔細解釋 driver 實做原理,詳細可參考第二部份 vcam 運作原理內容**
新建一個 user application 即跑在 user mode 的程式。目的是為了熟悉和確認對 driver 間的關係是否正確,所以做了一些基本操作。首先在 main 打開在 `/dev` 底下的 video。這是我們載入 driver 時建立的。
```cpp
fd = open("/dev/video1", O_RDWR);
```
拿到 file descriptor 之後,就可以對其操作。可操作的東西定義在 `v4l2_ioctl_ops` 和 `v4l2_file_operations` 中。接著應證一下是否真為所述,以下嘗試使用 `ioctl` 中 query capability 的功能:
```cpp
ioctl(fd, VIDIOC_QUERYCAP, &caps)
```
接著可以在 device.c 的 `vcam_querycap` 中添加 `printk` 訊息,接著執行 `app.c` 可以發現 dmesg 中有剛剛添加的訊息,這代表我們在 user application 中的確呼叫了我們實做的 ioctl 操作。
此外,為了開發方便,可以在 Makefile 中加上
```shell
CFLAGS_device.o := -DDEBUG
```
這樣能讓 driver 中的 `pr_debug` 顯現在 dmesg 中。
[註]在追蹤程式碼時可透過 `grep --include=\*.{c,h} -rnw -e "regular expression"` 來找想找的關鍵字。
### 深入操作
* 本來想透過官網提供的範例撰寫 capture 的 user application,但是試了老半天猛然發現.....
> This Linux module implements a simplified virtual V4L2 compatible camera device driver with raw framebuffer input.
沒錯!起初我還對 v4l2 不太熟時忽略了文件的第一行,原來 vcam 是一個提供介面讓使用者輸入 raw data 進入了裝置。
* 在 vcam 中可以選擇兩種編碼,分別輸入的編碼和輸出的編碼,兩者可選是 rgb 或 yuyv。從 device.c 中可以發現輸出的編碼會和輸入的編碼一樣
```cpp
if (dev_spec && dev_spec->pix_fmt == VCAM_PIXFMT_YUYV)
vcam->out_fmts[0] = vcam_supported_fmts[1];
else
vcam->out_fmts[0] = vcam_supported_fmts[0];
```
也就是說在這種安排下根本不會進入第 597 行的轉行格式。當然,如果你把 `==` 改成 `!=` 就會進入轉換的程式碼區塊。但是結果都一樣。
* 承上,可透過 `vcam-util` 新增不同輸入編碼的 video,若今天想創一個吃 yuyv 編碼的裝置可透過
```cpp
sudo ./vcam-util -c -p yuyv
```
若餵入 {65, 66, 67} 進 yuyv 裝置會得到綠色。反之, rbg 裝置會產生灰色。關於兩種編碼的轉換可以透過這個[網站](https://www.mikekohn.net/file_formats/yuv_rgb_converter.php)來看。
* 使用 [v4l2-capture](https://gist.github.com/maxlapshin/1253534) 程式來抓取 video 的資料。若開啟的是 video0 (視訊鏡頭) 的話,產生的 raw data 可以直接開起來變一張圖片。但是如果要開 vcam-util 建立的 videox 的話就比較奇怪了。首先,想要讀 videox 時候(透過 capture.c),不能同時用 vlc 開 videox 來看目前的畫面該長怎樣(會出現 buffer is busy 的錯誤),這個問題聽說要用 double buffering 來解。但是仍有辦法讀出 videox 的 raw data frame,方式是(`app` 是參考[官方範例](https://gist.github.com/maxlapshin/1253534)對其改造)
```shell
$ ./rgb > /proc/vcamfb1 # terminal 1
```
```shell
$ ./app -o # terminal 2
```
如此一來,你可在資料夾底下看到一堆生成的 raw data。但是比較尷尬的是這些 raw data 不像前面開 video0 產生的 raw data 能開起來。我猜可能是因為我們在紀錄 raw data 的格式不對,導致根本開不起來(關於這點在稍後會提出解法)。
* 在 device.c 中有分 in-buffer 和 out-buffer,分別是編碼轉換的前後。而透過 capture.c 拿出來的資料是 out-buffer 也就是轉換後的結果。可以透過
```shell
$ ./rgb > /proc/vcamfb1 # terminal 1: format: rgb, write {65, 66, 67}
```
```shell
$ ./app -o # terminal 2
```
接在可以在產生的 raw data 拿到 ABCABC......
反之,如果寫到 yuyv 編碼的,你會拿到 ASCII {0, 150, 0} 的資料。
[註]由於 capture 會產生大量 raw date,為了方便,可以執行 `find capture_frame_data/ -regex '.*.raw' -delete` 即一次刪除那些 raw data。
### 添加功能
#### 將 raw data 轉成可看的圖片
倘若我們是透過官方網站提供的範例(capture.c),我們讀取出來的資料,即輸入的 raw input(e.g. ABCABC...)。產生這種格式的檔案其實一點幫助都沒有,因為無法開啟觀看。在專案下 [cpature_frame_data](https://github.com/eecheng87/vcam/tree/master/capture_frame_data) 底下的 capture.c 中寫檔(捕捉裝置的 buffer 的 pixel 值)的程式片段是:
```cpp
static void process_image(void *p, int size)
{
frame_number++;
char filename[15];
sprintf(filename, "frame-%d.raw", frame_number);
FILE *fp=fopen(filename,"wb");
if (out_buf)
fwrite(p, size, 1, fp);
fflush(fp);
fclose(fp);
}
```
以上是直接把 `buffers[0].start` 的 data 直接寫入檔案,而非正確的影像格式(如: jpg)。為了解決這個問題,可以增加將 raw data 轉成 jpg 格式的函數 `jpegWrite`:
```cpp
static void process_image(void *p, int size)
{
frame_number++;
char filename[15];
sprintf(filename, "frame-%d.jpg", frame_number);
FILE *fp=fopen(filename,"wb");
jpegWrite(p, filename);
fflush(fp);
fclose(fp);
}
```
這樣就可以在同個資料夾下看到『可觀測』之照片。
jpeg 的轉換可以參考第 61 行開始的 `static void jpegWrite(unsigned char* img, char* jpegFilename)`,這邊需要注意的是因為有使用 `jpeglib.h` 函式庫,所以在 Makefile 中必須加引數,否則會編譯失敗:
```shell
app: capture.c vdev2.h
$(CC) -o $@ $< -ljpeg
```
#### 撰寫能在 video 開啟指定圖片的功能
預期效果是能將特定的圖片,如將專案底下 `sample_image` 內的圖片顯示在 video1 上(透過 vlc)。
首先找一張圖片,透過 python 提供的套件把圖片大小改成專案統一的規格($640\times480$),接著透過 `PIL ` 套件將 pixel 值存成文字檔。(可在專底下的 [/python](https://github.com/eecheng87/vcam/tree/master/python) 找到相關程式碼 )
```python
for j in range(480):
for i in range(640):
f.write(str(pix[i,j][0])+' '+str(pix[i,j][1])+' '+str(pix[i,j][2])+'\n')
```
執行程式後預期會得到 `../sample_image/1.raw` 的 raw data,格式大概如下:
```shell
88 145 200
91 148 203
95 152 207
96 153 208
96 153 208
97 154 209
101 158 213
104 161 216
101 158 213
100 157 212
100 157 212
...
```
接著在寫一支 C 程式,將 raw data 讀出來再餵入 vcamfb,這邊唯一需要注意的是 unsigned char 的 specifier 是 `%hhu`。
```cpp
while (1) {
FILE *file = fopen("sample_image/1.raw", "r");
for (i = 0; i < 640; i++) {
for (j = 0; j < 480; j++) {
fscanf(file,"%hhu %hhu %hhu", &rgb[0], &rgb[1], &rgb[2]);
fwrite(rgb, sizeof(rgb), 1, stdout);
}
}
fclose(file);
}
```
測試的方式是:
```shell
$ ./rgb > /proc/vcamfb1 # terminal 1
$ vlc v4l2:///dev/video1 # terminal 2
```
預期在 vlc 可以看到:
![](https://i.imgur.com/eBJXIxV.jpg)
和範例圖片一樣。
#### 製作隨機 pattern 的畫面
倘若使用者沒有寫東西到 vcamfb 時,最出版本的狀況會看到漸層畫面,如以下示意圖:
![](https://i.imgur.com/dtrSwZJ.png)
本功能希望能夠達到和 [akvcam](https://github.com/webcamoid/akvcam) 內提供的功能一樣
> Configurable default picture in case no input signal available.
[註]關於 akvcam 的建置和操作可以參考下一個部份的整理
為了達到目的,我們需要先找出在 vcam 中實做此功能的程式碼。在 device.c 中`submitter_thread`處理著編碼的轉換(`in_buf` -> 編碼轉換 -> `out_buf`)。雖然看似毫無關聯,但實際上倘若沒有東西寫入 vcamfb 則會造成 `in_buf` 沒東西,所以這種情況會在 `submitter_thread` 中處理。以下是`submitter_thread`持續在做的區塊,主要判斷是否有東西寫入 frame buffer、buffer 空了等狀況。
```cpp
while (!kthread_should_stop()) {
struct vcam_out_buffer *buf;
struct vcam_out_buffer *dum_buf;
int timeout_ms, timeout;
/* Do something and sleep */
int computation_time_jiff = jiffies;
spin_lock_irqsave(&dev->out_q_slock, flags);
if (list_empty(&q->active)) {
/* A */
..
pr_debug("Buffer queue is empty\n");
goto have_a_nap;
}
buf = list_entry(q->active.next, struct vcam_out_buffer, list);
list_del(&buf->list);
spin_unlock_irqrestore(&dev->out_q_slock, flags);
if (!dev->fb_isopen) {
/* B */
submit_noinput_buffer(buf, dev);
} else {
/* C */
..
}
have_a_nap:
..
}
```
在沒有寫入東西的進 vcamfb 的情況下,大部分會走 `B` 區塊,偶爾會走 `A`(但目前找不太到如何控制進 A 或 B)。而有寫東西的進 vcamfb 則進 C,C 會做 scaling 等事情,但不是目前考量的重點。我們現在真正在意的是在 B 會呼叫 `submit_noinput_buffer`,接著往這裡仔細看。
原本的效果是會產生漸層畫面,以下是實做的程式碼:
```cpp
size_t rowsize = dev->output_format.bytesperline;
size_t rows = dev->output_format.height;
int stripe_size = (rows / 255);
for (i = 0; i < 255; i++) {
memset(vbuf_ptr, i, rowsize * stripe_size);
vbuf_ptr += rowsize * stripe_size;
}
if (rows % 255)
memset(vbuf_ptr, 0xff, rowsize * (rows % 255));
```
以上實做看起來還算符合預期,手法是一次寫一排(厚度由`stripe_size` 決定),而每排顏色一樣,隨著排數增加,pixel value 上升。直到 pixel 達灰階最高值 255,剩餘的排數全部填 `0xff`。
既然已經知道原版本程式碼寫在哪裡和怎麼實做,接著著手修改想要的效果。在 linux kernel 中要產生隨機的效果需要使用 [get_random_bytes](https://elixir.bootlin.com/linux/v4.3/source/drivers/char/random.c#L1253),這個函數的使用其實也是 [akvcam](https://github.com/webcamoid/akvcam) 產生雜訊效果的所採用。達成效果只要把前面的程式碼改成:
```cpp
else {
get_random_bytes(vbuf_ptr, rowsize * rows);
}
```
檢視效果的指令:
```shell
$ vlc v4l2:///dev/video1
```
輸出結果:
![](https://i.imgur.com/0Nhoh4f.jpg)
#### 視訊特效
在 line 視訊的時候,可以選擇特殊的濾鏡功能,在人臉上增加一些物件。如:人的頭上增加一對兔子耳朵。
示意圖如下:
![](https://i.imgur.com/PusaBls.png)
本部份希望能夠建立在 vcam 的基礎上達到此功能,此功能的程式碼實做在 [/extension]() 底下。以下是模擬的架構:
* video1 會有最初的視訊畫面 (e.g. 人臉的移動)
* 透過 ioctl 操作對 video1 做 capture,得到每個 pixel 的 rgb 值
* 將得到的 rgb 值 redirect 到另外一支程式,這個程式會偵測人臉並在**算出**兔耳朵應該出現的位置,最後輸出 $640\times480$ 個 rgb 值
* 將上個步驟的輸出 redirect 到 video2
* 透過 vlc 開啟 video2 則可以得到移動的人臉加上跟著移動的兔耳朵
##### 環境建立
為了模擬出上述效果,我們需要先建立一些「環境」。首先,我們要模擬移動的人臉,這裡我採用紅方塊代表人臉。而物件(兔子耳朵)會隨著人臉的移動而跟著移動到適合的位置,所以我們也需要製造出人臉(紅方塊)移動的感覺。
```shell
$ ./basic > vcmafb1 # terminal 1
$ vlc v4l2:///dev/video1 # terminal 2
```
預期可以看到以下畫面,且紅方塊會隨機移動。
![](https://i.imgur.com/w67Ccfr.png)
...
#### 將編碼轉換移出核心
原專案在傳入 rgb 格式的編碼,會在 kernel module 中做編碼的轉換,轉成 yuyv。因為在 kernel 沒辦法做浮點數運算,所以 vcam 用了一些方法避開此點。本功能是希望能夠在 user space 做提前的轉換,再在輸入餵入 vcamfb。雖然不知道這樣的效益怎麼樣,硬要講的話,好處大概就是能夠縮減 kernel 大小,和更精準。
## 參考相關專案 akvcam
在 vcam 中有提到 3 個 v4l2 相關的專案,選擇 akvcam 的原因是因為它較年輕,而且目前仍有在維護,所以問題可能會少一點,接著會簡單介紹如何建置,讓它順利跑起來。並提供簡單的範例。
### 下載原始碼
```shell
$ git clone https://github.com/webcamoid/akvcam.git
```
### 專案架構
![](https://i.imgur.com/t3UC3Fq.png)
稍後的實際操作可以透過這張圖更了解 akvcam 到底在幹麻
### 建置
首先你需要準備一個初始化檔案 .ini,當插入模組時 akvcam 會抓取這個檔案來判斷需要建什麼裝置和功能。以下提供一個**正確的**初始化檔案
```shell
[Cameras]
cameras/size = 2
cameras/1/type = output
cameras/1/mode = mmap, userptr, rw
cameras/1/description = Virtual Camera (output device)
cameras/1/formats = 2
cameras/1/videonr = 7
cameras/2/type = capture
cameras/2/mode = mmap, rw
cameras/2/description = Virtual Camera
cameras/2/formats = 1, 2
cameras/2/videonr = 9
[Formats]
formats/size = 2
formats/1/format = YUY2
formats/1/width = 640
formats/1/height = 480
formats/1/fps = 30
formats/2/format = RGB24, YUY2
formats/2/width = 640
formats/2/height = 480
formats/2/fps = 20/1, 15/2
[Connections]
connections/size = 1
connections/1/connection = 1:2
```
接著編譯專案
```shell
$ make
```
插入模組(需要傳遞參數,將剛剛建立的 config 檔案傳入)
```shell
$ sudo insmod akvcam.ko config_file="config.ini"
```
如果順利的話你將會在 `/dev` 底下發現新增了兩個 video device。
### 執行範例
在資料夾底下準備一個範例影片 small.webm,透過 ffmpeg 來傳入 output device。
```shell
$ ffmpeg -i small.webm -s 640x480 -r 30 -f v4l2 -vcodec rawvideo -pix_fmt rgb24 /dev/video7
```
接著透過 vlc 打開 capture device,則可看到傳入的影片 small.webm
```shell
$ vlc v4l2:///dev/video9
```
## 參考資料
* [device driver](https://silverfoxkkk.pixnet.net/blog/post/41156465)
* [v4l2](https://cuteparrot.pixnet.net/blog/post/205328299-v4l2-driver-step-by-step%28part1%29)
* [v4l2 框架概述](https://blog.csdn.net/u013904227/article/details/80718831)
* [linux docs](https://linuxtv.org/downloads/v4l-dvb-apis/driver-api/v4l2-intro.html)
* [v4l2 flow](https://blog.csdn.net/eastmoon502136/article/details/8190262)
* [v4l2 lwn](https://lwn.net/Articles/203924/)
* [Device driver Intro](https://events19.linuxfoundation.org/wp-content/uploads/2017/12/Introduction-to-Linux-Kernel-Driver-Programming-Michael-Opdenacker-Bootlin-.pdf)
* [kernel module](http://derekmolloy.ie/writing-a-linux-kernel-module-part-1-introduction/)
* [charac device](http://derekmolloy.ie/writing-a-linux-kernel-module-part-2-a-character-device/)
* [vfs](https://www.kernel.org/doc/Documentation/filesystems/vfs.txt)
* [videobuffer](https://lwn.net/Articles/447435/)
* [videobuffer(中文)](https://b8807053.pixnet.net/blog/post/9856492-the-videobuf2-api)
* [videobuffer(2)](http://www.yellowmax2001.com/2018/07/15/V4L2%E6%A1%86%E6%9E%B6-videobuf2/)
* [v4l2 capture example](https://gist.github.com/maxlapshin/1253534)
* [v4l2 capture explain](https://www.cnblogs.com/kevin-heyongyuan/articles/11070935.html)
* [v4l2 real time](https://lightbits.github.io/v4l2_real_time/)