import { getRandomString } from 'billd-utils'; import { computed, onUnmounted, ref, watch } from 'vue'; import { fetchRtcV1Publish } from '@/api/srs'; import { WEBSOCKET_URL } from '@/constant'; import { DanmuMsgTypeEnum, IDanmu, ILiveRoom, ILiveUser, IUser, LiveRoomTypeEnum, } from '@/interface'; import { WSGetRoomAllUserType, WsGetLiveUserType, WsHeartbeatType, WsJoinType, WsLeavedType, WsMessageType, WsOtherJoinType, WsRoomLivingType, WsStartLiveType, WsUpdateJoinInfoType, } from '@/interface-ws'; import { WebRTCClass } from '@/network/webRTC'; import { WebSocketClass, WsConnectStatusEnum, WsMsgTypeEnum, prettierReceiveWsMsg, } from '@/network/webSocket'; import { AppRootState, useAppStore } from '@/store/app'; import { useNetworkStore } from '@/store/network'; import { useUserStore } from '@/store/user'; import { createVideo } from '@/utils'; import { useRTCParams } from './use-rtc-params'; export const useRtcWs = () => { const appStore = useAppStore(); const userStore = useUserStore(); const networkStore = useNetworkStore(); const loopHeartbeatTimer = ref(); const liveUserList = ref([]); const roomId = ref(''); const roomLiving = ref(false); const liveRoomInfo = ref(); const anchorInfo = ref(); const isAnchor = ref(false); const localStream = ref(); const canvasVideoStream = ref(); const lastCoverImg = ref(''); const { maxBitrate, maxFramerate, resolutionRatio } = useRTCParams(); const currentMaxBitrate = ref(maxBitrate.value[2].value); const currentResolutionRatio = ref(resolutionRatio.value[3].value); const currentMaxFramerate = ref(maxFramerate.value[2].value); const damuList = ref([]); watch( () => appStore.allTrack, (newTrack, oldTrack) => { console.log('appStore.allTrack变了', newTrack, oldTrack); const mixedStream = new MediaStream(); newTrack.forEach((item) => { if (item.track) { mixedStream.addTrack(item.track); } }); console.log('新的allTrack音频轨', mixedStream.getAudioTracks()); console.log('新的allTrack视频轨', mixedStream.getVideoTracks()); console.log('旧的allTrack音频轨', localStream.value?.getAudioTracks()); console.log('旧的allTrack视频轨', localStream.value?.getVideoTracks()); localStream.value = mixedStream; }, { deep: true } ); onUnmounted(() => { clearInterval(loopHeartbeatTimer.value); }); watch( () => currentResolutionRatio.value, (newVal) => { if (canvasVideoStream.value) { canvasVideoStream.value.getVideoTracks().forEach((track) => { track.applyConstraints({ frameRate: { max: currentMaxFramerate.value }, height: newVal, }); }); } else { appStore.allTrack.forEach((info) => { info.track?.applyConstraints({ frameRate: { max: currentMaxFramerate.value }, height: newVal, }); }); } networkStore.rtcMap.forEach(async (rtc) => { const res = await rtc.setResolutionRatio(newVal); if (res === 1) { window.$message.success('切换分辨率成功!'); } else { window.$message.success('切换分辨率失败!'); } }); } ); watch( () => currentMaxFramerate.value, (newVal) => { console.log(currentMaxFramerate.value, 'currentMaxFramerate.value'); if (canvasVideoStream.value) { canvasVideoStream.value.getVideoTracks().forEach((track) => { track.applyConstraints({ frameRate: { max: newVal }, height: currentResolutionRatio.value, }); }); } else { appStore.allTrack.forEach((info) => { info.track?.applyConstraints({ frameRate: { max: newVal }, height: currentResolutionRatio.value, }); }); } networkStore.rtcMap.forEach(async (rtc) => { const res = await rtc.setMaxFramerate(newVal); if (res === 1) { window.$message.success('切换帧率成功!'); } else { window.$message.success('切换帧率失败!'); } }); } ); watch( () => currentMaxBitrate.value, (newVal) => { networkStore.rtcMap.forEach(async (rtc) => { const res = await rtc.setMaxBitrate(newVal); if (res === 1) { window.$message.success('切换码率成功!'); } else { window.$message.success('切换码率失败!'); } }); } ); function addTrack(addTrackInfo: { track; stream }) { if (isAnchor.value) { networkStore.rtcMap.forEach((rtc) => { const sender = rtc.peerConnection ?.getSenders() .find((sender) => sender.track?.id === addTrackInfo.track?.id); if (!sender) { console.log('pc添加track-开播后中途添加', addTrackInfo.track?.id); rtc.peerConnection ?.getSenders() ?.find((sender) => sender.track?.kind === 'audio') ?.replaceTrack(canvasVideoStream.value!.getAudioTracks()[0]); const vel = createVideo({}); vel.srcObject = canvasVideoStream.value!; } }); } const mixedStream = new MediaStream(); appStore.allTrack.forEach((item) => { if (item.track) { mixedStream.addTrack(item.track); } }); console.log('addTrack后结果的音频轨', mixedStream.getAudioTracks()); console.log('addTrack后结果的视频轨', mixedStream.getVideoTracks()); localStream.value = mixedStream; } function delTrack(delTrackInfo: AppRootState['allTrack'][0]) { if (isAnchor.value) { networkStore.rtcMap.forEach((rtc) => { const sender = rtc.peerConnection ?.getSenders() .find((sender) => sender.track?.id === delTrackInfo.track?.id); if (sender) { console.log('删除track', delTrackInfo, sender); rtc.peerConnection?.removeTrack(sender); } }); } const mixedStream = new MediaStream(); appStore.allTrack.forEach((item) => { if (item.track) { mixedStream.addTrack(item.track); } }); console.log('delTrack后结果的音频轨', mixedStream.getAudioTracks()); console.log('delTrack后结果的视频轨', mixedStream.getVideoTracks()); localStream.value = mixedStream; } const mySocketId = computed(() => { return networkStore.wsMap.get(roomId.value)?.socketIo?.id || '-1'; }); function handleHeartbeat(socketId: string) { loopHeartbeatTimer.value = setInterval(() => { const ws = networkStore.wsMap.get(roomId.value); if (!ws) return; ws.send({ msgType: WsMsgTypeEnum.heartbeat, data: { socket_id: socketId, }, }); }, 1000 * 5); } async function sendOffer({ receiver }: { receiver: string }) { console.log('开始sendOffer'); const ws = networkStore.wsMap.get(roomId.value); if (!ws) return; const rtc = networkStore.getRtcMap(`${roomId.value}___${receiver}`); if (!rtc) return; const sdp = await rtc.createOffer(); await rtc.setLocalDescription(sdp!); const myLiveRoom = userStore.userInfo!.live_rooms![0]; const res = await fetchRtcV1Publish({ api: `/rtc/v1/publish/`, clientip: null, sdp: sdp!.sdp!, streamurl: `${myLiveRoom.rtmp_url!}?token=${myLiveRoom.key!}&type=${ LiveRoomTypeEnum.user_srs }`, tid: getRandomString(10), }); networkStore.wsMap.get(roomId.value)?.send({ msgType: WsMsgTypeEnum.updateJoinInfo, data: { live_room_id: Number(roomId.value), track: { audio: 1, video: 1, }, }, }); if (res.data.code !== 0) { console.error('/rtc/v1/publish/拿不到sdp'); window.$message.error('/rtc/v1/publish/拿不到sdp'); return; } await rtc.setRemoteDescription( new RTCSessionDescription({ type: 'answer', sdp: res.data.sdp }) ); } function handleStartLive({ coverImg, name }) { networkStore.wsMap.get(roomId.value)?.send({ msgType: WsMsgTypeEnum.startLive, data: { cover_img: coverImg, name, type: LiveRoomTypeEnum.user_srs, }, }); startNewSrsWebRtc({ videoEl: document.createElement('video'), receiver: 'srs', }); } function sendJoin() { const instance = networkStore.wsMap.get(roomId.value); if (!instance) return; instance.send({ msgType: WsMsgTypeEnum.join, data: { socket_id: mySocketId.value, live_room: { id: Number(roomId.value), }, user_info: userStore.userInfo, }, }); } /** 原生的webrtc时,receiver必传 */ function startNewSrsWebRtc({ receiver, videoEl, }: { receiver: string; videoEl: HTMLVideoElement; }) { console.warn('SRS开始new WebRTCClass', `${roomId.value}___${receiver!}`); const rtc = new WebRTCClass({ maxBitrate: currentMaxBitrate.value, maxFramerate: currentMaxFramerate.value, resolutionRatio: currentResolutionRatio.value, roomId: `${roomId.value}___${receiver!}`, videoEl, isSRS: true, receiver, }); if (canvasVideoStream.value) { localStream.value = canvasVideoStream.value; rtc.localStream = canvasVideoStream.value; canvasVideoStream.value.getTracks().forEach((track) => { console.log('pc添加track-srs', track.kind, track.id); rtc.peerConnection?.addTrack(track, localStream.value!); }); } sendOffer({ receiver, }); } function initReceive() { const ws = networkStore.wsMap.get(roomId.value); if (!ws?.socketIo) return; // websocket连接成功 ws.socketIo.on(WsConnectStatusEnum.connect, () => { prettierReceiveWsMsg(WsConnectStatusEnum.connect, ws.socketIo); handleHeartbeat(ws.socketIo!.id); if (!ws) return; ws.status = WsConnectStatusEnum.connect; ws.update(); sendJoin(); }); // websocket连接断开 ws.socketIo.on(WsConnectStatusEnum.disconnect, () => { prettierReceiveWsMsg(WsConnectStatusEnum.disconnect, ws); if (!ws) return; ws.status = WsConnectStatusEnum.disconnect; ws.update(); }); // 主播正在直播 ws.socketIo.on(WsMsgTypeEnum.roomLiving, (data: WsRoomLivingType) => { prettierReceiveWsMsg(WsMsgTypeEnum.roomLiving, data); roomLiving.value = true; }); // 主播不在直播 ws.socketIo.on(WsMsgTypeEnum.roomNoLive, (data) => { prettierReceiveWsMsg(WsMsgTypeEnum.roomNoLive, data); roomLiving.value = false; }); // 当前所有在线用户 ws.socketIo.on( WsMsgTypeEnum.liveUser, (data: WSGetRoomAllUserType['data']) => { prettierReceiveWsMsg(WsMsgTypeEnum.liveUser, data); const res = data.liveUser.map((item) => { return { id: item.id, // userInfo: item.id, }; }); liveUserList.value = res; } ); // 收到用户发送消息 ws.socketIo.on(WsMsgTypeEnum.message, (data: WsMessageType) => { prettierReceiveWsMsg(WsMsgTypeEnum.message, data); damuList.value.push({ socket_id: data.socket_id, msgType: DanmuMsgTypeEnum.danmu, msg: data.data.msg, userInfo: data.user_info, }); }); // 用户加入房间完成 ws.socketIo.on(WsMsgTypeEnum.joined, (data: WsJoinType['data']) => { prettierReceiveWsMsg(WsMsgTypeEnum.joined, data); liveUserList.value.push({ id: data.socket_id, userInfo: data.user_info, }); liveRoomInfo.value = data.live_room; anchorInfo.value = data.anchor_info; ws.send({ msgType: WsMsgTypeEnum.getLiveUser, data: { live_room_id: data.live_room.id!, }, }); }); // 其他用户加入房间 ws.socketIo.on(WsMsgTypeEnum.otherJoin, (data: WsOtherJoinType['data']) => { prettierReceiveWsMsg(WsMsgTypeEnum.otherJoin, data); liveUserList.value.push({ id: data.join_socket_id, userInfo: data.join_user_info, }); const danmu: IDanmu = { msgType: DanmuMsgTypeEnum.otherJoin, socket_id: data.join_socket_id, userInfo: data.join_user_info, msg: '', }; damuList.value.push(danmu); ws.send({ msgType: WsMsgTypeEnum.getLiveUser, data: { live_room_id: data.live_room.id!, }, }); }); // 用户离开房间 ws.socketIo.on(WsMsgTypeEnum.leave, (data) => { prettierReceiveWsMsg(WsMsgTypeEnum.leave, data); }); // 用户离开房间完成 ws.socketIo.on(WsMsgTypeEnum.leaved, (data: WsLeavedType['data']) => { prettierReceiveWsMsg(WsMsgTypeEnum.leaved, data); networkStore.rtcMap .get(`${roomId.value}___${data.socket_id as string}`) ?.close(); networkStore.removeRtc(`${roomId.value}___${data.socket_id as string}`); const res = liveUserList.value.filter( (item) => item.id !== data.socket_id ); liveUserList.value = res; damuList.value.push({ socket_id: data.socket_id, msgType: DanmuMsgTypeEnum.userLeaved, userInfo: data.user_info, msg: '', }); }); } function initSrsWs(data: { isAnchor: boolean; roomId: string; currentResolutionRatio?: number; currentMaxFramerate?: number; currentMaxBitrate?: number; }) { roomId.value = data.roomId; isAnchor.value = data.isAnchor; if (data.currentMaxBitrate) { currentMaxBitrate.value = data.currentMaxBitrate; } if (data.currentMaxFramerate) { currentMaxFramerate.value = data.currentMaxFramerate; } if (data.currentResolutionRatio) { currentResolutionRatio.value = data.currentResolutionRatio; } new WebSocketClass({ roomId: roomId.value, url: WEBSOCKET_URL, isAnchor: data.isAnchor, }); initReceive(); } return { initSrsWs, addTrack, delTrack, handleStartLive, mySocketId, canvasVideoStream, lastCoverImg, roomLiving, liveRoomInfo, anchorInfo, localStream, liveUserList, damuList, currentMaxFramerate, currentMaxBitrate, currentResolutionRatio, }; };