技术背景

在实现Android平台GB28181前端设备接入之前,我们几年前就有了非常成熟的RTMP推送、RTSP推送和轻量级RTSP服务等模块,特别是RTMP推送,行业内应用非常广泛,好多开发者可能会问,既然有了以上模块,干嘛还要实现GB28181的前端接入呢?

首先,我们了解下GB/T28181:
国标GB/T28181协议全称《安全防范视频监控联网系统信息传输、交换、控制技术要求》,是一个定义视频联网传输和设备控制标准的白皮书,由公安部科技信息化局提出,该标准规定了城市监控报警联网系统中信息传输、交换、控制的互联结构、通信协议结构,传输、交换、控制的基本要求和安全性要求,以及控制、传输流程和协议接口等技术要求。解决了视频间互联互通,数据共享,以及设备控制的问题,这个问题从顶层解决了视频信息各自为战的问题,打通了视频联网的信息孤岛。

技术特点

GB28181协议实现分两块,一块是信令部分,一块是流媒体数据传输。GB28181相对RTMP,支持TCP和UDP模式,信令流负责session交互,数据流负责数据传输,适合标准协议规范的平台级产品对接。

Android终端除支持常规的音视频数据接入外,还可以支持Subscribe订阅实时位置(MobilePosition)、实时目录查询等,支持标准28181服务对接。

此外,产品设计这块,媒体流支持最新GB28181-2016的UDP和TCP被动模式,参数配置,支持注册有效期、心跳间隔、心跳间隔次数、TCP/UDP信令设置,支持RTP Sender IP地址类型、RTP Socket本地端口、SSRC、RTP socket 发送Buffer大小、RTP时间戳时钟频率设置,支持注册成功、注册超时、INVIT、ACK、BYE状态回调。

功能设计

