[轻量级RTSP服务]Linux|麒麟操作系统下实现屏幕|摄像头|声音采集

时间:2023-02-13 13:01:59

背景

随着国产操作系统的推进,传统行业对Linux平台的呼声和需求越来越大,之前几年,我们发布了Linux平台运营商级的RTSP转RTMP推送模块、RTMP推送模块和RTSP、RTMP播放模块,前段时间,有开发者问我们,是不是可以在Linux平台实现轻量级RTSP服务,通过采集摄像头或者屏幕,在Linux平台实现类似于IPC的功能,便于第三方系统对接。

技术实现

轻量级RTSP服务实际上前几天我们在做Linux模块的时候,已经实现了,只是没有在demo上加这块,原因很简单,这块诉求一方面比较少,另一方面,我们windows、Android和iOS平台都有相关的接口和demo,接口调用基本类似。

废话不多说,上代码:

启动RTSP服务:

bool StartRTSPService(NT_SmartPublisherSDKAPI* push_api)
{
push_api->OpenRtspServer(&rtsp_server_handle_, 0);

if (!rtsp_server_handle_)
{
fprintf(stderr, "Create Rtsp Server failed..\n");
return false;
}
else
{
int port = 18554;
if (NT_ERC_OK != push_api->SetRtspServerPort(rtsp_server_handle_, port))
{
push_api->CloseRtspServer(rtsp_server_handle_);
rtsp_server_handle_ = nullptr;
fprintf(stderr, "Set Rtsp Server port failed, not in range..\n");
return false;
}

//std::string user_name = "admin";
//std::string password = "123456";
//push_api->SetRtspServerUserNamePassword(rtsp_server_handle_, user_name.c_str(), password.c_str());

//bool is_multicast = false;
//push_api->SetRtspServerMulticast(rtsp_server_handle_, is_multicast ? 1 : 0);

if (push_api->StartRtspServer(rtsp_server_handle_, 0) == 0) {
fprintf(stdout, "Start Rtsp server succeed!\n");
}
else
{
push_api->CloseRtspServer(rtsp_server_handle_);
rtsp_server_handle_ = nullptr;
fprintf(stderr, "Start Rtsp server failed, please check if port in usage..");
return false;
}
}

return true;
}

开始发布RTSP流:

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->IsCaptureXWindowSupported(NULL, &is_support_window_capture))
{
if (is_support_window_capture)
fprintf(stdout, "Support window capture\n");
else
fprintf(stdout, "UnSupport window capture\n");
}

if (is_support_window_capture)
{
NT_INT32 win_count = 0;
if (NT_ERC_OK == push_api->UpdateCaptureXWindowList(NULL, &win_count) && win_count > 0 )
{

fprintf(stdout, "X Capture Winows list++\n");

for (auto i = 0; i < win_count; ++i)
{
NT_UINT64 wid;
char title[512];

if (NT_ERC_OK == push_api->GetCaptureXWindowInfo(i, &wid, title, sizeof(title) / sizeof(char)))
{
x_win_list.push_back(wid);
fprintf(stdout, "wid:%llu, title:%s\n", wid, title);
}
}

fprintf(stdout, "X Capture Winows list--\n");
}
}

std::vector<CameraInfo> cameras;
GetCameraInfo(push_api, cameras);

if (!cameras.empty())
{
fprintf(stdout, "cameras count:%d\n", (int)cameras.size());

for (const auto& c : cameras)
{
fprintf(stdout, "camera name:%s, id:%s, cap_num:%d\n", c.name_.c_str(), c.id_.c_str(), (int)c.capabilities_.size());

for (const auto& i : c.capabilities_)
{
fprintf(stdout, "cap w:%d, h:%d, fps:%d\n", i.width_, i.height_, i.max_frame_rate_);
}
}
}

NT_UINT32 auido_option = NT_PB_E_AUDIO_OPTION_NO_AUDIO;

if (pulse_device_number > 0 || alsa_device_number > 0)
{
auido_option = NT_PB_E_AUDIO_OPTION_CAPTURE_MIC;
}
else if (capture_speaker_flag)
{
auido_option = NT_PB_E_AUDIO_OPTION_CAPTURE_SPEAKER;
}

//auido_option = NT_PB_E_AUDIO_OPTION_CAPTURE_MIC_SPEAKER_MIXER;

