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'
})
}