Android端GB28181前端设备模块,支持常规的视频采集、编码设定,功能设计如下:

  • [本地预览]支持本地前后置摄像头预览;
  • [视频格式]H.264/H.265(Android H.265硬编码);
  • [音频格式]AAC;
  • [音量调节]Android平台采集端支持实时音量调节;
  • [H.264硬编码]支持H.264特定机型硬编码;
  • [H.265硬编码]支持H.265特定机型硬编码;
  • [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
  • [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
  • [横竖屏推流]Android平台支持支持横屏、竖屏推流;
  • [多分辨率支持]支持摄像头或屏幕多种分辨率设置;
  • [移动端推屏]Android平台支持后台service推送屏幕(推送屏幕需要5.0+版本);
  • [模式支持]媒体流支持最新GB28181-2016的UDP和TCP被动模式;
  • [参数设置]支持注册有效期、心跳间隔、心跳间隔次数、TCP/UDP信令设置;
  • [参数设置]支持RTP Sender IP地址类型、RTP Socket本地端口、SSRC、RTP socket 发送Buffer大小、RTP时间戳时钟频率设置;
  • [状态回调]支持注册成功、注册超时、INVIT、ACK、BYE状态回调;
  • [水印]支持文字水印、png水印;
  • [镜像]Android平台支持前置摄像头实时镜像功能;
  • [前后摄像头实时切换]Android平台支持采集过程中,前后摄像头切换;
  • [复杂网络处理]支持断网重连等各种网络环境自动适配;
  • [动态码率]支持根据网络情况自动调整推流码率;
  • [实时静音]支持推送过程中,实时静音/取消静音;
  • [实时快照]支持推流过程中,实时快照;
  • [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  • [外部编码前视频数据对接]支持YUV数据对接;
  • [外部编码前音频数据对接]支持PCM对接;
  • [外部编码后视频数据对接]支持外部H.264数据对接;
  • [外部编码后音频数据对接]外部AAC数据对接;
  • [扩展录像功能]支持和录像模块组合使用,录像相关功能;
  • [服务器兼容]支持标准GB28181服务。

接口设计

接口设计,我们分两块:RTP Sender接口和GB28181接口;

RTP Sender接口描述:

1. 创建RTP Sender实例,返回实例句柄:

 /** 创建RTP Sender实例** @param reserve:保留参数传0** @return RTP Sender 句柄,0表示失败*/public native long CreateRTPSender(int reserve);

2. 设置 RTP Sender传输协议,0:UDP, 1:TCP, 默认是UDP

 /***设置 RTP Sender传输协议** @param rtp_sender_handle, CreateRTPSender返回值* @param transport_protocol, 0:UDP, 1:TCP, 默认是UDP** @return {0} if successful*/public native int SetRTPSenderTransportProtocol(long rtp_sender_handle, int transport_protocol);

3. 设置RTP Sender IP地址类型,如IPv4和IPv6,当前仅支持IPv4

 /***设置 RTP Sender IP地址类型** @param rtp_sender_handle, CreateRTPSender返回值* @param ip_address_type, 0:IPV4, 1:IPV6, 默认是IPV4, 当前仅支持IPV4** @return {0} if successful*/public native int SetRTPSenderIPAddressType(long rtp_sender_handle, int ip_address_type);

4. 设置 RTP Sender RTP Socket本地端口,port, 必须是偶数,设置0的话SDK会自动分配, 默认值是0

 /***设置 RTP Sender RTP Socket本地端口** @param rtp_sender_handle, CreateRTPSender返回值* @param port, 必须是偶数,设置0的话SDK会自动分配, 默认值是0** @return {0} if successful*/public native int SetRTPSenderLocalPort(long rtp_sender_handle, int port);

5. 设置 RTP Sender SSRC

 /***设置 RTP Sender SSRC** @param rtp_sender_handle, CreateRTPSender返回值* @param ssrc, 如果设置的话,这个字符串要能转换成uint32类型, 否则设置失败** @return {0} if successful*/public native int SetRTPSenderSSRC(long rtp_sender_handle, String ssrc);

6. 设置 RTP Sender RTP socket 发送Buffer大小

 /***设置 RTP Sender RTP socket 发送Buffer大小** @param rtp_sender_handle, CreateRTPSender返回值* @param buffer_size, 必须大于0, 默认是512*1024, 当前仅对UDP socket有效, 根据视频码率考虑设置合适的值** @return {0} if successful*/public native int SetRTPSenderSocketSendBuffer(long rtp_sender_handle, int buffer_size);

7. 设置 RTP Sender RTP时间戳时钟频率

 /***设置 RTP Sender RTP时间戳时钟频率** @param rtp_sender_handle, CreateRTPSender返回值* @param clock_rate, 必须大于0, 对于GB28181 PS规定是90kHz, 也就是90000** @return {0} if successful*/public native int SetRTPSenderClockRate(long rtp_sender_handle, int clock_rate);

8. 设置 RTP Sender 目的IP地址, 注意当前用在GB2818推送上,只设置一个地址,将来扩展如果用在其他地方,可能要设置多个目的地址,到时候接口可能会调整

 /***设置 RTP Sender 目的IP地址, 注意当前用在GB2818推送上,只设置一个地址,将来扩展如果用在其他地方,可能要设置多个目的地址,到时候接口可能会调整** @param rtp_sender_handle, CreateRTPSender返回值* @param address, IP地址* @param port, 端口** @return {0} if successful*/public native int SetRTPSenderDestination(long rtp_sender_handle, String address, int port);

9. 初始化RTP Sender, 初始化之前先调用上面的接口配置相关参数

 /***初始化RTP Sender, 初始化之前先调用上面的接口配置相关参数** @param rtp_sender_handle, CreateRTPSender返回值** @return {0} if successful*/public native int InitRTPSender(long rtp_sender_handle);

10. 获取RTP Sender RTP Socket本地端口

 /***获取RTP Sender RTP Socket本地端口** @param rtp_sender_handle, CreateRTPSender返回值** @return 失败返回0, 成功的话返回响应的端口, 请在InitRTPSender返回成功之后调用*/public native int GetRTPSenderLocalPort(long rtp_sender_handle);

11. UnInit RTP Sender

 /*** UnInit RTP Sender** @param rtp_sender_handle, CreateRTPSender返回值** @return {0} if successful*/public native int UnInitRTPSender(long rtp_sender_handle);

12. 释放RTP Sender, 释放之后rtp_sender_handle就无效了,请不要再使用

 /*** 释放RTP Sender, 释放之后rtp_sender_handle就无效了,请不要再使用** @param rtp_sender_handle, CreateRTPSender返回值** @return {0} if successful*/public native int DestoryRTPSender(long rtp_sender_handle);

GB28181相关接口

1. 设置GB28181 RTP Sender

 /*** 设置GB28181 RTP Sender** @param rtp_sender_handle, CreateRTPSender返回值* @param rtp_payload_type, 对于GB28181 PS, 协议定义是96, 具体以SDP为准** @return {0} if successful*/public native int SetGB28181RTPSender(long handle, long rtp_sender_handle, int rtp_payload_type);

2. 启动 GB28181 媒体流

 /*** 启动 GB28181 媒体流** @return {0} if successful*/public native int StartGB28181MediaStream(long handle);

3. 停止 GB28181 媒体流

 /*** 停止 GB28181 媒体流** @return {0} if successful*/public native int StopGB28181MediaStream(long handle);

接口调用实例

1. 相关参数初始化

    /*** GB28181 相关参数,可以修改相关参数后测试 ***/GBSIPAgent     gb28181_agent_             = null;private int    gb28181_sip_local_port_    = 12070;private String gb28181_sip_server_id_     = "34020000002000000001";private String gb28181_sip_server_domain_ = "3402000000";private String gb28181_sip_server_addr_   = "192.168.0.105";private int    gb28181_sip_server_port_   = 15060;private String gb28181_sip_user_agent_filed_  = "NT GB28181 User Agent V1.0";private String gb28181_sip_username_   = "31011500991320000069";private String gb28181_sip_password_   = "12345678";private int gb28181_reg_expired_           = 3600; // 注册有效期时间最小3600秒private int gb28181_heartbeat_interval_    = 20; // 心跳间隔GB28181默认是60, 目前调整到20秒private int gb28181_heartbeat_count_       = 3; // 心跳间隔3次失败,表示和服务器断开了private int gb28181_sip_trans_protocol_    = 0; // 0表示信令用UDP传输, 1表示信令用TCP传输private long gb28181_rtp_sender_handle_ = 0;private int  gb28181_rtp_payload_type_  = 96;/*** GB28181 相关参数,可以修改相关参数后测试 ***/

2. 启动或停止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");}}}}

3. InitGB28181Agent实现

    private boolean initGB28181Agent(){if ( gb28181_agent_ != null )return  true;String local_ip_addr = IPAddrUtils.getIpAddress(myContext);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_.setLocalAddressInfo(local_ip_addr, gb28181_sip_local_port_);gb28181_agent_.setServerParameter(gb28181_sip_server_addr_, gb28181_sip_server_port_, gb28181_sip_server_id_, gb28181_sip_server_domain_);gb28181_agent_.setUserInfo(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.gb28181.ntsignalling.Device gb_device = new com.gb28181.ntsignalling.Device("34020000001380000001", "安卓测试设备", Build.MANUFACTURER, Build.MODEL,"宇宙","火星1","火星", true);getLocation(this);gb_device.setLongitude(mLongitude);gb_device.setLatitude(mLatitude);gb28181_agent_.addDevice(gb_device);if (!gb28181_agent_.initialize()) {gb28181_agent_ = null;Log.e(TAG, "initGB28181Agent gb28181_agent_.initialize failed.");return  false;}return true;}

4. 注册成功后,返回注册时间

    @Overridepublic void ntsRegisterOK(String dateString) {Log.i(TAG, "ntsRegisterOK Date: " + (dateString!= null? dateString : ""));}

5. 注册超时回调

    @Overridepublic void ntsRegisterTimeout() {Log.e(TAG, "ntsRegisterTimeout");}

6. 注册transport异常回调

    @Overridepublic void ntsRegisterTransportError(String errorInfo) {Log.e(TAG, "ntsRegisterTransportError error:" + (errorInfo != null?errorInfo :""));}

7. 心跳异常回调

   @Overridepublic void ntsOnHeartBeatException(int exceptionCount,  String lastExceptionInfo) {Log.e(TAG, "ntsOnHeartBeatException heart beat timeout count reached, count:" + exceptionCount+", exception info:" + (lastExceptionInfo!=null?lastExceptionInfo:""));// 10毫秒后,停止信令, 然后重启handler.postDelayed(new Runnable() {@Overridepublic void run() {Log.i(TAG, "gb28281_heart_beart_timeout");stopGB28181Stream();destoryRTPSender();if (gb28181_agent_ != null) {Log.i(TAG, "gb28281_heart_beart_timeout sip stop");gb28181_agent_.stop();Log.i(TAG, "gb28281_heart_beart_timeout sip start");gb28181_agent_.start();}}},10);}

8. Invite返回OK后,创建RTP Sender,根据返回的信息,设定相关参数

   @Overridepublic void ntsOnInvitePlay(String deviceId, InvitePlaySessionDescription session_des) {handler.postDelayed(new Runnable() {@Overridepublic void run() {Log.i(TAG,"ntsInviteReceived, device_id:" +device_id_+", is_tcp:" + session_des_.isRTPOverTCP()+ " rtp_port:" + session_des_.getMediaPort() + " ssrc:" + session_des_.getSSRC()+ " address_type:" + session_des_.getAddressType() + " address:" + session_des_.getAddress());// 可以先给信令服务器发送临时振铃响应//sip_stack_android.respondPlayInvite(180, device_id_);long rtp_sender_handle = libPublisher.CreateRTPSender(0);if ( rtp_sender_handle == 0 ) {gb28181_agent_.respondPlayInvite(488, device_id_);Log.i(TAG, "ntsInviteReceived CreateRTPSender failed, response 488, device_id:" + device_id_);return;}gb28181_rtp_payload_type_ = session_des_.getPSRtpMapAttribute().getPayloadType();libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle, session_des_.isRTPOverUDP()?0:1);libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle, session_des_.isIPv4()?0:1);libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0);libPublisher.SetRTPSenderSSRC(rtp_sender_handle, session_des_.getSSRC());libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 设置到2MlibPublisher.SetRTPSenderClockRate(rtp_sender_handle, session_des_.getPSRtpMapAttribute().getClockRate());libPublisher.SetRTPSenderDestination(rtp_sender_handle, session_des_.getAddress(), session_des_.getMediaPort());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(myContext);gb28181_agent_.respondPlayInviteOK(device_id_,local_ip_addr, local_port);gb28181_rtp_sender_handle_ = rtp_sender_handle;}private String device_id_;private InvitePlaySessionDescription session_des_;public Runnable set(String device_id, InvitePlaySessionDescription session_des) {this.device_id_ = device_id;this.session_des_ = session_des;return this;}}.set(deviceId, session_des),0);}

9. 取消播放

    @Overridepublic void ntsOnCancelPlay(String deviceId) {// 这里取消Play会话handler.postDelayed(new Runnable() {@Overridepublic void run() {Log.i(TAG, "ntsOnCancelPlay, deviceId=" + device_id_);destoryRTPSender();}private String device_id_;public Runnable set(String device_id) {this.device_id_ = device_id;return this;}}.set(deviceId),0);}

10. Ack收到后,开始发送音视频数据

    @Overridepublic void ntsOnAckPlay(String deviceId) {handler.postDelayed(new Runnable() {@Overridepublic void run() {Log.i(TAG,"ntsOnACKPlay, device_id:" +device_id_);if (!isRecording && !isRTSPPublisherRunning && !isPushingRtsp && !isPushingRtmp) {InitAndSetConfig();}libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_);int startRet = libPublisher.StartGB28181MediaStream(publisherHandle);if (startRet != 0) {if (!isRecording && !isRTSPPublisherRunning && !isPushingRtmp && !isPushingRtsp) {if (publisherHandle != 0) {libPublisher.SmartPublisherClose(publisherHandle);publisherHandle = 0;}}destoryRTPSender();Log.e(TAG, "Failed to start GB28181 service..");return;}if (!isRecording && !isRTSPPublisherRunning && !isPushingRtsp && !isPushingRtmp) {if (pushType == 0 || pushType == 1) {CheckInitAudioRecorder();    //enable pure video publisher..}}isGB28181StreamRunning = true;}private String device_id_;public Runnable set(String device_id) {this.device_id_ = device_id;return this;}}.set(deviceId),0);}

11. Invite异常处理

    @Overridepublic void ntsOnPlayInviteResponseException(String deviceId, int statusCode, String errorInfo) {// 这里要释放掉响应的资源Log.i(TAG, "ntsOnPlayInviteResponseException, deviceId=" + deviceId + " statusCode=" +statusCode+ " errorInfo:" + errorInfo);handler.postDelayed(new Runnable() {@Overridepublic void run() {Log.i(TAG, "ntsOnPlayInviteResponseException, deviceId=" + device_id_);destoryRTPSender();}private String device_id_;public Runnable set(String device_id) {this.device_id_ = device_id;return this;}}.set(deviceId),0);}

12. 收到Bye停止发送数据

   @Overridepublic void ntsOnByePlay(String deviceId){handler.postDelayed(new Runnable() {@Overridepublic 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);}

13. Play Dialog终止处理

    @Overridepublic void ntsOnPlayDialogTerminated(String deviceId) {/*Play会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发收到这个请做相关清理处理*/handler.postDelayed(new Runnable() {@Overridepublic void run() {Log.i(TAG, "ntsOnPlayDialogTerminated, 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设计,除了支持TCP和UDP传输外,支持信令和数据传输分离,可实现其他终端针对前端设备的按需播放和处理,无需单独的信令支撑。缺点是外部支持GB28181的服务器不多,开源如SRS服务器针对GB28181的支持暂不够商用级,期待后续版本升级。

												

如何实现Android平台GB28181前端设备接入相关推荐

  1. Android平台GB28181设备接入端如何支持跨网段语音对讲

    技术背景 如果你是音视频开发者亦或寻求这块技术方案的公司,在探讨这个问题之前,你可能网上看了太多关于语音广播和语音对讲相关的资料,大多文章认为语音对讲和语音广播无本质区别,实现思路也大同小异. 今天我 ...

  2. Android平台GB28181设备接入端语音广播支持PS格式

    技术背景 对接Android平台GB28181设备接入端语音广播的时候,我们有遇到过INVITE SDP需要PCMA格式的audio,对方同时回了PS和PCMA两种,然后,发数据的时候,直接发了PS的 ...

  3. Android平台GB28181设备接入模块之球机/云台控制探究

    技术背景 好多开发者在做GB28181设备接入的时候,问云台控制是否可以处理(亦或拉取外部RTSP摄像头,通过命令中转的方式,控制摄像头),实际上云台控制命令相对来说还是比较好处理的.协议规范有明确说 ...

  4. Android平台GB28181接入模块技术接入说明

    技术背景 今天,我们主要讲讲Android平台GB28181接入模块的技术对接,Android平台GB28181接入模块设计的目的,可实现不具备国标音视频能力的 Android终端,通过平台注册接入到 ...

  5. Android平台GB28181接入端如何对接UVC摄像头?

    我们在对接Android平台GB28181接入的时候,有公司提出这样的需求,除了采集执法记录仪摄像头自带的数据外,还想通过执法记录仪采集外接UVC摄像头. 实际上,这块对我们来说有点炒冷饭了,不算新的 ...

  6. GB/T28181-2016传输要求和Android平台设备接入技术实现

    相关协议规范 GB/T28181-2016公共安全视频监控联网系统 信息传输.交换.控制技术要求相关的传输要求如下: 5.1 网络传输协议要求 联网系统网络层应支持IP协议,传输层应支持 TCP和 U ...

  7. LiveGBS国标视频平台如何获取接入视频通道的直播流地址HLS/HTTP-FLV/WS-FLV/WebRTC/RTMP/RTSP

    1.背景说明 LiveGBS国标GB/T28181流媒体服务器软件,支持设备|平台GB28181注册接入.向上级联第三方国标平台, 可视化的WEB页面管理(页面源码开源):支持云台控制.设备录像检索. ...

  8. 如何快速实现Android平台前端设备接入能力

    技术背景 SIP(会话初始化协议)是在 IP网络上进行多媒体通信的应用层控制协议,以几种RFC的形式提供,其中最重要的是包含核心协议规范的RFC3261.该协议用于创建,修改和终止与一个或多个参与者的 ...

  9. Android平台RTMP推流或轻量级RTSP服务(摄像头或同屏)编码前数据接入类型总结

    很多开发者在做Android平台RTMP推流或轻量级RTSP服务(摄像头或同屏)时,总感觉接口不够用,以大牛直播SDK为例 (Github) 我们来总结下,我们常规需要支持的编码前音视频数据有哪些类型 ...

最新文章

  1. 年收入百万美元AI科学家的烦恼与思考
  2. 《Linux总线、设备与驱动》USB设备发现机制
  3. 计算机组装学位,《计算机组装与维护》虚拟实验界面设计与制作学位论文 .doc...
  4. SQL基础操作_5_字符串处理
  5. MicroSDCard是什么
  6. [翻译] KGModal
  7. nginx 多个root_dockerfile定制自己的nginx
  8. Linux防火墙之介绍
  9. 【数据库原理实验(openGauss)】数据库的备份与恢复
  10. linux 查看内存和cup使用率
  11. VS2010 asp.net development server 无法展示svg图片
  12. python暴力破解
  13. 新手入门 哪个视频剪辑软件好用
  14. 程序员合同日期不到想辞职_在职场,辞职有时是难免的,要怎样写辞职信才好呢...
  15. phpspreadsheet 中文文档(七)技巧和诀窍
  16. thinkphp3.1迁移php7,ThinkPHP3.1迁移到PHP7
  17. PPC气箱脉冲除尘器
  18. AppStore隐私政策网址(URL)
  19. 头条权重是什么?头条权重怎么查询?
  20. 根据传入日期 往前或者往后 顺延月份

热门文章

  1. java 抽奖 高并发处理_如何设计高并发下的抽奖?
  2. react table里跳转页面_react路由配置基础篇:react-router4.0及以上
  3. docker简介与搭建
  4. dedecms 在模板里引入php文件夹,dedecms如何添加并引入php文件
  5. spring aop 必须的包 及里面用到的东西_Spring 原理初探——IoC、AOP
  6. 计算机C语言课交作业怎么交,第一份c语言作业
  7. 华为鸿蒙系统正式拜拜,从“哄蒙”到“鸿蒙”,现在,正式对华为鸿蒙OS说你好!...
  8. observable_Java Observable setChanged()方法与示例
  9. date.gethour_Java LocalDateTime类| 带示例的getHour()方法
  10. 带有Python示例的math.exp()方法