好多开发者有这样的诉求,想把本地录制的MP4文件,以实时流数据的形式,推送到RTMP服务器,注入轻量级RTSP服务,或者对接到GB28181平台,这块前几年我们就有对接。
本次以MediaExtractor为例,先利用MediaExtractor,把mp4文件的音视频数据分离,然后调用我们publisher模块,实现编码后的数据对接到RTMP服务器、轻量级RTSP服务或GB28181平台即可,废话不多说,上代码,由于实例代码比较简单,不再赘述用法:
/* * SmartPublisherActivity.java * Github: https://github.com/daniulive/SmarterStreaming */ private void InitMediaExtractor(){ File mFile = new File("/storage/emulated/0/","2022.mp4"); if (!mFile.exists()){ Log.e(TAG, "mp4文件不存在"); return; } MediaExtractor mediaExtractor = new MediaExtractor(); try { mediaExtractor.setDataSource(mFile.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); } int count = mediaExtractor.getTrackCount();//获取轨道数量 Log.e(TAG, "轨道数量 = "+count); for (int i = 0; i < count; i++) { MediaFormat trackFormat = mediaExtractor.getTrackFormat(i); String mineType = trackFormat.getString(MediaFormat.KEY_MIME); Log.e(TAG, i + "编号通道格式 = " + mineType); //视频信道 if (mineType.startsWith("video/")) { video_track_index = i; is_has_video = true; try { video_media_extractor.setDataSource(mFile.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); } if(mineType.equals("video/avc")) { video_codec_id = 1; } else if(mineType.equals("video/hevc")) { video_codec_id = 2; } int width = trackFormat.getInteger(MediaFormat.KEY_WIDTH); int height = trackFormat.getInteger(MediaFormat.KEY_HEIGHT); long duration = trackFormat.getLong(MediaFormat.KEY_DURATION);//总时间 int video_fps = trackFormat.getInteger(MediaFormat.KEY_FRAME_RATE);//帧率 max_sample_size = trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);//获取视频缓存输出的最大大小 Log.e(TAG, "video width " + width + ", height: " + height + ", duration: " + duration + ", max_sample_size: " + max_sample_size + ", fps: " + video_fps); } //音频信道 if (mineType.startsWith("audio/")) { audio_track_index = i; is_has_audio = true; try { audio_media_extractor.setDataSource(mFile.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); } if(mineType.equals("audio/mp4a-latm")) { audio_codec_id = 0x10002; } audio_sample_rate = trackFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);//获取采样率 int audioTrackBitrate = trackFormat.getInteger(MediaFormat.KEY_BIT_RATE); //获取比特率 int channels = trackFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); //获取声道数量 Log.e(TAG, "mp4 audio_sample_rate " + audio_sample_rate + ", audioTrackBitrate: " + audioTrackBitrate + ", channels: " + channels); } } }
视频数据处理,先切到视频信道,然后调用readSampleData(),读取到video数据后,判断是不是关键帧,是关键帧的话,带上sps pps(如果是h265 带上vps sps pps),一般来说,比如无人机等设备回调,大多都贴心的实现了关键帧前携带sps pps,也有的设备是单独发sps pps,所以,对接的时候,可以先把数据打印出来看看,具体问题具体分析即可,获取video数据后,通过SmartPublisherPostVideoEncodedData()投递到底层:
//切换到视频信道 video_media_extractor.selectTrack(video_track_index);
if(IsVpsSpsPps(video_header_checker_buffer, video_codec_id)) { is_key_frame = true; } if ( isPushing || isRTSPPublisherRunning || isGB28181StreamRunning) { libPublisher.SmartPublisherPostVideoEncodedData(publisherHandle, video_codec_id, byteBuffer, video_sample_size, is_key_frame?1:0, cur_sample_time, cur_sample_time); }
audio也是类似的流程,audio有一点,需要先拿到audio param info,然后,调用readSampleData()获取到audio数据,调用SmartPublisherPostAudioEncodedData()投递出去即可。
byte[] audio_param_info = GetAudioParamInfo(); ByteBuffer parameter_info = ByteBuffer.allocateDirect(2); parameter_info.put(audio_param_info); int parameter_info_size = 2; audio_media_extractor.selectTrack(audio_track_index);
int audio_sample_size = audio_media_extractor.readSampleData(byteBuffer, 0); if(audio_sample_size < 0) { Log.i(TAG, "audio reach the end.."); break; } long cur_sample_time = audio_media_extractor.getSampleTime()/1000; if ( isPushing || isRTSPPublisherRunning || isGB28181StreamRunning) { libPublisher.SmartPublisherPostAudioEncodedData(publisherHandle, audio_codec_id, byteBuffer, audio_sample_size, 0, cur_sample_time, parameter_info, parameter_info_size); }
数据投递讲完后,就是推送模块接口的处理,获取到的数据,是可以对接到RTMP推送模块,或者轻量级RTSP服务亦或GB28181设备接入模块,这些模块,都可以在一个实例内完成,所以,我们先调用OpenPushHandle()完成publisherHandle生成,并设置event callback。
private boolean OpenPushHandle() { if(publisherHandle != 0) { return true; } int audio_opt = 2; int video_opt = 2; int videoWidth = 640; int videoHeight = 480; publisherHandle = libPublisher.SmartPublisherOpen(context_, audio_opt, video_opt, videoWidth, videoHeight); if (publisherHandle == 0 ) { Log.e(TAG, "OpenPushHandle failed!"); return false; } Log.i(TAG, "publisherHandle=" + publisherHandle); libPublisher.SetSmartPublisherEventCallbackV2(publisherHandle, new EventHandePublisherV2()); return true; }
RTMP推送相关处理:
private boolean StartPush() { if (isPushing) return false; //relayStreamUrl = "rtmp://192.168.1.77/hls/stream1"; if (relayStreamUrl == null) { Log.e(TAG, "StartPush URL is null..."); return false; } if (!OpenPushHandle()) return false; if ( libPublisher.SmartPublisherSetURL(publisherHandle, relayStreamUrl) != 0 ) { Log.e(TAG, "StartPush failed!"); } int startRet = libPublisher.SmartPublisherStartPublisher(publisherHandle); if( startRet != 0) { Log.e(TAG, "Failed to call StartPublisher!"); if(!isRTSPPublisherRunning && !isGB28181StreamRunning) { libPublisher.SmartPublisherClose(publisherHandle); publisherHandle = 0; } return false; } isPushing = true; return true; } public void StopPush() { if (!isPushing) return; isPushing = false; libPublisher.SmartPublisherStopPublisher(publisherHandle); if(!isRTSPPublisherRunning && !isRTSPServiceRunning && !isGB28181StreamRunning) { libPublisher.SmartPublisherClose(publisherHandle); publisherHandle = 0; } }
轻量级RTSP服务相关处理:
//启动/停止RTSP服务 class ButtonRtspServiceListener implements OnClickListener { public void onClick(View v) { if (isRTSPServiceRunning) { stopRtspService(); btnRtspService.setText("启动RTSP服务"); btnRtspPublisher.setEnabled(false); isRTSPServiceRunning = false; return; } if(!OpenPushHandle()) { return; } Log.i(TAG, "onClick start rtsp service.."); rtsp_handle_ = libPublisher.OpenRtspServer(0); if (rtsp_handle_ == 0) { Log.e(TAG, "创建rtsp server实例失败! 请检查SDK有效性"); } else { int port = 8554; if (libPublisher.SetRtspServerPort(rtsp_handle_, port) != 0) { libPublisher.CloseRtspServer(rtsp_handle_); rtsp_handle_ = 0; Log.e(TAG, "创建rtsp server端口失败! 请检查端口是否重复或者端口不在范围内!"); } if (libPublisher.StartRtspServer(rtsp_handle_, 0) == 0) { Log.i(TAG, "启动rtsp server 成功!"); } else { libPublisher.CloseRtspServer(rtsp_handle_); rtsp_handle_ = 0; Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!"); } btnRtspService.setText("停止RTSP服务"); btnRtspPublisher.setEnabled(true); isRTSPServiceRunning = true; } } } //发布/停止RTSP流 class ButtonRtspPublisherListener implements OnClickListener { public void onClick(View v) { if (isRTSPPublisherRunning) { stopRtspPublisher(); btnRtspPublisher.setText("发布RTSP流"); btnGetRtspSessionNumbers.setEnabled(false); btnRtspService.setEnabled(true); } else { Log.i(TAG, "onClick start rtsp publisher.."); boolean startRet = StartRtspStream(); if (!startRet) { Log.e(TAG, "Failed to call StartRtspStream()."); return; } btnRtspPublisher.setText("停止RTSP流"); btnGetRtspSessionNumbers.setEnabled(true); btnRtspService.setEnabled(false); } } }; //当前RTSP会话数弹出框 private void PopRtspSessionNumberDialog(int session_numbers) { final EditText inputUrlTxt = new EditText(this); inputUrlTxt.setFocusable(true); inputUrlTxt.setEnabled(false); String session_numbers_tag = "RTSP服务当前客户会话数: " + session_numbers; inputUrlTxt.setText(session_numbers_tag); AlertDialog.Builder builderUrl = new AlertDialog.Builder(this); builderUrl .setTitle("内置RTSP服务") .setView(inputUrlTxt).setNegativeButton("确定", null); builderUrl.show(); } //获取RTSP会话数 class ButtonGetRtspSessionNumbersListener implements OnClickListener { public void onClick(View v) { if (libPublisher != null && rtsp_handle_ != 0) { int session_numbers = libPublisher.GetRtspServerClientSessionNumbers(rtsp_handle_); Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers); PopRtspSessionNumberDialog(session_numbers); } } };
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() { if(!isGB28181StreamRunning) return; if (libPublisher != null) { libPublisher.StopGB28181MediaStream(publisherHandle); } if (!isRecording && !isRTSPPublisherRunning && !isPushing) { if (publisherHandle != 0) { if (libPublisher != null) { libPublisher.SmartPublisherClose(publisherHandle); publisherHandle = 0; } } } isGB28181StreamRunning = false; }
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_.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("34020000001310000001", "安卓测试设备", 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 :"")); } @Override public void ntsOnHeartBeatException(int exceptionCount, String lastExceptionInfo) { Log.e(TAG, "ntsOnHeartBeatException heart beat timeout count reached, count:" + exceptionCount + ", exception info:" + (lastExceptionInfo != null ? lastExceptionInfo : "")); // 停止信令, 然后重启 handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "gb28281_heart_beart_timeout"); stopGB28181Stream(); destoryRTPSender(); if (gb28181_agent_ != null) { gb28181_agent_.terminateAllPlays(true); Log.i(TAG, "gb28281_heart_beart_timeout sip stop"); gb28181_agent_.stop(); String local_ip_addr = IPAddrUtils.getIpAddress(context_); if (local_ip_addr != null && !local_ip_addr.isEmpty()) { Log.i(TAG, "gb28281_heart_beart_timeout get local ip addr: " + local_ip_addr); gb28181_agent_.setLocalAddress(local_ip_addr); } Log.i(TAG, "gb28281_heart_beart_timeout sip start"); gb28181_agent_.start(); } } }, 0); } @Override public void ntsOnInvitePlay(String deviceId, PlaySessionDescription session_des) { handler_.postDelayed(new Runnable() { @Override public void run() { MediaSessionDescription video_des = session_des_.getVideoDescription(); SDPRtpMapAttribute ps_rtpmap_attr = video_des.getPSRtpMapAttribute(); Log.i(TAG,"ntsInviteReceived, 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()); // 可以先给信令服务器发送临时振铃响应 //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_ = 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_); gb28181_agent_.respondPlayInviteOK(device_id_,local_ip_addr, local_port); gb28181_rtp_sender_handle_ = rtp_sender_handle; } private String device_id_; private PlaySessionDescription session_des_; public Runnable set(String device_id, PlaySessionDescription session_des) { this.device_id_ = device_id; this.session_des_ = session_des; return this; } }.set(deviceId, session_des),0); } @Override public void ntsOnCancelPlay(String deviceId) { // 这里取消Play会话 handler_.postDelayed(new Runnable() { @Override public 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); } @Override public void ntsOnAckPlay(String deviceId) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG,"ntsOnACKPlay, device_id:" +device_id_); if (!isRecording && !isRTSPPublisherRunning && !isPushing) { OpenPushHandle(); } libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_); int startRet = libPublisher.StartGB28181MediaStream(publisherHandle); if (startRet != 0) { if (!isRecording && !isRTSPPublisherRunning && !isPushing) { if (publisherHandle != 0) { libPublisher.SmartPublisherClose(publisherHandle); publisherHandle = 0; } } destoryRTPSender(); Log.e(TAG, "Failed to start GB28181 service.."); return; } isGB28181StreamRunning = true; } private String device_id_; public Runnable set(String device_id) { this.device_id_ = device_id; return this; } }.set(deviceId),0); } @Override public void ntsOnPlayInviteResponseException(String deviceId, int statusCode, String errorInfo) { // 这里要释放掉响应的资源 Log.i(TAG, "ntsOnPlayInviteResponseException, deviceId=" + deviceId + " statusCode=" +statusCode + " errorInfo:" + errorInfo); handler_.postDelayed(new Runnable() { @Override public 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); } @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); } @Override public void ntsOnTerminatePlay(String deviceId) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "ntsOnTerminatePlay, 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); } @Override public void ntsOnPlayDialogTerminated(String deviceId) { /* Play会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发 收到这个请做相关清理处理 */ handler_.postDelayed(new Runnable() { @Override public 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); } @Override public void ntsOnDevicePositionRequest(String deviceId, int interval) { handler_.postDelayed(new Runnable() { @Override public void run() { getLocation(context_); Log.v(TAG, "ntsOnDevicePositionRequest, deviceId:" + this.device_id_ + ", Longitude:" + mLongitude + ", Latitude:" + mLatitude + ", Time:" + mLocationTime); 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); if (gb28181_agent_ != null ) { gb28181_agent_.updateDevicePosition(device_id_, device_pos); } } } private String device_id_; private int interval_; public Runnable set(String device_id, int interval) { this.device_id_ = device_id; this.interval_ = interval; return this; } }.set(deviceId, interval),0); } @Override public void ntsOnDeviceControlTeleBootCommand(String deviceId, String teleBootValue) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "ntsOnDeviceControlTeleBootCommand device_id:" + device_id_ + " tele_boot_value:" + tele_boot_value_); stopGB28181Stream(); destoryRTPSender(); if (gb28181_agent_ != null ) { gb28181_agent_.terminateAllPlays(true); gb28181_agent_.stop(); } // 发送注销消息后,等待2000毫秒, 再释放资源 handler_.postDelayed(new Runnable() { @Override public void run() { if (gb28181_agent_ != null ) { Log.i(TAG, " gb28181_agent_.unInitialize++"); gb28181_agent_.unInitialize(); gb28181_agent_.unBindLocalPort(); gb28181_agent_.releaseSipStack(); Log.i(TAG, " gb28181_agent_.unInitialize--"); gb28181_agent_ = null; } // 200毫秒后再重启 handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "restart gb sip agent."); if (null==gb28181_agent_) { if (!initGB28181Agent()) { Log.e(TAG, "init gb sip agent failed."); return; } } if (!gb28181_agent_.isRunning()) { if ( !gb28181_agent_.start() ) { Log.e(TAG, "restart gb sip agent failed."); } } } },200); } },2000); } private String device_id_; private String tele_boot_value_; public Runnable set(String device_id, String tele_boot_value) { this.device_id_ = device_id; this.tele_boot_value_ = tele_boot_value; return this; } }.set(deviceId, teleBootValue),0); }
以上就是大概流程,需要注意的是,本地MP4文件作为实时数据发送的时候,需要注意时间戳的问题,简单来说,确保“1分钟的数据,按照时间戳间隔,1分钟均匀的发出去”。