mdm-client 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/App.vue +72 -67
- package/src/assets/image/common/layout-active16.png +0 -0
- package/src/assets/image/common/layout-active25.png +0 -0
- package/src/assets/image/common/layout-active3.png +0 -0
- package/src/assets/image/common/layout-active9.png +0 -0
- package/src/assets/image/common/layout16.png +0 -0
- package/src/assets/image/common/layout25.png +0 -0
- package/src/assets/image/common/layout3.png +0 -0
- package/src/assets/image/common/layout9.png +0 -0
- package/src/assets/image/common/mirror.png +0 -0
- package/src/assets/image/common/rotate_icon1.png +0 -0
- package/src/assets/image/common/rotate_icon2.png +0 -0
- package/src/assets/image/common/rotate_icon3.png +0 -0
- package/src/assets/image/common/rotate_icon4.png +0 -0
- package/src/assets/style/base.scss +5 -0
- package/src/components/LiveMulti/LiveMulti.vue +27 -6
- package/src/components/LiveMultipleMeeting/LiveMultipleMeeting.vue +1163 -99
- package/src/components/LiveMultipleMeeting/style/index.scss +145 -14
- package/src/components/LivePoint/LivePoint.vue +71 -208
- package/src/components/LivePointMeeting/LivePointMeeting.vue +223 -13
- package/src/components/LivePointMeeting/style/index.scss +35 -0
- package/src/components/MeetingReadyDialog/MeetingReadyDialog.vue +96 -14
- package/src/components/MiniumVideoDialog/MiniumVideoDialog.vue +185 -50
- package/src/components/other/addressBook.vue +137 -20
- package/src/components/other/appointDialog.vue +1 -1
- package/src/components/other/customLayout.vue +368 -202
- package/src/components/other/layoutSwitch.vue +253 -37
- package/src/components/other/leadershipFocus.vue +422 -0
- package/src/components/other/leaveOptionDialog.vue +1 -1
- package/src/components/other/moreOptionDialog.vue +17 -1
- package/src/components/other/screenShareBoard.vue +2 -2
- package/src/components/other/selectDialog.vue +1 -1
- package/src/components/other/selectSpecialDialog.vue +1 -1
- package/src/utils/api.js +19 -0
- package/src/utils/livekit/live-client-esm.js +1 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div ref="
|
|
2
|
+
<div ref="roomElm" class="point-meeting" id="point-meeting" :style="{ resize: 'both' }">
|
|
3
3
|
<div class="point-meeting-top" v-drag="'point-meeting'">
|
|
4
4
|
<div class="top-group">
|
|
5
5
|
<div class="duration-icon"></div>
|
|
6
6
|
<span>{{ meetingDuration }}</span>
|
|
7
7
|
</div>
|
|
8
8
|
<div class="top-group">
|
|
9
|
+
<div class="minimize-btn" @click="minumDialog"></div>
|
|
9
10
|
<div class="full-screen-btn" @click="switchComponentFullScreen"></div>
|
|
11
|
+
<div class="close-btn" @click="confirmClose"></div>
|
|
10
12
|
</div>
|
|
11
13
|
</div>
|
|
12
14
|
<div class="point-meeting-wrapper">
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
|
|
79
81
|
<script>
|
|
80
82
|
import { calculateTime, ShowMessage } from "../../utils/index.js";
|
|
83
|
+
import { MessageBox } from "element-ui";
|
|
81
84
|
|
|
82
85
|
export default {
|
|
83
86
|
name: "LivePointMeeting",
|
|
@@ -142,7 +145,8 @@ export default {
|
|
|
142
145
|
isDurationCalc: true,
|
|
143
146
|
isFootShow: false,
|
|
144
147
|
footVisibleDuration: 3,
|
|
145
|
-
isRoomConnectedHandled: false
|
|
148
|
+
isRoomConnectedHandled: false,
|
|
149
|
+
rotateDegreeMap: new Map(), // 存储与会者identity对应的旋转角度
|
|
146
150
|
}
|
|
147
151
|
},
|
|
148
152
|
computed: {
|
|
@@ -164,6 +168,47 @@ export default {
|
|
|
164
168
|
}
|
|
165
169
|
},
|
|
166
170
|
methods: {
|
|
171
|
+
// 根据 identity 和度数应用旋转到视频元素,并切换旋转按钮图标
|
|
172
|
+
applyRotation(identity, degree) {
|
|
173
|
+
try {
|
|
174
|
+
const norm = ((degree % 360) + 360) % 360; // 归一化到0-359
|
|
175
|
+
const videoElm = document.getElementById(`video-${identity}`);
|
|
176
|
+
if (videoElm) {
|
|
177
|
+
videoElm.style.transformOrigin = 'center center';
|
|
178
|
+
if (norm === 90 || norm === 270) {
|
|
179
|
+
const parent = videoElm.parentElement;
|
|
180
|
+
if (parent) {
|
|
181
|
+
const rect = parent.getBoundingClientRect();
|
|
182
|
+
videoElm.style.top = '50%';
|
|
183
|
+
videoElm.style.left = '50%';
|
|
184
|
+
videoElm.style.width = `${rect.height}px`;
|
|
185
|
+
videoElm.style.height = `${rect.width}px`;
|
|
186
|
+
videoElm.style.transform = `translate(-50%, -50%) rotate(${norm}deg)`;
|
|
187
|
+
videoElm.style.objectFit = 'cover';
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
videoElm.style.top = '0';
|
|
191
|
+
videoElm.style.left = '0';
|
|
192
|
+
videoElm.style.width = '100%';
|
|
193
|
+
videoElm.style.height = '100%';
|
|
194
|
+
videoElm.style.transform = `rotate(${norm}deg)`;
|
|
195
|
+
videoElm.style.objectFit = 'contain';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const rotateElm = document.getElementById(`rotate-${identity}`);
|
|
200
|
+
if (rotateElm) {
|
|
201
|
+
let clsIndex = 1;
|
|
202
|
+
if (norm === 0) clsIndex = 1;
|
|
203
|
+
else if (norm === 90) clsIndex = 2;
|
|
204
|
+
else if (norm === 180) clsIndex = 3;
|
|
205
|
+
else if (norm === 270) clsIndex = 4;
|
|
206
|
+
rotateElm.className = `rotate-icon rotate-icon${clsIndex}`;
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.error('applyRotation error', err);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
167
212
|
async createRoom() {
|
|
168
213
|
if (!this.liveClient) {
|
|
169
214
|
return;
|
|
@@ -549,13 +594,63 @@ export default {
|
|
|
549
594
|
closeMeetingDialog() {
|
|
550
595
|
this.$emit("meetingDialogClose");
|
|
551
596
|
},
|
|
597
|
+
// 最小化窗口: 复用 multi 会议的最小化逻辑,向父组件抛出 pointMeetingMinum 事件
|
|
598
|
+
minumDialog() {
|
|
599
|
+
if (!this.isInMeeting) {
|
|
600
|
+
this.showMessage.message('error', '请先进入会议')
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
if (!this.localIdentity) {
|
|
604
|
+
this.showMessage.message('error', '获取本地与会者信息失败')
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
// 查找本地与会者渲染数据(视频或音频)
|
|
608
|
+
let localVideoTrack = null
|
|
609
|
+
if (this.liveClient?.roomMode === 'auto') {
|
|
610
|
+
const idx = this.participants.findIndex(item => item.identity === this.localIdentity)
|
|
611
|
+
if (idx > -1) {
|
|
612
|
+
localVideoTrack = this.participants[idx]?.videoTrack || null
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// 通知父组件打开最小化窗口
|
|
616
|
+
this.$emit('pointMeetingMinum', {
|
|
617
|
+
identity: this.localIdentity,
|
|
618
|
+
name: this.localName,
|
|
619
|
+
videoTrack: this.liveClient?.roomMode === 'auto' ? localVideoTrack : null,
|
|
620
|
+
})
|
|
621
|
+
// 隐藏当前窗口(保持状态以便恢复)
|
|
622
|
+
this.$refs.roomElm && (this.$refs.roomElm.style.visibility = 'hidden')
|
|
623
|
+
},
|
|
624
|
+
// 还原窗口(供父组件调用)
|
|
625
|
+
resetDialog() {
|
|
626
|
+
this.$refs.roomElm && (this.$refs.roomElm.style.visibility = 'visible')
|
|
627
|
+
},
|
|
552
628
|
async leaveRoom() {
|
|
629
|
+
if (!this.isInMeeting) {
|
|
630
|
+
this.closeMeetingDialog();
|
|
631
|
+
return
|
|
632
|
+
}
|
|
553
633
|
if (this.liveClient) {
|
|
554
634
|
await this.liveClient.leaveRoom();
|
|
555
635
|
}
|
|
556
636
|
// 重置房间连接处理标志
|
|
557
637
|
this.isRoomConnectedHandled = false;
|
|
558
638
|
},
|
|
639
|
+
confirmClose() {
|
|
640
|
+
if(!this.isInMeeting) {
|
|
641
|
+
this.closeMeetingDialog();
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
MessageBox.confirm('点击确定后会议将被解散,您确定要这么做么?', '警告', {
|
|
645
|
+
confirmButtonText: '确定',
|
|
646
|
+
cancelButtonText: '取消',
|
|
647
|
+
type: 'warning',
|
|
648
|
+
}).then(() => {
|
|
649
|
+
this.leaveRoom();
|
|
650
|
+
}).catch(() => {
|
|
651
|
+
// 取消操作
|
|
652
|
+
});
|
|
653
|
+
},
|
|
559
654
|
// 切换组件全屏状态
|
|
560
655
|
async switchComponentFullScreen(e) {
|
|
561
656
|
if (
|
|
@@ -719,6 +814,8 @@ export default {
|
|
|
719
814
|
// 与会者状态变更
|
|
720
815
|
// 视频元素
|
|
721
816
|
let videoElm = document.getElementById(`video-${videoItem.identity}`);
|
|
817
|
+
// 旋转按钮元素
|
|
818
|
+
let rotateElm = document.getElementById(`rotate-${videoItem.identity}`);
|
|
722
819
|
// 当与会者断开会议链接即remove为true
|
|
723
820
|
if (videoItem.remove) {
|
|
724
821
|
if (videoElm) {
|
|
@@ -731,6 +828,15 @@ export default {
|
|
|
731
828
|
}
|
|
732
829
|
this.removeFromParticipantList(videoItem);
|
|
733
830
|
|
|
831
|
+
// 清理旋转按钮与状态
|
|
832
|
+
if (rotateElm) {
|
|
833
|
+
try { videoDiv && videoDiv.removeChild(rotateElm); } catch (e) {}
|
|
834
|
+
rotateElm = null;
|
|
835
|
+
}
|
|
836
|
+
if (this.rotateDegreeMap.has(videoItem.identity)) {
|
|
837
|
+
this.rotateDegreeMap.delete(videoItem.identity);
|
|
838
|
+
}
|
|
839
|
+
|
|
734
840
|
// 音视频轨道与video元素解绑
|
|
735
841
|
if (videoItem.videoTrack) {
|
|
736
842
|
videoItem.videoTrack.detach();
|
|
@@ -760,6 +866,22 @@ export default {
|
|
|
760
866
|
let boardElm = document.getElementById(`board-${videoItem.identity}`);
|
|
761
867
|
// 底部麦克风图标元素
|
|
762
868
|
let microElm = document.getElementById(`microphone-${videoItem.identity}`);
|
|
869
|
+
// 根据platformID设定元素样式(与多方会议保持一致)
|
|
870
|
+
if (videoElm) {
|
|
871
|
+
if (
|
|
872
|
+
videoItem.metadata?.platformID == 1 ||
|
|
873
|
+
videoItem.metadata?.platformID == 4 ||
|
|
874
|
+
videoItem.metadata?.platformID == 7
|
|
875
|
+
) {
|
|
876
|
+
videoElm.style.objectFit = 'contain';
|
|
877
|
+
} else {
|
|
878
|
+
if (videoItem?.source == 'camera') {
|
|
879
|
+
videoElm.style.objectFit = 'cover';
|
|
880
|
+
} else {
|
|
881
|
+
videoElm.style.objectFit = 'contain';
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
763
885
|
// 声明麦克风按钮点击事件回调
|
|
764
886
|
const unableMicrophone = () => {
|
|
765
887
|
this.liveClient.changeParticipantMicrophoneStatus(videoItem.identity, true);
|
|
@@ -776,17 +898,43 @@ export default {
|
|
|
776
898
|
let layoutNormalElm = document.querySelector("#point-meeting-contain .layout-normal");
|
|
777
899
|
let localChild = layoutBlurElm.firstElementChild;
|
|
778
900
|
let currentEle = document.getElementById(`participant-${videoItem.identity}`);
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
901
|
+
// 核心逻辑:点击任意非焦点或焦点元素,切换位置
|
|
902
|
+
// 期望行为:layoutBlurElm 始终只保留一个“缩略/小画面”元素(本地),点击其他元素切换焦点并将之前焦点移到小画面
|
|
903
|
+
if (!layoutBlurElm || !layoutNormalElm || !currentEle) return;
|
|
904
|
+
|
|
905
|
+
const isCurrentInBlur = currentEle.parentElement === layoutBlurElm;
|
|
906
|
+
const isCurrentInNormal = currentEle.parentElement === layoutNormalElm;
|
|
907
|
+
const blurFirst = layoutBlurElm.firstElementChild;
|
|
908
|
+
const normalFirst = layoutNormalElm.firstElementChild;
|
|
909
|
+
|
|
910
|
+
// 如果当前元素已经在小画面区域(layoutBlurElm),则尝试与 normalFirst 交换(如果存在)
|
|
911
|
+
if (isCurrentInBlur) {
|
|
912
|
+
if (normalFirst) {
|
|
913
|
+
// 将 normalFirst 移到小画面
|
|
914
|
+
layoutBlurElm.appendChild(normalFirst);
|
|
915
|
+
// 将当前小画面移到大画面顶部
|
|
916
|
+
layoutNormalElm.insertBefore(currentEle, layoutNormalElm.firstChild ?? null);
|
|
782
917
|
} else {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
layoutBlurElm.appendChild(currentEle);
|
|
786
|
-
}
|
|
918
|
+
// 没有大画面,直接保持当前不动
|
|
919
|
+
return;
|
|
787
920
|
}
|
|
788
|
-
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// 当前在大画面区域
|
|
925
|
+
if (isCurrentInNormal) {
|
|
926
|
+
// 如果小画面里有元素,则和它交换
|
|
927
|
+
if (blurFirst) {
|
|
928
|
+
layoutNormalElm.insertBefore(blurFirst, layoutNormalElm.firstChild ?? null);
|
|
929
|
+
}
|
|
930
|
+
// 将点击的大画面元素移入小画面
|
|
789
931
|
layoutBlurElm.appendChild(currentEle);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// 兜底:如果元素不在两者内(理论上不发生),直接放入大画面
|
|
936
|
+
if (!isCurrentInBlur && !isCurrentInNormal) {
|
|
937
|
+
layoutNormalElm.insertBefore(currentEle, layoutNormalElm.firstChild ?? null);
|
|
790
938
|
}
|
|
791
939
|
};
|
|
792
940
|
// 为与会者元素绑定事件
|
|
@@ -810,6 +958,55 @@ export default {
|
|
|
810
958
|
`;
|
|
811
959
|
videoDiv.appendChild(describeElm);
|
|
812
960
|
}
|
|
961
|
+
// 仅在视频通话(roomMode=auto => 此组件中对应 meetingType==='video')、且为摄像头画面、且 platformID 为 4 时显示旋转按钮
|
|
962
|
+
if (
|
|
963
|
+
videoItem.isCameraEnabled &&
|
|
964
|
+
!videoItem.isScreenShareEnabled &&
|
|
965
|
+
videoItem?.metadata?.platformID === 4
|
|
966
|
+
) {
|
|
967
|
+
if (!rotateElm) {
|
|
968
|
+
rotateElm = document.createElement('div');
|
|
969
|
+
rotateElm.id = `rotate-${videoItem.identity}`;
|
|
970
|
+
rotateElm.className = 'rotate-icon rotate-icon1';
|
|
971
|
+
videoDiv && videoDiv.appendChild(rotateElm);
|
|
972
|
+
}
|
|
973
|
+
// 绑定(或重绑)事件处理,阻止冒泡,避免触发父容器的setBlurVideoLeft
|
|
974
|
+
const handleRotateClick = (event) => {
|
|
975
|
+
event?.stopPropagation?.();
|
|
976
|
+
event?.preventDefault?.();
|
|
977
|
+
const current = this.rotateDegreeMap.get(videoItem.identity) || 0;
|
|
978
|
+
const next = (current + 90) % 360;
|
|
979
|
+
this.rotateDegreeMap.set(videoItem.identity, next);
|
|
980
|
+
this.applyRotation(videoItem.identity, next);
|
|
981
|
+
};
|
|
982
|
+
rotateElm.onclick = handleRotateClick;
|
|
983
|
+
// 进一步保险:阻止按下就冒泡(处理某些浏览器差异与长按)
|
|
984
|
+
rotateElm.onmousedown = (e) => { e?.stopPropagation?.(); };
|
|
985
|
+
rotateElm.ontouchstart = (e) => { e?.stopPropagation?.(); };
|
|
986
|
+
// 初始化角度并应用
|
|
987
|
+
if (!this.rotateDegreeMap.has(videoItem.identity)) {
|
|
988
|
+
this.rotateDegreeMap.set(videoItem.identity, 0);
|
|
989
|
+
}
|
|
990
|
+
this.applyRotation(videoItem.identity, this.rotateDegreeMap.get(videoItem.identity));
|
|
991
|
+
} else {
|
|
992
|
+
// 不满足条件则移除旋转按钮并清除状态
|
|
993
|
+
if (rotateElm) {
|
|
994
|
+
try { videoDiv && videoDiv.removeChild(rotateElm); } catch (e) {}
|
|
995
|
+
rotateElm = null;
|
|
996
|
+
}
|
|
997
|
+
if (this.rotateDegreeMap.has(videoItem.identity)) {
|
|
998
|
+
this.rotateDegreeMap.delete(videoItem.identity);
|
|
999
|
+
}
|
|
1000
|
+
// 同时复原视频元素的旋转
|
|
1001
|
+
if (videoElm) {
|
|
1002
|
+
videoElm.style.transform = '';
|
|
1003
|
+
videoElm.style.transformOrigin = '';
|
|
1004
|
+
videoElm.style.top = '';
|
|
1005
|
+
videoElm.style.left = '';
|
|
1006
|
+
videoElm.style.width = '';
|
|
1007
|
+
videoElm.style.height = '';
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
813
1010
|
} else {
|
|
814
1011
|
if (describeElm) {
|
|
815
1012
|
videoDiv.removeChild(describeElm);
|
|
@@ -851,6 +1048,10 @@ export default {
|
|
|
851
1048
|
}
|
|
852
1049
|
}
|
|
853
1050
|
this.addToParticipantList(videoItem);
|
|
1051
|
+
// 每次渲染时,如存在旋转角度记录,则应用旋转(避免重新渲染后丢失旋转效果)
|
|
1052
|
+
if (this.rotateDegreeMap.has(videoItem.identity)) {
|
|
1053
|
+
this.applyRotation(videoItem.identity, this.rotateDegreeMap.get(videoItem.identity));
|
|
1054
|
+
}
|
|
854
1055
|
},
|
|
855
1056
|
renderAudioItem(audioItem) {
|
|
856
1057
|
console.log("语音通话渲染" + audioItem.identity + ":", audioItem);
|
|
@@ -887,7 +1088,8 @@ export default {
|
|
|
887
1088
|
}
|
|
888
1089
|
this.addToParticipantList(audioItem);
|
|
889
1090
|
}
|
|
890
|
-
|
|
1091
|
+
// 修复ID选择器错误:应为 audio-${identity}
|
|
1092
|
+
let audioElm = document.getElementById(`audio-${audioItem.identity}`);
|
|
891
1093
|
if (audioItem.remove) {
|
|
892
1094
|
if (audioElm) {
|
|
893
1095
|
audioElm.srcObject = null;
|
|
@@ -1002,6 +1204,13 @@ export default {
|
|
|
1002
1204
|
display: flex;
|
|
1003
1205
|
align-items: center;
|
|
1004
1206
|
|
|
1207
|
+
.minimize-btn {
|
|
1208
|
+
cursor: pointer;
|
|
1209
|
+
width: 16px;
|
|
1210
|
+
height: 16px;
|
|
1211
|
+
margin-right: 12px;
|
|
1212
|
+
background: url('../../assets/image/screenBlue/meeting_slide_small_icon.png') no-repeat center / 100% 100%;
|
|
1213
|
+
}
|
|
1005
1214
|
.duration-icon {
|
|
1006
1215
|
width: 15px;
|
|
1007
1216
|
height: 15px;
|
|
@@ -1022,8 +1231,9 @@ export default {
|
|
|
1022
1231
|
|
|
1023
1232
|
.close-btn {
|
|
1024
1233
|
cursor: pointer;
|
|
1025
|
-
width:
|
|
1026
|
-
height:
|
|
1234
|
+
width: 16px;
|
|
1235
|
+
height: 16px;
|
|
1236
|
+
margin-left: 12px;
|
|
1027
1237
|
background: var(--close-icon) no-repeat center / 100% 100%;
|
|
1028
1238
|
}
|
|
1029
1239
|
}
|
|
@@ -110,6 +110,8 @@
|
|
|
110
110
|
height: 100%;
|
|
111
111
|
position: relative;
|
|
112
112
|
border-radius: 6px;
|
|
113
|
+
background: var(--dialog-bg);
|
|
114
|
+
overflow: hidden; // 旋转后裁剪溢出内容,避免页面出现“怪异”形态
|
|
113
115
|
.p-video {
|
|
114
116
|
position: absolute;
|
|
115
117
|
width: 100%;
|
|
@@ -119,6 +121,39 @@
|
|
|
119
121
|
z-index: 10;
|
|
120
122
|
object-fit: contain;
|
|
121
123
|
border-radius: 6px;
|
|
124
|
+
backface-visibility: hidden; // 避免3D旋转引发的锯齿闪烁
|
|
125
|
+
will-change: transform; // 提示浏览器优化旋转
|
|
126
|
+
}
|
|
127
|
+
// 参会者视频旋转按钮(与 LiveMultipleMeeting 保持一致)
|
|
128
|
+
.rotate-icon {
|
|
129
|
+
visibility: hidden; // 默认隐藏,悬浮显示
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
position: absolute;
|
|
132
|
+
top: 10px;
|
|
133
|
+
left: 10px;
|
|
134
|
+
z-index: 50;
|
|
135
|
+
aspect-ratio: 1/1;
|
|
136
|
+
width: auto;
|
|
137
|
+
height: 3%;
|
|
138
|
+
min-height: 30px;
|
|
139
|
+
&1 {
|
|
140
|
+
background: var(--rotate-icon1) no-repeat center / 100% 100%;
|
|
141
|
+
}
|
|
142
|
+
&2 {
|
|
143
|
+
background: var(--rotate-icon2) no-repeat center / 100% 100%;
|
|
144
|
+
}
|
|
145
|
+
&3 {
|
|
146
|
+
background: var(--rotate-icon3) no-repeat center / 100% 100%;
|
|
147
|
+
}
|
|
148
|
+
&4 {
|
|
149
|
+
background: var(--rotate-icon4) no-repeat center / 100% 100%;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 悬浮显示旋转按钮
|
|
153
|
+
&:hover {
|
|
154
|
+
.rotate-icon {
|
|
155
|
+
visibility: visible;
|
|
156
|
+
}
|
|
122
157
|
}
|
|
123
158
|
.describe {
|
|
124
159
|
position: absolute;
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
<div class="video-contain">
|
|
20
20
|
<div class="avatar"></div>
|
|
21
21
|
<video id="video-ele" class="video-ele" muted autoplay v-show="cameraStatus"></video>
|
|
22
|
+
<div class="mirror-switch" @click="setMirror">
|
|
23
|
+
<img class="mirror-switch-icon" src="../../assets/image/common/mirror.png" />
|
|
24
|
+
<span>镜像</span>
|
|
25
|
+
</div>
|
|
22
26
|
</div>
|
|
23
27
|
<div class="tool-bar">
|
|
24
28
|
<div class="tool-bar-group">
|
|
@@ -124,6 +128,7 @@ export default {
|
|
|
124
128
|
videoSelectShow: false,
|
|
125
129
|
outputSelectShow: false,
|
|
126
130
|
allTracks: [],
|
|
131
|
+
isMirror: false,
|
|
127
132
|
audioDevices: [],
|
|
128
133
|
outputDevices: [],
|
|
129
134
|
videoDevices: [],
|
|
@@ -150,16 +155,16 @@ export default {
|
|
|
150
155
|
if (newVal) {
|
|
151
156
|
constrict = {
|
|
152
157
|
audio: this.activeDevice?.audioInputDevice
|
|
153
|
-
? { deviceId: this.activeDevice.audioInputDevice }
|
|
158
|
+
? { deviceId: { exact: this.activeDevice.audioInputDevice } }
|
|
154
159
|
: true,
|
|
155
160
|
video: this.activeDevice?.videoDevice
|
|
156
|
-
? { deviceId: this.activeDevice.videoDevice }
|
|
161
|
+
? { deviceId: { exact: this.activeDevice.videoDevice } }
|
|
157
162
|
: { facingMode: this.curFacingMode },
|
|
158
163
|
};
|
|
159
164
|
} else {
|
|
160
165
|
constrict = {
|
|
161
166
|
audio: this.activeDevice?.audioInputDevice
|
|
162
|
-
? { deviceId: this.activeDevice.audioInputDevice }
|
|
167
|
+
? { deviceId: { exact: this.activeDevice.audioInputDevice } }
|
|
163
168
|
: true,
|
|
164
169
|
video: false,
|
|
165
170
|
};
|
|
@@ -167,6 +172,33 @@ export default {
|
|
|
167
172
|
await this.getDefaultDeviceStream(constrict);
|
|
168
173
|
},
|
|
169
174
|
},
|
|
175
|
+
microphoneStatus: {
|
|
176
|
+
handler: async function (newVal) {
|
|
177
|
+
let constrict = null;
|
|
178
|
+
if (newVal) {
|
|
179
|
+
constrict = {
|
|
180
|
+
audio: this.activeDevice?.audioInputDevice
|
|
181
|
+
? { deviceId: { exact: this.activeDevice.audioInputDevice } }
|
|
182
|
+
: true,
|
|
183
|
+
video: this.cameraStatus
|
|
184
|
+
? this.activeDevice?.videoDevice
|
|
185
|
+
? { deviceId: { exact: this.activeDevice.videoDevice } }
|
|
186
|
+
: { facingMode: this.curFacingMode }
|
|
187
|
+
: false,
|
|
188
|
+
};
|
|
189
|
+
} else {
|
|
190
|
+
constrict = {
|
|
191
|
+
audio: false,
|
|
192
|
+
video: this.cameraStatus
|
|
193
|
+
? this.activeDevice?.videoDevice
|
|
194
|
+
? { deviceId: { exact: this.activeDevice.videoDevice } }
|
|
195
|
+
: { facingMode: this.curFacingMode }
|
|
196
|
+
: false,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
await this.getDefaultDeviceStream(constrict);
|
|
200
|
+
},
|
|
201
|
+
},
|
|
170
202
|
},
|
|
171
203
|
mounted() {
|
|
172
204
|
this.init();
|
|
@@ -198,6 +230,7 @@ export default {
|
|
|
198
230
|
this.showMessageInstance.message("error", "会议名称不能超过20个字符");
|
|
199
231
|
return;
|
|
200
232
|
}
|
|
233
|
+
this.streamPause();
|
|
201
234
|
this.$emit("launchMeeting", {
|
|
202
235
|
roomName: this.roomName,
|
|
203
236
|
cameraStatus: this.cameraStatus,
|
|
@@ -206,6 +239,7 @@ export default {
|
|
|
206
239
|
audioInputDevice: this.activeDevice.audioInputDevice,
|
|
207
240
|
audioOutputDevice: this.activeDevice.audioOutputDevice,
|
|
208
241
|
videoDevice: this.activeDevice.videoDevice,
|
|
242
|
+
isMirror: this.isMirror,
|
|
209
243
|
});
|
|
210
244
|
this.$emit("close");
|
|
211
245
|
},
|
|
@@ -215,6 +249,7 @@ export default {
|
|
|
215
249
|
this.showMessageInstance.message("error", "会议ID不能为空");
|
|
216
250
|
return;
|
|
217
251
|
}
|
|
252
|
+
this.streamPause();
|
|
218
253
|
this.$emit("joinMeeting", {
|
|
219
254
|
roomNum: this.roomNum,
|
|
220
255
|
cameraStatus: this.cameraStatus,
|
|
@@ -223,10 +258,23 @@ export default {
|
|
|
223
258
|
audioInputDevice: this.activeDevice.audioInputDevice,
|
|
224
259
|
audioOutputDevice: this.activeDevice.audioOutputDevice,
|
|
225
260
|
videoDevice: this.activeDevice.videoDevice,
|
|
261
|
+
isMirror: this.isMirror,
|
|
226
262
|
});
|
|
227
263
|
this.$emit("close");
|
|
228
264
|
},
|
|
229
265
|
|
|
266
|
+
setMirror() {
|
|
267
|
+
this.isMirror = !this.isMirror;
|
|
268
|
+
this.$nextTick(() => {
|
|
269
|
+
let videoDom = document.getElementById("video-ele");
|
|
270
|
+
if (videoDom) {
|
|
271
|
+
this.isMirror
|
|
272
|
+
? (videoDom.style.transform = "rotateY(180deg)")
|
|
273
|
+
: (videoDom.style.transform = "rotateY(0deg)");
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
|
|
230
278
|
async getDeviceList() {
|
|
231
279
|
try {
|
|
232
280
|
const { audioDevices, videoDevices, outputDevices } = await LiveClient.getDeviceList();
|
|
@@ -312,11 +360,16 @@ export default {
|
|
|
312
360
|
}
|
|
313
361
|
},
|
|
314
362
|
|
|
363
|
+
sleep(ms) {
|
|
364
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
365
|
+
},
|
|
366
|
+
|
|
315
367
|
async getDefaultDeviceStream(constrict) {
|
|
316
368
|
try {
|
|
369
|
+
this.clearAllTrack();
|
|
370
|
+
await this.sleep(500);
|
|
317
371
|
const stream = await navigator.mediaDevices.getUserMedia(constrict);
|
|
318
372
|
let videoEle = document.querySelector("#video-ele");
|
|
319
|
-
this.clearAllTrack();
|
|
320
373
|
if (videoEle) {
|
|
321
374
|
this.allTracks = stream.getTracks();
|
|
322
375
|
this.getActiveDeviceId(stream);
|
|
@@ -325,8 +378,10 @@ export default {
|
|
|
325
378
|
videoEle.play();
|
|
326
379
|
};
|
|
327
380
|
}
|
|
381
|
+
return stream;
|
|
328
382
|
} catch (err) {
|
|
329
|
-
|
|
383
|
+
console.error("Failed to get media stream:", err);
|
|
384
|
+
this.showMessageInstance.message("error", "获取设备媒体流失败,请检查浏览器权限状态或者设备是否可用");
|
|
330
385
|
}
|
|
331
386
|
},
|
|
332
387
|
|
|
@@ -343,12 +398,16 @@ export default {
|
|
|
343
398
|
async initVideoStream() {
|
|
344
399
|
this.curFacingMode = "user";
|
|
345
400
|
const constrict = {
|
|
346
|
-
audio: this.microphoneStatus
|
|
401
|
+
audio: this.microphoneStatus
|
|
402
|
+
? this.activeDevice?.audioInputDevice
|
|
403
|
+
? { deviceId: { exact: this.activeDevice.audioInputDevice } }
|
|
404
|
+
: true
|
|
405
|
+
: false,
|
|
347
406
|
video: this.cameraStatus
|
|
348
|
-
?
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
:
|
|
407
|
+
? this.activeDevice?.videoDevice
|
|
408
|
+
? { deviceId: { exact: this.activeDevice.videoDevice } }
|
|
409
|
+
: { facingMode: this.curFacingMode }
|
|
410
|
+
: false,
|
|
352
411
|
};
|
|
353
412
|
await this.getDefaultDeviceStream(constrict);
|
|
354
413
|
},
|
|
@@ -356,10 +415,10 @@ export default {
|
|
|
356
415
|
async handleAudioInputDeviceChoice(e) {
|
|
357
416
|
if (e && e !== this.activeDevice?.audioInputDevice) {
|
|
358
417
|
const constrict = {
|
|
359
|
-
audio: this.microphoneStatus ? { deviceId: e } : false,
|
|
418
|
+
audio: this.microphoneStatus ? { deviceId: { exact: e } } : false,
|
|
360
419
|
video: this.cameraStatus
|
|
361
420
|
? this.activeDevice?.videoDevice
|
|
362
|
-
? { deviceId: this.activeDevice.videoDevice }
|
|
421
|
+
? { deviceId: { exact: this.activeDevice.videoDevice } }
|
|
363
422
|
: { facingMode: this.curFacingMode }
|
|
364
423
|
: this.cameraStatus,
|
|
365
424
|
};
|
|
@@ -378,10 +437,10 @@ export default {
|
|
|
378
437
|
const constrict = {
|
|
379
438
|
audio: this.microphoneStatus
|
|
380
439
|
? this.activeDevice?.audioInputDevice
|
|
381
|
-
? { deviceId: this.activeDevice.audioInputDevice }
|
|
440
|
+
? { deviceId: { exact: this.activeDevice.audioInputDevice } }
|
|
382
441
|
: this.microphoneStatus
|
|
383
442
|
: this.microphoneStatus,
|
|
384
|
-
video: this.cameraStatus ? { deviceId: e } : this.cameraStatus,
|
|
443
|
+
video: this.cameraStatus ? { deviceId: { exact: e } } : this.cameraStatus,
|
|
385
444
|
};
|
|
386
445
|
await this.getDefaultDeviceStream(constrict);
|
|
387
446
|
}
|
|
@@ -493,6 +552,29 @@ export default {
|
|
|
493
552
|
top: 0;
|
|
494
553
|
object-fit: cover;
|
|
495
554
|
}
|
|
555
|
+
|
|
556
|
+
.mirror-switch {
|
|
557
|
+
cursor: pointer;
|
|
558
|
+
position: absolute;
|
|
559
|
+
top: 23px;
|
|
560
|
+
right: 17px;
|
|
561
|
+
background: rgba(28, 36, 47, 0.6);
|
|
562
|
+
border-radius: 8px 8px 8px 8px;
|
|
563
|
+
display: flex;
|
|
564
|
+
align-items: center;
|
|
565
|
+
height: 40px;
|
|
566
|
+
padding: 0 10px;
|
|
567
|
+
font-weight: 400;
|
|
568
|
+
font-size: 12px;
|
|
569
|
+
z-index: 10;
|
|
570
|
+
color: #fff;
|
|
571
|
+
|
|
572
|
+
&-icon {
|
|
573
|
+
width: 16px;
|
|
574
|
+
height: 14px;
|
|
575
|
+
margin-right: 6px;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
496
578
|
}
|
|
497
579
|
|
|
498
580
|
.tool-bar {
|