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 流程描述

  1. App准备与设备建立连接时(参考1.3 SDK配置), 必须先发送命令 APP_ACCESS 给设备; 设备根据当前情况回复App允许或拒绝状态。

  2. 同时,App必须要通过http方式获设备描述文档: http://ip/:1080/mnt/spiflash/res/dev_desc.txt。此文档是JSon格式,描述设备支持什么功能。

digraph G { rankdir="LR"; node[shape="point", width=0, height=0]; edge[arrowhead="none", style="dashed"] { rank="same"; edge[style="solided"]; LC [shape="Record", label="App"]; LC -> step00 -> step01 -> step02 -> step03 -> step04 -> step05; } { rank="same"; edge[style="solided"]; Agency [shape="Record", label="Dev"]; Agency -> step10 -> step11 -> step12 -> step13 -> step14 -> step15; } step00 -> step10 [label="发送命令APP_ACCESS", arrowhead="normal"]; step11 -> step01 [label="回复允许", arrowhead="normal"]; step12 -> step02 [label="发送心跳、SD、电池等状态", arrowhead="normal"]; step03 -> step13 [label="获取设备描述文档(http获取)", arrowhead="normal"]; step14 -> step04 [label="发送描述文档", arrowhead="normal"]; }

App接入设备的过程

2.2.2 使用示例

// 获取设备描述文档
String url = "http://ip/:1080/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);
    }
}

具体可参考工程中 MainActivityBaseActivityCommunicationService 类。

2.3 实时预览

2.3.1 流程描述

digraph G3 { rankdir="LR"; node[shape="point", width=0, height=0]; edge[arrowhead="none", style="dashed"] { rank="same"; edge[style="solided"]; LC [shape="Record", label="App"]; LC -> step00 -> step01 -> step02 -> step03 -> step04 -> step05 -> step06; } { rank="same"; edge[style="solided"]; Agency [shape="Record", label="Dev"]; Agency -> step10 -> step11 -> step12 -> step13 -> step14 -> step15 -> step16; } { rank="same"; edge[style="solided"]; player[shape="Record", label="Player"]; player -> step20 -> step21 -> step22 -> step23 -> step24 -> step25 -> step26; } step00 -> step10 [label="发送打开实时流命令请求", arrowhead="normal"]; step11 -> step01 [label="设备回复命令并携带参数信息", arrowhead="normal"]; step02 -> step12 [label="由参数信息建SDP服务器,\n也可以创建数据接收监听器", arrowhead="normal"]; step03 -> step13 [label="初始化", arrowhead="none"]; step13 -> step23 [arrowhead="normal"]; step14 -> step04 [label="发送音视频数据", arrowhead="normal"]; step05 -> step15 [label="App发送关闭实时流命令请求", arrowhead="normal"]; }

App打开实时流的过程

  1. 调用 DeviceClient#tryToOpenRTStream() 给设备,请求打开实时流。

  2. 设备回复同样命令并携带参数信息, 如视频格式、帧率、宽高; App根据这些参数来创建SDP服务器, 用于 ijkplayer 播放器初始化RTP SDP配置。

  3. 创建数据流接收通道 RealtimeStream#create(), 配置RTP端口 RealtimeStream#configure() (SDK中把接收的数据封装成RTP再转发给播放器)。

  4. 注册 RealtimeStream#registerStreamListener() 监听音视频帧数据、数据流状态等。

  5. 初始化 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});

具体可参考工程中 VedioFragmentSDPServerCommunicationService 类.

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 流程描述

在浏览设备端视频文件时,通常要先获取设备的视频缩略图,再展示给用户看。

  1. 调用 DeviceClient#tryToRequestVideoCover() 发送命令 MULTI_VIDEO_COVER 给设备, 参数是要请求的设备视频文件路径的列表。

  2. 从注册 DeviceClient#registerNotifyListener() 监听命令 MULTI_VIDEO_COVER 回复, 命令会附带参数 status=0, 表示数据流准备好了; status=1, 表示设备已经把数据发送完了。

  3. 在附带参数 status=0 时, 创建数据接收通道 VideoThumbnail#create(), 注册 VideoThumbnail#setOnFrameListener() 监听每个视频缩略图数据的回调。

  4. 若回调的数据格式是H.264, 需要对它进行转码为JPEG或者其它格式; 若是JPEG, 则不需要再转码。

  5. 工程中利用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 流程描述

这里的截图是针对设备端的视频文件,通过指定视频时间、间隔、图片张数来获取对应的视频截图。

  1. 通过调用 DeviceClient#tryToRequestVideoContentThumbnail() 发送命令 VIDEO_CONTENT_THUMBNAILS (实际是 THUMBNAILS)给设备, 参数可设置时间间隔、要获取图片的张数等。

  2. 注册 DeviceClient#registerNotifyListener() 监听命令 VIDEO_CONTENT_THUMBNAILS 回复, 命令会附带参数status=0, 表示数据流准备好了; 命令附带参数 status=1, 表示设备已经把数据发送完了。

  3. 创建数据接收通道 VideoThumbnail#create(), 注册 VideoThumbnail#setOnFrameListener() 监听每一张预览图数据的回调。

  4. 若回调回来的是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节示例