NT_UINT32 video_option = NT_PB_E_VIDEO_OPTION_SCREEN;

if (!cameras.empty())
{
video_option = NT_PB_E_VIDEO_OPTION_CAMERA;
}
else if (is_support_window_capture)
{
video_option = NT_PB_E_VIDEO_OPTION_WINDOW;
}

// video_option = NT_PB_E_VIDEO_OPTION_LAYER;

//video_option = NT_PB_E_VIDEO_OPTION_NO_VIDEO;

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, video_option, auido_option, 0, NULL))
{
return nullptr;
}

push_api->SetEventCallBack(push_handle, nullptr, OnSDKEventHandle);

//push_api->SetXDisplayName(push_handle, ":0");
//push_api->SetXDisplayName(push_handle, NULL);

// 视频层配置方式
if (NT_PB_E_VIDEO_OPTION_LAYER == video_option)
{
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_ = 200;
rgba_layer_c0->conf_.green_ = 200;
rgba_layer_c0->conf_.blue_ = 200;
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);

//// 第一层为窗口
if (!x_win_list.empty())
{
auto window_layer_c1 = std::make_shared<nt_pb_sdk::WindowLayerConfigWrapper>(index++, true, 0, 0, 640, 360);

window_layer_c1->conf_.xwindow_ = x_win_list.back();

layer_confs.push_back(window_layer_c1);
}

//// 摄像头层
if (!cameras.empty())
{
auto camera_layer_c1 = std::make_shared<nt_pb_sdk::CameraLayerConfigWrapper>(index++, true,
640, 0, 640, 360);

strcpy(camera_layer_c1->conf_.device_unique_id_, cameras.front().id_.c_str());

camera_layer_c1->conf_.is_flip_horizontal_ = 0;
camera_layer_c1->conf_.is_flip_vertical_ = 0;
camera_layer_c1->conf_.rotate_degress_ = 0;

layer_confs.push_back(camera_layer_c1);

if (cameras.size() > 1)
{
auto camera_layer_c2 = std::make_shared<nt_pb_sdk::CameraLayerConfigWrapper>(index++, true,
640, 0, 320, 240);

strcpy(camera_layer_c2->conf_.device_unique_id_, cameras.back().id_.c_str());

camera_layer_c2->conf_.is_flip_horizontal_ = 0;
camera_layer_c2->conf_.is_flip_vertical_ = 0;
camera_layer_c2->conf_.rotate_degress_ = 0;

layer_confs.push_back(camera_layer_c2);
}
}

auto image_layer1 = std::make_shared<nt_pb_sdk::ImageLayerConfigWrapper>(index++, true, 650, 120, 324, 300);

strcpy(image_layer1->conf_.file_name_utf8_, "./testpng/tca.png");

layer_confs.push_back(image_layer1);

auto image_layer2 = std::make_shared<nt_pb_sdk::ImageLayerConfigWrapper>(index++, true, 120, 380, 182, 138);

strcpy(image_layer2->conf_.file_name_utf8_, "./testpng/t4.png");

layer_confs.push_back(image_layer2);

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);

if (video_option == NT_PB_E_VIDEO_OPTION_CAMERA)
{
if (!cameras.empty())
{
push_api->SetVideoCaptureDeviceBaseParameter(push_handle, cameras.front().id_.c_str(),
640, 480);

//push_api->FlipVerticalCamera(push_handle, 1);
//push_api->FlipHorizontalCamera(push_handle, 1);
//push_api->RotateCamera(push_handle, 0);
}
}

if (video_option == NT_PB_E_VIDEO_OPTION_WINDOW)
{
if (!x_win_list.empty())
{
//push_api->SetCaptureXWindow(push_handle, x_win_list[0]);
push_api->SetCaptureXWindow(push_handle, x_win_list.back());
}
}

push_api->SetFrameRate(push_handle, dst_fps); // 帧率设置

push_api->SetVideoEncoder(push_handle, 0, 1, NT_MEDIA_CODEC_ID_H264, 0);

push_api->SetVideoBitRate(push_handle, 2000); // 平均码率2000kbps
push_api->SetVideoQuality(push_handle, 26);
push_api->SetVideoMaxBitRate(push_handle, 4000); // 最大码率4000kbps

