技术背景
好多了解我们模块的开发者都知道,我们有非常成熟的轻量级RTSP服务模块,可以采集摄像头或屏幕的数据,编码打包注入Android平台的轻量级RTSP服务模块,让Android设备端,充当个类似于网络摄像头的角色,对外提供个RTSP拉流的URL,实现内网环境下的无服务部署直播场景,这种在内网监控或智慧教室、无纸化场景等,非常实用。
技术实现
今天要探讨的是,如何把外部的RTSP|RTMP流,注入到轻量级RTSP服务?实际上,这块对大牛直播SDK来说,算不上新模块或技术,因为前些年已经实现了,对应的是我们的内网RTSP网关模块。
实现方式,和RTSP转RTMP推送有些类似,先把RTSP或RTMP流,拉取下来,回调编码后的H.264/H.265/AAC/PCMU/PCMA数据到上层。
上层模块,通过轻量级RTSP服务模块提供的编码后数据投递接口,实现数据源的对接。
先说拉取RTSP或RTMP流数据,并回调到上层:
/* SmartRelayDemo.java
* Created by daniusdk.com
*/
class ButtonPullListener implements View.OnClickListener {
public void onClick(View v) {
if (stream_player_.is_pulling()) {
Log.i(TAG, "Stop Pull..");
boolean iRet = stream_player_.StopPull();
if (!iRet) {
Log.e(TAG, "Call StopPull failed..");
return;
}
stream_player_.try_release();
btnPullStream.setText("开始拉流");
} else {
Log.i(TAG, "Start playback stream++");
if (!stream_player_.OpenPlayerHandle(playback_url_, play_buffer_, is_using_tcp_))
return;
if(audio_opt_ == 2)
{
libPlayer.SmartPlayerSetAudioDataCallback(stream_player_.get(), new PlayerAudioDataCallback(stream_publisher_));
}
if(video_opt_ == 2)
{
libPlayer.SmartPlayerSetVideoDataCallback(stream_player_.get(), new PlayerVideoDataCallback(stream_publisher_));
}
int is_pull_trans_code = 1;
boolean iPlaybackRet = stream_player_.StartPull(is_pull_trans_code);
if (!iPlaybackRet) {
Log.e(TAG, "Call StartPlayer failed..");
return;
}
btnPullStream.setText("停止拉流");
}
}
}
拉取到的视音频数据,投递到轻量级RTSP服务模块即可,先看视频数据回调处理:
class PlayerVideoDataCallback implements NTVideoDataCallback
{
private WeakReference<LibPublisherWrapper> publisher_;
private int video_buffer_size = 0;
private ByteBuffer video_buffer_ = null;
public PlayerVideoDataCallback(LibPublisherWrapper publisher) {
if (publisher != null)
publisher_ = new WeakReference<>(publisher);
}
@Override
public ByteBuffer getVideoByteBuffer(int size)
{
//Log.i("getVideoByteBuffer", "size: " + size);
if( size < 1 )
{
return null;
}
if ( size <= video_buffer_size && video_buffer_ != null )
{
return video_buffer_;
}
video_buffer_size = size + 1024;
video_buffer_size = (video_buffer_size+0xf) & (~0xf);
video_buffer_ = ByteBuffer.allocateDirect(video_buffer_size);
// Log.i("getVideoByteBuffer", "size: " + size + " buffer_size:" + video_buffer_size);
return video_buffer_;
}
public void onVideoDataCallback(int ret, int video_codec_id, int sample_size, int is_key_frame, long timestamp, int width, int height, long presentation_timestamp)
{
//Log.i("onVideoDataCallback", "ret: " + ret + ", video_codec_id: " + video_codec_id + ", sample_size: " + sample_size + ", is_key_frame: "+ is_key_frame + ", timestamp: " + timestamp +
// ",presentation_timestamp:" + presentation_timestamp);
if ( video_buffer_ == null)
return;
LibPublisherWrapper publisher = publisher_.get();
if (null == publisher)
return;
if (!publisher.is_publishing())
return;
video_buffer_.rewind();
publisher.PostVideoEncodedData(video_codec_id, video_buffer_, sample_size, is_key_frame, timestamp, presentation_timestamp);
}
}
音频数据回调处理:
class PlayerAudioDataCallback implements NTAudioDataCallback
{
private WeakReference<LibPublisherWrapper> publisher_;
private int audio_buffer_size = 0;
private int param_info_size = 0;
private ByteBuffer audio_buffer_ = null;
private ByteBuffer parameter_info_ = null;
public PlayerAudioDataCallback(LibPublisherWrapper publisher) {
if (publisher != null)
publisher_ = new WeakReference<>(publisher);
}
@Override
public ByteBuffer getAudioByteBuffer(int size)
{
//Log.i("getAudioByteBuffer", "size: " + size);
if( size < 1 )
{
return null;
}
if ( size <= audio_buffer_size && audio_buffer_ != null )
{
return audio_buffer_;
}
audio_buffer_size = size + 512;
audio_buffer_size = (audio_buffer_size+0xf) & (~0xf);
audio_buffer_ = ByteBuffer.allocateDirect(audio_buffer_size);
// Log.i("getAudioByteBuffer", "size: " + size + " buffer_size:" + audio_buffer_size);
return audio_buffer_;
}
@Override
public ByteBuffer getAudioParameterInfo(int size)
{
//Log.i("getAudioParameterInfo", "size: " + size);
if(size < 1)
{
return null;
}
if ( size <= param_info_size && parameter_info_ != null )
{
return parameter_info_;
}
param_info_size = size + 32;
param_info_size = (param_info_size+0xf) & (~0xf);
parameter_info_ = ByteBuffer.allocateDirect(param_info_size);
//Log.i("getAudioParameterInfo", "size: " + size + " buffer_size:" + param_info_size);
return parameter_info_;
}
public void onAudioDataCallback(int ret, int audio_codec_id, int sample_size, int is_key_frame, long timestamp, int sample_rate, int channel, int parameter_info_size, long reserve)
{
//Log.i("onAudioDataCallback", "ret: " + ret + ", audio_codec_id: " + audio_codec_id + ", sample_size: " + sample_size + ", timestamp: " + timestamp +
// ",sample_rate:" + sample_rate);
if ( audio_buffer_ == null)
return;
LibPublisherWrapper publisher = publisher_.get();
if (null == publisher)
return;
if (!publisher.is_publishing())
return;
audio_buffer_.rewind();
publisher.PostAudioEncodedData(audio_codec_id, audio_buffer_, sample_size, is_key_frame, timestamp, parameter_info_, parameter_info_size);
}
}
启动、停止RTSP服务:
//启动/停止RTSP服务
class ButtonRtspServiceListener implements View.OnClickListener {
public void onClick(View v) {
if (!rtsp_server_.empty()) {
rtsp_server_.reset();
btnRtspService.setText("启动RTSP服务");
btnRtspPublisher.setEnabled(false);
return;
}
Log.i(TAG, "onClick start rtsp service..");
int port = 8554;
String user_name = null;
String password = null;
LibPublisherWrapper.RTSPServer.Handle server_handle = LibPublisherWrapper.RTSPServer.create_and_start_server(libPublisher,
port, user_name, password);
if (null == server_handle) {
Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!");
return;
}
rtsp_server_.reset(server_handle);
btnRtspService.setText("停止RTSP服务");
btnRtspPublisher.setEnabled(true);
}
}
发布RTSP流:
//发布/停止RTSP流
class ButtonRtspPublisherListener implements View.OnClickListener {
public void onClick(View v) {
if (stream_publisher_.is_rtsp_publishing()) {
stopRtspPublisher();
btnRtspPublisher.setText("发布RTSP流");
btnGetRtspSessionNumbers.setEnabled(false);
btnRtspService.setEnabled(true);
return;
}
Log.i(TAG, "onClick start rtsp publisher..");
PusherInitAndSetConfig();
String rtsp_stream_name = "stream1";
stream_publisher_.SetRtspStreamName(rtsp_stream_name);
stream_publisher_.ClearRtspStreamServer();
stream_publisher_.AddRtspStreamServer(rtsp_server_.get_native());
if (!stream_publisher_.StartRtspStream()) {
stream_publisher_.try_release();
Log.e(TAG, "调用发布rtsp流接口失败!");
return;
}
startAudioRecorder();
btnRtspPublisher.setText("停止RTSP流");
btnGetRtspSessionNumbers.setEnabled(true);
btnRtspService.setEnabled(false);
}
}
获取RTSP会话数:
//获取RTSP会话数
class ButtonGetRtspSessionNumbersListener implements View.OnClickListener {
public void onClick(View v) {
if (rtsp_server_.is_running()) {
int session_numbers = rtsp_server_.get_client_session_number();
Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers);
PopRtspSessionNumberDialog(session_numbers);
}
}
}
总结
Android平台内网RTSP网关模块,系内置轻量级RTSP服务模块扩展,完成外部RTSP/RTMP数据拉取并注入到轻量级RTSP服务模块工作,多个内网客户端直接访问内网轻量级RTSP服务获取公网数据,无需部署单独的服务器,支持RTSP/RTMP H.265数据接入。
内置轻量级RTSP服务模块和内置RTSP网关模块共同点:
内置轻量级RTSP服务模块和内置RTSP网关模块,核心痛点是避免用户或者开发者单独部署RTSP或者RTMP服务,数据汇聚到内置RTSP服务,对外提供可供拉流的RTSP URL,适用于内网环境下,对并发要求不高的场景,支持H.264/H.265,支持RTSP鉴权、单播、组播模式,考虑到单个服务承载能力,我们支持同时创建多个RTSP服务,并支持获取当前RTSP服务会话连接数。
内置轻量级RTSP服务模块和内置RTSP网关模块不同点:数据来源不同
1. 内置轻量级RTSP服务模块,数据源来自摄像头、屏幕、麦克风等编码前数据,或者本地编码后的对接数据;
2. 内置RTSP网关模块,实际上是RTSP/RTMP拉流模块+内置轻量级RTSP服务模块组合出来的。数据源来自RTSP或RTMP网络流,拉流模块完成编码后的音视频数据回调,然后,汇聚到内置轻量级RTSP服务模块。