|
|
|
@ -92,7 +92,7 @@ |
|
|
|
<div class="video-area remote-video-area"> |
|
|
|
<div class="video-area remote-video-area"> |
|
|
|
<div class="video" :ref="`stream-${stream.subscribeID}`"></div> |
|
|
|
<div class="video" :ref="`stream-${stream.subscribeID}`"></div> |
|
|
|
<div class="video-controls remote-controls"> |
|
|
|
<div class="video-controls remote-controls"> |
|
|
|
<span class="user-label">{{ stream.userID }}</span> |
|
|
|
<span class="user-label">{{ getUserName(stream.userID) }}</span> |
|
|
|
<div class="stream-status"> |
|
|
|
<div class="stream-status"> |
|
|
|
<svg-icon v-if="!stream.muteVideo" icon-name="video" class="control-icon"></svg-icon> |
|
|
|
<svg-icon v-if="!stream.muteVideo" icon-name="video" class="control-icon"></svg-icon> |
|
|
|
<svg-icon v-else icon-name="video-muted" class="control-icon muted"></svg-icon> |
|
|
|
<svg-icon v-else icon-name="video-muted" class="control-icon muted"></svg-icon> |
|
|
|
@ -121,14 +121,14 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="tools-section"> |
|
|
|
<div class="tools-section"> |
|
|
|
<div class="tool-item" @click="toggleMic"> |
|
|
|
<div class="tool-item" @click="localStreamToggleAudio"> |
|
|
|
<div class="icon-wrapper"> |
|
|
|
<div class="icon-wrapper"> |
|
|
|
<i class="el-icon-microphone"></i> |
|
|
|
<i class="el-icon-microphone"></i> |
|
|
|
<span v-if="!micOn" class="slash-line"></span> |
|
|
|
<span v-if="!micOn" class="slash-line"></span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<span>麦克风</span> |
|
|
|
<span>麦克风</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="tool-item" @click="toggleCam"> |
|
|
|
<div class="tool-item" @click="localStreamToggleVideo"> |
|
|
|
<div class="icon-wrapper"> |
|
|
|
<div class="icon-wrapper"> |
|
|
|
<i class="el-icon-video-camera"></i> |
|
|
|
<i class="el-icon-video-camera"></i> |
|
|
|
<span v-if="!camOn" class="slash-line"></span> |
|
|
|
<span v-if="!camOn" class="slash-line"></span> |
|
|
|
@ -390,6 +390,7 @@ export default { |
|
|
|
captureCheckTimer: null, |
|
|
|
captureCheckTimer: null, |
|
|
|
isLocalCameraShared: false, // 双击本地摄像头切换到采集卡窗口的状态 |
|
|
|
isLocalCameraShared: false, // 双击本地摄像头切换到采集卡窗口的状态 |
|
|
|
sharedStreamId: null, // 当前在主画面显示的流来源: null | 'local' | subscribeID |
|
|
|
sharedStreamId: null, // 当前在主画面显示的流来源: null | 'local' | subscribeID |
|
|
|
|
|
|
|
userNameMap: {}, // userID → userName 映射 |
|
|
|
|
|
|
|
|
|
|
|
// ==================== 设置模块 ==================== |
|
|
|
// ==================== 设置模块 ==================== |
|
|
|
settingDialogVisible: false, |
|
|
|
settingDialogVisible: false, |
|
|
|
@ -771,6 +772,9 @@ export default { |
|
|
|
if (users && users.length > 0) { |
|
|
|
if (users && users.length > 0) { |
|
|
|
this.userList = users; |
|
|
|
this.userList = users; |
|
|
|
users.forEach((user) => { |
|
|
|
users.forEach((user) => { |
|
|
|
|
|
|
|
// 构建 userID → userName 映射 |
|
|
|
|
|
|
|
const userName = user.userName || user.name || user.userdef || user.userDefined || user.userID; |
|
|
|
|
|
|
|
this.$set(this.userNameMap, user.userID, userName); |
|
|
|
if (user.streams && user.streams.length > 0) { |
|
|
|
if (user.streams && user.streams.length > 0) { |
|
|
|
user.streams.forEach((stream) => { |
|
|
|
user.streams.forEach((stream) => { |
|
|
|
this.subscribe({ |
|
|
|
this.subscribe({ |
|
|
|
@ -894,14 +898,17 @@ export default { |
|
|
|
|
|
|
|
|
|
|
|
handleSomeoneJoined(userID) { |
|
|
|
handleSomeoneJoined(userID) { |
|
|
|
this.userList.push({ userID }); |
|
|
|
this.userList.push({ userID }); |
|
|
|
this.$message({ message: `${userID}进入房间`, type: "info" }); |
|
|
|
const userName = this.getUserName(userID); |
|
|
|
|
|
|
|
this.$message({ message: `${userName}进入房间`, type: "info" }); |
|
|
|
this.updateParticipantCount(); |
|
|
|
this.updateParticipantCount(); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
handleSomeoneLeft(userID) { |
|
|
|
handleSomeoneLeft(userID) { |
|
|
|
this.userList = this.userList.filter((o) => o.userID !== userID); |
|
|
|
this.userList = this.userList.filter((o) => o.userID !== userID); |
|
|
|
this.remoteStream = this.remoteStream.filter((o) => o.userID !== userID); |
|
|
|
this.remoteStream = this.remoteStream.filter((o) => o.userID !== userID); |
|
|
|
this.$message({ message: `${userID}离开房间`, type: "info" }); |
|
|
|
this.$delete(this.userNameMap, userID); |
|
|
|
|
|
|
|
const userName = this.getUserName(userID); |
|
|
|
|
|
|
|
this.$message({ message: `${userName}离开房间`, type: "info" }); |
|
|
|
this.updateParticipantCount(); |
|
|
|
this.updateParticipantCount(); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
@ -1185,7 +1192,6 @@ export default { |
|
|
|
// 如果当前已显示本地摄像头画面 → 取消显示 |
|
|
|
// 如果当前已显示本地摄像头画面 → 取消显示 |
|
|
|
if (this.isLocalCameraShared && this.sharedStreamId === 'local') { |
|
|
|
if (this.isLocalCameraShared && this.sharedStreamId === 'local') { |
|
|
|
this.cancelMainDisplay(); |
|
|
|
this.cancelMainDisplay(); |
|
|
|
this.$message.success('已取消采集卡窗口画面'); |
|
|
|
|
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -1199,7 +1205,6 @@ export default { |
|
|
|
const videoEl = this.$refs.shareVideo; |
|
|
|
const videoEl = this.$refs.shareVideo; |
|
|
|
if (!videoEl) return; |
|
|
|
if (!videoEl) return; |
|
|
|
videoEl.srcObject = this.localStream; |
|
|
|
videoEl.srcObject = this.localStream; |
|
|
|
this.$message.success('本地摄像头画面已切换到采集卡窗口'); |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
@ -1211,7 +1216,6 @@ export default { |
|
|
|
// 如果当前已显示该远端流 → 取消显示 |
|
|
|
// 如果当前已显示该远端流 → 取消显示 |
|
|
|
if (this.sharedStreamId === subscribeID) { |
|
|
|
if (this.sharedStreamId === subscribeID) { |
|
|
|
this.cancelMainDisplay(); |
|
|
|
this.cancelMainDisplay(); |
|
|
|
this.$message.success('已取消采集卡窗口画面'); |
|
|
|
|
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -1225,7 +1229,6 @@ export default { |
|
|
|
const mainContainer = this.$refs.mainRemoteContainer; |
|
|
|
const mainContainer = this.$refs.mainRemoteContainer; |
|
|
|
if (!mainContainer) return; |
|
|
|
if (!mainContainer) return; |
|
|
|
mainContainer.appendChild(stream.video); |
|
|
|
mainContainer.appendChild(stream.video); |
|
|
|
this.$message.success('远端画面已切换到采集卡窗口'); |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
@ -1342,50 +1345,61 @@ export default { |
|
|
|
|
|
|
|
|
|
|
|
// ==================== 【5】屏幕共享 ==================== |
|
|
|
// ==================== 【5】屏幕共享 ==================== |
|
|
|
async toggleShare() { |
|
|
|
async toggleShare() { |
|
|
|
if (this.isSharing) { |
|
|
|
// 进房后才能推流 |
|
|
|
this.stopShare(); |
|
|
|
if (!this.isJoined) { |
|
|
|
return; |
|
|
|
this.$message({ |
|
|
|
} |
|
|
|
message: "请进房成功后,再发布", |
|
|
|
try { |
|
|
|
type: "warning", |
|
|
|
this.isLocalCameraShared = false; // 开启屏幕共享时重置本地摄像头共享状态 |
|
|
|
|
|
|
|
this.sharedStreamId = null; |
|
|
|
|
|
|
|
const stream = await navigator.mediaDevices.getDisplayMedia({ |
|
|
|
|
|
|
|
video: true, |
|
|
|
|
|
|
|
audio: true, |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const track = stream.getVideoTracks()[0]; |
|
|
|
|
|
|
|
track.addEventListener('ended', () => { |
|
|
|
|
|
|
|
this.stopShare(); |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
return; |
|
|
|
this.shareStream = stream; |
|
|
|
|
|
|
|
// 不绑定到本地 <video>,避免"无限镜子"嵌套:捕获的画面包含自身时产生递归 |
|
|
|
|
|
|
|
// 流仅通过 SDK 发布给远端参会者 |
|
|
|
|
|
|
|
this.isSharing = true; |
|
|
|
|
|
|
|
this.$message.success('开始共享'); |
|
|
|
|
|
|
|
window.hirtcwebsdk?.publish(stream); |
|
|
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
|
|
this.isSharing = false; |
|
|
|
|
|
|
|
if (e.name !== 'AbortError') { |
|
|
|
|
|
|
|
this.$message.error('共享失败'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.initLocalStream(this.localScreenStream.type); |
|
|
|
|
|
|
|
// if (this.isSharing) { |
|
|
|
|
|
|
|
// this.stopShare(); |
|
|
|
|
|
|
|
// return; |
|
|
|
|
|
|
|
// } |
|
|
|
|
|
|
|
// try { |
|
|
|
|
|
|
|
// this.isLocalCameraShared = false; // 开启屏幕共享时重置本地摄像头共享状态 |
|
|
|
|
|
|
|
// this.sharedStreamId = null; |
|
|
|
|
|
|
|
// const stream = await navigator.mediaDevices.getDisplayMedia({ |
|
|
|
|
|
|
|
// video: true, |
|
|
|
|
|
|
|
// audio: true, |
|
|
|
|
|
|
|
// }); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// const track = stream.getVideoTracks()[0]; |
|
|
|
|
|
|
|
// track.addEventListener('ended', () => { |
|
|
|
|
|
|
|
// this.stopShare(); |
|
|
|
|
|
|
|
// }); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// this.shareStream = stream; |
|
|
|
|
|
|
|
// // 不绑定到本地 <video>,避免"无限镜子"嵌套:捕获的画面包含自身时产生递归 |
|
|
|
|
|
|
|
// // 流仅通过 SDK 发布给远端参会者 |
|
|
|
|
|
|
|
// this.isSharing = true; |
|
|
|
|
|
|
|
// this.$message.success('开始共享'); |
|
|
|
|
|
|
|
// window.hirtcwebsdk?.publish(stream); |
|
|
|
|
|
|
|
// } catch (e) { |
|
|
|
|
|
|
|
// this.isSharing = false; |
|
|
|
|
|
|
|
// if (e.name !== 'AbortError') { |
|
|
|
|
|
|
|
// this.$message.error('共享失败'); |
|
|
|
|
|
|
|
// } |
|
|
|
|
|
|
|
// } |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 停止共享 |
|
|
|
// 停止共享 |
|
|
|
stopShare() { |
|
|
|
stopShare() { |
|
|
|
this.isLocalCameraShared = false; |
|
|
|
window.hirtcwebsdk.unpublish(this.localScreenStream.type); |
|
|
|
this.sharedStreamId = null; |
|
|
|
this.localScreenStream.publishID = ""; |
|
|
|
if (this.shareStream) { |
|
|
|
// this.isLocalCameraShared = false; |
|
|
|
this.shareStream.getTracks().forEach(t => { |
|
|
|
// this.sharedStreamId = null; |
|
|
|
t.stop(); |
|
|
|
// if (this.shareStream) { |
|
|
|
}); |
|
|
|
// this.shareStream.getTracks().forEach(t => { |
|
|
|
} |
|
|
|
// t.stop(); |
|
|
|
this.shareStream = null; |
|
|
|
// }); |
|
|
|
this.isSharing = false; |
|
|
|
// } |
|
|
|
this.$message.info('已停止共享'); |
|
|
|
// this.shareStream = null; |
|
|
|
window.hirtcwebsdk?.unpublish(); |
|
|
|
// this.isSharing = false; |
|
|
|
|
|
|
|
// this.$message.info('已停止共享'); |
|
|
|
|
|
|
|
// window.hirtcwebsdk?.unpublish(); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// ==================== 【6】画笔标注 ==================== |
|
|
|
// ==================== 【6】画笔标注 ==================== |
|
|
|
@ -1848,6 +1862,11 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 通过 userID 获取用户显示名称 |
|
|
|
|
|
|
|
getUserName(userID) { |
|
|
|
|
|
|
|
return this.userNameMap[userID] || userID; |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// ==================== 【14】窗口与会议控制 ==================== |
|
|
|
// ==================== 【14】窗口与会议控制 ==================== |
|
|
|
updateClock() { |
|
|
|
updateClock() { |
|
|
|
this.currentTime = new Date().toLocaleTimeString('zh-CN', { |
|
|
|
this.currentTime = new Date().toLocaleTimeString('zh-CN', { |
|
|
|
@ -2292,14 +2311,17 @@ export default { |
|
|
|
.remote-stream-list { |
|
|
|
.remote-stream-list { |
|
|
|
flex: 1; |
|
|
|
flex: 1; |
|
|
|
overflow-y: auto; |
|
|
|
overflow-y: auto; |
|
|
|
padding: 4px 6px; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.remote-box { |
|
|
|
.remote-box { |
|
|
|
margin-bottom: 4px; |
|
|
|
margin-bottom: 4px; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.remote-video-area { |
|
|
|
.remote-video-area { |
|
|
|
height: 140px; |
|
|
|
width: 100%; |
|
|
|
|
|
|
|
height: 210px; |
|
|
|
|
|
|
|
background-color: #00584d; |
|
|
|
|
|
|
|
border: 1px solid #00796b; |
|
|
|
|
|
|
|
position: relative; |
|
|
|
|
|
|
|
|
|
|
|
.video { |
|
|
|
.video { |
|
|
|
width: 100%; |
|
|
|
width: 100%; |
|
|
|
|