SDK开发
Xp2p实时视频功能列表

Xp2p实时视频功能列表

在此章节,我们将介绍Xp2p实时视频的各个功能,以及如何使用它们。

  • 该章节的代码中,将用this.player来引用播放器的实例,app代表小程序实例
  • 本章节只介绍具体功能点,不介绍如何初始化。初始化流程请参考 实时视频流程
  • wxml中的播放器配置如下
<!-- 直播播放器 -->
<iot-p2p-player-with-mjpg id="{{playerId}}" 
  class="player" 
  deviceInfo="{{XP2PDeviceInfo}}" 
  xp2pInfo="{{xp2pInfo}}" 
  streamQuality="{{streamQuality}}" 
  muted="{{muted}}" 
  sceneType="{{'live'}}" 
  mode="{{'RTC'}}">
      <view wx:if="{{isFullScreen}}" 
      slot="flvInner" 
      class="player-fullscreen-tools">
          <view class="player-fullscreen-tools__tool" 
          bind:tap="switchFullScreen">退出全屏</view>
      </view>
</iot-p2p-player-with-mjpg>
 
<!-- 音视频播放器 -->
<iot-p2p-intercom id="{{intercomPlayerId}}" 
  pusherStyle="height: 280rpx; width:210rpx" 
  deviceInfo="{{XP2PDeviceInfo}}" 
  pusherProps="{{pusherProps}}" 
  bind:intercomstatechange="onIntercomStateChange" 
  bind:livepushercontextchange="onIntercomLivePusherContextChange">
</iot-p2p-intercom>
  • this.data中的配置如下
    data: {
        xp2pInfo: '',
        playerId: 'p2p-player',
        intercomPlayerId: 'intercom-player',
        XP2PDeviceInfo: null,
        streamQuality: 'high',
        muted: true,
        isShowPlayer: false,
        isShowPTZ: false,
        isFullScreen: false,
        pusherProps: {
            enableCamera: true,
            enableMic: true,
            aspect: "3:4",
            devicePosition: 'front',
            fps: 10,
            maxBitrate: 512,
            minBitrate: 200
        },
 
        // 播放器状态
        isPlaySuccess: false,
        isPlayLoading: true,
        isPlayError: false,
        isPlayerFull: false, // 是否观看人数达到上限
 
        intercomState: ''
    }

1. 切换静音模式

switchMute() {
  this.setData({
    muted: !this.data.muted
  })
}

2. 切换高标清模式

  • Xp2p官方文档中支持standard/high/super 三种模式,但是我们的设备只支持high/super
switchStreamQuality() {
  const streamQuality = this.data.streamQuality
  this.setData({
      streamQuality: streamQuality === 'high' ? 'super' : 'high'
  })
}

3. 切换全屏模式

switchFullScreen() {
  if (!this.data.isFullScreen) {
      player.requestFullScreen({
          direction: 90,
      });
  } else {
      player.exitFullScreen();
  }
}

4. 发送云台控制命令

  • 固定用法,使用app.xp2pManager.sendUserCommand()
  • topic固定传'ptz'
  • data固定传value: 'left'/'right'/'up'/'down'/'stop'
  • 若设备回复'end'则代表云台已经转到底
sendPtz() {
  app.xp2pManager.sendUserCommand(this.data.XP2PDeviceInfo.deviceId, {
    cmd: {
        topic: `ptz`,
        data: {
            value: 'left',
        },
    }
  }).then(res => {
    if (res?.data?.value === "end") {
        wx.showToast({
            title: "已经转到底了",
            icon: "none",
        });
    }
  })
}

5. 开始双向音视频

  • 对于UI层需要判断是否已经播放成功再调用intercomPlayer.intercomCall()。
  • 需要通过onPlayStateEvent方法来获取播放状态
  • isPlaySuccess为"true"代表播放成功
onPlayStateEvent({
    type,
    detail
}) {
    console.log(`播放器状态变更。类型:${type}${detail ? "详情:" + JSON.stringify(detail) : ""}`)
    if (type === 'playstart') {
        this.setData({
            isPlaySuccess: false,
            isPlayError: false,
            isPlayLoading: true,
            isPlayerFull: false
        })
    } else if (type === 'playsuccess') {
        this.setData({
            isPlaySuccess: true,
            isPlayError: false,
            isPlayLoading: false,
            isPlayerFull: false
        })
    } else if (type === 'playstop' || type === 'playend') {
        this.setData({
            isPlaying: false,
            isPlaySuccess: false,
            isPlayLoading: false,
            isPlayerFull: false
        });
    } else if (type === 'playerror') {
        this.setData({
            isPlaySuccess: false,
            isPlayError: true,
            isPlayLoading: false,
            isPlayerFull: false
        });
    }
}
 
startIntercomCall() {
  const intercomPlayer = this.selectComponent(`#${this.data.intercomPlayerId}`)
  if (intercomPlayer) intercomPlayer.intercomCall()
}

