阿里云首页 物联网视频服务

录像播放

设备端Android版本LinkVisual SDK提供录像播放功能,本文介绍实现录像播放功能的过程。

下文简称设备端Android版本LinkVisual SDK为LinkVisual SDK。

前提条件

  • 已创建产品和设备,具体操作,请参见设备接入

  • 已获取LinkVisual SDK,具体操作,请参见获取SDK

  • 已完成初始化LinkVisual SDK,具体操作,请参见初始化SDK

背景信息

录像播放功能通过RTMP协议推流。其支持的视频编码格式和音频编码格式如下:

  • 视频编码格式:H264和H265。

  • 音频编码格式:G711a、G711u和AAC_LC。

操作步骤

步骤一:注册录像播放事件监听器和流错误监听器。

  • 录像播放事件监听器OnVodStreamListener:通知开始推流、结束推流、暂停、恢复、Seek等事件。

  • 流错误监听器OnStreamErrorListener:通知推流中发送的错误。

步骤二:处理查询设备端录像文件列表的请求。

  1. 应用端App发起查询设备端录像文件列表的请求。

  2. 设备端收到调用查询设备录像文件列表的请求。

    查询设备录像文件列表的请求,由调用IPC设备的同步设备服务完成。设备服务的详细信息,请参考设备服务

  3. 设备端响应请求,并将查询范围内的文件列表返回至应用端App。

注意

录像文件名需进行Base64编码。

@Override
        public void onNotify(String connectId, String topic, AMessage aMessage){
            Log.d(TAG, "onNotify() called with: connectId = [" + connectId + "], topic = [" + topic + "], aMessage = ["
                    + new String((byte[]) aMessage.data) + "]");
            /**
             * 添加SDK的监听
             */
            IPCDev.getInstance().notifySyncTopicReceived(connectId, topic, aMessage);

            // 处理同步服务调用
            if (CONNECT_ID.equals(connectId) && !TextUtils.isEmpty(topic) &&
                    topic.contains("rrpc")) {
                Log.d(TAG, "IConnectNotifyListener   onNotify() called with: connectId = [" + connectId + "], topic = ["
                        + topic + "], aMessage = ["
                        + new String((byte[]) aMessage.data) + "]");

                int code = 200;
                String data = "{}";

                JSONObject json = JSON.parseObject(new String((byte[]) aMessage.data));
                if (json != null) {
                    String method = json.getString("method");
                    JSONObject params = json.getJSONObject("params");
                    switch (method) {
                        // 查询设备录像列表请求
                        case "thing.service.QueryRecordList":
                            int beginTime = params.getIntValue("BeginTime");
                            int endTime = params.getIntValue("EndTime");
                            int querySize = params.getIntValue("QuerySize");
                            int type = params.getIntValue("Type");
                            appendLog("收到查询设备录像列表的请求: beginTime=" + beginTime +
                                    "\tendTime=" + endTime + "\tquerySize=" + querySize + "\ttype=" + type);

                            JSONArray resultArray = new JSONArray();
                            JSONObject item1 = new JSONObject();
                            item1.put("FileName", Base64.encode("file1".getBytes(), Base64.DEFAULT));
                            item1.put("BeginTime", System.currentTimeMillis() / 1000 - 200);
                            item1.put("EndTime", System.currentTimeMillis() / 1000 - 100);
                            item1.put("Size", 1024000);
                            item1.put("Type", 0);
                            resultArray.add(item1);

                            JSONObject item2 = new JSONObject();
                            item2.put("FileName", Base64.encode("file2".getBytes(), Base64.DEFAULT));
                            item2.put("BeginTime", System.currentTimeMillis() / 1000 - 100);
                            item2.put("EndTime", System.currentTimeMillis() / 1000);
                            item2.put("Size", 1024000);
                            item2.put("Type", 0);
                            resultArray.add(item2);

                            JSONObject result = new JSONObject();
                            result.put("RecordList", resultArray);

                            code = 200;
                            data = result.toJSONString();
                            break;
                        default:
                            break;
                    }
                }

                MqttPublishRequest request = new MqttPublishRequest();
                request.isRPC = false;
                request.topic = topic.replace("request", "response");
                String resId = topic.substring(topic.indexOf("rrpc/request/") + 13);
                request.msgId = resId;
                request.payloadObj = "{\"id\":\"" + resId + "\", \"code\":" + code + ",\"data\":" + data + "}";
                LinkKit.getInstance().publish(request, new IConnectSendListener() {
                    @Override
                    public void onResponse(ARequest aRequest, AResponse aResponse) {
                        appendLog("上报成功");
                    }

                    @Override
                    public void onFailure(ARequest aRequest, AError aError) {
                        appendLog("上报失败:" + aError.toString());
                    }
                });
            }
        }

