为什么要做GB28181设备接入侧?
实际上,在做Android平台GB28181设备接入模块的时候,我们已经有了非常好的技术积累,比如RTMP推送、轻量级RTSP服务、一对一互动模块、业内几乎最好的RTMP|RTSP低延迟播放器。
Android平台GB28181接入SDK(SmartGBD),主要实现不具备国标音视频能力的 Android终端,通过平台注册接入到现有的GB/T28181—2016(包括后续的GB/T28181—2022)服务,可用于如执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景,可能是业内为数不多功能齐全性能优异的商业级水准GB28181接入SDK。
Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、图像抓拍、语音广播和语音对讲、历史视音频下载和回放,支持对接数据类型如下:
- 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型),其中,Android平台前后摄像头数据,或者屏幕数据,或者Unity拿到的数据,均属编码前数据;
- 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
- 拉取RTSP或RTMP流并接入至GB28181平台(比如其他IPC的RTSP流,可通过Android平台GB28181接入到国标平台)。
目前,我们支持到的功能如下:
- [视频格式]H.264/H.265(Android H.265硬编码);
- [音频格式]G.711 A律、AAC;
- [音量调节]Android平台采集端支持实时音量调节;
- [H.264硬编码]支持H.264特定机型硬编码;
- [H.265硬编码]支持H.265特定机型硬编码;
- [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
- [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
- 支持横屏、竖屏推流;
- Android平台支持后台service推送屏幕(推送屏幕需要5.0+版本);
- 支持纯视频、音视频PS打包传输;
- 支持RTP OVER UDP和RTP OVER TCP被动模式(TCP媒体流传输客户端);
- 支持信令通道网络传输协议TCP/UDP设置;
- 支持注册、注销,支持注册刷新及注册有效期设置;
- 支持设备目录查询应答;
- 支持心跳机制,支持心跳间隔、心跳检测次数设置;
- 支持移动设备位置(MobilePosition)订阅和通知;
- 适用国家标准:GB/T 28181—2016;
- 支持语音广播;
- 支持语音对讲;
- 支持图像抓拍;
- 支持历史视音频文件检索;
- 支持历史视音频文件下载;
- 支持历史视音频文件回放;
- 支持云台控制和预置位查询;
- [实时水印]支持动态文字水印、png水印;
- [镜像]Android平台支持前置摄像头实时镜像功能;
- [实时静音]支持实时静音/取消静音;
- [实时快照]支持实时快照;
- [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
- [外部编码前视频数据对接]支持YUV数据对接;
- [外部编码前音频数据对接]支持PCM对接;
- [外部编码后视频数据对接]支持外部H.264数据对接;
- [外部编码后音频数据对接]外部AAC数据对接;
- [扩展录像功能]支持和录像SDK组合使用,录像相关功能。
本篇blog,我们主要讲的是如何把RTSP的流,转GB28181投递到国标平台。
技术实现
由于我们已经有非常成熟的RTSP直播播放模块和RTSP转RTMP推送模块,实际上,RTSP转GB28181这块,和转RTMP原理类似,把拉流过来的RTSP音视频数据,回调上来,然后通过推送接口,把数据投递到GB28181模块即可。
废话不多说,上代码,APP启动起来后,启动GB28181即可完成和国标平台侧的注册在线:
class ButtonGB28181AgentListener implements OnClickListener {
public void onClick(View v) {
stopGB28181Stream();
destoryRTPSender();
if (null == gb28181_agent_ ) {
if( !initGB28181Agent() )
return;
}
if (gb28181_agent_.isRunning()) {
gb28181_agent_.terminateAllPlays(true);// 目前测试下来,发送BYE之后,有些服务器会立即发送INVITE,是否发送BYE根据实际情况看
gb28181_agent_.stop();
btnGB28181Agent.setText("启动GB28181");
}
else {
if ( gb28181_agent_.start() ) {
btnGB28181Agent.setText("停止GB28181");
}
}
}
}
//停止GB28181 媒体流
private void stopGB28181Stream() {
stream_publisher_.StopGB28181MediaStream();
stream_publisher_.try_release();
}
对应InitGB28181Agent()实现:
/*
* SmartRtsp2GB28181.java
* Author: daniusdk.com
*/
private boolean initGB28181Agent() {
if ( gb28181_agent_ != null )
return true;
getLocation(context_);
String local_ip_addr = IPAddrUtils.getIpAddress(context_);
Log.i(TAG, "initGB28181Agent local ip addr: " + local_ip_addr);
if ( local_ip_addr == null || local_ip_addr.isEmpty() ) {
Log.e(TAG, "initGB28181Agent local ip is empty");
return false;
}
gb28181_agent_ = GBSIPAgentFactory.getInstance().create();
if ( gb28181_agent_ == null ) {
Log.e(TAG, "initGB28181Agent create agent failed");
return false;
}
gb28181_agent_.addListener(this);
gb28181_agent_.addPlayListener(this);
gb28181_agent_.addDeviceControlListener(this);
// 必填信息
gb28181_agent_.setLocalAddress(local_ip_addr);
gb28181_agent_.setServerParameter(gb28181_sip_server_addr_, gb28181_sip_server_port_, gb28181_sip_server_id_, gb28181_sip_domain_);
gb28181_agent_.setUserInfo(gb28181_sip_username_, gb28181_sip_password_);
//gb28181_agent_.setUserInfo(gb28181_sip_username_, gb28181_sip_username_, gb28181_sip_password_);
// 可选参数
gb28181_agent_.setUserAgent(gb28181_sip_user_agent_filed_);
gb28181_agent_.setTransportProtocol(gb28181_sip_trans_protocol_==0?"UDP":"TCP");
// GB28181配置
gb28181_agent_.config(gb28181_reg_expired_, gb28181_heartbeat_interval_, gb28181_heartbeat_count_);
com.gb.ntsignalling.Device gb_device = new com.gb.ntsignalling.Device("34020000001380000001", "安卓测试设备", Build.MANUFACTURER, Build.MODEL,
"宇宙","火星1","火星", true);
if (mLongitude != null && mLatitude != null) {
com.gb.ntsignalling.DevicePosition device_pos = new com.gb.ntsignalling.DevicePosition();
device_pos.setTime(mLocationTime);
device_pos.setLongitude(mLongitude);
device_pos.setLatitude(mLatitude);
gb_device.setPosition(device_pos);
gb_device.setSupportMobilePosition(true); // 设置支持移动位置上报
}
gb28181_agent_.addDevice(gb_device);
/*
com.gb28181.ntsignalling.Device gb_device1 = new com.gb28181.ntsignalling.Device("34020000001380000002", "安卓测试设备2", Build.MANUFACTURER, Build.MODEL,
"宇宙","火星1","火星", true);
if (mLongitude != null && mLatitude != null) {
com.gb28181.ntsignalling.DevicePosition device_pos = new com.gb28181.ntsignalling.DevicePosition();
device_pos.setTime(mLocationTime);
device_pos.setLongitude(mLongitude);
device_pos.setLatitude(mLatitude);
gb_device1.setPosition(device_pos);
gb_device1.setSupportMobilePosition(true);
}
gb28181_agent_.addDevice(gb_device1);
*/
if (!gb28181_agent_.createSipStack()) {
gb28181_agent_ = null;
Log.e(TAG, "initGB28181Agent gb28181_agent_.createSipStack failed.");
return false;
}
boolean is_bind_local_port_ok = false;
// 最多尝试5000个端口
int try_end_port = gb28181_sip_local_port_base_ + 5000;
try_end_port = try_end_port > 65536 ?65536: try_end_port;
for (int i = gb28181_sip_local_port_base_; i < try_end_port; ++i) {
if (gb28181_agent_.bindLocalPort(i)) {
is_bind_local_port_ok = true;
break;
}
}
if (!is_bind_local_port_ok) {
gb28181_agent_.releaseSipStack();
gb28181_agent_ = null;
Log.e(TAG, "initGB28181Agent gb28181_agent_.bindLocalPort failed.");
return false;
}
if (!gb28181_agent_.initialize()) {
gb28181_agent_.unBindLocalPort();
gb28181_agent_.releaseSipStack();
gb28181_agent_ = null;
Log.e(TAG, "initGB28181Agent gb28181_agent_.initialize failed.");
return false;
}
return true;
}
注册后,会有以下回调:
@Override
public void ntsRegisterOK(String dateString) {
Log.i(TAG, "ntsRegisterOK Date: " + (dateString!= null? dateString : ""));
}
@Override
public void ntsRegisterTimeout() {
Log.e(TAG, "ntsRegisterTimeout");
}
@Override
public void ntsRegisterTransportError(String errorInfo) {
Log.e(TAG, "ntsRegisterTransportError error:" + (errorInfo != null?errorInfo :""));
}
如果国标平台侧有实时查看请求,先发invite过来:
@Override
public void ntsOnInvitePlay(String deviceId, SessionDescription session_des) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
// 先振铃响应下
gb28181_agent_.respondPlayInvite(180, device_id_);
MediaSessionDescription video_des = null;
SDPRtpMapAttribute ps_rtpmap_attr = null;
// 28181 视频使用PS打包
Vector<MediaSessionDescription> video_des_list = session_des_.getVideoPSDescriptions();
if (video_des_list != null && !video_des_list.isEmpty()) {
for(MediaSessionDescription m : video_des_list) {
if (m != null && m.isValidAddressType() && m.isHasAddress() ) {
video_des = m;
ps_rtpmap_attr = video_des.getPSRtpMapAttribute();
break;
}
}
}
if (null == video_des) {
gb28181_agent_.respondPlayInvite(488, device_id_);
Log.i(TAG, "ntsOnInvitePlay get video description is null, response 488, device_id:" + device_id_);
return;
}
if (null == ps_rtpmap_attr) {
gb28181_agent_.respondPlayInvite(488, device_id_);
Log.i(TAG, "ntsOnInvitePlay get ps rtp map attribute is null, response 488, device_id:" + device_id_);
return;
}
Log.i(TAG,"ntsOnInvitePlay, device_id:" +device_id_+", is_tcp:" + video_des.isRTPOverTCP()
+ " rtp_port:" + video_des.getPort() + " ssrc:" + video_des.getSSRC()
+ " address_type:" + video_des.getAddressType() + " address:" + video_des.getAddress());
long rtp_sender_handle = libPublisher.CreateRTPSender(0);
if ( rtp_sender_handle == 0 ) {
gb28181_agent_.respondPlayInvite(488, device_id_);
Log.i(TAG, "ntsOnInvitePlay CreateRTPSender failed, response 488, device_id:" + device_id_);
return;
}
gb28181_rtp_payload_type_ = ps_rtpmap_attr.getPayloadType();
gb28181_rtp_encoding_name_ = ps_rtpmap_attr.getEncodingName();
libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle, video_des.isRTPOverUDP()?0:1);
libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle, video_des.isIPv4()?0:1);
libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0);
libPublisher.SetRTPSenderSSRC(rtp_sender_handle, video_des.getSSRC());
libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 设置到2M
libPublisher.SetRTPSenderClockRate(rtp_sender_handle, ps_rtpmap_attr.getClockRate());
libPublisher.SetRTPSenderDestination(rtp_sender_handle, video_des.getAddress(), video_des.getPort());
if ( libPublisher.InitRTPSender(rtp_sender_handle) != 0 ) {
gb28181_agent_.respondPlayInvite(488, device_id_);
libPublisher.DestoryRTPSender(rtp_sender_handle);
return;
}
int local_port = libPublisher.GetRTPSenderLocalPort(rtp_sender_handle);
if (local_port == 0) {
gb28181_agent_.respondPlayInvite(488, device_id_);
libPublisher.DestoryRTPSender(rtp_sender_handle);
return;
}
Log.i(TAG,"get local_port:" + local_port);
String local_ip_addr = IPAddrUtils.getIpAddress(context_);
MediaSessionDescription local_video_des = new MediaSessionDescription(video_des.getType());
local_video_des.addFormat(String.valueOf(ps_rtpmap_attr.getPayloadType()));
local_video_des.addRtpMapAttribute(ps_rtpmap_attr);
local_video_des.setAddressType(video_des.getAddressType());
local_video_des.setAddress(local_ip_addr);
local_video_des.setPort(local_port);
local_video_des.setTransportProtocol(video_des.getTransportProtocol());
local_video_des.setSSRC(video_des.getSSRC());
if (!gb28181_agent_.respondPlayInviteOK(device_id_,local_video_des) ) {
libPublisher.DestoryRTPSender(rtp_sender_handle);
Log.e(TAG, "ntsOnInvitePlay call respondPlayInviteOK failed.");
return;
}
gb28181_rtp_sender_handle_ = rtp_sender_handle;
}
private String device_id_;
private SessionDescription session_des_;
public Runnable set(String device_id, SessionDescription session_des) {
this.device_id_ = device_id;
this.session_des_ = session_des;
return this;
}
}.set(deviceId, session_des),0);
}
收到平台侧的Ack后,开始投递数据到国标平台侧:
@Override
public void ntsOnAckPlay(String deviceId) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG,"ntsOnACKPlay, device_id:" +device_id_);
InitAndSetConfig();
stream_publisher_.SetGB28181RTPSender(gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_);
//libPublisher.SetGBTCPConnectTimeout(publisherHandle, 10*60*1000);
//libPublisher.SetGBInitialTCPReconnectInterval(publisherHandle, 1000);
//libPublisher.SetGBInitialTCPMaxReconnectAttempts(publisherHandle, 3);
boolean start_ret = stream_publisher_.StartGB28181MediaStream();
if (!start_ret) {
stream_publisher_.try_release();
destoryRTPSender();
Log.e(TAG, "Failed to start GB28181 service..");
return;
}
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
国标平台侧停止查看:
@Override
public void ntsOnByePlay(String deviceId) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnByePlay, stop GB28181 media stream, deviceId=" + device_id_);
stopGB28181Stream();
destoryRTPSender();
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
GB28181这块介绍过,再说数据源的问题,由于本次是拉取RTSP流转推GB28181平台,拉取RTSP流的时候,设置音视频数据回调。
/*
* SmartRtsp2GB28181.java
* Author: daniusdk.com
*/
private boolean StartPull()
{
if ( isPulling )
return false;
if(!isPlaying)
{
if (!OpenPullHandle())
return false;
}
libPlayer.SmartPlayerSetAudioDataCallback(player_handle_, new PlayerAudioDataCallback(stream_publisher_));
libPlayer.SmartPlayerSetVideoDataCallback(player_handle_, new PlayerVideoDataCallback(stream_publisher_));
int is_pull_trans_code = 1;
libPlayer.SmartPlayerSetPullStreamAudioTranscodeAAC(player_handle_, is_pull_trans_code);
int startRet = libPlayer.SmartPlayerStartPullStream(player_handle_);
if (startRet != 0) {
Log.e(TAG, "Failed to start pull stream!");
if(!isPlaying)
{
releasePlayerHandle();
}
return false;
}
isPulling = true;
return true;
}
对应的OpenPullHandle()实现如下:
private boolean OpenPullHandle()
{
//playbackUrl可自定义
//playbackUrl = "rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream";
if (playbackUrl == null) {
Log.e(TAG, "playback URL is null...");
return false;
}
player_handle_ = libPlayer.SmartPlayerOpen(context_);
if (player_handle_ == 0) {
Log.e(TAG, "playerHandle is null..");
return false;
}
libPlayer.SetSmartPlayerEventCallbackV2(player_handle_,
new EventHandlePlayerV2());
libPlayer.SmartPlayerSetBuffer(player_handle_, playBuffer);
// set report download speed
libPlayer.SmartPlayerSetReportDownloadSpeed(player_handle_, 1, 3);
//设置RTSP超时时间
int rtsp_timeout = 10;
libPlayer.SmartPlayerSetRTSPTimeout(player_handle_, rtsp_timeout);
//设置RTSP TCP/UDP模式自动切换
int is_auto_switch_tcp_udp = 1;
libPlayer.SmartPlayerSetRTSPAutoSwitchTcpUdp(player_handle_, is_auto_switch_tcp_udp);
// It only used when playback RTSP stream..
//libPlayer.SmartPlayerSetRTSPTcpMode(playerHandle, 1);
libPlayer.SmartPlayerSetUrl(player_handle_, playbackUrl);
return true;
}
这里设置RTSP拉流参数,比如缓冲时间,下载速度实时回调间隔,RTSP超时时间、RTSP-TCP/UDP模式切换等。
音频处理如下:
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);
}
}
视频处理如下:
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);
}
}
除此之外,如果需要本地预览RTSP流数据,可以调用播放操作:
private boolean StartPlay()
{
if(isPlaying)
return false;
if(!isPulling)
{
if (!OpenPullHandle())
return false;
}
// 如果第二个参数设置为null,则播放纯音频
libPlayer.SmartPlayerSetSurface(player_handle_, sSurfaceView);
//libPlayer.SmartPlayerSetSurface(player_handle_, null);
libPlayer.SmartPlayerSetRenderScaleMode(player_handle_, 1);
libPlayer.SmartPlayerSetFastStartup(player_handle_, isFastStartup ? 1 : 0);
libPlayer.SmartPlayerSetAudioOutputType(player_handle_, 1);
if (isMute) {
libPlayer.SmartPlayerSetMute(player_handle_, isMute ? 1 : 0);
}
if (isHardwareDecoder)
{
int isSupportH264HwDecoder = libPlayer.SetSmartPlayerVideoHWDecoder(player_handle_, 1);
int isSupportHevcHwDecoder = libPlayer.SetSmartPlayerVideoHevcHWDecoder(player_handle_, 1);
Log.i(TAG, "isSupportH264HwDecoder: " + isSupportH264HwDecoder + ", isSupportHevcHwDecoder: " + isSupportHevcHwDecoder);
}
libPlayer.SmartPlayerSetLowLatencyMode(player_handle_, isLowLatency ? 1 : 0);
libPlayer.SmartPlayerSetRotation(player_handle_, rotate_degrees);
int iPlaybackRet = libPlayer.SmartPlayerStartPlay(player_handle_);
if (iPlaybackRet != 0 && !isPulling) {
Log.e(TAG, "StartPlay failed!");
releasePlayerHandle();
return false;
}
isPlaying = true;
return true;
}
private void StopPlay()
{
if ( !isPlaying )
return;
isPlaying = false;
if (null == libPlayer || 0 == player_handle_)
return;
libPlayer.SmartPlayerStopPlay(player_handle_);
}
如果需要把拉取到的RTSP流,本地录制留存,那么可以调用录像逻辑:
private boolean StartRecorder()
{
if (!OpenPullHandle())
return false;
ConfigRecorderFunction();
int iRecRet = libPlayer
.SmartPlayerStartRecorder(player_handle_);
if (iRecRet != 0) {
Log.e(TAG, "StartRecorder failed!");
if ( !isPulling &&!isPlaying && !stream_publisher_.is_rtmp_publishing() && !stream_publisher_.is_rtsp_publishing() && !stream_publisher_.is_gb_stream_publishing())
{
libPlayer.SmartPlayerClose(player_handle_);
player_handle_ = 0;
}
return false;
}
isRecording = true;
return true;
}
private void StopRecorder()
{
if ( !isRecording )
return;
isRecording = false;
libPlayer.SmartPlayerStopRecorder(player_handle_);
if ( !isPlaying && !isPulling && !stream_publisher_.is_rtmp_publishing() && !stream_publisher_.is_rtsp_publishing() && !stream_publisher_.is_gb_stream_publishing())
{
libPlayer.SmartPlayerClose(player_handle_);
player_handle_ = 0;
}
}
如果需要实时快照,可以调用快照接口,实现snapshot,快照可以保存jpg或png格式:
btnCaptureImage.setOnClickListener(new Button.OnClickListener() {
@SuppressLint("SimpleDateFormat")
public void onClick(View v) {
if (0 == player_handle_)
return;
if (null == capture_image_date_format_)
capture_image_date_format_ = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS");
String timestamp = capture_image_date_format_.format(new Date());
String imageFileName = timestamp;
String image_path = imageSavePath + "/" + imageFileName;
int quality;
boolean is_jpeg = true;
if (is_jpeg) {
image_path += ".jpeg";
quality = 100;
}
else {
image_path += ".png";
quality = 100;
}
int capture_ret = libPlayer.CaptureImage(player_handle_,is_jpeg?0:1, quality, image_path, "test cix");
Log.i(TAG, "capture image ret:" + capture_ret + ", file:" + image_path);
}
});
总结
RTSP转GB28181到国标平台侧,涉及到两个模块,RTSP拉流和GB28181设备接入,如果需要本地录像留存数据,还需要有功能齐全的录像模块。实现起来,如果没有成熟的技术储备,短期内确实很难做出来真正可用的产品。以上是大概的流程,感兴趣的开发者,可以跟我探讨。