push_api->SetVideoEncoderSpecialInt32Option(push_handle, "usage_type", 0); //0是摄像头编码, 1是屏幕编码
push_api->SetVideoEncoderSpecialInt32Option(push_handle, "rc_mode", 1); // 0是质量模式, 1是码率模式
push_api->SetVideoEncoderSpecialInt32Option(push_handle, "enable_frame_skip", 0); // 0是关闭跳帧, 1是打开跳帧

push_api->SetVideoKeyFrameInterval(push_handle, dst_fps*2); // 关键帧间隔
push_api->SetVideoEncoderProfile(push_handle, 3); // H264 high
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->SetEchoCancellation(push_handle, 1, 0);
push_api->SetNoiseSuppression(push_handle, 1);
push_api->SetAGC(push_handle, 1);
push_api->SetVAD(push_handle, 1);

push_api->SetInputAudioVolume(push_handle, 0, 1.0);
push_api->SetInputAudioVolume(push_handle, 1, 0.2);

// 音频配置
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;
}

//启动轻量级RTSP服务
bool is_rtsp_service_started = StartRTSPService(push_api);

if (is_rtsp_service_started)
{
if (!rtsp_server_handle_)
{
fprintf(stderr, "StartRtspStream rtsp server handle is null..");
return nullptr;
}

std::string rtsp_stream_name = "stream1";
push_api->SetRtspStreamName(push_handle, rtsp_stream_name.c_str());

push_api->ClearRtspStreamServer(push_handle);

push_api->AddRtspStreamServer(push_handle, rtsp_server_handle_, 0);

if (NT_ERC_OK != push_api->StartRtspStream(push_handle, 0))
{
push_api->Close(push_handle);
push_handle = nullptr;
return nullptr;
}

fprintf(stdout, "StartRtspStream succeed..\n");
}

return push_handle;
}
}

main函数调用如下:

/*
* Author: daniusdk.com
*/
int main(int argc, char *argv[])
{
//signal(SIGINT, &OnSigIntHandler);
//signal(SIGFPE, &OnSigIntHandler);

struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_sigaction = OnSaSigaction;
act.sa_flags = SA_SIGINFO;

sigaction(SIGINT, &act, NULL);
sigaction(SIGFPE, &act, NULL);

XInitThreads(); // X支持多线程, 必须调用

auto display = XOpenDisplay(nullptr);
if (!display)
{
fprintf(stderr, "Cannot connect to X server\n");
return 0;
}

auto screen = DefaultScreen(display);
auto root = XRootWindow(display, screen);

XWindowAttributes root_win_att;
if (!XGetWindowAttributes(display, root, &root_win_att))
{
fprintf(stderr, "Get Root window attri failed\n");
XCloseDisplay(display);
return 0;
}

int main_w = root_win_att.width / 2, main_h = root_win_att.height / 2;

auto black_pixel = BlackPixel(display, screen);
auto white_pixel = WhitePixel(display, screen);

auto main_wid = XCreateSimpleWindow(display, root, 0, 0, main_w, main_h, 0, white_pixel, black_pixel);
if (!main_wid)
{
fprintf(stderr, "Cannot Create Main Window\n");
XCloseDisplay(display);
return 0;
}

XSelectInput(display, main_wid, StructureNotifyMask | KeyPressMask);

auto sub_wid = CreateSubWindow(display, screen, main_wid);
if (!sub_wid)
{
fprintf(stderr, "Cannot Create Render Window\n");
XDestroyWindow(display, main_wid);
XCloseDisplay(display);
return 0;
}

XMapWindow(display, main_wid);
XStoreName(display, main_wid, "Video Preview");
XMapWindow(display, sub_wid);


LogInit();

NT_SmartPublisherSDKAPI push_api;
if (!PushSDKInit(push_api))
{
XDestroyWindow(display, sub_wid);
XDestroyWindow(display, main_wid);
XCloseDisplay(display);

return 0;
}

auto push_handle = StartPush(&push_api, "rtmp://192.168.0.103:1935/hls/test1", 30);
if (!push_handle)
{
fprintf(stderr, "start push failed.\n");

XDestroyWindow(display, sub_wid);
XDestroyWindow(display, main_wid);
XCloseDisplay(display);

push_api.UnInit();
return 0;
}

// 开启预览,也可以不开启, 根据需求来
push_api.SetPreviewXWindow(push_handle, "", sub_wid);
push_api.StartPreview(push_handle, 0, nullptr);

// auto push_handle1 = StartPush(&push_api, "rtmp://192.168.0.154:1935/live/test1", 30);

while (!g_is_exit)
{
while (MY_X11_Pending(display, 10))
{
XEvent xev;
memset(&xev, 0, sizeof(xev));
XNextEvent(display, &xev);

if (xev.type == ConfigureNotify)
{
if (xev.xconfigure.window == main_wid)
{
if (xev.xconfigure.width != main_w || xev.xconfigure.height != main_h)
{
main_w = xev.xconfigure.width;
main_h = xev.xconfigure.height;

XMoveResizeWindow(display, sub_wid, 0, 0, main_w - 4, main_h - 4);
}
}
}
else if (xev.type == KeyPress)
{
if (xev.xkey.keycode == XKeysymToKeycode(display, XK_Escape))
{
fprintf(stdout, "ESC Key Press\n");
g_is_exit = true;
}
}

if (g_is_exit)
break;
}
}

fprintf(stdout, "Skip run loop, is_exit:%d\n", g_is_exit);

push_api.StopPreview(push_handle);

push_api.StopRtspStream(push_handle);

push_api.StopRtspServer(rtsp_server_handle_);

push_api.CloseRtspServer(rtsp_server_handle_);

rtsp_server_handle_ = nullptr;

push_api.Close(push_handle);

push_handle = nullptr;

XDestroyWindow(display, sub_wid);
XDestroyWindow(display, main_wid);
XCloseDisplay(display);

push_api.UnInit();

fprintf(stdout, "SDK UnInit..\n");

return 0;
}

