2. 功能与流程
2.1 命令交互
2.1.1 功能描述
App与设备是通过TCP/UDP协议来通信,具体的各种操控是以协商好的指令来定义。已定义的命令可在SDK中 Topic 类中查看(app/sdk-doc中也有说明), 其被封装成接口以供使用(
DeviceClient
类)。在App向设备发送命令后, 设备以相同的命令回复App(附录中有关于命令的返回内容格式),如拍照接口:
2.1.2 使用示例
1// 1、发送命令。ClientManager类参考1.3.2节;发送拍照命令
2 ClientManager.getClient().tryToTakePhoto(new SendResponse() {
3 @Override
4 public void onResponse(Integer code) {
5 if (code != SEND_SUCCESS) {
6 Dbug.e(tag, "Send failed");
7 }
8 }
9});
10
11//2、创建接收命令监听器对象
12private OnNotifyListener onNotifyResponse = new OnNotifyListener() {
13 @Override
14 public void onNotify(NotifyInfo data) {
15 String topic = data.getTopic();
16 if (data.getErrorType() != Code.ERROR_NONE) {
17 return;//实际开发和使用, 一定要处理设备返回的错误回复
18 }
19 if (Topic.PHOTO_CTRL.equals(topic)) {
20 // TODO: 设备回复命令携带 照片文件名
21 Log.i(TAG, "data:" + data);
22 }
23 }
24};
25
26// 3、注册命令回复监听器
27ClientManager.getClient().registerNotifyListener(onNotifyResponse);
28
29// 4、最后,不用的时要注销监听器,否则会有内存泄漏
30ClientManager.getClient().unregisterNotifyListener(onNotifyResponse);
2.1.3 Topic命令
小技巧
这里只列出部分,详细的命令请看Topic类
1/**
2 * 拍照控制
3 * 拍照命令,当设备拍照完成后,会返回照片文件名给APP。
4 * 操作类型:(“PUT”,“NOTIFY”)
5 * 参数:
6 * dir 照片文件名
7 */
8public static final String PHOTO_CTRL = "PHOTO_CTRL";
9
10/**
11 * 功能描述:
12 * 用于保持手机和设备间连接,当手机接入到设备端以后,必须在心跳包超时时间内发送心跳包给设备,设备接收到心跳包会立刻返回心跳包给手机。
13 * 手机可根据设备描述文档获得设备心跳包超时时间。
14 * 操作类型:“PUT” “NOTIFY"
15 * 参数:无
16 */
17public static final String KEEP_ALIVE = "CTP_KEEP_ALIVE";
18
19/**
20 * 心跳包间隔
21 * 操作类型:“GET”, “NOTIFY”
22 * 参数:
23 * timeout 间隔时间(单位:ms)
24 */
25public static final String KEEP_ALIVE_INTERVAL = "KEEP_ALIVE_INTERVAL";
26
27/**
28 * 功能描述
29 * APP每次建立连接,必须发送此Topic给设备。设备根据当前情况允许或拒绝连接。当app接入设备后,设备会主动将心跳包间隔、白平衡、sd卡状态、
30 * 电池状态等信息发送给app,app根据这些信息进行界面初始化。
31 * 操作类型:(“PUT”, “NOTIFY”)
32 * 参数:
33 * type 手机类型(0:Android, 1:iOS)
34 * ver APP版本号
35 */
36public static final String APP_ACCESS = "APP_ACCESS";
37
38/**
39 * TF卡状态: 在线状态(0:离线, 1:在线)
40 * 操作类型:“GET” “NOTIFY”
41 * 参数:
42 * online 在线状态(0:离线, 1:在线)
43 */
44public static final String TF_STATUS = "SD_STATUS";
2.2 描述文档
设备支持部分功能:在App连接上后,通过一种描述文档来告诉App是否支持,或支持什么类型。具体的描述文档可查找附录。
2.2.1 流程描述
App准备与设备建立连接时(参考1.3 SDK配置), 必须先发送命令
APP_ACCESS
给设备; 设备根据当前情况回复App允许或拒绝状态。同时,App必须要通过http方式获设备描述文档:
http://ip/:8080/mnt/spiflash/res/dev_desc.txt
。此文档是JSon格式,描述设备支持什么功能。
App接入设备的过程
2.2.2 使用示例
// 获取设备描述文档
String url = "http://ip/:8080/mnt/spiflash/res/dev_desc.txt";
HttpManager.downloadFile(url, new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// TODO:失败
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.code() == 200) {
ResponseBody responseBody = response.body();
if (responseBody != null) {
byte[] bytes = responseBody.bytes();
if (bytes != null) {
// TOTO:解析JSON
}
}
}
response.close();
}
});
public class HttpManager {
/**
* 下载文件
* @param url 文件下载路径
* @param callback 请求回调
*/
public static void downloadFile(String url, Callback callback) {
if (TextUtils.isEmpty(url)) {
Dbug.e("downloadFile", "url is null");
return;
}
Request request = new Request.Builder().url(url).build();
HttpLoggingInterceptor logInterceptor = new HttpLoggingInterceptor(new HttpLogger());
logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
Call mCall = new OkHttpClient().newBuilder()
.writeTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(logInterceptor)
.build().newCall(request);
mCall.enqueue(callback);
}
}
具体可参考工程中 MainActivity、 BaseActivity 、 CommunicationService 类。
2.3 实时预览
2.3.1 流程描述
App打开实时流的过程
调用
DeviceClient#tryToOpenRTStream()
给设备,请求打开实时流。设备回复同样命令并携带参数信息, 如视频格式、帧率、宽高; App根据这些参数来创建SDP服务器, 用于 ijkplayer 播放器初始化RTP SDP配置。
创建数据流接收通道
RealtimeStream#create()
, 配置RTP端口RealtimeStream#configure()
(SDK中把接收的数据封装成RTP再转发给播放器)。注册
RealtimeStream#registerStreamListener()
监听音视频帧数据、数据流状态等。初始化 ijkplayer 播放器, 并把SDP服务器链接传给播放器播放(相当于把RTP的参数信息告诉播放器)。
注意
使用工程的播放器,1)若不配置RTP端口, 按流程是不会播放实时流, 2)若不创建SDP服务器供ijkplayer播放器使用, 预览将会播放失败。
注册
RealtimeStream#registerStreamListener()
监听数据流, 回调将得的是音视频裸流; 其中视频帧,若是JPEG格式,则是一张JPEG数据; 若是H.264格式, 则是一帧H.264数据; 回调得到的音频数据, 一般是一帧PCM裸数据ijkplayer是工程默认的播放器, 开发者可通过不配置RTP端口来禁用自带播放器。然后, 从监听视频流接口得到音视频数据, 自行实现播放实时流
2.3.2 使用示例
1// 1、发送打开实时流命令,函数的参数分别是:摄像头类型(前/后)、视频格式(JPEG/H264)、分辨率宽、分辨率高、帧率、发送命令的状态回调
2ClientManager.getClient().tryToOpenRTStream(cameraType, mRTSType, resolution[0], resolution[1], getVideoRate(cameraType), new SendResponse() {
3 @Override
4 public void onResponse(Integer code) {
5 if (code != SEND_SUCCESS) {
6 Dbug.e(tag, "Send failed!!!");
7 }
8 }
9});
10
11// 2、创建接收命令监听器对象, 参考2.1.2节
12
13// 3、创建接收数据监听器,同时可以监听状态变化、错误状态
14private final OnRealTimeListener streamListener = new OnRealTimeListener() {
15 @Override
16 public void onVideo(int type, int channel, byte[] data, long sequence, long timestamp) {
17 // TODO:视频帧数据
18 }
19
20 @Override
21 public void onAudio(int type, int channel, byte[] data, long sequence, long timestamp) {
22 // TODO:音频帧数据
23 }
24
25 @Override
26 public void onStateChanged(int state) {
27 // TODO:有Stream.Status.PLAYING、Stream.Status.STOP
28 }
29
30 @Override
31 public void onError(int code, String message) {
32 // TODO 处理错误情况
33 }
34};
35
36// 4、根据参数信息创建SDP服务器
37private void startSdpServer(NotifyInfo data, String preferencesName) {
38 mSdpServer = new SDPServer(IConstant.SDP_PORT, format);
39 mSdpServer.setFrameRate(frameRate);
40 mSdpServer.setSampleRate(sampleRate);
41 mSdpServer.setRtpVideoPort(IConstant.RTP_VIDEO_PORT1);
42 mSdpServer.setRtpAudioPort(IConstant.RTP_AUDIO_PORT1);
43 mSdpServer.start();
44}
45// 关闭服务器
46private void stopSdpServer() {
47 if (mSdpServer != null) {
48 mSdpServer.stopRunning();
49 mSdpServer = null;
50 }
51}
52
53// 5、初始化播放器
54private void initPlayer(String videoPath) {
55 final Uri uri = Uri.parse(videoPath);
56 IjkMediaPlayer.loadLibrariesOnce(null);
57 IjkMediaPlayer.native_profileBegin("libijkplayer.so");
58 mVideoView.setRealtime(true);
59 mVideoView.setVideoURI(uri);
60 mVideoView.start();
61}
62// 释放播放器
63private void deinitPlayer() {
64 if (mVideoView != null) {
65 mVideoView.stopPlayback();
66 mVideoView.release(true);
67 mVideoView.stopBackgroundPlay();
68 }
69
70 IjkMediaPlayer.native_profileEnd();
71}
72
73// 6、发送关闭实时流命令
74ClientManager.getClient().tryToCloseRTStream(cameraType, new SendResponse() {
75 @Override
76 public void onResponse(Integer code) {
77 if (code != SEND_SUCCESS) {
78 Dbug.e(tag, "Send failed!!!");
79 }
80 }
81});
具体可参考工程中 VedioFragment、 SDPServer、 CommunicationService 类.
2.4 设备照片
2.4.1 功能描述
设备支持http方式下载照片。例如:
String url = http:// ip : 8080/ deviceFilePath
, 通过使用设备IP和指定要下载的照片 路径的组合成为下载链接, 即可获取设备的每张照片。
2.4.2 使用示例
小技巧
略。可参考工程使用方法,或用系统接口实现。
2.5 视频缩略图
2.5.1 http方式
通过http可以获取设备端MOV、AVI视频文件的缩略图。
注意
设备端的固件要开启对应配置才能支持http方式
2.5.1.1 流程描述
不需要发命令,用标准http即可实现。但是,要修改http的偏移属性,即告诉设备在文件中取缩指定范围的缩略图数据。
conn.addRequestProperty("Range", "bytes=512-33280");
2.5.1.2 使用示例
1// 1.通过连接的IP、端口、文件路径组成URL
2String ip = ClientManager.getClient().getAddress();
3String url = AppUtils.formatUrl(ip, DEFAULT_HTTP_PORT, fileInfo.getPath());
4
5// 2.定义回调接口
6public interface OnImageLoaderListener {
7 void onStart();
8 void onComplete(Bitmap loadedImage);
9 void onFailed();
10}
11
12// 3.使用线程来获取
13class ImageLoaderTask extends Thread {
14 private final String mUrl;// 具体的设备文件URL
15 private final OnImageLoaderListener listener;
16
17 public ImageLoaderTask(String url, OnImageLoaderListener listener) {
18 this.mUrl = url;
19 this.listener = listener;
20 }
21
22 private HttpURLConnection createConnection(String url) throws IOException {
23 HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
24 conn.setDoInput(true);
25 conn.setConnectTimeout(5 * 1000);
26 conn.setReadTimeout(20 * 1000);
27 // 这里的bytes=512-33280是固定偏移
28 conn.addRequestProperty("Range", "bytes=512-33280");
29
30 return conn;
31 }
32
33 @Override
34 public void run() {
35 listener.onStart();
36 HttpURLConnection conn = null;
37 try {
38 conn = createConnection(mUrl);
39 } catch (IOException e) {
40 e.printStackTrace();
41 }
42
43 if (conn == null) {
44 listener.onFailed();
45 return;
46 }
47
48 InputStream inputStream = null;
49 try {
50 inputStream = conn.getInputStream();
51 } catch (IOException e) {
52 e.printStackTrace();
53 }
54
55 if (inputStream != null) {
56 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
57 if (bitmap == null) {
58 listener.onFailed();
59 } else {
60 listener.onComplete(bitmap);
61 }
62
63 conn.disconnect();
64 try {
65 inputStream.close();
66 } catch (IOException e) {
67 e.printStackTrace();
68 }
69 } else {
70 listener.onFailed();
71 }
72 }
73}
2.5.2 私有协议方式
2.5.2.1 流程描述
在浏览设备端视频文件时,通常要先获取设备的视频缩略图,再展示给用户看。
调用
DeviceClient#tryToRequestVideoCover()
发送命令MULTI_VIDEO_COVER
给设备, 参数是要请求的设备视频文件路径的列表。从注册
DeviceClient#registerNotifyListener()
监听命令MULTI_VIDEO_COVER
回复, 命令会附带参数 status=0, 表示数据流准备好了; status=1, 表示设备已经把数据发送完了。在附带参数 status=0 时, 创建数据接收通道
VideoThumbnail#create()
, 注册VideoThumbnail#setOnFrameListener()
监听每个视频缩略图数据的回调。若回调的数据格式是H.264, 需要对它进行转码为JPEG或者其它格式; 若是JPEG, 则不需要再转码。
工程中利用FFmpeg编解码库,调用
FrameCodec#convertToJPG()
对H.264实现转码,注册FrameCodec#setOnFrameCodecListener()
监听转码后的JPEG数据。 开发者可根据自己需要去使用其他方法转码。
2.5.2.2 使用示例
1// 1、请求设备视频缩略图
2ClientManager.getClient().tryToRequestVideoCover(list, new SendResponse() {
3 @Override
4 public void onResponse(Integer code) {
5 if (code != SEND_SUCCESS) {
6 Dbug.e(tag, "Send failed");
7 } else {
8 Dbug.e(tag, "Send OK");
9 thumbRequest = e;//For video cover thumbnail
10 }
11 }
12});
13
14// 2、创建接收命令监听器对象, 参考2.1.2节
15
16// 3、收到设备MULTI_VIDEO_COVER 回复,处理视频缩略图
17public void handleVideoCover(CmdInfo cmdInfo) {
18 if (Topic.MULTI_VIDEO_COVER.equals(cmdInfo.getTopic())) {
19 if (cmdInfo.getParams() != null && TopicParam.START.equals(cmdInfo.getParams().get(TopicKey.STATUS))) {
20 // status=0 时, 创建数据接收通道
21 createConnection();
22 } else {
23 Log.w(tag, "-MULTI_VIDEO_COVER- complete");
24 }
25 }
26}
27
28// 4、创建数据接收通道,还有视频帧接收监听器、接收状态监听器
29private void createConnection() {
30 String ip = ClientManager.getClient().getAddress();
31 if (!mVideoThumbnail.create(IConstant.THUMBNAIL_TCP_PORT, ip)) {
32 mVideoThumbnail.close();
33 mVideoThumbnail.create(IConstant.THUMBNAIL_TCP_PORT, ip);
34 }
35 mVideoThumbnail.setOnFrameListener(onFrameReceivedListener);
36 mVideoThumbnail.setOnCompletedListener(onCompletedListener);
37}
38
39// 5、从设备端接收到一帧JPEG/H264数据时回调
40private final OnFrameListener onFrameReceivedListener = new OnFrameListener() {
41 @Override
42 public void onFrame(byte[] data, PictureInfo mediaInfo) {
43 if (data != null && mediaInfo != null) {
44 String path = mediaInfo.getPath();
45 if (!TextUtils.isEmpty(path) && (path.endsWith(".AVI") || path.endsWith(".avi"))) {
46 // 若是MJPEG流,无需再转码
47 onCompleteCallback(data);
48 } else {
49 int width = mediaInfo.getWidth();
50 int height = mediaInfo.getHeight();
51 if (width > 0 && height > 0) {
52 mFrameCodec.setOnFrameCodecListener(onFrameCodecListener);
53 // 若是H264,转码为JPEG
54 if (!mFrameCodec.convertToJPG(data, width, height, path)) {
55 Dbug.e(tag, "-convertToJPG- fail!!");
56 } else {
57 Dbug.i(tag, "-convertToJPG- success");
58 }
59 } else {
60 onFailureCallback("The width and height of frame incorrect");
61 }
62 }
63 } else {
64 onFailureCallback("Received frame error");
65 }
66 }
67};
68
69// 请求设备端所有的数据完成时回调
70private final OnCompletedListener onCompletedListener = () -> {
71 // TODO:Receive data from device completed...
72};
具体可参考工程中的 PlaybackActivity, DeviceThumbHelper 类
2.6 视频截图
2.6.1 流程描述
这里的截图是针对设备端的视频文件,通过指定视频时间、间隔、图片张数来获取对应的视频截图。
通过调用
DeviceClient#tryToRequestVideoContentThumbnail()
发送命令VIDEO_CONTENT_THUMBNAILS
(实际是 THUMBNAILS)给设备, 参数可设置时间间隔、要获取图片的张数等。注册
DeviceClient#registerNotifyListener()
监听命令 VIDEO_CONTENT_THUMBNAILS 回复, 命令会附带参数status=0, 表示数据流准备好了; 命令附带参数 status=1, 表示设备已经把数据发送完了。创建数据接收通道
VideoThumbnail#create()
, 注册VideoThumbnail#setOnFrameListener()
监听每一张预览图数据的回调。若回调回来的是JPEG,可直接显示;若回调的数据是H.264, 需要对它进行转码为JPEG或者其它格式。工程利用FFmpeg编解码库,调用
FrameCodec#convertToJPG()
实现转码, 注册FrameCodec#setOnFrameCodecListener()
监听转码后的JPEG数据。开发者可根据自己需要去使用其他方法转码。
2.6.2 使用示例
1// 1、发命令请求视频内的多张截图,参数分别是:视频路径、视频偏移、时间间隔、截图张数、命令发送状态回调
2ClientManager.getClient().tryToRequestVideoContentThumbnail(fileInfo.getPath(), fileInfo.getOffset(), 1000, 1, new SendResponse() {
3 @Override
4 public void onResponse(Integer code) {
5 if (code != SEND_SUCCESS) {
6 Dbug.e(tag, "Send failed");
7 }
8 }
9});
10
11// 2、创建接收命令监听器对象, 参考2.1.2节
12
13// 3、收到设备VIDEO_CONTENT_THUMBNAILS 回复,处理视频缩略图
14public void handleVideoCover(CmdInfo cmdInfo) {
15 if (Topic.VIDEO_CONTENT_THUMBNAILS.equals(cmdInfo.getTopic())) {
16 if (TopicParam.START.equals(cmdInfo.getParams().get(TopicKey.STATUS))) {
17 createConnection();
18 } else {
19 Dbug.w(tag, "-Frame THUMBNAILS- complete");
20 }
21 }
22}
23
24// 4、参考2.5.2节示例
具体可参考工程中 PlaybackActivity、 ImageLoader、 DeviceThumbHelper 类
2.7 AP与STA
2.7.1 AP切到STA
2.7.1.1 流程描述
在AP模式下, 调用
DeviceClient#tryToSetSTAAccount()
发送 STA_SSID_INFO 命令给设备, 参数指定路由名称和密码。设备收到后,不会立刻连接路由器,而是下次开机才会连接路由器。
2.7.1.2 使用示例
1// 1、发命令请求App切换到STA模式,参数分别是:WiFi名称、WiFi密码、是否让设备端保存路由器信息、命令发送状态回调
2ClientManager.getClient().tryToSetSTAAccount(ssid, pwd, isSaveMsg, new SendResponse() {
3 @Override
4 public void onResponse(Integer code) {
5 if(code == SEND_SUCCESS){
6 // TODO:命令发送成功后,可以重新走连接流程
7 }
8 }
9});
10
11// 2、创建接收命令监听器对象, 参考2.1.2节。
具体例子参考工作中的 DeviceStaModeFragment 类.
注意
切换为STA后,App要想再搜索设备,需要走以下步骤:
手机端要连接上面同样的路由, 让手机与设备处于同一个局域网.
使用 Discovery 类的接口搜索设备IP地址,注册
Discovery#registerOnDiscoveryListener()
监听回调的搜索结果.通过得到的设备IP去连接设备,连接流程与AP模式类似.
参考 StaDeviceListFragment 类.
2.7.2 STA切到AP
2.7.2.1 流程描述
在STA模式,调用
DeviceClient#tryToSetApAccount()
发送 AP_SSID_INFO 命令给设备, 参数指定设备名字和密码, 以及是否立即生效。若设置为立即生效, 在发送命令后, 设备将立即重启,App在AP模式下搜索设备名称, 再进行连接。
注意
如果不知道当前设备的AP信息,可调用 DeviceClient#tryToRequestApInfo()
获取。
2.7.2.2 使用示例
1// 1、发命令让设备切到AP模式,参数分别是:当前设备的SSID、PWD、是否马上重启、命令发送状态回调
2ClientManager.getClient().tryToSetApAccount(saveApSSID, savePwd, true, new SendResponse() {
3 @Override
4 public void onResponse(Integer code) {
5 if (code == SEND_SUCCESS) {
6 // TODO: 可重新走连接流程
7 }
8 }
9});
具体例子参考工程中的 DeviceSettingFragment
类.
2.8 在线回放
2.8.1 私有协议播放方式
注意
这里介绍的是默认的回放播放流程,开发者可通过不配置RTP端口来禁用自带播放器。然后,从监听接口获取音视频数据,最后根据需求自定义播放器来实现回放。
2.8.1.1 流程描述
注册
DeviceClient#registerNotifyListener()
监听设备命令回复。调用
DeviceClient#tryToStartPlayback()
发送命令给设备, 参数分别是要播放的设备视频文件路径和时间偏移。若监听到设备命令回复
PLAYBACK
(实际是 TIME_AXIS_PLAY ), 接着创建数据流接收通道StreamPlayer#create(RTS_TCP_PORT, ConnectedIP)
; 配置RTP播放端口信息StreamPlayer#configure(RTP_VIDEO_PORT1, RTP_AUDIO_PORT1)
。注册
StreamPlayer#registerPlayerListener()
监听数据流接收状态。当从监听数据流接收状态中
onStateChanged()
得到PlaybackStream.Status.PREPARE
状态时,初始化IJK播放器和创建SDP服务器。
回放流程
2.8.1.2 使用示例
1// 1、发命令请求回放,参数分别:选中视频文件路径、视频偏移、命令发送状态回调
2ClientManager.getClient().tryToStartPlayback(path, offset, new SendResponse() {
3 @Override
4 public void onResponse(Integer code) {
5 if (code != SEND_SUCCESS) {
6 Dbug.e(tag, "Send failed");
7 }
8 }
9});
10
11// 2、创建接收命令监听器对象, 参考2.1.2节。
12
13// 3、处理设备回复的命令
14public void handleCommand(CmdInfo data) {
15 if (data.getErrorType() != Code.ERROR_NONE) {
16 if (Topic.PLAYBACK.equals(data.getTopic())) {
17 mHandler.postDelayed(this::onBackPressed, 1000);
18 }
19 return;
20 }
21
22 switch (data.getTopic()) {
23 case Topic.PLAYBACK:
24 if (mStreamPlayer == null) {
25 mStreamPlayer = PlaybackManager.getInstance();
26 mStreamPlayer.registerStreamListener(mPlayerListener);
27 }
28 // 创建数据流接收通道
29 mStreamPlayer.create(RTS_TCP_PORT, ClientManager.getClient().getAddress());
30 // 配置RTP播放端口信息
31 mStreamPlayer.configure(RTP_VIDEO_PORT1, RTP_AUDIO_PORT1);
32 break;
33
34 case Topic.PLAYBACK_FAST_FORWARD:// 快进
35 String str = data.getParams().get(TopicKey.LEVEL);
36 if (null != str && TextUtils.isDigitsOnly(str)) {
37 fastForwardLevel = Integer.parseInt(str);
38 }
39 if (fastForwardLevel < fastForwardRes.length) {
40 mFastForward.setImageResource(fastForwardRes[fastForwardLevel]);
41 }
42 break;
43 }
44}
45
46// 4、注册监听数据流接监听器
47mStreamPlayer.registerStreamListener(mPlayerListener);
48
49private final IPlayerListener mPlayerListener = new IPlayerListener() {
50 @Override
51 public void onVideo(int t, int channel, byte[] data, long sequence, long timestamp) {
52 // TODO:视频帧数据
53 }
54
55 @Override
56 public void onAudio(int t, int channel, byte[] data, long sequence, long timestamp) {
57 // TODO:音频帧数据
58 }
59
60 @Override
61 public void onStateChanged(int state) {// 回放的状态回调
62 switch (state) {
63 case PlaybackStream.Status.PREPARE:
64 Dbug.i(tag, "prepare-------");
65 String RTS_URL = "tcp://127.0.0.1:" + mTCPPort;
66 initPlayer(RTS_URL);
67 break;
68 case PlaybackStream.Status.END:
69 Dbug.i(tag, "end of file-------");
70 break;
71 case PlaybackStream.Status.PLAYING:
72 Dbug.i(tag, "playing-------");
73 break;
74 case PlaybackStream.Status.PAUSE:
75 Dbug.i(tag, "pause-------");
76 break;
77 case PlaybackStream.Status.STOP:
78 Dbug.i(tag, "finish-------");
79 onBackPressed();
80 break;
81 }
82 }
83
84 @Override
85 public void onError(int code, final String message) {
86 if(code == 0){
87 // 超时
88 }else{
89 // 其他错误
90 }
91 }
92};
93
94mStreamPlayer.unregisterStreamListener(mPlayerListener);
具体例子参考工程中的 PlaybackDialogActivity 类。
2.8.2 http播放方式
2.8.2.1 功能说明
通过标准http协议来访问设备视频文件,并用支持http的播放器来播放。格式:
http://ip:8080/deviceFilePath
, 如:http://ip:8080/storage/sd2/C/DCIM/1/VID_363.MOV
。
2.8.2.2 使用示例
略。
2.9 视频下载
进入回放模式, 在播放前注册
PlaybackStream#registerPlayerListener()
,回放过程中会有音视频的裸数据、播放状态回调回来, 开发者可根据项目需求对音视频数据进行二次处理。
2.9.1 回放时边播边下载
这种方式可实现跨文件下载。
2.9.1.1 功能描述
先注册 PlaybackStream#registerPlayerListener()
函数进行监听,分别在音频和视频的回调函数中,使用SDK的多媒体文件封装类的接口封装对应用的数据。
视频数据流格式是JPEG,使用 AviWrapper 类的接口;视频数据流格式是H264,使用 MovWrapper 类的接口。
2.9.1.2 使用示例
1// 1、发命令请求回放,参数分别:选中视频文件路径、视频偏移、命令发送状态回调
2ClientManager.getClient().tryToStartPlayback(path, offset, new SendResponse() {
3 @Override
4 public void onResponse(Integer code) {
5 if (code != SEND_SUCCESS) {
6 Dbug.e(tag, "Send failed");
7 }
8 }
9});
10
11// 2、创建接收命令监听器对象, 参考2.1.2节。
12
13// 3、创建音视频封装器对象
14private AbsOutputStream createLocalOutputStream() {
15 if (mRecordVideo != null) {
16 Dbug.w(tag, "mRecordingVideo not null");
17 return mRecordVideo;
18 }
19 AbsOutputStream.VideoConfig config = VideoUtil.getDownloadStreamConfig(mFileInfo);
20 return OutputStreamFactory.createOutputStream(mStreamPlayer, config);
21}
22
23// 开始封装数据
24private void startLocalRecording() {
25 mRecordVideo = createLocalOutputStream();
26 mStreamPlayer.registerStreamListener(mRecordVideo.getStreamListener());
27 mRecordVideo.setOnRecordStateListener(new OnRecordStateListener() {
28 @Override
29 public void onPrepared() {
30 isRecordPrepared = true;
31 handleStartRecode();
32 }
33
34 @Override
35 public void onCompletion(String filePath) {
36 ToastUtil.showToastShort(getString(R.string.record_success));
37 }
38
39 @Override
40 public void onStop() {
41 Dbug.i(tag, "Record onStop");
42 isRecordPrepared = false;
43 handleStopRecode();
44 }
45
46 @Override
47 public void onError(String message) {
48 Dbug.e(tag, "Record error:" + message);
49 if (mRecordVideo != null) {
50 String outputPath = mRecordVideo.getPath();
51 if (!TextUtils.isEmpty(outputPath)) {
52 File file = new File(outputPath);
53 if (file.exists() && file.delete()) {
54 Dbug.i(tag, "Delete file:" + outputPath);
55 }
56 }
57 }
58 handleStopRecode();
59 ToastUtil.showToastShort(getString(R.string.record_fail));
60
61 mRecordVideo = null;
62 isRecordPrepared = false;
63 }
64 });
65 mRecordVideo.start();
66}
67
68// 停止保存
69private void stopLocalRecording() {
70 if (mRecordVideo != null) {
71 isRecordPrepared = false;
72 mStreamPlayer.unregisterStreamListener(mRecordVideo.getStreamListener());
73 mRecordVideo.destroy();
74 mRecordVideo = null;
75 }
76}
具体实现可以参考工程中的 PlaybackDialogActivity
类。
2.9.2 单个文件下载
2.9.2.1 功能描述
选中某个视频文件,通过
PlaybackStream
来实现下载功能(回放也是用这个类,这里只是不渲染画面)。当然,也可以指定某个视频时间点开始下载。
2.9.2.2 使用示例
1// 1、创建回放对象
2mStreamPlayer = new PlaybackStream();
3mStreamPlayer.setDownloadDuration(info.getDuration());// 设置文件的时长
4
5// 2、创建接收命令监听器对象, 参考2.1.2节。
6// 3、发命令请求回放,参数分别:选中视频文件路径、视频偏移、命令发送状态回调
7ClientManager.getClient().tryToStartPlayback(info.getPath(), info.getOffset(), new SendResponse() {
8 @Override
9 public void onResponse(Integer integer) {
10 if (integer != Constants.SEND_SUCCESS) {
11 Dbug.e(tag, "Send failed");
12 }
13 }
14});
15
16// 监听到设备回复的命令
17public void handleCommand(CmdInfo data) {
18 if (data.getErrorType() != Code.ERROR_NONE) {
19 Dbug.e(tag, "data" + data);
20 return;
21 }
22 if (Topic.PLAYBACK.equals(data.getTopic())) {
23 // 4、注册下载监听器
24 mStreamPlayer.setOnDownloadListener(onDownloadListener);
25 // 创建数据接收通道,参数分别:TCP端口号、连接地址、下载模式
26 mStreamPlayer.create(RTS_TCP_PORT, ClientManager.getClient().getAddress(), PlaybackStream.Mode.DOWNLOAD);
27 }
28}
29
30// 5、数据回调监听器
31private final OnDownloadListener onDownloadListener = new OnDownloadListener() {
32 @Override
33 public void onStart() {
34 startRecording();// 开始保存
35 }
36
37 @Override
38 public synchronized void onReceived(int type, byte[] data) {
39 if (mRecordVideo != null && !mRecordVideo.write(type, data)) {// 真正封装数据
40 Dbug.e(tag, "Write failed");
41 }
42 }
43
44 @Override
45 public void onProgress(float progress) {// 进度显示
46 setProgress((int) (progress * 100));
47 }
48
49 @Override
50 public void onStop() {// 下载完成
51 release();
52 dismiss();
53 }
54
55 @Override
56 public void onError(int code, String message) {// 错误处理
57 enoughFrames = 0;
58 stopRecording();
59 deleteBadFile();
60 dismiss();
61 }
62};
具体实现可以参考工程中的 DownloadDialog
类。
2.10 固件升级
2.10.1 拷贝到卡升级
把固件文件拷贝到TF卡根目录上;然后把卡插到设备上;最后断开电源重启设备,完成升级。
2.10.2 App上传固件升级
2.10.2.1 流程描述
把手机本地的固件升级文件通过FTP方式传输到设备端
调用
tryToResetDev()
发重启命令让小机重启,或者手动重启设备
2.10.2.2 使用示例
1// 1、FTPClientUtil为FTP封装类,参数分别:文件名、固件绝对路径、Android handler(用于主线程消息通知)
2// FTP账号用户名: FTPX, 密码:12345678
3FTPClientUtil.getInstance().uploadFile(file.getName(), select, mHandler);
4
5// 2、请求设备重启,完成升级
6ClientManager.getClient().tryToResetDev(new SendResponse() {
7 @Override
8 public void onResponse(Integer code) {
9 if(code != Constants.SEND_SUCCESS){
10 Dbug.e(TAG, "send reset cmd failed!");
11 } else {
12 mHandler.sendEmptyMessage(MSG_UPLOAD_FINISH);
13 }
14 }
15});
具体例子可参考工程中的固件上传功能和代码(AboutFragment类)。
2.11 心跳包机制
2.11.1 功能描述
设备与App的连接状态是由心跳包机制来保持的。当手机App接入到设备端以后, 必须在心跳包超时时间前发送心跳包给设备, 设备接收到心跳包会立刻返回心跳包给手机App。
手机App接入设备时就会接收到
KEEP_ALIVE_INTERVAL
命令,通过此命令携带的参数可获得设备心跳包超时时间,App再调用tryToKeepAlive()
发送心跳命令给设备, 设备以同样的命令回复App。至此, 完成了一个心跳包机制的流程。
2.11.2 使用示例
1// 1、App接入设备时会接收到的 KEEP_ALIVE_INTERVAL命令, 根据命令携带的参数得到设备心跳包超时时间
2
3// 2、发送命令请求保活操作
4ClientManager.getClient().tryToKeepAlive(new SendResponse() {
5 @Override
6 public void onResponse(Integer code) {
7 if (code != SEND_SUCCESS) {
8 Dbug.e(tag, "Send failed!!!");
9 }
10 }
11});
12
13// 3、重置超时时间
具体例子可参考工程中的 HeartbeatTask 类。
2.12 自定义命令
2.12.1 功能描述
如果已有命令无法满足需求,可以与固件开发人员协商增加新命令来实现功能需求。其中,命令格式是以JSon格式收发, 如下所示:
1{
2 "errno": 0, // 错误返回,若无可省略此字段
3 "op": "GET",//操作类型,包括“PUT","GET","NOTIFY"
4 "param": //参数里面键值对应
5 {
6 "key_0": "value1",
7 "key_1": "value2",
8 "key_2": "value3",
9 "key_3": "value4"
10 }
11}
op为操作类型, 当手机”PUT”或”GET”命令时, 设备统一回复”NOTIFY”。示例: Topic: “SD_STATUS”:
1{
2 "errno":0,
3 "op":"NOTIFY",
4 "prarm":{
5 "key_0":"1",//在线状态(0:离线, 1:在线)
6 }
7}
2.12.2 使用示例
下面以如 KEY_SOUND 为例:
1// 1.创建DeviceClient对象, ClientManager类参考1.3.2节
2DeviceClient client = ClientManager.getClient();
3
4// 2.自定义命令函数
5public void tryToSetDeviceKeySound(SendResponse sendResponse) {
6 SettingCmd settingCmd = new SettingCmd();
7 settingCmd.setTopic("KEY_SOUND");
8 settingCmd.setOperation(Operation.TYPE_PUT);
9 ArrayMap<String, String> arrayMap = new ArrayMap<>();
10 arrayMap.put("status", "1");//1 turn on, 0 turn off
11 settingCmd.setParams(arrayMap);
12 client.tryToPut(settingCmd, sendResponse);
13}
14
15// 3.注册命令回复回调
16client.registerNotifyListener(onNotifyResponse);
17
18// 4.创建回调监听对象
19private OnNotifyListener onNotifyResponse = new OnNotifyListener() {
20 @Override
21 public void onNotify(NotifyInfo data) {
22 String topic = data.getTopic();
23 if (data.getErrorType() != Code.ERROR_NONE) {
24 return;//实际开发和使用, 一定要处理设备返回的错误回复
25 }
26 switch (topic) {
27 case "KEY_SOUND"
28 break;
29 }
30 }
31};
32
33// 最后, 要注销命令回复回调
34client.unregisterNotifyListener(onNotifyResponse);
2.13 设备升级
2.13.1 双备份升级
2.13.1.1 流程描述
双备份升级相对简单,只需要把升级文件传到设备端即可,最后发命令让设备重启,或者手动让设备断电重启
2.13.1.1 使用示例
略
具体例子可参考工程中的 AboutFragment 、FTPClientUtil 类。
2.13.2 单备份升级
2.13.2.1 流程描述
单备份升级分两个阶段来完成:
第一阶段:
App连接设备后,通过描述文档解析得到
ota_type
和ota_step
。当ota_type=1且ota_step = 0,说明设备支持单备份升级升级前,App先发命令让设备进入单备份升级状态,发送命令:NET_OTA_SINGLE,参数:
param":{"status":"1"}
App收到设备的”NET_OTA_SINGLE”回复后,status=1,则表示设备成功进入单备份升级模式
连接3335 TCP端口,开始进行单备份升级,即发固件数据给设备
升级成功后,设备会自动重启,App要重新连接设备,准备第二阶段的升级
第二阶段:
App重新连接设备后,通过描述文档解析得到
ota_type
和ota_step
。当ota_type值为1且ota_step = 1,则进入单备份升级第二阶段。注意,这阶段不需要再发命令连接3335端口TCP,进行单备份升级,即发固件数据给设备
升级成功,设备重启
2.13.2.2 使用示例
1// 1.判断是否单备份
2if (mApplication.getDeviceDesc().isOtaSingleBackup()) {
3}
4
5// 2.发命令请求设备进入升级状态,进行第一阶段的升级
6ClientManager.getClient().send(new NetOtaSingleCmd(), new CommandCallback() {
7 @Override
8 public void onDeviceRespond(NotifyInfo cmdNotify) {// 设备回复同样的命令
9 if(cmdNotify instanceof NetOtaSingleCmd) {
10 NetOtaSingleCmd netOtaSingleCmd = (NetOtaSingleCmd) cmdNotify;
11 setupOta(netOtaSingleCmd.isReady());
12 }
13 }
14});
15
16// 3.连接设备的socket端口,开始升级
17private void setupOta(boolean isReady) {
18 if (isReady) {// 设备是否已准备就绪
19 singleBackupUpgrade = new SingleBackupUpgrade();
20 String ip = ClientManager.getClient().getAddress();
21 if (singleBackupUpgrade.create(ip, IConstant.SINGLE_OTA_PORT, selectedPath)) {
22 singleBackupUpgrade.setOnUpgradeListener(onUpgradeListener);// 注册升级监听器
23 singleBackupUpgrade.setSoTimeout(20000);// 20秒超时
24 if (singleBackupUpgrade.start()) { // 开始传数据
25 // do something
26 } else {
27 Jlog.e(tag, "Start OTA failed");
28 }
29 } else {
30 Jlog.e(tag, "Create OTA client failed");
31 }
32 }
33}
34
35// 4.重新连接设备后,判断设备是否处于单备份升级的第二阶段
36if (mApplication.getDeviceDesc().getOtaStep() == IConstant.OTA_STEP_2ND) {
37 // 无须再发命令,直接重复上面的步骤
38 setupOta(true);
39}
具体例子可参考工程中的 SingleUpgradeFragment 类。
附录
1. 设备vf_list
设备端的vf_list.txt文件内容是以JSon格式生成, 是设备端用来组织录像和拍照文件。App请求设备文件列表时,会返回这个文件。下面示例是一个视频文件的JSON封装格式:
1{
2 "y": 1,//视频文件类型 0:无效文件 1:普通文件 2:SOS文件 3:延时文件
3 "f": "storage/sd2/C/DCIM/1/VID_233.MOV",//设备文件的完整路径
4 "t": "20170502181820",//生成文件日期
5 "d": "180",//视频文件的时长(秒为单位)
6 "h": 1080,//视频高
7 "w": 1920,//视频宽
8 "p": 30,//视频帧率
9 "s": "247500000",//文件大小
10 "c": "0",//0:前视视频 1:后视视频 2:....
11 "e": "1493720480"//视频文件的结束时间
12}
2. 设备描述文档
注意
设备描述文档内容与具体固件相关,请以最新的为准
通过http得到的内容是以JSon格式传过来, 下面是一个样例:
1{
2 "app_list": {
3 "match_android_ver": [//适配Android版本
4 "1.0",
5 "2.0"
6 ],
7 "match_ios_ver": [//适配iOS版本
8 "1.0",
9 "2.0"
10 ]
11 },
12 "behind_support": [//后置摄像头支持分辨率种类
13 "0",//VGA
14 "1"//720p
15 ],
16 "device_type": "1",//是否支持双路
17 "firmware_version": "1.0.1",//固件版本号
18 "forward_support": [//前置摄像头支持分辨率类型
19 "0",//VGA
20 "1",//720p
21 "2"//1080p
22 ],
23 "match_app_type": "DVRunning 2",//匹配工程名
24 "net_type": "1",//实时流传输类型 1:udp 0:tcp
25 "product_type": "AC54xx_wifi_car_camera", //设备WiFi名字
26 "rts_type": "0",//实时流视频数据格式 0:jpeg 1:h264
27 "uuid": "xxxxxx"//设备uuid
28}
3. CTP命令错误列表
设备命令错误,对应SDK中的
com.jieli.lib.dv.control.utils.Code
类
1//错误列表
2#define ENONE 0 // 无错误
3#define E_SDCARD 1 // SD卡错误
4#define E_SD_OFFLINE 2 // SD卡离线
5#define E_ACCESS_RFU 3 // 拒绝访问
6#define E_REQUEST 4 // 请求错误
7#define E_VER_UMATCH 5 // 版本不匹配
8#define E_NO_TOPIC 6 // Topic未实现
9#define E_IN_USB 7 // 正处于USB模式
10#define E_IN_VIDEO 8 // 正在录像
11#define E_IN_BROWSE 9 // 正在浏览模式
12#define E_IN_PARKING 10 // 正在停车
13#define E_OPEN_FILE 11 // 打开文件失败
14#define E_SYS_EXCEP 12 // 系统异常
15#define E_NET_ERR 14 // 网络异常
16#define CTP_PULL_OFFLINE 15 //后拉不在线
17#define CTP_PULL_NOSUPPORT 16 //后拉不支持
18#define CTP_RT_OPEN_FAIL 17 //实时流打开失败