Linux(含麒麟操作系统)如何实现多显示器屏幕采集录制
技术背景
在操作系统领域,很多核心技术掌握在国外企业手中。如果过度依赖国外技术,在国际形势变化、贸易摩擦等情况下,可能面临技术封锁和断供风险。开发国产操作系统可以降低这种风险,确保国家关键信息基础设施的稳定运行。在一些敏感行业如国防、金融等,对技术的自主可控要求极高。
音视频信息在很多场合涉及国家安全和敏感内容。如果操作系统的音视频模块依赖国外技术,可能存在安全漏洞被利用的风险,导致国家机密信息泄露。自主开发的音视频模块可以进行更严格的安全管控,增强国家信息安全防护能力。
在这样的背景下,我们实现了Linux平台下的以屏幕采集、摄像头采集、麦克风扬声器采集为数据源的RTMP推送模块、轻量级RTSP服务模块,和RTMP播放器和RTSP播放器模块,并同时覆盖了x86-64架构和aarch64架构。
技术实现
xrandr
本文我们要讨论的是,如何在Linux平台实现多显示器的屏幕采集录制。我们知道,Linux下,X Window Sysem支持多显示器的配置和显示器列表获取。可以使用xrandr查看显示器列表:
“xrandr --listactivemonitors” 可在Linux 系统中用于显示当前活动监视器信息的命令。
命令作用
-
显示连接状态
- 该命令可以列出当前连接到系统的所有活动监视器,包括其名称、分辨率、刷新率以及位置信息等。通过查看这些信息,你可以了解到每个监视器的连接状态和基本参数。
- 例如,如果你连接了多个显示器,这个命令可以帮助你确定哪些显示器是处于活动状态的,以及它们的具体配置。
-
帮助配置多显示器
- 对于使用多显示器的用户来说,这个命令非常有用。它可以让你了解当前的显示器布局,以便更好地进行配置和调整。
- 你可以根据命令输出的信息,使用其他 xrandr 命令来设置显示器的分辨率、位置、旋转等参数,实现个性化的多显示器设置。
如何使用libXrandr获取显示器列表
先看看Xrandr.h
/*
* Copyright © 2000 Compaq Computer Corporation, Inc.
* Copyright © 2002 Hewlett-Packard Company, Inc.
* Copyright © 2006 Intel Corporation
* Copyright © 2008 Red Hat, Inc.
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that copyright
* notice and this permission notice appear in supporting documentation, and
* that the name of the copyright holders not be used in advertising or
* publicity pertaining to distribution of the software without specific,
* written prior permission. The copyright holders make no representations
* about the suitability of this software for any purpose. It is provided "as
* is" without express or implied warranty.
*
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
* INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
* EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
* DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
* OF THIS SOFTWARE.
*
* Author: Jim Gettys, HP Labs, Hewlett-Packard, Inc.
* Keith Packard, Intel Corporation
*/
#ifndef _XRANDR_H_
#define _XRANDR_H_
#include <X11/extensions/randr.h>
#include <X11/extensions/Xrender.h>
#include <X11/Xfuncproto.h>
_XFUNCPROTOBEGIN
typedef struct _XRRMonitorInfo {
Atom name;
Bool primary;
Bool automatic;
int noutput;
int x;
int y;
int width;
int height;
int mwidth;
int mheight;
RROutput *outputs;
} XRRMonitorInfo;
XRRMonitorInfo *
XRRAllocateMonitor(Display *dpy, int noutput);
XRRMonitorInfo *
XRRGetMonitors(Display *dpy, Window window, Bool get_active, int *nmonitors);
void
XRRSetMonitor(Display *dpy, Window window, XRRMonitorInfo *monitor);
void
XRRDeleteMonitor(Display *dpy, Window window, Atom name);
void
XRRFreeMonitors(XRRMonitorInfo *monitors);
_XFUNCPROTOEND
#endif /* _XRANDR_H_ */
我们Linux平台RTMP推送、轻量级RTSP服务设计的选择接口如下:
/*
* nt_liunx_smart_publisher_sdk.h
* Author: daniusdk.com
* WeChat: xinsheng120
*/
/*
* 获取active XRR-Monitor列表, X RandR 1.5及以上版本支持(可以用:"xrandr --version"查看版本)
* x_display_name:传入实际使用的名称或者NULL,调用XOpenDisplay时使用.
* monitors: 预分配的XRR-Monitor数组
* monitors_array_size: 预分配的XRR-Monitor数组长度
* out_count: 返回实际的XRR-Monitor数量
* 成功返回:NT_ERC_OK, 如果返回值是:NT_ERC_MORE_DATA, 说明预分配的数组长度不够, 请分配更大的数组再尝试.
*/
NT_UINT32 NT_API NT_PB_GetXRRMonitors(NT_PCSTR x_display_name, NT_PB_XRR_MonitorBaseInfo* monitors, NT_INT32 monitors_array_size, NT_INT32* out_count);
/*
* 设置要采集的XRRMonitor id, 采集X屏幕时使用
* xrr_monitor_id: -1:采集所有屏幕, SDK默认为-1.
*/
NT_UINT32 NT_API NT_PB_SetCaptureXRRMonitor(NT_HANDLE handle, NT_INT64 xrr_monitor_id);
NT_PB_GetXRRMonitors()可以获取active XRR-Monitor列表。
NT_PB_SetCaptureXRRMonitor()设置要采集的XRRMonitor id, 采集X屏幕时使用。
以RTMP推送模块为例,目前我们的功能设计如下:
Linux平台x64_64架构|aarch64架构RTMP直播推送模块
- 音频编码:AAC/SPEEX;
- 视频编码:H.264;
- 推流协议:RTMP;
- [音视频]支持纯音频/纯视频/音视频推送;
- 支持X11屏幕采集;
- 支持部分V4L2摄像头设备采集;
- [屏幕/V4L2摄像头]支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
- [V4L2摄像头]支持V4L2摄像头设备选择(设备文件名范围:[/dev/video0, /dev/video63])、分辨率设置、帧率设置;
- [V4L2摄像头]支持水平反转、垂直反转、0° 90° 180° 270°旋转;
- [音频]支持基于alsa-lib接口的音频采集;
- [音频]支持基于libpulse接口采集本机PulseAudio服务音频;
- [预览]支持推送端实时预览;
- [对接服务器]支持自建标准RTMP服务器或CDN;
- 支持断网自动重连、网络状态回调;
- 屏幕和摄像头合成/多层合成;
- 支持窗口采集(一般不建议使用);
- 支持实时快照;
- 支持降噪处理、自动增益控制、VAD端点检测;
- 支持扬声器和麦克风混音;
- 支持外部编码前音视频数据对接;
- 支持外部编码后音视频数据对接;
- 支持实时音量调节;
- 支持扩展录像模块;
- 支持Unity接口;
- 支持H.264扩展SEI发送模块;
- 支持x64_64架构、aarch64架构(需要glibc-2.21及以上版本的Linux系统, 需要libX11.so.6, 需要GLib–2.0, 需安装 libstdc++.so.6.0.21、GLIBCXX_3.4.21、 CXXABI_1.3.9)。
RTMP推送调用示例
以大牛直播SDK的Linux平台RTMP直播推送模块为例,本Demo实现的是Linux上实现桌面和系统声音采集,然后使用RTMP协议推出去的一个SDK. 集成调用非常简单。
int main(int argc, char *argv[])
{
signal(SIGINT, &OnSigIntHandler);
//printf("sizeof(NT_SmartPublisherSDKAPI)=%d\n", sizeof(NT_SmartPublisherSDKAPI));
LogInit();
NT_SmartPublisherSDKAPI push_api;
if (!PushSDKInit(push_api))
{
return 0;
}
auto push_handle = StartPush(&push_api, "rtmp://192.168.0.154:1935/live/test1", 30);
if (!push_handle)
{
fprintf(stderr, "start push failed.\n");
push_api.UnInit();
return 0;
}
while (!g_is_exit)
{
sleep(2);
}
fprintf(stdout, "Skip run loop, is_exit:%d\n", g_is_exit);
push_api.StopPublisher(push_handle);
push_api.Close(push_handle);
push_handle = nullptr;
push_api.UnInit();
fprintf(stdout, "SDK UnInit..\n");
return 0;
}
相关初始化
void OnSigIntHandler(int sig)
{
if (SIGINT == sig)
{
g_is_exit = true;
}
}
void LogInit()
{
SmartLogAPI log_api;
memset(&log_api, 0, sizeof(log_api));
GetSmartLogAPI(&log_api);
log_api.SetLevel(SL_INFO_LEVEL);
log_api.SetPath((NT_PVOID)"./");
}
bool PushSDKInit(NT_SmartPublisherSDKAPI& push_api)
{
memset(&push_api, 0, sizeof(push_api));
NT_GetSmartPublisherSDKAPI(&push_api);
auto ret = push_api.Init(0, nullptr);
if (NT_ERC_OK != ret)
{
fprintf(stderr, "push_api.Init failed!\n");
return false;
}
else
{
fprintf(stdout, "push_api.Init ok!\n");
}
return true;
}
推送接口封装
NT_HANDLE StartPush(NT_SmartPublisherSDKAPI* push_api, const std::string& rtmp_url, int dst_fps)
{
NT_INT32 pulse_device_number = 0;
if (NT_ERC_OK == push_api->GetAuidoInputDeviceNumber(2, &pulse_device_number))
{
fprintf(stdout, "Pulse device num:%d\n", pulse_device_number);
char device_name[512];
for (auto i = 0; i < pulse_device_number; ++i)
{
if (NT_ERC_OK == push_api->GetAuidoInputDeviceName(2, i, device_name, 512))
{
fprintf(stdout, "index:%d name:%s\n", i, device_name);
}
}
}
NT_INT32 alsa_device_number = 0;
if (pulse_device_number < 1)
{
if (NT_ERC_OK == push_api->GetAuidoInputDeviceNumber(1, &alsa_device_number))
{
fprintf(stdout, "Alsa device num:%d\n", alsa_device_number);
char device_name[512];
for (auto i = 0; i < alsa_device_number; ++i)
{
if (NT_ERC_OK == push_api->GetAuidoInputDeviceName(1, i, device_name, 512))
{
fprintf(stdout, "index:%d name:%s\n", i, device_name);
}
}
}
}
NT_INT32 capture_speaker_flag = 0;
if ( NT_ERC_OK == push_api->IsCanCaptureSpeaker(2, &capture_speaker_flag) )
{
if (capture_speaker_flag)
fprintf(stdout, "Support speaker capture\n");
else
fprintf(stdout, "UnSupport speaker capture\n");
}
NT_INT32 is_support_window_capture = 0;
if (NT_ERC_OK == push_api->IsCaptureWindowSupported(NULL, &is_support_window_capture))
{
if (is_support_window_capture)
fprintf(stdout, "Support window capture\n");
else
fprintf(stdout, "UnSupport window capture\n");
}
NT_HANDLE push_handle = nullptr;
// if (NT_ERC_OK != push_api->Open(&push_handle, NT_PB_E_VIDEO_OPTION_LAYER, NT_PB_E_AUDIO_OPTION_CAPTURE_SPEAKER, 0, NULL))
if (NT_ERC_OK != push_api->Open(&push_handle, NT_PB_E_VIDEO_OPTION_SCREEN, NT_PB_E_AUDIO_OPTION_CAPTURE_SPEAKER, 0, NULL))
{
return nullptr;
}
//push_api->SetXDisplayName(push_handle, ":0");
//push_api->SetXDisplayName(push_handle, NULL);
// 视频层配置方式
//std::vector<std::shared_ptr<nt_pb_sdk::layer_conf_wrapper_base> > layer_confs;
//auto index = 0;
第0层填充RGBA矩形, 目的是保证帧率, 颜色就填充全黑
//auto rgba_layer_c0 = std::make_shared<nt_pb_sdk::RGBARectangleLayerConfigWrapper>(index++, true, 0, 0, 1280, 720);
//rgba_layer_c0->conf_.red_ = 0;
//rgba_layer_c0->conf_.green_ = 0;
//rgba_layer_c0->conf_.blue_ = 0;
//rgba_layer_c0->conf_.alpha_ = 255;
//layer_confs.push_back(rgba_layer_c0);
第一层为桌面层
//auto screen_layer_c1 = std::make_shared<nt_pb_sdk::ScreenLayerConfigWrapper>(index++, true, 0, 0, 1280, 720);
//
//screen_layer_c1->conf_.scale_filter_mode_ = 3;
//layer_confs.push_back(screen_layer_c1);
//std::vector<const NT_PB_LayerBaseConfig* > layer_base_confs;
//for (const auto& i : layer_confs)
//{
// layer_base_confs.push_back(i->getBase());
//}
//if (NT_ERC_OK != push_api->SetLayersConfig(push_handle, 0, layer_base_confs.data(),
// layer_base_confs.size(), 0, nullptr))
//{
// push_api->Close(push_handle);
// push_handle = nullptr;
// return nullptr;
//}
// push_api->SetScreenClip(push_handle, 0, 0, 1280, 720);
push_api->SetFrameRate(push_handle, dst_fps); // 帧率设置
push_api->SetVideoBitRate(push_handle, 2000); // 平均码率2000kbps
push_api->SetVideoQualityV2(push_handle, 26);
push_api->SetVideoMaxBitRate(push_handle, 4000); // 最大码率4000kbps
push_api->SetVideoKeyFrameInterval(push_handle, dst_fps*2); // 关键帧间隔
push_api->SetVideoEncoderProfile(push_handle, 3); // h264 baseline
push_api->SetVideoEncoderSpeed(push_handle, 3); // 编码速度设置到3
if (pulse_device_number > 0)
{
push_api->SetAudioInputLayer(push_handle, 2);
push_api->SetAuidoInputDeviceId(push_handle, 0);
}
else if (alsa_device_number > 0)
{
push_api->SetAudioInputLayer(push_handle, 1);
push_api->SetAuidoInputDeviceId(push_handle, 0);
}
// 音频配置
push_api->SetPublisherAudioCodecType(push_handle, 1);
//push_api->SetMute(push_handle, 1);
if ( NT_ERC_OK != push_api->SetURL(push_handle, rtmp_url.c_str(), NULL) )
{
push_api->Close(push_handle);
push_handle = nullptr;
return nullptr;
}
if ( NT_ERC_OK != push_api->StartPublisher(push_handle, NULL) )
{
push_api->Close(push_handle);
push_handle = nullptr;
return nullptr;
}
return push_handle;
}