具体可参考工程中 PlaybackActivityImageLoaderDeviceThumbHelper

2.7 AP与STA

2.7.1 AP切到STA

2.7.1.1 流程描述

  1. 在AP模式下, 调用 DeviceClient#tryToSetSTAAccount() 发送 STA_SSID_INFO 命令给设备, 参数指定路由名称和密码。

  2. 设备收到后,不会立刻连接路由器,而是下次开机才会连接路由器。

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要想再搜索设备,需要走以下步骤:

  1. 手机端要连接上面同样的路由, 让手机与设备处于同一个局域网.

  2. 使用 Discovery 类的接口搜索设备IP地址,注册 Discovery#registerOnDiscoveryListener() 监听回调的搜索结果.

  3. 通过得到的设备IP去连接设备,连接流程与AP模式类似.

  4. 参考 StaDeviceListFragment 类.

2.7.2 STA切到AP

2.7.2.1 流程描述

  1. 在STA模式,调用 DeviceClient#tryToSetApAccount() 发送 AP_SSID_INFO 命令给设备, 参数指定设备名字和密码, 以及是否立即生效。

  2. 若设置为立即生效, 在发送命令后, 设备将立即重启,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 流程描述

  1. 注册 DeviceClient#registerNotifyListener() 监听设备命令回复。

  2. 调用 DeviceClient#tryToStartPlayback() 发送命令给设备, 参数分别是要播放的设备视频文件路径和时间偏移。

  3. 若监听到设备命令回复 PLAYBACK (实际是 TIME_AXIS_PLAY ), 接着创建数据流接收通道 StreamPlayer#create(RTS_TCP_PORT, ConnectedIP); 配置RTP播放端口信息 StreamPlayer#configure(RTP_VIDEO_PORT1, RTP_AUDIO_PORT1)

  4. 注册 StreamPlayer#registerPlayerListener() 监听数据流接收状态。

  5. 当从监听数据流接收状态中 onStateChanged() 得到 PlaybackStream.Status.PREPARE 状态时,初始化IJK播放器和创建SDP服务器。

digraph { start [label="开始", shape="hexagon"]; step1 [label="App发回放命令给设备,\n开启回放流程", shape="rect"] step2 [label="监听设备\n是否回复命令?", shape="diamond"] step3 [label="配置RTP端口", shape="rect"] step4 [label="监听数据流接收状态", shape="rect"] step5 [label="if(state==PREPARE)", shape="diamond"] step6 [label="初始化IJK和创建SDP服务器", shape="rect"] end [label="结束", shape="hexagon"] // invisible nodes node[shape = point, width=0, height=0]; i0 [style="invis"]; i1 [style="invis"]; i2 [style="invis"]; i3 [style="invis"]; start -> step1 -> step2; step2 -> step3 [label="Yes"]; step3 -> step4 -> step5; step5 -> step6 [label="Yes"]; step6 -> i0[arrowhead = none]; i1 -> i2[arrowhead = none]; i2 -> i3[arrowhead = none, label="No"]; i0 -> end; { rank=same; step2 -> i1 [arrowhead = none]; } { rank=same; i0 -> i3 [minlen=4, dir=back]; } { rank=same; step5 -> i2 [minlen=1]; } }

回放流程

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 流程描述

  1. 把手机本地的固件升级文件通过FTP方式传输到设备端

  2. 调用 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 功能描述

  1. 设备与App的连接状态是由心跳包机制来保持的。当手机App接入到设备端以后, 必须在心跳包超时时间前发送心跳包给设备, 设备接收到心跳包会立刻返回心跳包给手机App。

  2. 手机App接入设备时就会接收到 KEEP_ALIVE_INTERVAL 命令,通过此命令携带的参数可获得设备心跳包超时时间,App再调用 tryToKeepAlive() 发送心跳命令给设备, 设备以同样的命令回复App。

  3. 至此, 完成了一个心跳包机制的流程。

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 流程描述

单备份升级分两个阶段来完成:

第一阶段:

  1. App连接设备后,通过描述文档解析得到 ota_typeota_step。当ota_type=1且ota_step = 0,说明设备支持单备份升级

  2. 升级前,App先发命令让设备进入单备份升级状态,发送命令:NET_OTA_SINGLE,参数: param":{"status":"1"}

  3. App收到设备的”NET_OTA_SINGLE”回复后,status=1,则表示设备成功进入单备份升级模式

  4. 连接3335 TCP端口,开始进行单备份升级,即发固件数据给设备

  5. 升级成功后,设备会自动重启,App要重新连接设备,准备第二阶段的升级

第二阶段:

  1. App重新连接设备后,通过描述文档解析得到 ota_typeota_step。当ota_type值为1且ota_step = 1,则进入单备份升级第二阶段。注意,这阶段不需要再发命令

  2. 连接3335端口TCP,进行单备份升级,即发固件数据给设备

  3. 升级成功,设备重启

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 //实时流打开失败