@tencentcloud/roomkit-web-vue3
Version:
<h1 align="center"> TUIRoomKit</h1> Conference (TUIRoomKit) is a product suitable for multi-person audio and video conversation scenarios such as business meetings, webinars, and online education. By integrating this product, you can add room management,
364 lines (335 loc) • 10.9 kB
text/typescript
import TUIRoomEngine, {
TUIRoomEvents,
TUIChangeReason,
TUIVideoStreamType,
TRTCVideoStreamType,
TRTCVideoFillMode,
TRTCVideoMirrorType,
TRTCVideoRotation,
} from '@tencentcloud/tuiroom-engine-js';
import { IRoomService, EventType } from '../types';
import { isMobile } from '../../utils/environment';
import { MESSAGE_DURATION } from '../../constants/message';
import logger from '../../utils/common/logger';
interface ObserverData {
userId: string;
streamType: TUIVideoStreamType;
isIntersection?: boolean;
}
export enum StreamPlayMode {
PLAY = 'play',
STOP = 'stop',
PLAY_IN_VISIBLE = 'playInVisible',
}
export enum StreamPlayQuality {
HIGH = 'high',
LOW = 'low',
Default = 'default',
}
export class MediaManager {
private service: IRoomService;
intersectionObserver: IntersectionObserver | null = null;
observerDataMap: Map<HTMLElement | string, ObserverData> = new Map();
observerRoot: HTMLElement | null = null;
constructor(service: IRoomService) {
this.service = service;
this.bindRoomEngineEvents();
}
public async startPlayVideo(options: {
userId: string;
streamType: TUIVideoStreamType;
view: HTMLElement;
observerViewInVisible: boolean;
}) {
const { userId, streamType, view, observerViewInVisible } = options;
logger.info(
'MediaManager.startPlayVideo',
userId,
streamType,
view,
observerViewInVisible
);
const streamInfo = this.service.roomStore.getStreamInfo(userId, streamType);
if (!streamInfo) {
return;
}
streamInfo!.playDomMap?.set(view, streamType);
if (!observerViewInVisible) {
await this.doStartPlayVideo({ userId, streamType });
return;
}
this.initIntersectionObserver();
this.observerDataMap.set(
view,
Object.assign(this.observerDataMap.get(view) || {}, {
userId,
streamType,
})
);
this.intersectionObserver?.observe(view);
// The dom that was playing stream A is going to play stream B.
// At this point, the dom already exists and will no longer trigger the intersection event,
// so when you realize it's already visible, you should play it immediately.
if (this.observerDataMap.get(view)?.isIntersection) {
await this.doStartPlayVideo({ userId, streamType });
}
}
// If only one view wants to stop playing, update the viewList, but don't stop the stream.
// If no view is passed in, the stream is stopped.
public async stopPlayVideo(options: {
userId: string;
streamType: TUIVideoStreamType;
view: HTMLElement;
}) {
const { userId, streamType, view } = options;
logger.info('MediaManager.stopPlayVideo', userId, streamType, view);
const streamInfo = this.service.roomStore.getStreamInfo(userId, streamType);
if (!streamInfo?.playDomMap || streamInfo?.playDomMap.size === 0) {
return;
}
if (this.observerDataMap.get(view)) {
this.observerDataMap.delete(view);
this.intersectionObserver?.unobserve(view);
}
streamInfo?.playDomMap.delete(view);
if (streamInfo?.playDomMap.size > 0) {
await this.doStartPlayVideo({
userId,
streamType,
});
} else {
await this.doStopPlayVideo(options);
}
}
private initIntersectionObserver() {
if (
!this.intersectionObserver ||
document.getElementById('roomContainer') !== this.observerRoot
) {
const observerRoot = document.getElementById('roomContainer');
this.intersectionObserver = new IntersectionObserver(
this.intersectionObserverCallback.bind(this),
{
root: observerRoot,
rootMargin: '0px',
}
);
this.observerDataMap = new Map();
this.observerRoot = observerRoot;
}
}
private intersectionObserverCallback(entries: IntersectionObserverEntry[]) {
entries.forEach((entry: IntersectionObserverEntry) => {
const { isIntersecting, target } = entry;
const observerData = this.observerDataMap.get(target as HTMLElement);
if (!observerData) {
return;
}
const { userId, streamType } = observerData;
const streamInfo = this.service.roomStore.getStreamInfo(
userId,
streamType
);
if (!streamInfo) {
return;
}
if (isIntersecting) {
observerData.isIntersection = true;
if (streamInfo?.playDomMap && streamInfo?.playDomMap.size > 0) {
this.doStartPlayVideo({ userId, streamType });
}
} else {
observerData.isIntersection = false;
const isContinuePlay = Array.from(
streamInfo?.playDomMap?.keys() || []
).find(
item =>
!this.observerDataMap.get(item) ||
this.observerDataMap.get(item)?.isIntersection
);
if (!isContinuePlay) {
this.doStopPlayVideo({ userId, streamType });
}
}
});
}
private getPlayStreamType(userId: string, streamType: TUIVideoStreamType) {
if (streamType === TUIVideoStreamType.kScreenStream) {
return streamType;
}
const streamInfo = this.service.roomStore.getStreamInfo(userId, streamType);
if (streamInfo?.playDomMap && streamInfo?.playDomMap.size > 0) {
const playStreamTypeList = Array.from(streamInfo?.playDomMap?.values());
if (playStreamTypeList.includes(TUIVideoStreamType.kCameraStream)) {
return TUIVideoStreamType.kCameraStream;
}
return TUIVideoStreamType.kCameraStreamLow;
}
return streamType;
}
private async doStartPlayVideo(options: {
userId: string;
streamType: TUIVideoStreamType;
}) {
const { userId, streamType } = options;
const streamInfo = this.service.roomStore.getStreamInfo(userId, streamType);
if (!streamInfo) {
return;
}
const playStreamType = this.getPlayStreamType(userId, streamType);
const viewIdList = Array.from(streamInfo!.playDomMap!.keys()).map(item => {
if (item instanceof HTMLElement) {
return item?.id;
}
return item;
});
this.service.roomStore.updateStreamInfo({
userId,
streamType,
isLoading: true,
});
if (
userId === this.service.basicStore.userId &&
streamType === TUIVideoStreamType.kCameraStream
) {
this.service.roomEngine.instance?.setLocalVideoView({ view: viewIdList });
} else {
this.service.roomEngine.instance?.setRemoteVideoView({
userId,
streamType: playStreamType,
view: viewIdList,
});
await this.setVideoRenderParams({ userId, streamType });
await this.service.roomEngine.instance?.startPlayRemoteVideo({
userId,
streamType: playStreamType,
});
}
this.service.roomStore.updateStreamInfo({
userId,
streamType,
isLoading: false,
});
}
private async setVideoRenderParams(options: {
userId: string;
streamType: TUIVideoStreamType;
}) {
const { userId, streamType } = options;
if (userId !== this.service.basicStore.userId) {
const trtcCloud = this.service.roomEngine.instance?.getTRTCCloud();
const trtcStreamType =
streamType === TUIVideoStreamType.kScreenStream
? TRTCVideoStreamType.TRTCVideoStreamTypeSub
: TRTCVideoStreamType.TRTCVideoStreamTypeBig;
let trtcFillMode = TRTCVideoFillMode.TRTCVideoFillMode_Fit;
if (isMobile && streamType !== TUIVideoStreamType.kScreenStream) {
trtcFillMode = TRTCVideoFillMode.TRTCVideoFillMode_Fill;
}
await trtcCloud?.setRemoteRenderParams(userId, trtcStreamType, {
mirrorType: TRTCVideoMirrorType.TRTCVideoMirrorType_Disable,
rotation: TRTCVideoRotation.TRTCVideoRotation0,
fillMode: trtcFillMode,
});
}
}
private async doStopPlayVideo(options: {
userId: string;
streamType: TUIVideoStreamType;
}) {
const { userId, streamType } = options;
this.service.roomStore.updateStreamInfo({
userId,
streamType,
isLoading: false,
});
if (
userId === this.service.basicStore.userId &&
streamType === TUIVideoStreamType.kCameraStream
) {
this.service.roomEngine.instance?.setLocalVideoView({ view: null });
} else {
await this.service.roomEngine.instance?.stopPlayRemoteVideo({
userId,
streamType,
});
}
}
private onUserAudioStateChanged(eventInfo: {
userId: string;
hasAudio: boolean;
reason: TUIChangeReason;
}) {
const { userId, hasAudio, reason } = eventInfo;
if (
userId === this.service.basicStore.userId &&
!hasAudio &&
reason === TUIChangeReason.kChangedByAdmin
) {
this.service.emit(EventType.ROOM_NOTICE_MESSAGE, {
type: 'warning',
message: this.service.t('Your microphone has been turned off'),
duration: MESSAGE_DURATION.NORMAL,
});
}
}
private onUserVideoStateChanged = (eventInfo: {
userId: string;
streamType: TUIVideoStreamType;
hasVideo: boolean;
reason: TUIChangeReason;
}) => {
const { userId, streamType, hasVideo, reason } = eventInfo;
// Handle status changes
if (
userId === this.service.basicStore.userId &&
!hasVideo &&
reason === TUIChangeReason.kChangedByAdmin
) {
// The host turns off the camera
if (streamType === TUIVideoStreamType.kCameraStream) {
this.service.emit(EventType.ROOM_NOTICE_MESSAGE, {
type: 'warning',
message: this.service.t('Your camera has been turned off'),
duration: MESSAGE_DURATION.NORMAL,
});
}
// Host turns off screen sharing
if (streamType === TUIVideoStreamType.kScreenStream) {
this.service.emit(EventType.ROOM_NOTICE_MESSAGE_BOX, {
title: this.service.t('Your screen sharing has been stopped'),
message: this.service.t(
'Your screen sharing has been stopped, Now only the host/admin can share the screen'
),
confirmButtonText: this.service.t('I got it'),
});
}
}
};
dispose() {
this.intersectionObserver = null;
this.observerDataMap = new Map();
}
private bindRoomEngineEvents() {
TUIRoomEngine.once('ready', () => {
this.service.roomEngine.instance?.on(
TUIRoomEvents.onUserVideoStateChanged,
this.onUserVideoStateChanged.bind(this)
);
this.service.roomEngine.instance?.on(
TUIRoomEvents.onUserAudioStateChanged,
this.onUserAudioStateChanged.bind(this)
);
});
}
private unbindRoomEngineEvents() {
this.service.roomEngine.instance?.off(
TUIRoomEvents.onUserVideoStateChanged,
this.onUserVideoStateChanged
);
this.service.roomEngine.instance?.off(
TUIRoomEvents.onUserAudioStateChanged,
this.onUserAudioStateChanged
);
}
}