音视频选项,可设置的类型如下:

/*定义Video源选项*/
typedef enum _NT_PB_E_VIDEO_OPTION
{
NT_PB_E_VIDEO_OPTION_NO_VIDEO = 0x0,
NT_PB_E_VIDEO_OPTION_SCREEN = 0x1, // 采集屏幕
NT_PB_E_VIDEO_OPTION_CAMERA = 0x2, // 摄像头采集
NT_PB_E_VIDEO_OPTION_LAYER = 0x3, // 视频合并,比如桌面叠加摄像头等
NT_PB_E_VIDEO_OPTION_ENCODED_DATA = 0x4, // 已经编码的视频数据,目前支持H264
NT_PB_E_VIDEO_OPTION_WINDOW = 0x5, // 采集窗口
} NT_PB_E_VIDEO_OPTION;

/*定义Auido源选项*/
typedef enum _NT_PB_E_AUDIO_OPTION
{
NT_PB_E_AUDIO_OPTION_NO_AUDIO = 0x0,
NT_PB_E_AUDIO_OPTION_CAPTURE_MIC = 0x1, // 采集麦克风音频
NT_PB_E_AUDIO_OPTION_CAPTURE_SPEAKER = 0x2, // 采集扬声器
NT_PB_E_AUDIO_OPTION_CAPTURE_MIC_SPEAKER_MIXER = 0x3, // 麦克风扬声器混音
NT_PB_E_AUDIO_OPTION_ENCODED_DATA = 0x4, // 编码后的音频数据,目前支持AAC, speex宽带(wideband mode)
NT_PB_E_AUDIO_OPTION_EXTERNAL_PCM_DATA = 0x5, /*外部PCM数据*/
NT_PB_E_AUDIO_OPTION_MIC_EXTERNAL_PCM_MIXER = 0x6, /* 麦克风和外部PCM数据混音 当前只支持一路外部音频和内置麦克风混音*/
NT_PB_E_AUDIO_OPTION_TWO_EXTERNAL_PCM_MIXER = 0x7, /* 两路外部PCM数据混音*/
} NT_PB_E_AUDIO_OPTION;

调用流程和Windows平台轻量级RTSP服务基本一致(实际上接口本身几无差别)。

总结

Linux平台轻量级RTSP服务,对我们来说,轻车熟路了,主要是demo展示这块,整体编码性能和延迟,配合我们的RTSP播放器测试下来,几百毫秒,可完全满足无纸化同屏、教育类等传统行业技术诉求。