技术背景
无论是Windows平台还是Linux,多路播放诉求非常普遍,比如针对智慧工地、展馆、教育等宏观场景下的摄像头展示,关于RTSP或RTMP直播播放器开发需要注意的点,可参考之前博客,总的来说有以下一些点:
1. 低延迟:大多数RTSP的播放都面向直播场景,所以,如果延迟过大,比如监控行业,小偷都走了,客户端才看到,或者别人已经按过门铃几秒,主人才看到图像,严重影响体验,所以,低延迟是衡量一个好的RTSP播放器非常重要的指标,目前大牛直播SDK的RTSP播放延迟控制在几百毫秒,VLC在几秒,这个延迟,是长时间的低延迟,比如运行1天、一周、一个月甚至更久;
2. 音视频同步或跳转:有些开发者为了追求低延迟体验,甚至不做音视频同步,拿到audio video直接播放,导致a/v不同步,还有就是时间戳乱跳;
3. 支持多实例:一个好的播放器,需要支持同时播放多路音视频数据,比如4-8-9-16-32窗口;
4. 支持buffer time设置:在一些有网络抖动的场景,播放器需要支持精准的buffer time设置,一般来说,以毫秒计;
5. H.265的播放和录制:除了H.264,还需要支持H.265,目前市面上的RTSP H.265摄像头越来越多,支持H.265的RTSP播放器迫在眉睫,此外,单纯的播放H.265还不够,还需要可以能把H.265的数据能录制下来;
6. TCP/UDP模式切换:考虑到好多服务器仅支持TCP或UDP模式,一个好的RTSP播放器需要支持TCP/UDP模式自动切换;
7. 静音支持:比如,多窗口播放RTSP流,如果每个audio都播放出来,体验非常不好,所以实时静音功能非常必要;
8. 视频view旋转:好多摄像头由于安装限制,导致图像倒置,所以一个好的RTSP播放器应该支持如视频view实时旋转(0° 90° 180° 270°)、水平反转、垂直反转;
9. 支持解码后audio/video数据输出(可选):大牛直播SDK接触到好多开发者,希望能在播放的同时,获取到YUV或RGB数据,进行人脸匹配等算法分析,所以音视频回调可选;
10. 快照:感兴趣或重要的画面,实时截取下来非常必要;
11. 网络抖动处理(如断网重连):基本功能,不再赘述;
12. 跨平台:一个好的播放器,跨平台(Windows/Android/iOS)很有必要,起码为了后续扩展性考虑,开发的时候,有这方面的考虑,目前大牛直播SDK的RTSP播放器,完美支持以上平台;
13. 长期运行稳定性:提到稳定性,好多开发者不以为然,实际上,一个好的产品,稳定是最基本的前提,不容忽视! 14. 可以录像:播放的过程中,随时录制下来感兴趣的视频片断,存档或其他二次处理;
15. log信息记录:整体流程机制实时反馈,不多打log,但是不能一些重要的log,如播放过程中出错等;
16. download速度实时反馈:可以看到实时下载速度反馈,以此来监听网络状态;
17. 异常状态处理:如播放的过程中,断网、网络抖动、来电话、切后台后返回等各种场景的处理。
代码实现
本文以大牛直播SDK(官方)的Linux平台为例,介绍下RTMP或RTSP流多路播放集成。
代码语言:javascript复制int main(int argc, char *argv[])
{
XInitThreads(); // X支持多线程, 必须调用
NT_SDKLogInit();
// SDK初始化
SmartPlayerSDKAPI player_api;
if (!NT_PlayerSDKInit(player_api))
{
fprintf(stderr, "SDK init failed.n");
return 0;
}
auto display = XOpenDisplay(nullptr);
if (!display)
{
fprintf(stderr, "Cannot connect to X servern");
player_api.UnInit();
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 failedn");
player_api.UnInit();
XCloseDisplay(display);
return 0;
}
if (root_win_att.width < 100 || root_win_att.height < 100)
{
fprintf(stderr, "Root window size error.n");
player_api.UnInit();
XCloseDisplay(display);
return 0;
}
fprintf(stdout, "Root Window Size:%d*%dn", root_win_att.width, root_win_att.height);
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)
{
player_api.UnInit();
XCloseDisplay(display);
fprintf(stderr, "Cannot create main windowsn");
return 0;
}
XSelectInput(display, main_wid, StructureNotifyMask | KeyPressMask);
XMapWindow(display, main_wid);
XStoreName(display, main_wid, win_base_title);
std::vector<std::shared_ptr<NT_PlayerSDKWrapper> > players;
for (auto url: players_url_)
{
auto i = std::make_shared<NT_PlayerSDKWrapper>(&player_api);
i->SetDisplay(display);
i->SetScreen(screen);
i->SetURL(url);
players.push_back(i);
if ( players.size() > 3 )
break;
}
auto border_w = 2;
std::vector<NT_LayoutRect> layout_rects;
SubWindowsLayout(main_w, main_h, border_w, static_cast<int>(players.size()), layout_rects);
for (auto i = 0; i < static_cast<int>(players.size()); i)
{
assert(players[i]);
players[i]->SetWindow(CreateSubWindow(display, screen, main_wid, layout_rects[i], border_w));
}
for (const auto& i : players)
{
assert(i);
if (i->GetWindow())
XMapWindow(display, i->GetWindow());
}
for (auto i = 0; i < static_cast<int>(players.size()); i)
{
assert(players[i]);
// 第一路不静音, 其他全部静音
players[i]->Start(0, i!=0, 1, false);
//players[i]->Start(0, false, 1, false);
}
while (true)
{
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;
SubWindowsLayout(main_w, main_h, border_w, static_cast<int>(players.size()), layout_rects);
for (auto i = 0; i < static_cast<int>(players.size()); i)
{
if (players[i]->GetWindow())
{
XMoveResizeWindow(display, players[i]->GetWindow(), layout_rects[i].x_, layout_rects[i].y_, layout_rects[i].w_, layout_rects[i].h_);
}
}
}
}
else
{
for (const auto& i: players)
{
assert(i);
if (i->GetWindow() && i->GetWindow() == xev.xconfigure.window)
{
i->OnWindowSize(xev.xconfigure.width, xev.xconfigure.height);
}
}
}
}
else if (xev.type == KeyPress)
{
if (xev.xkey.keycode == XKeysymToKeycode(display, XK_Escape))
{
fprintf(stdout, "ESC Key Pressn");
for (const auto& i : players)
{
i->Stop();
if (i->GetWindow())
{
XDestroyWindow(display, i->GetWindow());
i->SetWindow(None);
}
}
players.clear();
XDestroyWindow(display, main_wid);
XCloseDisplay(display);
player_api.UnInit();
fprintf(stdout, "Close Players....n");
return 0;
}
}
}
}
}
开始播放封装
代码语言:javascript复制bool NT_PlayerSDKWrapper::Start(int buffer, bool is_mute, int render_scale_mode, bool is_only_dec_key_frame)
{
if (is_playing_)
return false;
if (url_.empty())
return false;
if (!OpenHandle(url_, buffer))
return false;
assert(handle_ && handle_->Handle());
// 音频参数
player_api_->SetMute(handle_->Handle(), is_mute ? 1 : 0);
player_api_->SetIsOutputAudioDevice(handle_->Handle(), 1);
player_api_->SetAudioOutputLayer(handle_->Handle(), 0); // 使用pluse 或者 alsa播放, 两个可以选择一个
// 视频参数
player_api_->SetVideoSizeCallBack(handle_->Handle(), this, &NT_Player_SDK_WRAPPER_OnVideoSizeHandle);
player_api_->SetXDisplay(handle_->Handle(), display_);
player_api_->SetXScreenNumber(handle_->Handle(),screen_);
player_api_->SetRenderXWindow(handle_->Handle(), window_);
player_api_->SetRenderScaleMode(handle_->Handle(), render_scale_mode);
player_api_->SetRenderTextureScaleFilterMode(handle_->Handle(), 3);
player_api_->SetOnlyDecodeVideoKeyFrame(handle_->Handle(), is_only_dec_key_frame ? 1 : 0);
auto ret = player_api_->StartPlay(handle_->Handle());
if (NT_ERC_OK != ret)
{
ResetHandle();
return false;
}
is_playing_ = true;
return true;
}
停止播放
代码语言:javascript复制void NT_PlayerSDKWrapper::Stop()
{
if (!is_playing_)
return;
assert(handle_);
player_api_->StopPlay(handle_->Handle());
video_width_ = 0;
video_height_ = 0;
ResetHandle();
is_playing_ = false;
}
视频宽高回调
代码语言:javascript复制extern "C" NT_VOID NT_CALLBACK NT_Player_SDK_WRAPPER_OnVideoSizeHandle(NT_HANDLE handle, NT_PVOID user_data,
NT_INT32 width, NT_INT32 height)
{
auto sdk_wrapper = reinterpret_cast<NT_PlayerSDKWrapper*>(user_data);
if (nullptr == sdk_wrapper)
return;
sdk_wrapper->VideoSizeHandle(handle, width, height);
}
实时快照
代码语言:javascript复制extern "C" NT_VOID NT_CALLBACK NT_Player_SDK_WRAPPER_OnCaptureImageCallBack(NT_HANDLE handle, NT_PVOID user_data, NT_UINT32 result, NT_PCSTR file_name)
{
auto sdk_wrapper = reinterpret_cast<NT_PlayerSDKWrapper*>(user_data);
if (nullptr == sdk_wrapper)
return;
sdk_wrapper->CaptureImageHandle(handle, result, file_name);
}
实时静音
代码语言:javascript复制void NT_PlayerSDKWrapper::SetMute(bool is_mute)
{
if (is_playing_ && handle_)
{
player_api_->SetMute(handle_->Handle(), is_mute?1:0);
}
}
设置绘制模式
代码语言:javascript复制void NT_PlayerSDKWrapper::SetRenderScaleMode(int render_scale_mode)
{
if (is_playing_ && handle_)
{
player_api_->SetRenderScaleMode(handle_->Handle(), render_scale_mode);
}
}
设置只解关键帧
代码语言:javascript复制void NT_PlayerSDKWrapper::SetOnlyDecodeVideoKeyFrame(bool is_only_dec_key_frame)
{
if (is_playing_ && handle_)
{
player_api_->SetOnlyDecodeVideoKeyFrame(handle_->Handle(), is_only_dec_key_frame ? 1 : 0);
}
}
Handler管理
代码语言:javascript复制bool NT_PlayerSDKWrapper::OpenHandle(const std::string& url, int buffer)
{
if (handle_)
{
if (handle_->IsOpened()
&& handle_->URL() == url)
{
return true;
}
}
ResetHandle();
auto handle = std::make_shared<NT_SDK_HandleWrapper>(player_api_);
if (!handle->Open(url, buffer))
{
return false;
}
handle_ = handle;
handle_->AddEventHandler(shared_from_this());
return true;
}
void NT_PlayerSDKWrapper::ResetHandle()
{
if (handle_)
{
handle_->RemoveHandler(this);
handle_.reset();
}
}
录像等其他接口不再赘述,可Windows平台一致。
总结
多路RTMP或RTSP播放,涉及到性能和多路之间音视频同步、长时间播放稳定性等问题,Linux平台可参考的资料比较少,可选的方案比较少,感兴趣的可酌情参考。