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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/App.vue +72 -67
  3. package/src/assets/image/common/layout-active16.png +0 -0
  4. package/src/assets/image/common/layout-active25.png +0 -0
  5. package/src/assets/image/common/layout-active3.png +0 -0
  6. package/src/assets/image/common/layout-active9.png +0 -0
  7. package/src/assets/image/common/layout16.png +0 -0
  8. package/src/assets/image/common/layout25.png +0 -0
  9. package/src/assets/image/common/layout3.png +0 -0
  10. package/src/assets/image/common/layout9.png +0 -0
  11. package/src/assets/image/common/mirror.png +0 -0
  12. package/src/assets/image/common/rotate_icon1.png +0 -0
  13. package/src/assets/image/common/rotate_icon2.png +0 -0
  14. package/src/assets/image/common/rotate_icon3.png +0 -0
  15. package/src/assets/image/common/rotate_icon4.png +0 -0
  16. package/src/assets/style/base.scss +5 -0
  17. package/src/components/LiveMulti/LiveMulti.vue +27 -6
  18. package/src/components/LiveMultipleMeeting/LiveMultipleMeeting.vue +1163 -99
  19. package/src/components/LiveMultipleMeeting/style/index.scss +145 -14
  20. package/src/components/LivePoint/LivePoint.vue +71 -208
  21. package/src/components/LivePointMeeting/LivePointMeeting.vue +223 -13
  22. package/src/components/LivePointMeeting/style/index.scss +35 -0
  23. package/src/components/MeetingReadyDialog/MeetingReadyDialog.vue +96 -14
  24. package/src/components/MiniumVideoDialog/MiniumVideoDialog.vue +185 -50
  25. package/src/components/other/addressBook.vue +137 -20
  26. package/src/components/other/appointDialog.vue +1 -1
  27. package/src/components/other/customLayout.vue +368 -202
  28. package/src/components/other/layoutSwitch.vue +253 -37
  29. package/src/components/other/leadershipFocus.vue +422 -0
  30. package/src/components/other/leaveOptionDialog.vue +1 -1
  31. package/src/components/other/moreOptionDialog.vue +17 -1
  32. package/src/components/other/screenShareBoard.vue +2 -2
  33. package/src/components/other/selectDialog.vue +1 -1
  34. package/src/components/other/selectSpecialDialog.vue +1 -1
  35. package/src/utils/api.js +19 -0
  36. package/src/utils/livekit/live-client-esm.js +1 -1
@@ -1,12 +1,14 @@
1
1
  <template>
2
- <div ref="rootElm" class="point-meeting" id="point-meeting" :style="{ resize: 'both' }">
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
- if (localChild) {
780
- if (localChild.id === `participant-${this.localIdentity}`) {
781
- return;
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
- layoutNormalElm.insertBefore(localChild, layoutNormalElm.firstChild ?? null);
784
- if (currentEle) {
785
- layoutBlurElm.appendChild(currentEle);
786
- }
918
+ // 没有大画面,直接保持当前不动
919
+ return;
787
920
  }
788
- } else {
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
- let audioElm = document.getElementById(`audio=${audioItem.identity}`);
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: 20px;
1026
- height: 20px;
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
- throw new Error(err);
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
- facingMode: this.curFacingMode,
350
- }
351
- : this.cameraStatus,
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 {