6. 停止双向音视频

  • 对于UI层需要判断是否处于双向视频通话中
  • 需要通过onIntercomStateChange方法来获取通话状态
  • intercomState为"Sending"或"Calling"代表通话中
onIntercomStateChange({
    detail
}) {
    this.setData({
        intercomState: detail.state
    })
}
 
stopIntercomCall() {
  const intercomPlayer = this.selectComponent(`#${this.data.intercomPlayerId}`)
  if (intercomPlayer) intercomPlayer.intercomHangup();
}

7. 切换本地麦克风开关

switchMic() {
  const status = this.data.pusherProps.enableMic;
  this.setData({
      'pusherProps.enableMic': !status
  })
}

8. 切换本地摄像头开关

  • setData后只会开关本地摄像头,不会通知设备 因此要额外发送sendUserCommand()
  • topic固定传'display'
  • data固定传value: 'on'/'off'
switchCamera() {
  const status = this.data.pusherProps.enableCamera
  this.setData({
      'pusherProps.enableCamera': !status
  })
  app.xp2pManager.sendUserCommand(this.data.XP2PDeviceInfo.deviceId, {
      cmd: {
          topic: "display",
          data: {
              value: !status ? "on" : 'off',
          },
      },
  })
}

9. 切换前置/后置摄像头

  • 需要先使用onIntercomLivePusherContextChange获取livePusher实例
  • 然后通过livePusher实例的switchCamera方法来进行切换
onIntercomLivePusherContextChange({
    detail
}) { 
    this.livePusherContext = detail.livePusherContext;
}
 
switchCameraDirection() {
  if (this.livePusherContext && this.livePusherContext.switchCamera) {
      const devicePosition = this.data.pusherProps.devicePosition
      this.livePusherContext.switchCamera({
          success: () => {
              this.setData({
                  'pusherProps.devicePosition': devicePosition === 'front' ? 'back' : 'front'
              })
          }
      })
  }
}

10. 获取/设置设备音量

  • 调用getDeviceVolume方法获取设备音量
  • 通过sendUserCommand方法设置设备音量
  • topic固定传'volume'
  • data固定传value: '80' (string 0-100)
/* 获取设备音量 */
getVolume() {
  const volumeRes = await app.imcamWx.getDeviceVolume({
    sn: 'xxx' // 此处填入实际SN
  })
  if (volumeRes.errCode === 0) {
    const volume = volumeRes.volume
  }
}
 
/* 设置设备音量 */
setVolume(){
  app.xp2pManager.sendUserCommand(this.data.XP2PDeviceInfo.deviceId, {
    cmd: {
      topic: "volume",
      data: {
        value: '80'
      }
    }
  })
}

11. 截屏

snapShot() {
  this.player.snapshot()
    .then(res => {
      const filePath = res.tempImagePath
      wx.saveImageToPhotosAlbum({
        filePath,
        success: () => {
          wx.showToast({
            title: '截图已保存至手机相册'
          })
        }
      })
    })
}

12. 录屏

  • 此处只提供最基础的录屏功能,详细功能实现及参数设置请参考官方Demo (opens in a new tab)
  • 录屏开始的瞬间要调用sendUserCommand通知设备请求i帧。不然录屏文件开头会有黑屏
  • 录屏的状态变更更以及录屏文件状态变更,通过onRecordStateChange和onRecordFileStateChange进行处理
startRecord(){
  if (this.isRecording) return
  this.isRecording = true
  const recordFlvOptions = {
      maxFileSize: 100 * 1024 * 1024, // 单个flv文件的最大字节数,默认 100 * 1024 * 1024
      needAutoStartNextIfFull: false, // 当文件大小达到 maxFileSize 时,是否自动开始下一个文件,但是中间可能会丢失一部分数据,默认 false
      needSaveToAlbum: true, // 是否保存到相册,设为 true 时插件内实现转mp4再保存,默认 false
      showLog: true,
  }
  app.xp2pManager.sendUserCommand(this.data.XP2PDeviceInfo.deviceId, {
    cmd: {
        topic: "p2p", // topic固定传p2p
        data: {
            value: 1, // value固定传1
        },
    },
  }) // 请求i帧。此userCommand可能会在小程序端报错,但不影响正常使用
  this.player.startRecordFlv(recordFlvOptions)  // 开始录屏
}
 
stopRecord(){
  if (!this.isRecording) return
  this.isRecording = false
  this.player.stopRecordFlv() // 停止录屏
}
 
/* 录屏状态变更 */
onRecordStateChange({ 
    detail
}) {
    console.log('录屏状态变更', detail);
    if (!detail.record && this.isRecording) {
        this.stopRecord()
    }
}
 
