
我们在对接RTSP直播播放器相关技术诉求的时候,好多开发者,除了选用成熟的RTSP播放器外,还想知其然知其所以然,对RTSP播放器的整体开发有个基础的了解,方便方案之作和技术延伸。本文抛砖引玉,做个大概的介绍。
在探讨RTSP直播播放器技术实现之前,我们先来看,为什么RTSP播放器的开发看似简单,实则复杂,或者说做播放器容易,做个好的播放器,为什么就那么难?
以大牛直播SDK的SmartPlayer RTSP直播播放模块为例,我们来看看,如何实现低延迟的RTSP播放器。大牛直播SDK自2015年发布RTSP、RTMP直播播放模块,迭代从未停止,SmartPlayer功能强大、性能强劲、高稳定、超低延迟、超低资源占用。无需赘述,全自研内核,行业内一致认可的跨平台RTSP、RTMP直播播放器。先说功能设计,如不单独说明,Windows、Linux(x86_64|aarch64架构)、Android、iOS全平台支持。
1. 低延迟:大多数RTSP的播放都面向直播场景,所以,如果延迟过大,严重影响体验,所以,低延迟是衡量一个好的RTSP播放器非常重要的指标,目前大牛直播SDK的RTSP直播播放延迟比开源播放器更优异,而且长时间运行下,不会造成延迟累积;
2. 音视频同步处理:有些播放器为了追求低延迟,甚至不做音视频同步,拿到audio video直接播放,导致a/v不同步,还有就是时间戳乱跳等各种问题,大牛直播SDK提供的播放器,具备好的时间戳同步和异常时间戳矫正机制;
3. 支持多实例:大牛直播SDK提供的播放器支持同时播放多路音视频数据,比如4-8-9窗口,大多开源播放器对多实例支持不太友好;
4. 支持buffer time设置:在一些有网络抖动的场景,播放器需要支持buffer time设置,一般来说,以毫秒计,开源播放器对此支持不够友好;
5. TCP/UDP模式设定、自动切换:考虑到好多服务器仅支持TCP或UDP模式,一个好的RTSP播放器需要支持TCP/UDP模式设置,如链接不支持TCP或UDP,大牛直播SDK可自动切换,,开源播放器不具备自动切换TCP/UDP能力;
6. 实时静音:比如,多窗口播放RTSP流,如果每个audio都播放出来,体验非常不好,所以实时静音功能非常必要,开源播放器不具备实时静音功能;
7. 视频view旋转:好多摄像头由于安装限制,导致图像倒置,所以一个好的RTSP播放器应该支持如视频view实时旋转(0° 90° 180° 270°)、水平反转、垂直反转,开源播放器不具备此功能;
8. 支持解码后audio/video数据输出:大牛直播SDK接触到好多开发者,希望能在播放的同时,获取到YUV或RGB数据,进行人脸匹配等算法分析,开源播放器不具备此功能;
9. 实时快照:感兴趣或重要的画面,实时截取下来非常必要,一般播放器不具备快照能力,开源播放器不具备此功能;
10. 网络抖动处理(如断网重连):稳定的网络处理机制、支持如断网重连等,开源播放器对网络异常处理支持较差;
11. 长期运行稳定性:不同于市面上的开源播放器,大牛直播SDK提供的Windows平台RTSP直播播放SDK适用于数天长时间运行,开源播放器对长时间运行稳定性支持较差;
12. log信息记录:整体流程机制记录到LOG文件,确保出问题时,有据可依,开源播放器几无log记录。
13. 实时下载速度反馈:大牛直播SDK提供音视频流实时下载回调,并可设置回调时间间隔,确保实时下载速度反馈,以此来监听网络状态,开源播放器不具备此能力;
14. 异常状态处理、Event状态回调:如播放的过程中,断网、网络抖动、等各种场景,大牛直播SDK提供的播放器可实时回调相关状态,确保上层模块感知处理,开源播放器对此支持不好;
15. 关键帧/全帧播放实时切换:特别是播放多路画面的时候,如果路数过多,全部解码、绘制,系统资源占用会加大,如果能灵活的处理,可以随时只播放关键帧,全帧播放切换,对系统性能要求大幅降低。
下面以Android平台多实例RTSP播放为例,探讨下接口设计和调用说明。
我们针对的功能展示,主要是播放和录像这块,先说播放:
/*
* SmartPlayer.java
* Author: https://daniusdk.com
* WeChat: xinsheng120
* Created by DaniuLive on 2015/09/26.
*/
class ButtonPlayback1Listener implements View.OnClickListener {
public void onClick(View v) {
if (stream_player_1_.is_playing()) {
Log.i(TAG, "Stop player1..");
boolean iRet = stream_player_1_.StopPlayer();
if (!iRet) {
Log.e(TAG, "Call StopPlayer failed..");
return;
}
stream_player_1_.try_release();
btn_playback1.setText("开始播放1");
SetViewVisibility(surface_view_1_);
} else {
Log.i(TAG, "Start playback stream1++");
int play_buffer = 0;
int is_using_tcp = 0;
if(!stream_player_1_.OpenPlayerHandle(playback_url_1_, play_buffer, is_using_tcp))
return;
stream_player_1_.SetView(surface_view_1_);
boolean is_mute = false;
boolean iPlaybackRet = stream_player_1_.StartPlayer(isHardwareDecoder, is_enable_hardware_render_mode, is_mute);
if (!iPlaybackRet) {
Log.e(TAG, "Call StartPlayer failed..");
return;
}
btn_playback1.setText("停止播放1");
}
}
}
对应的OpenPlayerHandle()实现如下:
/*
* LibPlayerWrapper.java.java
* Author: https://daniusdk.com
*/
public boolean OpenPlayerHandle(String playback_url, int play_buffer, int is_using_tcp) {
if (check_native_handle())
return true;
if(!isValidRtspOrRtmpUrl(playback_url))
return false;
long handle = lib_player_.SmartPlayerOpen(application_context());
if (0==handle) {
Log.e(TAG, "sdk open failed!");
return false;
}
lib_player_.SetSmartPlayerEventCallbackV2(handle, new EventHandleV2());
lib_player_.SmartPlayerSetBuffer(handle, play_buffer);
// set report download speed(默认2秒一次回调 用户可自行调整report间隔)
lib_player_.SmartPlayerSetReportDownloadSpeed(handle, 1, 4);
boolean isFastStartup = true;
lib_player_.SmartPlayerSetFastStartup(handle, isFastStartup ? 1 : 0);
//设置RTSP超时时间
int rtsp_timeout = 10;
lib_player_.SmartPlayerSetRTSPTimeout(handle, rtsp_timeout);
//设置RTSP TCP/UDP模式自动切换
int is_auto_switch_tcp_udp = 1;
lib_player_.SmartPlayerSetRTSPAutoSwitchTcpUdp(handle, is_auto_switch_tcp_udp);
lib_player_.SmartPlayerSaveImageFlag(handle, 1);
// It only used when playback RTSP stream..
lib_player_.SmartPlayerSetRTSPTcpMode(handle, is_using_tcp);
lib_player_.DisableEnhancedRTMP(handle, 0);
lib_player_.SmartPlayerSetUrl(handle, playback_url);
set(handle);
return true;
}
对应的开始播放、停止播放设计:
/*
* LibPlayerWrapper.java
* Author: https://daniusdk.com
*/
public boolean StartPlayer(boolean is_hardware_decoder, boolean is_enable_hardware_render_mode, boolean is_mute) {
if (is_playing()) {
Log.e(TAG, "already playing, native_handle:" + get());
return false;
}
SetPlayerParam(is_hardware_decoder, is_enable_hardware_render_mode, is_mute);
int ret = lib_player_.SmartPlayerStartPlay(get());
if (ret != OK) {
Log.e(TAG, "call StartPlay failed, native_handle:" + get() + ", ret:" + ret);
return false;
}
write_lock_.lock();
try {
this.is_playing_ = true;
} finally {
write_lock_.unlock();
}
Log.i(TAG, "call StartPlayer OK, native_handle:" + get());
return true;
}
public boolean StopPlayer() {
if (!check_native_handle())
return false;
if (!is_playing()) {
Log.w(TAG, "it's not playing, native_handle:" + get());
return false;
}
boolean is_need_call = false;
write_lock_.lock();
try {
if (this.is_playing_) {
this.is_playing_ = false;
is_need_call = true;
}
} finally {
write_lock_.unlock();
}
if (is_need_call)
lib_player_.SmartPlayerStopPlay(get());
return true;
}录像设计:
/*
* SmartPlayer.java
* Author: https://daniusdk.com
*/
class ButtonRecorder1Listener implements View.OnClickListener {
public void onClick(View v) {
if (stream_player_1_.is_recording()) {
Log.i(TAG, "Stop recorder1..");
boolean iRet = stream_player_1_.StopRecorder();
if (!iRet) {
Log.e(TAG, "Call StopRecorder failed..");
return;
}
stream_player_1_.try_release();
btn_recorder1.setText("开始录像1");
} else {
Log.i(TAG, "Start recorder stream1++");
int play_buffer = 0;
int is_using_tcp = 0;
if(!stream_player_1_.OpenPlayerHandle(playback_url_1_, play_buffer, is_using_tcp))
return;
stream_player_1_.ConfigRecorderParam(recDir, 400, 1, 1, 1);
boolean iRecRet = stream_player_1_.StartRecorder();
if (!iRecRet) {
Log.e(TAG, "Call StartRecorder failed..");
return;
}
btn_recorder1.setText("停止录像1");
}
}
}录像参数配置选项:
/*
* LibPlayerWrapper.java
* Author: https://daniusdk.com
*/
public boolean ConfigRecorderParam(String rec_dir, int file_max_size, int is_transcode_aac,
int is_record_video, int is_record_audio) {
if(!check_native_handle())
return false;
if (null == rec_dir || rec_dir.isEmpty())
return false;
int ret = lib_player_.SmartPlayerCreateFileDirectory(rec_dir);
if (ret != 0) {
Log.e(TAG, "Create record dir failed, path:" + rec_dir);
return false;
}
if (lib_player_.SmartPlayerSetRecorderDirectory(get(), rec_dir) != 0) {
Log.e(TAG, "Set record dir failed , path:" + rec_dir);
return false;
}
if (lib_player_.SmartPlayerSetRecorderFileMaxSize(get(),file_max_size) != 0) {
Log.e(TAG, "SmartPlayerSetRecorderFileMaxSize failed.");
return false;
}
lib_player_.SmartPlayerSetRecorderAudioTranscodeAAC(get(), is_transcode_aac);
// 更细粒度控制录像的, 一般情况无需调用
lib_player_.SmartPlayerSetRecorderVideo(get(), is_record_video);
lib_player_.SmartPlayerSetRecorderAudio(get(), is_record_audio);
return true;
}开始录像、结束录像:
/*
* LibPlayerWrapper.java
* Author: https://daniusdk.com
*/
public boolean StartRecorder() {
if (is_recording()) {
Log.e(TAG, "already recording, native_handle:" + get());
return false;
}
int ret = lib_player_.SmartPlayerStartRecorder(get());
if (ret != OK) {
Log.e(TAG, "call SmartPlayerStartRecorder failed, native_handle:" + get() + ", ret:" + ret);
return false;
}
write_lock_.lock();
try {
this.is_recording_ = true;
} finally {
write_lock_.unlock();
}
Log.i(TAG, "call SmartPlayerStartRecorder OK, native_handle:" + get());
return true;
}
public boolean StopRecorder() {
if (!check_native_handle())
return false;
if (!is_recording()) {
Log.w(TAG, "it's not recording, native_handle:" + get());
return false;
}
boolean is_need_call = false;
write_lock_.lock();
try {
if (this.is_recording_) {
this.is_recording_ = false;
is_need_call = true;
}
} finally {
write_lock_.unlock();
}
if (is_need_call)
lib_player_.SmartPlayerStopRecorder(get());
return true;
}做RTSP播放器容易,做个可以稳定用于实际场景的低延迟RTSP播放器,真的非常困难,首先,RTSP协议本身的复杂度,如果不涉及底层协议栈,只是开源的项目编译调试小修小改,遇到问题,很难处理。还有就是网络环境的不确定性,视频解码和播放的复杂性,视频同步问题的复杂性及考虑因素。最后在跨平台开发的挑战,不同操作系统和设备以及处理不同硬件特性,都需要考虑。以上抛砖引玉,感兴趣的开发者,可以单独跟我沟通探讨。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。