好多开发者有这样的诉求,想把本地录制的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;}@Overridepublic void ntsRegisterOK(String dateString) {Log.i(TAG, "ntsRegisterOK Date: " + (dateString!= null? dateString : ""));}@Overridepublic void ntsRegisterTimeout() {Log.e(TAG, "ntsRegisterTimeout");}@Overridepublic void ntsRegisterTransportError(String errorInfo) {Log.e(TAG, "ntsRegisterTransportError error:" + (errorInfo != null?errorInfo :""));}@Overridepublic 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() {@Overridepublic 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);}@Overridepublic void ntsOnInvitePlay(String deviceId, PlaySessionDescription session_des) {handler_.postDelayed(new Runnable() {@Overridepublic 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); // 设置到2MlibPublisher.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);}@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);}@Overridepublic void ntsOnAckPlay(String deviceId) {handler_.postDelayed(new Runnable() {@Overridepublic 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);}@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);}@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);}@Overridepublic void ntsOnTerminatePlay(String deviceId) {handler_.postDelayed(new Runnable() {@Overridepublic 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);}@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);}@Overridepublic void ntsOnDevicePositionRequest(String deviceId, int interval) {handler_.postDelayed(new Runnable() {@Overridepublic 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);}@Overridepublic void ntsOnDeviceControlTeleBootCommand(String deviceId, String teleBootValue) {handler_.postDelayed(new Runnable() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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分钟均匀的发出去”。