背景
随着国产操作系统的推进,传统行业对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 // openh264 配置特定参数 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播放器测试下来,几百毫秒,可完全满足无纸化同屏、教育类等传统行业技术诉求。