/* 录屏文件状态变更 */
onRecordFileStateChange({
    detail
}) 
  /*
   detail: {
     fileName: string;
     state: string; // Start / Write / WriteSuccess / Extract / Export / Save / SaveSuccess / Error;
     filePath?: string;
     fileSize?: number;
     errType?: string; // fileEmpty / writeError / extractError / exportError / saveError
     errMsg?: string;
   }
  */
  witch (detail.state) {
     case 'Extract':
         wx.showLoading({
             title: '正在导出视频文件...'
         });
         break;
     case 'Save':
         break;
     case 'SaveSuccess':
         wx.hideLoading();
         wx.showToast({
             title: '录像已保存到相册',
             icon: 'success',
         });
         break;
     case 'Error':
         wx.hideLoading();
         // 要保存到相册时,保存成功会自动删除flv文件,出错时不自动删除,可以自行删除
         // 如果不需要管理出错的文件,需要自行删除,以免占用空间导致后续录像失败
         // 详见removeFileByPath(detail.filePath);
         console.error('onRecordFileError', detail);
         if (detail.errType === 'saveError' && /cancel/.test(detail.errMsg)) {
             // 用户取消保存,不用提示
         } else {
             if (detail.errMsg.includes('auth deny')) {
                 wx.showModal({
                     title: "保存失败",
                     content: "您未开启[保存到相册]权限。请点击右上角三个点->前往小程序设置页面,打开[保存到相册]权限"
                 })
             } else {
                 wx.showToast({
                     title: '录像保存出错',
                     icon: 'error'
                 })
             }
         }
         
         const removeFileByPath = (filePath) => {
             if (!filePath) {
                 return;
             }
             try {
                 fs.accessSync(filePath);
             } catch (err) {
                 if (~err.message.indexOf('fail no such file or directory')) {
                     // 文件不存在,算是成功
                 } else {
                     console.error('removeFileByPath access error', err);
                 }
                 return;
             }
             try {
                 fs.unlinkSync(filePath);
             } catch (err) {
                 console.error('removeFileByPath error', err);
             }
         }
         removeFileByPath(detail.filePath)
         break;
}

13. 错误处理

1. 播放器出错时的重试
retry() {
  this.player.retry()
}
2. 开始P2P通道时判断通道是否已满
  • 请在每次p2p通道建立成功后调用本方法,避免出现通道已满的情况
  • 固定用法,使用sendUserCommand
  • topic固定传'channel_full'
  • data固定传空
checkIsChannelFull() {
  app.xp2pManager
      .sendUserCommand(this.data.XP2PDeviceInfo.deviceId, {
          cmd: {
              topic: `channel_full`,
              data: {},
          },
      })
      .then(res => {
        if (res[0]?.status === '405' || res[0]?.status === 405) {
          this.setData({
            isPlayerFull: true,
            isPlaySuccess: false,
            isPlayLoading: false,
            isPlayError: false
          })
          wx.showToast({
            title: '通道已满,无法继续播放',
          })
        }
      })
}
3. 处理设备主动挂断的情况
  • 在onPlayStateEvent方法里,处理设备主动挂断的情况
onPlayStateEvent({
    type,
    detail
}){
  // ... 省略前面的代码
  else if (type === 'playstop' || type === 'playend') {
    this.setData({
      isPlaying: false,
      isPlaySuccess: false,
      isPlayLoading: false,
      isPlayerFull: false
    });
    if (detail?.reason === 'detached' 
    || detail?.reason === 'changeStreamQuality' 
    || detail?.reason === 'autoReconnect') {} else {
      wx.navigateBack()
      setTimeout(() => {
          wx.showToast({
              title: '对方已挂断',
              icon: 'none'
          })
      }, 200);
    }
  }
}
4. 打印音视频播放器的错误信息
  • 在onIntercomEventChange方法里,打印音视频播放器的错误信息
onIntercomEventChange({
    detail
}) {
  console.log("双向通话player事件变更。", detail);
  let tips = "";
  const isCalling = ["calling", "Sending"].includes(this.data.intercomState);
  switch (detail.event) {
      // 插件自身事件
      case "IntercomError": {
          tips = "呼叫异常";
          break;
      }
      case "VoiceError": {
          tips = "初始化语音对讲异常";
          break;
      }
      case "CancelCalling": {
          tips = "小程序取消呼叫";
          break;
      }
      case "Hangup": {
          tips = "小程序挂断";
          break;
      }
      // 设备端响应事件
      case "BeHangup": {
          tips = isCalling && "设备端挂断";
          break;
      }
      case "RejectCalling": {
          tips = isCalling && "设备端拒接";
          break;
      }
      case "CallCanceled": {
          tips = isCalling && "设备端取消呼叫";
          break;
      }
      case "Busy": {
          tips = isCalling && "设备端繁忙";
          break;
      }
      case "CallTimeout": {
          tips = isCalling && "设备端呼叫超时";
          break;
      }
      default: {
          break;
      }
  }
},
5. 处理请求双向视频通话时已有其他人正在通话中
  • 在onIntercomEventChange方法里进行处理
onIntercomEventChange({
    detail
}) {
  // ... 省略前面的代码
if(detail?.errMsg?.includes('设备正忙')) {
  wx.showToast({
      title: '已有其他人正在通话中',
      icon: 'error'
  })
}
© 2024 Cylan