步骤三:处理开始推流指令。

  1. 应用端App请求步骤二返回的录像文件列表中的文件。

  2. 服务端下发推流指令,提供回调OnVodStreamListener.onStartPushVodStreaming(int streamId, String fileName)OnVodStreamListener.onStartPushVodStreaming(int streamId, int beginTimeUtc, int endTimeUtc)通知视频设备端进行音视频数据推流,即录像播放。LinkVisual SDK提供了下面两种录像播放的方式:

    • 根据文件名播放设备端录像

          @Override
          public void onStartPushVodStreaming(int streamId, String fileName){
              appendLog("开始推点播流 " + streamId + " 文件名:" + new String(Base64.decode(fileName, Base64.NO_WRAP)));
      
              try {
                  // 构造视频参数
                  VideoStreamParams videoStreamParams = new VideoStreamParams();
                  // 该视频文件的时长,单位为秒
                  videoStreamParams.setDurationInS(H264_DURATION_IN_S);
                  videoStreamParams.setVideoFormat(VideoStreamParams.VIDEO_FORMAT_H264);
      
                  // 构造音频参数
                  AudioStreamParams audioStreamParams = new AudioStreamParams();
                  audioStreamParams.setAudioChannel(AudioStreamParams.AUDIO_CHANNEL_MONO);
                  audioStreamParams.setAudioFormat(AudioStreamParams.AUDIO_FORMAT_G711A);
                  audioStreamParams.setAudioEncoding(AudioStreamParams.AUDIO_ENCODING_16BIT);
                  audioStreamParams.setAudioSampleRate(AudioStreamParams.AUDIO_SAMPLE_RATE_8000);
      
                  // 设置推流参数
                  IPCDev.getInstance().getIpcStreamManager().setStreamParams(streamId, videoStreamParams, audioStreamParams);
      
                  // TODO 读取fileName文件,调用发送音视频数据接口进行推流
                  // 文件推流完毕后应调用 IPCDev.getInstance().getIpcStreamManager().notifyVodComplete(streamId) 通知推流完成
      
              } catch (NoSuchStreamException e) {
                  e.printStackTrace();
              }
          }
    • 根据录像时间播放设备端录像

          @Override
          public void onStartPushVodStreaming(int streamId, int beginTimeUtc, int endTimeUtc){
              appendLog("开始推点播流 " + streamId + " beginTimeUtc: "+beginTimeUtc + " endTimeUtc:"+endTimeUtc);
      
              //TODO 推流逻辑需要添加:
              // 1. beginTimeUtc和endTimeUtc是一天的开始和结束时间
              // 2. 当收到onStartPushVodStreaming回调后,应从beginTimeUtc开始向后最近的I帧开始推流,时间戳应使用对应帧的UTC时间
              // 3. 若beginTimeUtc到endTimeUtc范围内没有录像或范围内推流已经完成了,则应调用 IPCDev.getInstance().getIpcStreamManager().notifyVodComplete(streamId) 通知推流完成
              // 4. 只要是beginTimeUtc到endTimeUtc范围内有数据, 即使跨文件,推流应该持续不断
          }

步骤四:处理暂停推流指令或恢复推流指令。

通过响应暂停推流指令或恢复推流指令OnVodStreamListener,暂停或恢复发送音视频数据。

