Linux Screen Capture

Linux屏幕捕获最简单的方式就是使用FFmpeg,一些Linux上的开源录屏软件也是基于FFmpeg开发的,如vokoscreen。如果需要在Linux上实现录屏功能(单一屏幕捕获方式、单一编码格式),只需要使用FFmpeg中的一小部分功能,然而又不想把FFmpeg引到自己的项目中,那么可以参考FFmpeg实现自己的功能。第一、可以熟悉Linux屏幕的捕获方法;第二、如果出现bug(虽然FFmpeg出bug的可能性比较小),也便于自己调试和修复。本文主要介绍使用xcb实现Linux上的屏幕捕获功能。


The X protocol C-language Binding (XCB) is a replacement for Xlib featuring a small footprint, latency hiding, direct access to the protocol, improved threading support, and extensibility.[1]

在Linux上可以使用Ctrl+Alt+Fx键在字符终端与窗口界面之间进行切换。如果要捕获窗口界面又想跳过root权限,那么选用X协议窗口系统提供的接口是个不错的选择(什么是X protocol可以参看Wikipedia);如果要捕获字符终端的屏幕,X窗口系统就爱莫能助了,这个时候可以通过读取fb0来获取屏幕数据,但是需要root权限。为什么选用xcb呢?嗯嗯…… 除了Xlib,好像没得选了。以下是通过xcb捕获屏幕的具体步骤。

建立连接

X协议的窗口系统是基于网络的,存在一个服务端X-server,而我们就是客户端。
客户端在进行一系列操作之前需要先与服务端建立连接

1
2
/* Open the connection to the X server */
xcb_connection_t *connection = xcb_connect (NULL, NULL);

获取屏幕参数

可以理解窗口系统是基于树的,那么要找到根窗口。

1
2
3
4
/* Get the first screen */
const xcb_setup_t *setup = xcb_get_setup (connection);
xcb_screen_iterator_t iter = xcb_setup_roots_iterator (setup);
xcb_screen_t *screen = iter.data;

这个时候就知道屏幕的宽度和高度了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Get bpp */
const xcb_format_t* fmt = xcb_setup_pixmap_formats(setup);
int length = xcb_setup_pixmap_formats_length(setup);
int bpp = 0;
while (length--) {
if (screen->root_depth == fmt->depth) {
bpp = fmt->bits_per_pixel;
break;
}
fmt++;
}

uint32_t width = screen->width_in_pixels;
uint32_t height = screen->height_in_pixels;
uint32_t stride = screen->width_in_pixels * (bpp >> 3);

屏幕的数据格式中一个depth只会对应一个bpp,但一个bpp可以对应多个depth。

分配内存

知道屏幕参数了,那么在获取屏幕数据时要让X-server知道有那么一块地方可以存储数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
xcb_shm_segment_info_t shminfo;
shminfo.shmid = shmget(IPC_PRIVATE, stride * height, (IPC_CREAT | 0666));
if (shminfo.shmid == (uint32_t)-1) {
// error stuff
}

shminfo.shmaddr = (uint8_t*)shmat(shminfo.shmid, 0, 0);
if (shminfo.shmaddr == (uint8_t*)-1) {
// error stuff
}

shminfo.shmseg = xcb_generate_id(connection_);
xcb_shm_attach(connection_, shminfo.shmseg, shminfo.shmid, 0);

这就是Linux进程间通信的一种方式:共享内存,shminfo.shmaddr就是共享内存的首地址。

捕获屏幕像素

既然存储屏幕数据的内存有了,那么这个时候就可以让X-server把屏幕数据拷贝到这块内存了。

1
2
3
4
5
6
7
8
9
10
11
12
13
xcb_generic_error_t*       e      = nullptr;
xcb_shm_get_image_cookie_t cookie = xcb_shm_get_image(connection, screen->root, 0, 0, width, height, ~0, XCB_IMAGE_FORMAT_Z_PIXMAP, shminfo.shmseg, 0);
xcb_shm_get_image_reply_t* reply = xcb_shm_get_image_reply(connection, cookie, &e);

if (e) {
// error stuff
}

if (reply) {
free(reply);
}

xcb_flush(connection);

现在shminfo.shmaddr里面就是当前屏幕的像素数据了,这段代码一般放在while循环里。

释放内存和断开连接

最后如果结束了要记得断开连接和释放内存。

1
2
3
4
5
6
xcb_shm_detach(connection, shminfo.shmseg);

shmdt(shminfo.shmaddr);
shmctl(shminfo.shmid, IPC_RMID, 0);

xcb_disconnect(connection);

是不是很简单,别高兴的太早,这个时候好像还缺点什么?没有光标、没有编码、没有存储、没有音频。


参考资料

[1]. xcb
[2]. https://stackoverflow.com/questions/32972539/gdk-x11-screen-capture
[3]. https://github.com/bawNg/krad_radio
[4]. http://ffmpeg.org/doxygen/trunk/xcbgrab_8c_source.html