mdm-client 1.0.4 → 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.
@@ -19,6 +19,10 @@
19
19
  ></div>
20
20
  <div class="identity">{{ curSpeaker.name }}</div>
21
21
  </div>
22
+ <!-- 当当前讲话者未开启摄像头时,显示占位板(与 liveMultipleMeeting 的 board 样式一致) -->
23
+ <div v-if="!curSpeakerCameraStatus" class="board">
24
+ <div class="board-icon"></div>
25
+ </div>
22
26
  </div>
23
27
  <div class="speaking-wrapper" v-show="!isExpand">
24
28
  <div :class="['cur-speaking-mic', curSpeakerMicrophoneStatus ? 'mic-on' : 'mic-off']"></div>
@@ -34,8 +38,10 @@
34
38
  >
35
39
  <div class="minium-video-dialog-footer-btn mic-on" v-if="localMicrophoneStatus" @click="changeLocalMicrophoneStatus"></div>
36
40
  <div class="minium-video-dialog-footer-btn mic-off" v-else @click="changeLocalMicrophoneStatus"></div>
37
- <div class="minium-video-dialog-footer-btn camera-on" v-if="localCameraStatus" @click="changeLocalCameraStatus"></div>
38
- <div class="minium-video-dialog-footer-btn camera-off" v-else @click="changeLocalCameraStatus"></div>
41
+ <template v-if="roomMode === 'auto'">
42
+ <div class="minium-video-dialog-footer-btn camera-on" v-if="localCameraStatus" @click="changeLocalCameraStatus"></div>
43
+ <div class="minium-video-dialog-footer-btn camera-off" v-else @click="changeLocalCameraStatus"></div>
44
+ </template>
39
45
  <div class="minium-video-dialog-footer-btn reset" @click="reset"></div>
40
46
  <div :class="['minium-video-dialog-footer-btn', isExpand ? 'slide' : 'expand']" @click="changeExpand"></div>
41
47
  </div>
@@ -59,6 +65,8 @@ export default {
59
65
  isExpand: true,
60
66
  curSpeakers: [],
61
67
  curSpeaker: null,
68
+ // 统一维护的 liveClient 实例,避免在各处重复通过 window 访问
69
+ liveClient: null,
62
70
  localCameraStatus: false,
63
71
  localMicrophoneStatus: false,
64
72
  curSpeakerMicrophoneStatus: false,
@@ -66,6 +74,8 @@ export default {
66
74
  curSpeakerTrack: null,
67
75
  showFooter: false,
68
76
  footerTimer: null,
77
+ // room模式,auto-音视频,audio-仅音频
78
+ roomMode: 'auto',
69
79
  };
70
80
  },
