推广商用户数据权限
This commit is contained in:
@@ -71,25 +71,50 @@
|
|||||||
<view v-if="isVoiceMode" class="voice-duration">{{ durationText }}</view>
|
<view v-if="isVoiceMode" class="voice-duration">{{ durationText }}</view>
|
||||||
|
|
||||||
<view class="controls" :class="isVoiceMode ? 'voice-controls' : 'video-controls'">
|
<view class="controls" :class="isVoiceMode ? 'voice-controls' : 'video-controls'">
|
||||||
<view class="control-item" @click="onCamToggle" @tap="onCamToggle">
|
<view class="controls-row">
|
||||||
<view class="control-btn dark-btn" :class="{ disabled: !videoEnabled, loading: videoLoading }">
|
<view class="control-item" @click="onCamToggle" @tap="onCamToggle">
|
||||||
<image class="control-image" :src="cameraIcon" mode="aspectFit"></image>
|
<view class="control-btn dark-btn" :class="{ disabled: !videoEnabled, loading: videoLoading }">
|
||||||
|
<image class="control-image" :src="cameraIcon" mode="aspectFit"></image>
|
||||||
|
</view>
|
||||||
|
<text class="control-label">摄像头{{ videoEnabled ? '已开' : '已关' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="control-item" @click="toggleSpeaker">
|
||||||
|
<view class="control-btn dark-btn" :class="{ disabled: speakerMuted }">
|
||||||
|
<image class="control-image" :src="speakerIcon" mode="aspectFit"></image>
|
||||||
|
</view>
|
||||||
|
<text class="control-label">扬声器{{ speakerMuted ? '已关' : '已开' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="control-item" @click="onMicToggle" @tap="onMicToggle">
|
||||||
|
<view class="control-btn dark-btn" :class="{ disabled: !audioEnabled, loading: audioLoading }">
|
||||||
|
<image class="control-image" :src="micIcon" mode="aspectFit"></image>
|
||||||
|
</view>
|
||||||
|
<text class="control-label">麦克风{{ audioEnabled ? '已开' : '已关' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="control-label">摄像头{{ videoEnabled ? '已开' : '已关' }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="control-item" @click="hangup" @tap="hangup">
|
<view class="controls-row controls-row-bottom">
|
||||||
<view class="hangup-btn" :class="{ loading: leaving }">
|
<view class="control-item" @click="takeSnapshot">
|
||||||
<image class="hangup-image" :src="hangupIcon" mode="aspectFit"></image>
|
<view class="control-btn dark-btn text-icon-btn">
|
||||||
|
<text class="text-icon">截</text>
|
||||||
|
</view>
|
||||||
|
<text class="control-label">截屏</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="control-label">挂断</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="control-item" @click="onMicToggle" @tap="onMicToggle">
|
<view class="control-item" @click="hangup" @tap="hangup">
|
||||||
<view class="control-btn dark-btn" :class="{ disabled: !audioEnabled, loading: audioLoading }">
|
<view class="hangup-btn" :class="{ loading: leaving }">
|
||||||
<image class="control-image" :src="micIcon" mode="aspectFit"></image>
|
<image class="hangup-image" :src="hangupIcon" mode="aspectFit"></image>
|
||||||
|
</view>
|
||||||
|
<text class="control-label">挂断</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="control-item" @click="flipCamera">
|
||||||
|
<view class="control-btn dark-btn text-icon-btn">
|
||||||
|
<text class="text-icon">↻</text>
|
||||||
|
</view>
|
||||||
|
<text class="control-label">镜头翻转</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="control-label">麦克风{{ audioEnabled ? '已开' : '已关' }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -118,11 +143,15 @@ import voiceOnIcon from '@/static/vocie_calling_1.png'
|
|||||||
import voiceOffIcon from '@/static/vocie_stop.png'
|
import voiceOffIcon from '@/static/vocie_stop.png'
|
||||||
import videoOnIcon from '@/static/vodie_calling_1.png'
|
import videoOnIcon from '@/static/vodie_calling_1.png'
|
||||||
import videoOffIcon from '@/static/vodie_stop.png'
|
import videoOffIcon from '@/static/vodie_stop.png'
|
||||||
|
import speakerOnIcon from '@/static/laba_open.png'
|
||||||
|
import speakerOffIcon from '@/static/laba_stop.png'
|
||||||
|
|
||||||
const meetingStore = useMeetingStore()
|
const meetingStore = useMeetingStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const rawCode = ref('')
|
const rawCode = ref('')
|
||||||
|
const callId = ref('')
|
||||||
|
const cloudLawApiBase = ref('')
|
||||||
const callMode = ref('video')
|
const callMode = ref('video')
|
||||||
const joining = ref(false)
|
const joining = ref(false)
|
||||||
const joinError = ref('')
|
const joinError = ref('')
|
||||||
@@ -136,6 +165,10 @@ const preferSelfMain = ref(false)
|
|||||||
let timerHandle = null
|
let timerHandle = null
|
||||||
let stateStopWatch = null
|
let stateStopWatch = null
|
||||||
let videoRetryHandle = null
|
let videoRetryHandle = null
|
||||||
|
let commandPollHandle = null
|
||||||
|
let commandPolling = false
|
||||||
|
const speakerMuted = ref(false)
|
||||||
|
const cameraFacing = ref('user')
|
||||||
|
|
||||||
const digitsOf = (value) => String(value || '').replace(/\D/g, '').slice(0, 9)
|
const digitsOf = (value) => String(value || '').replace(/\D/g, '').slice(0, 9)
|
||||||
const sameUser = (a, b) => String(a || '') === String(b || '')
|
const sameUser = (a, b) => String(a || '') === String(b || '')
|
||||||
@@ -155,6 +188,7 @@ const speakingMap = computed(() => meetingStore.speakingMap || {})
|
|||||||
const isVoiceMode = computed(() => callMode.value === 'voice')
|
const isVoiceMode = computed(() => callMode.value === 'voice')
|
||||||
const cameraIcon = computed(() => videoEnabled.value ? videoOnIcon : videoOffIcon)
|
const cameraIcon = computed(() => videoEnabled.value ? videoOnIcon : videoOffIcon)
|
||||||
const micIcon = computed(() => audioEnabled.value ? voiceOnIcon : voiceOffIcon)
|
const micIcon = computed(() => audioEnabled.value ? voiceOnIcon : voiceOffIcon)
|
||||||
|
const speakerIcon = computed(() => speakerMuted.value ? speakerOffIcon : speakerOnIcon)
|
||||||
|
|
||||||
const entryInitial = computed(() => {
|
const entryInitial = computed(() => {
|
||||||
const name = userStore.userInfo?.nickname || userStore.userInfo?.username || '在'
|
const name = userStore.userInfo?.nickname || userStore.userInfo?.username || '在'
|
||||||
@@ -319,6 +353,152 @@ const startTimer = () => {
|
|||||||
|
|
||||||
const localMediaError = ref('')
|
const localMediaError = ref('')
|
||||||
|
|
||||||
|
const resolveCloudLawApiUrl = (path) => {
|
||||||
|
const base = String(cloudLawApiBase.value || '').replace(/\/+$/, '')
|
||||||
|
return `${base}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestCloudLawCommands = (url) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 8000,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
resolve(res.data || {})
|
||||||
|
} else {
|
||||||
|
reject(res.data || { message: `HTTP ${res.statusCode}` })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: reject
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const pollCommandsOnce = async () => {
|
||||||
|
if (!callId.value || !meetingStore.isInMeeting || leaving.value || ended.value || commandPolling) return
|
||||||
|
commandPolling = true
|
||||||
|
try {
|
||||||
|
const room = meetingStore.currentRoom?.room_code || formattedCode.value || ''
|
||||||
|
const path = `/api/applet/voiceConnection/publicCommands/${encodeURIComponent(callId.value)}${room ? `?roomCode=${encodeURIComponent(room)}` : ''}`
|
||||||
|
const resp = await requestCloudLawCommands(resolveCloudLawApiUrl(path))
|
||||||
|
const payload = resp?.data || resp || {}
|
||||||
|
const commands = payload.commands || payload.data?.commands || []
|
||||||
|
for (const command of commands) {
|
||||||
|
await executeCloudLawCommand(command)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[CloudLawVideoCall] poll commands failed', err)
|
||||||
|
} finally {
|
||||||
|
commandPolling = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startCommandPolling = () => {
|
||||||
|
if (!callId.value || commandPollHandle) return
|
||||||
|
pollCommandsOnce()
|
||||||
|
commandPollHandle = setInterval(pollCommandsOnce, 1200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopCommandPolling = () => {
|
||||||
|
if (commandPollHandle) {
|
||||||
|
clearInterval(commandPollHandle)
|
||||||
|
commandPollHandle = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSpeakerMuted = (muted) => {
|
||||||
|
speakerMuted.value = muted
|
||||||
|
// #ifdef H5
|
||||||
|
try {
|
||||||
|
const audioEls = document.querySelectorAll('.cloud-law-call audio')
|
||||||
|
audioEls.forEach(el => {
|
||||||
|
el.muted = muted
|
||||||
|
el.volume = muted ? 0 : 1
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[CloudLawVideoCall] set speaker failed', err)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSpeaker = () => {
|
||||||
|
setSpeakerMuted(!speakerMuted.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const takeSnapshot = () => {
|
||||||
|
// #ifdef H5
|
||||||
|
try {
|
||||||
|
const video = document.querySelector('.cloud-law-call .main-video video') || document.querySelector('.cloud-law-call video')
|
||||||
|
if (!video || !video.videoWidth || !video.videoHeight) {
|
||||||
|
uni.showToast({ title: '当前无视频画面可截屏', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = video.videoWidth
|
||||||
|
canvas.height = video.videoHeight
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = canvas.toDataURL('image/png')
|
||||||
|
a.download = `cloud-law-video-${Date.now()}.png`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
uni.showToast({ title: '截屏已保存', icon: 'success' })
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[CloudLawVideoCall] snapshot failed', err)
|
||||||
|
uni.showToast({ title: '截屏失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const flipCameraByCommand = async (value) => {
|
||||||
|
const nextFacing = value === 'front' || value === 'user'
|
||||||
|
? 'user'
|
||||||
|
: value === 'back' || value === 'environment'
|
||||||
|
? 'environment'
|
||||||
|
: (cameraFacing.value === 'user' ? 'environment' : 'user')
|
||||||
|
cameraFacing.value = nextFacing
|
||||||
|
try {
|
||||||
|
if (meetingStore.localProducers?.video) {
|
||||||
|
await meetingStore.switchLocalCameraFacing(nextFacing)
|
||||||
|
} else {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: false,
|
||||||
|
video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: nextFacing }
|
||||||
|
})
|
||||||
|
const track = stream.getVideoTracks()[0]
|
||||||
|
await meetingStore.startLocalVideo(undefined, track)
|
||||||
|
}
|
||||||
|
localMediaError.value = ''
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[CloudLawVideoCall] flip camera failed', err)
|
||||||
|
localMediaError.value = `镜头翻转:${err?.message || err}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flipCamera = () => {
|
||||||
|
flipCameraByCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeCloudLawCommand = async (cmd = {}) => {
|
||||||
|
const command = String(cmd.command || cmd.type || '').toLowerCase()
|
||||||
|
const value = cmd.value
|
||||||
|
if (command === 'speaker') {
|
||||||
|
setSpeakerMuted(value === 'off' || value === false || value === 'false' || value === '0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (command === 'screenshot') {
|
||||||
|
takeSnapshot()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (command === 'flipcamera' || command === 'flip_camera') {
|
||||||
|
await flipCameraByCommand(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const joinMeeting = async () => {
|
const joinMeeting = async () => {
|
||||||
if (joining.value) return
|
if (joining.value) return
|
||||||
@@ -357,6 +537,7 @@ const joinMeeting = async () => {
|
|||||||
await meetingStore.joinAndEnter(code, '', prefs)
|
await meetingStore.joinAndEnter(code, '', prefs)
|
||||||
startTimer()
|
startTimer()
|
||||||
scheduleVideoRetry()
|
scheduleVideoRetry()
|
||||||
|
startCommandPolling()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
joinError.value = err?.message || '视频咨询接入失败'
|
joinError.value = err?.message || '视频咨询接入失败'
|
||||||
console.error('[CloudLawVideoCall] join failed', err)
|
console.error('[CloudLawVideoCall] join failed', err)
|
||||||
@@ -446,11 +627,11 @@ const acquireAudioTrack = async () => {
|
|||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false })
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false })
|
||||||
return stream.getAudioTracks()[0]
|
return stream.getAudioTracks()[0]
|
||||||
}
|
}
|
||||||
const acquireVideoTrack = async () => {
|
const acquireVideoTrack = async (facingMode = 'user') => {
|
||||||
if (!navigator?.mediaDevices?.getUserMedia) throw new Error('当前环境不支持 getUserMedia')
|
if (!navigator?.mediaDevices?.getUserMedia) throw new Error('当前环境不支持 getUserMedia')
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: false,
|
audio: false,
|
||||||
video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: 'user' }
|
video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode }
|
||||||
})
|
})
|
||||||
return stream.getVideoTracks()[0]
|
return stream.getVideoTracks()[0]
|
||||||
}
|
}
|
||||||
@@ -483,7 +664,7 @@ const onCamToggle = async () => {
|
|||||||
if (meetingStore.localProducers?.video) {
|
if (meetingStore.localProducers?.video) {
|
||||||
await meetingStore.setLocalVideoMuted(videoEnabled.value)
|
await meetingStore.setLocalVideoMuted(videoEnabled.value)
|
||||||
} else {
|
} else {
|
||||||
const track = await acquireVideoTrack()
|
const track = await acquireVideoTrack(cameraFacing.value)
|
||||||
await meetingStore.startLocalVideo(undefined, track)
|
await meetingStore.startLocalVideo(undefined, track)
|
||||||
}
|
}
|
||||||
localMediaError.value = ''
|
localMediaError.value = ''
|
||||||
@@ -498,6 +679,7 @@ const onCamToggle = async () => {
|
|||||||
const hangup = async () => {
|
const hangup = async () => {
|
||||||
if (leaving.value) return
|
if (leaving.value) return
|
||||||
leaving.value = true
|
leaving.value = true
|
||||||
|
stopCommandPolling()
|
||||||
try {
|
try {
|
||||||
// 云律视频咨询:发起人(小程序端用户)在 CreateRoomForInternal 时被加成 host,
|
// 云律视频咨询:发起人(小程序端用户)在 CreateRoomForInternal 时被加成 host,
|
||||||
// 用户挂机 = 咨询结束,无条件 endMeeting 让律师 iframe 收到 room.ended 自动退出。
|
// 用户挂机 = 咨询结束,无条件 endMeeting 让律师 iframe 收到 room.ended 自动退出。
|
||||||
@@ -522,6 +704,8 @@ const hangup = async () => {
|
|||||||
|
|
||||||
onLoad(query => {
|
onLoad(query => {
|
||||||
rawCode.value = query?.code || query?.roomCode || ''
|
rawCode.value = query?.code || query?.roomCode || ''
|
||||||
|
callId.value = query?.callId || query?.call_id || ''
|
||||||
|
cloudLawApiBase.value = query?.cloudLawApiBase || query?.cloud_law_api_base || ''
|
||||||
const mode = String(query?.mode || query?.callType || query?.type || '').toLowerCase()
|
const mode = String(query?.mode || query?.callType || query?.type || '').toLowerCase()
|
||||||
callMode.value = mode === 'voice' || mode === 'audio' ? 'voice' : 'video'
|
callMode.value = mode === 'voice' || mode === 'audio' ? 'voice' : 'video'
|
||||||
joinMeeting()
|
joinMeeting()
|
||||||
@@ -532,8 +716,10 @@ onMounted(() => {
|
|||||||
if (s === MEETING_LOCAL_STATE_CONNECTED) {
|
if (s === MEETING_LOCAL_STATE_CONNECTED) {
|
||||||
startTimer()
|
startTimer()
|
||||||
scheduleVideoRetry()
|
scheduleVideoRetry()
|
||||||
|
startCommandPolling()
|
||||||
}
|
}
|
||||||
if (s === MEETING_LOCAL_STATE_ENDED) {
|
if (s === MEETING_LOCAL_STATE_ENDED) {
|
||||||
|
stopCommandPolling()
|
||||||
const reason = meetingStore.lastEndedReason
|
const reason = meetingStore.lastEndedReason
|
||||||
const label = MEETING_ENDED_REASON_LABEL?.[reason] || '视频咨询已结束'
|
const label = MEETING_ENDED_REASON_LABEL?.[reason] || '视频咨询已结束'
|
||||||
uni.showToast({ title: label, icon: 'none' })
|
uni.showToast({ title: label, icon: 'none' })
|
||||||
@@ -555,9 +741,11 @@ onBeforeUnmount(() => {
|
|||||||
clearTimeout(videoRetryHandle)
|
clearTimeout(videoRetryHandle)
|
||||||
videoRetryHandle = null
|
videoRetryHandle = null
|
||||||
}
|
}
|
||||||
|
stopCommandPolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnload(() => {
|
onUnload(() => {
|
||||||
|
stopCommandPolling()
|
||||||
if (meetingStore.isInMeeting && meetingStore.localState !== MEETING_LOCAL_STATE_LEAVING) {
|
if (meetingStore.isInMeeting && meetingStore.localState !== MEETING_LOCAL_STATE_LEAVING) {
|
||||||
meetingStore.leave().catch(() => {})
|
meetingStore.leave().catch(() => {})
|
||||||
}
|
}
|
||||||
@@ -795,24 +983,26 @@ onUnload(() => {
|
|||||||
}
|
}
|
||||||
.video-controls {
|
.video-controls {
|
||||||
padding: 0 58rpx;
|
padding: 0 58rpx;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
flex-direction: column;
|
||||||
align-items: end;
|
gap: 30rpx;
|
||||||
column-gap: 44rpx;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
.voice-controls {
|
.voice-controls {
|
||||||
padding: 0 58rpx;
|
padding: 0 58rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30rpx;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.controls-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
align-items: end;
|
align-items: end;
|
||||||
column-gap: 44rpx;
|
column-gap: 44rpx;
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
.voice-control-col {
|
.controls-row-bottom {
|
||||||
display: flex;
|
align-items: end;
|
||||||
flex-direction: column;
|
|
||||||
gap: 36rpx;
|
|
||||||
}
|
}
|
||||||
.control-item {
|
.control-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -858,6 +1048,15 @@ onUnload(() => {
|
|||||||
width: 150rpx;
|
width: 150rpx;
|
||||||
height: 150rpx;
|
height: 150rpx;
|
||||||
}
|
}
|
||||||
|
.text-icon-btn {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.text-icon {
|
||||||
|
font-size: 54rpx;
|
||||||
|
line-height: 1;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
.hangup-image {
|
.hangup-image {
|
||||||
width: 150rpx;
|
width: 150rpx;
|
||||||
height: 150rpx;
|
height: 150rpx;
|
||||||
|
|||||||
@@ -1494,6 +1494,43 @@ export const useMeetingStore = defineStore('meeting', () => {
|
|||||||
return producer ? producer.track : null
|
return producer ? producer.track : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const switchLocalCameraFacing = async (facingMode = 'user') => {
|
||||||
|
if (!_engine || !localProducers.video) return false
|
||||||
|
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
throw new Error('当前环境不支持切换摄像头')
|
||||||
|
}
|
||||||
|
const oldProducerId = localProducers.video
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: false,
|
||||||
|
video: {
|
||||||
|
width: { ideal: 640 },
|
||||||
|
height: { ideal: 480 },
|
||||||
|
frameRate: { ideal: 15, max: 24 },
|
||||||
|
facingMode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const track = stream.getVideoTracks()[0]
|
||||||
|
if (!track) {
|
||||||
|
try { stream.getTracks().forEach(t => t.stop()) } catch {}
|
||||||
|
throw new Error('未获取到摄像头画面')
|
||||||
|
}
|
||||||
|
let newProducer = null
|
||||||
|
try {
|
||||||
|
newProducer = await _engine.produce({ kind: 'video', track })
|
||||||
|
localProducers.video = newProducer.id
|
||||||
|
localVideoEnabled.value = true
|
||||||
|
try { await _engine.closeProducer(oldProducerId) } catch (err) { _log('warn', '[Meeting] 切换摄像头时关闭旧 Producer 失败', err) }
|
||||||
|
_broadcastSelfState({ video_enabled: true })
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
try { track.stop() } catch {}
|
||||||
|
if (newProducer && newProducer.id) {
|
||||||
|
try { await _engine.closeProducer(newProducer.id) } catch {}
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Actions:聊天/管理 ====================
|
// ==================== Actions:聊天/管理 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1649,6 +1686,7 @@ export const useMeetingStore = defineStore('meeting', () => {
|
|||||||
stopLocalVideo,
|
stopLocalVideo,
|
||||||
setLocalAudioMuted,
|
setLocalAudioMuted,
|
||||||
setLocalVideoMuted,
|
setLocalVideoMuted,
|
||||||
|
switchLocalCameraFacing,
|
||||||
startScreenShare,
|
startScreenShare,
|
||||||
stopScreenShare,
|
stopScreenShare,
|
||||||
recording,
|
recording,
|
||||||
|
|||||||
Reference in New Issue
Block a user