wx-starlock/pages/p2p/p2pPlayer.vue
2025-06-19 14:10:17 +08:00

628 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<view>
<!-- #ifdef MP-WEIXIN -->
<iot-p2p-player-with-mjpg
v-if="deviceInfo"
id="playerRef"
:deviceInfo="deviceInfo"
:xp2pInfo="xp2pInfo"
:rotate="90"
:muted="isMute"
mode="RTC"
:acceptPlayerEvents="true"
soundMode="speaker"
sceneType="live"
:streamQuality="range[index].value"
:minCache="0.2"
:maxCache="0.8"
:fill="true"
orientation="horizontal"
@playsuccess="handlePlaySuccess"
>
</iot-p2p-player-with-mjpg>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<video
autoplay
id="playerRef"
v-if="url"
:muted="isMute"
:src="urlPrefix"
object-fit="cover"
class="w-[100vw] h-[100vh]"
:is-live="true"
:play-strategy="2"
:controls="false"
:show-progress="false"
:show-fullscreen-btn="false"
:show-play-btn="false"
:enable-progress-gesture="false"
:vslide-gesture="true"
/>
<!-- #endif -->
</view>
<image
v-if="!isVideoLoaded"
src="https://oss-lock.xhjcn.ltd/mp/background_monitor.png"
class="w-full h-full absolute top-0 left-0"
></image>
<cover-view
v-if="isApp || buttonInfo"
:style="{
top: isApp ? '60px' : buttonInfo.bottom + 15 + 'px'
}"
class="bg-[rgba(0,0,0,0.35)] rounded-full px-4 py-1.5 fixed right-32"
>
<cover-view class="text-white text-xs" @click="showQualitySelector">
{{ range[index].name }}
</cover-view>
</cover-view>
<!-- #ifdef MP-WEIXIN -->
<iot-p2p-voice
v-if="deviceInfo"
id="voiceComponent"
:deviceInfo="deviceInfo"
:xp2pInfo="xp2pInfo"
voiceType="Pusher"
>
</iot-p2p-voice>
<!-- #endif -->
<cover-view
v-if="isVideoLoaded"
class="fixed bottom-[calc(32rpx+env(safe-area-inset-bottom))] bg-[rgba(0,0,0,0.3)] rounded-xl p-4 shadow-lg left-1/2 -translate-x-1/2 w-622"
>
<cover-view class="flex items-center justify-around mx-15">
<cover-view class="relative">
<cover-image
v-show="isMute"
src="https://oss-lock.xhjcn.ltd/mp/icon_mute.png"
:class="isApp ? 'size-48' : 'size-48 p-2'"
></cover-image>
<cover-image
v-show="!isMute"
src="https://oss-lock.xhjcn.ltd/mp/icon_not_mute.png"
:class="isApp ? 'size-48' : 'size-48 p-2'"
></cover-image>
<cover-view
@click="toggleMute"
class="absolute top-0 left-0 w-full h-full size-48 p-2"
></cover-view>
</cover-view>
<cover-view class="relative">
<cover-image
src="https://oss-lock.xhjcn.ltd/mp/icon_screenshot.png"
:class="isApp ? 'size-48' : 'size-48 p-2'"
></cover-image>
<cover-view
@click="handleScreenshot"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
</cover-view>
<cover-view class="flex items-center justify-between text-white mt-2 px-10">
<cover-view class="flex flex-col items-center">
<cover-view
class="bg-white w-80 h-80 rounded-full flex items-center justify-center relative"
>
<cover-image
:src="
isVoice
? 'https://oss-lock.xhjcn.ltd/mp/icon_microphone.png'
: 'https://oss-lock.xhjcn.ltd/mp/icon_no_microphone.png'
"
class="w-55 h-55"
></cover-image>
<cover-view
@click="toggleVoice"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
<cover-view class="mt-2 text-center whitespace-nowrap text-xs">{{
isVoice ? '点击停止' : '点击说话'
}}</cover-view>
</cover-view>
<cover-view class="flex flex-col items-center">
<cover-view
class="bg-[#eb292b] w-80 h-80 rounded-full flex items-center justify-center relative"
>
<cover-image
src="https://oss-lock.xhjcn.ltd/mp/icon_hang_up.png"
class="w-60 h-60"
></cover-image>
<cover-view
@click="handleHangUp"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
<cover-view class="mt-2 text-center whitespace-nowrap text-xs">挂断</cover-view>
</cover-view>
<cover-view class="flex flex-col items-center">
<cover-view
class="bg-[#63b8af] w-80 h-80 rounded-full flex items-center justify-center relative"
>
<cover-image
src="https://oss-lock.xhjcn.ltd/mp/icon_lock_white.png"
class="w-60 h-60"
></cover-image>
<cover-view
@click="handleLock"
class="absolute top-0 left-0 w-full h-full"
></cover-view>
</cover-view>
<cover-view class="mt-2 text-center whitespace-nowrap text-xs">开锁</cover-view>
</cover-view>
</cover-view>
</cover-view>
<view
v-if="!isVideoLoaded"
class="fixed bottom-[calc(48rpx+env(safe-area-inset-bottom))] w-full flex justify-center"
>
<up-loading-icon
size="70rpx"
:vertical="true"
textSize="28rpx"
text="连接中"
mode="circle"
></up-loading-icon>
</view>
</view>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue'
import { onUnload } from '@dcloudio/uni-app'
import { useBluetoothStore } from '@/stores/bluetooth'
import { useBasicStore } from '@/stores/basic'
import { useUserStore } from '@/stores/user'
import { passthrough } from '@/api/sdk'
import { getLockNetTokenRequest } from '@/api/lock'
// #ifdef MP-WEIXIN
import * as A from './exportForXp2pPlugin'
import * as B from './exportForPlayerPlugin'
// #endif
// #ifdef APP-PLUS
import {
startService,
getLiveUrl,
stopService,
runSendService,
stopSendService,
dataSend
} from '@/uni_modules/xhj-tencent-xp2p'
// #endif
const $bluetooth = useBluetoothStore()
const $basic = useBasicStore()
const $user = useUserStore()
const buttonInfo = ref(null)
const isApp = ref(false)
const onlineToken = ref('0')
const lockId = ref()
const time = ref(0)
const pending = ref(false)
const index = ref(1)
const range = ref([
{ name: '标清', value: 'standard' },
{ name: '高清', value: 'high' },
{ name: '超清', value: 'super' }
])
const deviceInfo = ref()
const xp2pInfo = ref()
const url = ref()
const isVoice = ref(false)
const isMute = ref(false)
const isVideoLoaded = ref(false)
const urlPrefix = computed(() => {
return url.value + `ipc.flv?action=live&channel=0&quality=${range.value[index.value].value}`
})
onMounted(async () => {
// #ifdef APP-PLUS
isApp.value = true
// 初始化录音管理器
initRecorder()
// #endif
// #ifdef MP-WEIXIN
console.log(A, B)
// #endif
buttonInfo.value = await $basic.getButtonInfo()
const { code, data, message } = await passthrough({
request_method: 'GET',
request_uri: '/api/v1/tencentYun/getDeviceDetailData',
post_args: {
lockId: $bluetooth.currentLockInfo.lockId
}
})
// #ifdef MP-WEIXIN
if (code === 0) {
deviceInfo.value = {
deviceId: `${data.productId}/${data.deviceName}`,
productId: data.productId,
deviceName: data.deviceName
}
xp2pInfo.value = data.xp2pInfo
} else {
$basic.backAndToast(message)
}
await getServeTime()
// #endif
// #ifdef APP-PLUS
if (code === 0) {
deviceInfo.value = data
const result = await startService({
appKey: 'aanuJXFtISXFYVVsd',
appSecret: 'SsnOMHJUcazCvpULSVWY',
productId: deviceInfo.value.productId,
deviceName: deviceInfo.value.deviceName,
xp2pInfo: deviceInfo.value.xp2pInfo
})
if (result.code === 0) {
setTimeout(async () => {
const urlResult = await getLiveUrl({
id: `${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`
})
if (urlResult.code === 0) {
url.value = urlResult.data.url
runSendService(
`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`,
'channel=0',
true
)
handlePlaySuccess()
} else {
$basic.backAndToast(message)
}
}, 200)
} else {
$basic.backAndToast(message)
}
} else {
$basic.backAndToast(message)
}
// #endif
})
onUnload(() => {
// #ifdef APP-PLUS
// 停止录音
if (isVoice.value && recorderManager.value) {
stopRecording()
}
stopSendService(`${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`)
stopService({
id: `${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`
})
// #endif
// #ifdef MP-WEIXIN
const page = getCurrentPages().pop()
const player = page.selectComponent('#playerRef')
if (player.stopAll) {
player.stopAll()
}
// #endif
})
const showQualitySelector = () => {
uni.showActionSheet({
itemList: range.value.map(item => item.name),
success: res => {
index.value = res.tapIndex
}
})
}
const handleScreenshot = () => {
// #ifdef MP-WEIXIN
const page = getCurrentPages().pop()
const player = page.selectComponent('#playerRef')
player.snapshot().then(res => {
uni.saveImageToPhotosAlbum({
filePath: res.tempImagePath,
success: () => {
uni.showToast({
title: '截图已保存到相册',
icon: 'none'
})
},
fail: () => {
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
})
})
// #endif
// #ifdef APP-PLUS
uni.showModal({
title: '提示',
content:
'由于技术限制APP端无法直接截取视频画面。请使用手机系统截图功能\n\niOS同时按下电源键+音量上键\nAndroid同时按下电源键+音量下键',
confirmText: '我知道了',
showCancel: false
})
// #endif
}
const handleHangUp = () => {
// #ifdef MP-WEIXIN
const page = getCurrentPages().pop()
const player = page.selectComponent('#playerRef')
if (player.stopAll) {
player.stopAll()
}
// #endif
uni.navigateBack()
}
const handleLock = () => {
uni.showModal({
title: '提示',
content: '是否确认开锁?',
success: res => {
if (res.confirm) {
openDoorOperate()
}
}
})
}
const getServeTime = async () => {
const { code, data } = await $bluetooth.updateServerTimestamp()
if (code === 0) {
time.value = parseInt((data.date - new Date().getTime()) / 1000, 10)
}
}
const openDoorOperate = async () => {
const timestamp = new Date().getTime()
if (pending.value) {
return
}
const netWork = await $basic.getNetworkType()
if (!netWork) {
return
}
if ($bluetooth.currentLockInfo.appUnlockOnline) {
const result = await getNetToken()
if (!result) {
pending.value = false
return
}
}
uni.showLoading({
title: '开锁中'
})
uni.vibrateLong()
pending.value = true
const openMode = $bluetooth.currentLockInfo.appUnlockOnline ? 1 : 0
const { code } = await $bluetooth.openDoor({
name: $bluetooth.currentLockInfo.lockName,
uid: $user.userInfo.uid.toString(),
openMode,
openTime: parseInt(new Date().getTime() / 1000, 10) + time.value,
onlineToken: onlineToken.value
})
$bluetooth
.syncRecord({
keyId: $bluetooth.keyId.toString(),
uid: $user.userInfo.uid.toString()
})
.then(() => {
$bluetooth.closeBluetoothConnection()
})
// #ifdef MP-WEIXIN
uni.reportEvent('open_door', {
result: code,
duration: new Date().getTime() - timestamp
})
// #endif
if (code === 0) {
uni.showToast({
title: `开门成功`,
icon: 'none'
})
} else if (code === 7) {
uni.showToast({
title: `钥匙过期`,
icon: 'none'
})
} else if (code === 13) {
uni.showToast({
title: `钥匙当前不可用`,
icon: 'none'
})
} else if (code === -1) {
uni.showToast({
title: `开锁失败`,
icon: 'none'
})
}
uni.hideLoading()
pending.value = false
}
const getNetToken = async () => {
const { code, data, message } = await getLockNetTokenRequest({
lockId: lockId.value
})
if (code === 0) {
onlineToken.value = data.token
return true
}
uni.showToast({
title: message,
icon: 'none'
})
return false
}
// #ifdef APP-PLUS
const recorderManager = ref(null)
const recordingFilePath = ref('')
// #endif
const convertAudioFileToUint8Array = filePath => {
// #ifdef APP-PLUS
try {
plus.io.resolveLocalFileSystemURL(filePath, entry => {
entry.file(
file => {
console.log(11111, file)
if (file.size === 0) return
const fileReader = new plus.io.FileReader()
fileReader.onloadend = async evt => {
try {
console.log(evt)
const base64 = evt.target.result.split(',')[1]
const binaryString = atob(base64)
const byteArray = []
for (let i = 0; i < binaryString.length; i++) {
byteArray[i] = binaryString.charCodeAt(i)
}
const id = `${deviceInfo.value.productId}/${deviceInfo.value.deviceName}`
await dataSend(id, byteArray)
} catch (error) {
console.error('音频数据转换失败:', error)
}
}
fileReader.onerror = () => {
console.error('音频文件读取失败')
}
fileReader.readAsDataURL(file)
},
error => console.error('获取文件对象失败:', error)
)
})
} catch (error) {
console.error('音频文件处理异常:', error)
}
// #endif
}
const initRecorder = () => {
// #ifdef APP-PLUS
recorderManager.value = uni.getRecorderManager()
recorderManager.value.onStart(() => {
console.log('录音开始')
})
recorderManager.value.onError(error => {
console.error('录音出错:', error)
})
recorderManager.value.onStop(res => {
console.log('录音结束:', res)
recordingFilePath.value = res.tempFilePath
convertAudioFileToUint8Array(res.tempFilePath)
})
// #endif
}
// 开始录音
const startRecording = () => {
// #ifdef APP-PLUS
if (!recorderManager.value) {
initRecorder()
}
recorderManager.value.start({
duration: 60000,
sampleRate: 16000,
format: 'aac'
})
console.log('开始录音')
// #endif
}
// 停止录音
const stopRecording = () => {
// #ifdef APP-PLUS
if (recorderManager.value) {
recorderManager.value.stop()
console.log('停止录音')
}
// #endif
}
const toggleVoice = () => {
// #ifdef MP-WEIXIN
if (isVoice.value) {
isVoice.value = false
const page = getCurrentPages().pop()
const voice = page.selectComponent('#voiceComponent')
if (voice) {
voice.stopVoice()
}
} else {
uni.vibrateLong()
isVoice.value = true
const page = getCurrentPages().pop()
const voice = page.selectComponent('#voiceComponent')
if (voice) {
voice.startVoice()
}
}
// #endif
// #ifdef APP-PLUS
if (isVoice.value) {
isVoice.value = false
stopRecording()
} else {
uni.vibrateLong()
isVoice.value = true
// 初始化并开始录音
startRecording()
}
// #endif
}
const handlePlaySuccess = () => {
isVideoLoaded.value = true
}
const toggleMute = () => {
isMute.value = !isMute.value
}
</script>
<style lang="scss" scoped>
:deep(.mjpg-player--iot-player) {
height: 100vh !important;
}
</style>