71
81
  computed: {
@@ -84,7 +94,10 @@ export default {
84
94
  },
85
95
  beforeDestroy() {
86
96
  this.clearFooterTimer();
97
+ this.detachCurrentTrack();
87
98
  this.dispatchLiveClientEvent();
99
+ // 组件销毁时清理引用
100
+ this.liveClient = null;
88
101
  },
89
102
  methods: {
90
103
  changeExpand() {
@@ -131,19 +144,21 @@ export default {
131
144
 
132
145
  // 初始化liveClient
133
146
  initLiveClient() {
134
- const liveClient = window['liveClient'];
135
- if(!liveClient) {
147
+ this.liveClient = window['liveClient'] || null;
148
+ if(!this.liveClient) {
136
149
  return;
137
150
  }
151
+ this.roomMode = this.liveClient?.roomMode || 'auto';
138
152
  this.curSpeaker = {
139
- identity: liveClient.room.localParticipant.identity,
140
- name: liveClient.room.localParticipant.name,
153
+ identity: this.liveClient.room.localParticipant.identity,
154
+ name: this.liveClient.room.localParticipant.name,
141
155
  };
142
- this.localCameraStatus = liveClient.room.localParticipant.isCameraEnabled;
143
- this.localMicrophoneStatus = liveClient.room.localParticipant.isMicrophoneEnabled;
144
- this.curSpeakerMicrophoneStatus = liveClient.room.localParticipant.isMicrophoneEnabled;
145
- this.curSpeakerCameraStatus = liveClient.room.localParticipant.isCameraEnabled;
146
- this.curSpeakerTrack = this.trackData.videoTrack;
156
+ this.localCameraStatus = this.liveClient.room.localParticipant.isCameraEnabled;
157
+ this.localMicrophoneStatus = this.liveClient.room.localParticipant.isMicrophoneEnabled;
158
+ this.curSpeakerMicrophoneStatus = this.liveClient.room.localParticipant.isMicrophoneEnabled;
159
+ this.curSpeakerCameraStatus = this.liveClient.room.localParticipant.isCameraEnabled;
160
+ // 仅在视频会议模式下记录并绑定视频轨道
161
+ this.curSpeakerTrack = this.roomMode === 'auto' ? this.trackData.videoTrack : null;
147
162
  // 初始化视频轨道绑定
148
163
  this.initVideoAttach();
149
164
  // 监听并处理liveclient事件
@@ -194,22 +209,27 @@ export default {
194
209
  },
195
210
 
196
211
  initVideoAttach() {
197
- if(this.curSpeakerTrack) {
198
- const videoElm = document.getElementById('video-contain');
199
- videoElm && this.curSpeakerTrack.attach(videoElm);
212
+ if (this.roomMode !== 'auto') return
213
+ if (this.curSpeakerTrack) {
214
+ const videoElm = document.getElementById('video-contain')
215
+ if (videoElm) {
216
+ // 先解绑,确保不会重复附加
217
+ try { this.curSpeakerTrack.detach(videoElm) } catch (e) {}
218
+ this.curSpeakerTrack.attach(videoElm)
219
+ }
200
220
  }
201
221
  },
202
222
 
203
223
  initLiveClientEvent() {
204
- const liveClient = window['liveClient'];
205
- if(!liveClient) {
224
+ if(!this.liveClient) {
206
225
  return;
207
226
  }
208
- liveClient.on("localCameraChange", this.handleLocalCameraChange);
209
- liveClient.on("localMicrophoneChange", this.handleLocalMicrophoneChange);
210
- liveClient.on("activeSpeakerChange", this.handleActiveSpeakerChange);
211
- liveClient.on("videoCallRender", this.handleVideoCallRender);
212
- liveClient.on("roomDisconnected", this.handleRoomDisconnected);
227
+ this.liveClient.on("localCameraChange", this.handleLocalCameraChange);
228
+ this.liveClient.on("localMicrophoneChange", this.handleLocalMicrophoneChange);
229
+ this.liveClient.on("activeSpeakerChange", this.handleActiveSpeakerChange);
230
+ this.liveClient.on("videoCallRender", this.handleVideoCallRender);
231
+ this.liveClient.on("audioCallRender", this.handleAudioCallRender);
232
+ this.liveClient.on("roomDisconnected", this.handleRoomDisconnected);
213
233
  },
214
234
 
215
235
  handleLocalCameraChange(e) {
@@ -221,19 +241,35 @@ export default {
221
241
  },
222
242
 
223
243
  handleActiveSpeakerChange(e) {
224
- if (e.length > 0) {
225
- let maxAudioLevel = 0;
226
- let maxAudioLevelIndex = -1;
227
- e.forEach((speaker, index) => {
228
- if (speaker.audioLevel > maxAudioLevel) {
229
- maxAudioLevel = speaker.audioLevel;
230
- maxAudioLevelIndex = index;
244
+ this.curSpeakers = Array.isArray(e) ? e : []
245
+
246
+ if(this.curSpeakers.length > 0) {
247
+ // 找到音量最大的讲话者
248
+ let maxAudioLevel = -1
249
+ let winner = null
250
+ this.curSpeakers.forEach(speaker => {
251
+ const level = typeof speaker.audioLevel === 'number' ? speaker.audioLevel : 0
252
+ if (level > maxAudioLevel) {
253
+ maxAudioLevel = level
254
+ winner = speaker
255
+ }
256
+ })
257
+
258
+ if (!winner) return
259
+
260
+ // 仅当 identity 变化时才切换,以减少抖动
261
+ if (winner.identity !== this.curSpeaker?.identity) {
262
+ // 解绑旧轨道,等待 videoCallRender 到来后再绑定新轨道
263
+ // detachCurrentTrack()
264
+ this.curSpeaker = {
265
+ identity: winner.identity,
266
+ name: winner.name || winner.identity,
267
+ isLocal: !!winner.isLocal,
231
268
  }
232
- });
233
- if (maxAudioLevelIndex >= 0) {
234
- this.curSpeaker = e[maxAudioLevelIndex];
269
+ // 在未收到渲染事件前,暂时认为摄像头状态未知/关闭,用以显示占位
270
+ // curSpeakerCameraStatus.value = false
271
+ // curSpeakerMicrophoneStatus.value = false
235
272
  }
236
- this.curSpeakers = e;
237
273
  }
238
274
  },
239
275
 
@@ -242,15 +278,61 @@ export default {
242
278
  this.localCameraStatus = e.isCameraEnabled;
243
279
  this.localMicrophoneStatus = e.isMicrophoneEnabled;
244
280
  }
245
- if(e.identity === this.curSpeaker?.identity) {
246
- this.curSpeakerCameraStatus = e.isCameraEnabled;
247
- this.curSpeakerMicrophoneStatus = e.isMicrophoneEnabled;
248
- if(e?.videoTrack) {
249
- this.curSpeakerTrack = e.videoTrack;
281
+ // 利用 videoCallRender 的 remove 标记判断离会
282
+ if (e?.remove === true && e.identity === this.curSpeaker?.identity) {
283
+ this.switchToLocalAsCurrentSpeaker()
284
+ return
285
+ }
286
+ if (e.identity === this.curSpeaker?.identity) {
287
+ // 更新当前讲话者音视频状态
288
+ this.curSpeakerCameraStatus = this.roomMode === 'auto' ? !!e.isCameraEnabled : false
289
+ this.curSpeakerMicrophoneStatus = !!e.isMicrophoneEnabled
290
+
291
+ const videoElm = document.getElementById('video-contain')
292
+
293
+ if (this.roomMode === 'auto' && e.videoTrack && videoElm) {
294
+ // 使用 sid 作为更稳定的身份标识;sid 发布后会保持不变
295
+ const oldTrack = this.curSpeakerTrack
296
+ const newTrack = e.videoTrack
297
+ if (oldTrack) {
298
+ const sameBySid = oldTrack.sid && newTrack?.sid && oldTrack.sid === newTrack.sid
299
+ // 当 sid 不同或对象引用不同(无 sid 时),说明发生实际轨道切换,需要解绑旧轨道
300
+ if (!sameBySid) {
301
+ try { oldTrack.detach(videoElm) } catch (err) {}
302
+ }
303
+ }
304
+ this.curSpeakerTrack = newTrack
305
+ // 重新绑定(同轨道重复 attach 时内部已处理,不影响稳定性)
306
+ try { this.curSpeakerTrack.attach(videoElm) } catch (err) {}
307
+ } else {
308
+ // 摄像头关闭或无轨道,解绑任何已绑定轨道
309
+ if (this.curSpeakerTrack && videoElm) {
310
+ try { this.curSpeakerTrack.detach(videoElm) } catch (err) {}
311
+ }
312
+ this.curSpeakerTrack = null
250
313
  }
251
314
  }
252
315
  },
253
316
 
317
+ // 音频会议渲染事件:不进行任何视频轨道的绑定,仅更新状态并显示占位
318
+ handleAudioCallRender(e) {
319
+ if (e?.remove === true && e.identity === this.curSpeaker?.identity) {
320
+ this.switchToLocalAsCurrentSpeaker()
321
+ return
322
+ }
323
+ if (e.local) {
324
+ this.localMicrophoneStatus = !!e.isMicrophoneEnabled
325
+ }
326
+ if (e.identity === this.curSpeaker?.identity) {
327
+ this.curSpeakerMicrophoneStatus = !!e.isMicrophoneEnabled
328
+ // 音频模式下不显示摄像头画面
329
+ this.curSpeakerCameraStatus = false
330
+ // 确保解绑任何已绑定的视频轨道
331
+ this.detachCurrentTrack()
332
+ this.curSpeakerTrack = null
333
+ }
334
+ },
335
+
254
336
  handleRoomDisconnected() {
255
337
  this.$emit('miniVideoDialogClose');
256
338
  },
@@ -260,27 +342,25 @@ export default {
260
342
  },
261
343
 
262
344
  dispatchLiveClientEvent() {
263
- const liveClient = window['liveClient'];
264
- if(liveClient) {
265
- liveClient.off("localCameraChange", this.handleLocalCameraChange);
266
- liveClient.off("localMicrophoneChange", this.handleLocalMicrophoneChange);
267
- liveClient.off("activeSpeakerChange", this.handleActiveSpeakerChange);
268
- liveClient.off("videoCallRender", this.handleVideoCallRender);
269
- liveClient.off("roomDisconnected", this.handleRoomDisconnected);
345
+ if(this.liveClient) {
346
+ this.liveClient.off("localCameraChange", this.handleLocalCameraChange);
347
+ this.liveClient.off("localMicrophoneChange", this.handleLocalMicrophoneChange);
348
+ this.liveClient.off("activeSpeakerChange", this.handleActiveSpeakerChange);
349
+ this.liveClient.off("videoCallRender", this.handleVideoCallRender);
350
+ this.liveClient.off("audioCallRender", this.handleAudioCallRender)
351
+ this.liveClient.off("roomDisconnected", this.handleRoomDisconnected);
270
352
  }
271
353
  },
272
354
 
273
355
  async changeLocalMicrophoneStatus() {
274
- const liveClient = window['liveClient'];
275
- if(liveClient) {
276
- await liveClient.changeMicrophoneStatus();
356
+ if(this.liveClient) {
357
+ await this.liveClient.changeMicrophoneStatus();
277
358
  }
278
359
  },
279
360
 
280
361
  async changeLocalCameraStatus() {
281
- const liveClient = window['liveClient'];
282
- if(liveClient) {
283
- await liveClient.changeCameraStatus();
362
+ if(this.liveClient) {
363
+ await this.liveClient.changeCameraStatus();
284
364
  }
285
365
  },
286
366
 
@@ -290,6 +370,42 @@ export default {
290
370
  this.footerTimer = null;
291
371
  }
292
372
  },
373
+
374
+ // 工具:解绑当前视频轨道
375
+ detachCurrentTrack() {
376
+ try {
377
+ const videoElm = document.getElementById('video-contain')
378
+ if (this.curSpeakerTrack && videoElm) {
379
+ this.curSpeakerTrack.detach(videoElm)
380
+ }
381
+ } catch (e) {}
382
+ },
383
+ // 工具:切换当前讲话者为本地参会者
384
+ switchToLocalAsCurrentSpeaker() {
385
+ try {
386
+ const lp = this.liveClient?.room?.localParticipant
387
+ this.detachCurrentTrack()
388
+ if (lp) {
389
+ this.curSpeaker = {
390
+ identity: lp.identity,
391
+ name: lp.name,
392
+ isLocal: true,
393
+ }
394
+ this.curSpeakerCameraStatus = this.roomMode === 'auto' ? !!lp.isCameraEnabled : false
395
+ this.curSpeakerMicrophoneStatus = !!lp.isMicrophoneEnabled
396
+ // 立即尝试绑定已知的本地轨道(如存在),后续仍会由 videoCallRender 纠正
397
+ if (this.roomMode === 'auto') {
398
+ const track = this.trackData?.videoTrack
399
+ if (track && this.curSpeakerCameraStatus) {
400
+ this.curSpeakerTrack = track
401
+ this.initVideoAttach()
402
+ }
403
+ } else {
404
+ this.curSpeakerTrack = null
405
+ }
406
+ }
407
+ } catch (e) {}
408
+ }
293
409
  },
294
410
  }
295
411
  </script>
@@ -318,6 +434,25 @@ export default {
318
434
  border-radius: 4px;
319
435
  object-fit: contain;
320
436
  }
437
+ // 与 liveMultipleMeeting 保持一致的缺省板样式
438
+ .board {
439
+ position: absolute;
440
+ width: 100%;
441
+ height: 100%;
442
+ left: 0;
443
+ top: 0;
444
+ z-index: 30;
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ background: var(--meeting-board-bg) no-repeat center / 100% 100%;
449
+ .board-icon {
450
+ width: 20%;
451
+ height: auto;
452
+ aspect-ratio: 1/1;
453
+ background: url("../../assets/image/common/default_avatar.png") no-repeat center / 100% 100%;
454
+ }
455
+ }
321
456
  .describe {
322
457
  position: absolute;
323
458
  bottom: 6px;
@@ -123,7 +123,7 @@ export default {
123
123
  .appoint-dialog {
124
124
  width: 220px;
125
125
  position: absolute;
126
- z-index: 2100;
126
+ z-index: 3000;
127
127
  background: var(--dialog-bg);
128
128
  box-shadow: 0px 0px 8px 0px var(--dialog-shadow-color);
129
129
  border-radius: 8px;
@@ -137,7 +137,7 @@ export default {
137
137
  <style lang="scss" scoped>
138
138
  .dialog {
139
139
  position: absolute;
140
- z-index: 2100;
140
+ z-index: 2200;
141
141
  padding-top: 40px;
142
142
  // background: transparent;
143
143
  width: fit-content;
@@ -57,7 +57,7 @@ export default {
57
57
  .check-dialog {
58
58
  width: 160px;
59
59
  position: absolute;
60
- z-index: 2100;
60
+ z-index: 3000;
61
61
  &-inner {
62
62
  width: 100%;
63
63
  background: var(--dialog-bg);
@@ -281,7 +281,7 @@ export default {
281
281
  border: 1px solid var(--dialog-border-color);
282
282
  padding: 4px 6px;
283
283
  position: absolute;
284
- z-index: 2100;
284
+ z-index: 2200;
285
285
  &-item {
286
286
  width: 100%;
287
287
  height: 36px;
@@ -49,7 +49,7 @@ export default {
49
49
  }
50
50
  },
51
51
  mounted() {
52
- this.isFluencyPriority = (this.screenShareCaptureOption.frameRate === 60);
52
+ this.isFluencyPriority = (this.screenShareCaptureOption?.resolution?.frameRate === 60);
53
53
  this.systemAudioInclude = this.screenShareCaptureOption.systemAudio === "include";
54
54
  },
55
55
  methods: {
@@ -64,7 +64,7 @@ export default {
64
64
  @import "../../assets/style/mixin.scss";
65
65
  .screen-share-board {
66
66
  position: absolute;
67
- z-index: 4000;
67
+ z-index: 2050;
68
68
  left: 0;
69
69
  top: 0;
70
70
  width: 100%;