/**
     * 收到暂停推流请求
     *
     * @param streamId 流ID
     */
    void onPausePushVodStreaming(int streamId);

    /**
     * 收到恢复推流的请求
     *
     * @param streamId 流ID
     */
    void onResumePushVodStreaming(int streamId);

步骤五:处理Seek指令。

响应Seek指令时,会回调onSeekTo方法,然后从timeStampInS时间点最近的I帧开始继续推流。例如应用端App的播放器进度条Seek到80秒时,会从80秒最近的I帧开始继续推流。

/**
     * 收到重新定位请求
     *
     * @param streamId     流ID
     * @param timeStampInS 时间偏移量,相对于视频开始时间,单位为秒
     */
    void onSeekTo(int streamId, long timeStampInS);

步骤六:处理停止推流指令。

当服务端下发停止推流请求时:

  1. 通过回调OnVodStreamListener.onStopPushLiveStreaming()方法通知设备端停止推流。

  2. 通常情况下需要停止调用音视频发送接口,关闭视频文件,并调用IPCStreamManager.getInstance().stopStreaming(int streamId)方法。

/**
     * 收到停止推流请求
     *
     * @param streamId 流ID
     */
    @Override
    public void onStopPushStreaming(int streamId){
        // TODO 停止音视频数据的发送
        try {
            // 调用停止推流接口
            IPCDev.getInstance().getIpcStreamManager().stopStreaming(streamId);
         catch (NoSuchStreamException e) {
            e.printStackTrace();
        }
    }

步骤七:处理流错误。

推流过程中通过OnStreamErrorListener.onError(int streamId, StreamError error)方法接收和处理流错误。错误码详细信息,请参考本文下方错误码

错误码

流错误码

错误码

标志符

描述

解决方法

1

StreamError.ERROR_STREAM_CREATE_FAILED

创建流实例失败。

该错误通常由系统资源不足引起,请您申请内存后重试。

2

StreamError.ERROR_STREAM_START_FAILED

建立RTMP链接失败。

请检查网络是否正常然后重试。

3

StreamError.ERROR_STREAM_STOP_FAILED

停止流失败。

因引入了无效的StreamId而引发的错误,该错误可忽略。

4

StreamError.ERROR_STREAM_SEND_VIDEO_FAILED

发送视频数据失败。

请根据RTMP错误码判断具体的出错原因。RTMP错误码的详细信息,请参考本文下方的RTMP错误码

5

StreamError.ERROR_STREAM_SEND_AUDIO_FAILED

发送音频数据失败。

请根据RTMP错误码判断具体的出错原因。RTMP错误码的详细信息,请参考本文下方的RTMP错误码

6

StreamError.ERROR_STREAM_INVALID_PARAMS

无效的流参数。

setStreamParams接口设置的参数无效,请检查并修改后重试。

RTMP错误码

说明
RTMP错误码只出现在日志中,仅用于排查详细问题。

错误码

标志符

描述

解决方法

-1

RTMP_ILLEGAL_INPUT

输入不合法。

请检查并修改输入参数后重试。

-2

RTMP_MALLOC_FAILED

内存分配失败。

请检查视频设备当前内存占用情况后重试。

-3

RTMP_CONNECT_FAILED

RTMP建立连接失败。

请检查网络是否正常后重试。

-4

RTMP_IS_DISCONNECTED

RTMP连接未建立。

该错误通常因服务端断开导致,可忽略。

-5

RTMP_UNSUPPORT_FORMAT

不支持的音视频格式。

setStreamParams接口设置的参数无效,请检查并修改后重试。

-6

RTMP_SEND_FAILED

RTMP数据包发送失败。

与服务端断开连接后再调用send接口会导致报该错误,可忽略。

-7

RTMP_READ_MESSAGE_FAILED

RTMP消息读取失败。

该错误通常因服务端断开导致,可忽略。

-8

RTMP_READ_TIMESTAMP_ERROR

输入时间戳错误。

直播推流时需保证时间戳未出现回退。请检查时间戳是否合法后重试。