Files
ColdBreeze-chat-web/src/app/slices/voice.ts
T
2023-05-18 06:35:28 +08:00

263 lines
8.1 KiB
TypeScript

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { KEY_UID } from "../config";
import { ConnectionState } from "agora-rtc-sdk-ng";
import { resetAuthData } from "./auth.data";
import { ChatContext } from "@/types/common";
export type VoiceBasicInfo = {
context: ChatContext,
from?: number,
id: number,// means to in dm context
}
export type VoicingInfo = {
downlinkNetworkQuality?: number,
joining?: boolean,
connectionState?: ConnectionState
} & VoiceBasicInfo & VoicingMemberInfo
export type VoicingMemberInfo = {
speakingVolume?: number,
muted?: boolean,
deafen?: boolean,
video?: boolean,
shareScreen?: boolean
}
export type VoicingMembers = {
ids: number[],
byId: {
[key: number]: VoicingMemberInfo
},
pin?: number
}
export type VoiceInfo = {
channelName: string,
memberCount: number
} & VoiceBasicInfo
export type DeviceInfo = Pick<MediaDeviceInfo, "deviceId" | "groupId" | "kind" | "label">;
export type MediaDeviceKind = "audioinput" | "audiooutput" | "videoinput"
interface State {
callingFrom: number,
callingTo: number,
calling: boolean,
voicing: VoicingInfo | null,
voicingMembers: VoicingMembers,
list: VoiceInfo[],
devices: DeviceInfo[],
audioInputDeviceId: string,
audioOutputDeviceId: string,
videoInputDeviceId: string,
}
const initialState: State = {
calling: false,
callingFrom: 0,
callingTo: 0,
voicing: null,
voicingMembers: {
ids: [],
byId: {}
},
list: [],
devices: [],
audioInputDeviceId: "",
audioOutputDeviceId: "",
videoInputDeviceId: ""
};
const voiceSlice = createSlice({
name: "voice",
initialState,
reducers: {
updateCallInfo(state, { payload }: PayloadAction<{ from: number, to?: number, calling?: boolean }>) {
const { from, to = 0, calling } = payload;
state.callingFrom = from;
state.callingTo = to;
if (typeof calling === "boolean") {
state.calling = calling;
}
},
updateDevices(state, { payload }: PayloadAction<DeviceInfo[]>) {
state.devices = payload;
if (payload.length > 0) {
// 默认选择第一个
const audioInputDevice = payload.find(v => v.kind == "audioinput");
const audioOutputDevice = payload.find(v => v.kind == "audiooutput");
const videoInputDevice = payload.find(v => v.kind == "videoinput");
if (audioInputDevice) {
state.audioInputDeviceId = audioInputDevice.deviceId;
}
if (audioOutputDevice) {
state.audioOutputDeviceId = audioOutputDevice.deviceId;
}
if (videoInputDevice) {
state.videoInputDeviceId = videoInputDevice.deviceId;
}
}
},
updateCalling(state, { payload }: PayloadAction<boolean>) {
state.calling = payload;
},
updateSelectDeviceId(state, { payload }: PayloadAction<{ kind: MediaDeviceKind, value: string }>) {
const { kind, value } = payload;
switch (kind) {
case "audioinput":
state.audioInputDeviceId = value;
break;
case "audiooutput":
state.audioOutputDeviceId = value;
break;
case "videoinput":
state.videoInputDeviceId = value;
break;
default:
break;
}
},
updateVoicingInfo(state, { payload }: PayloadAction<VoicingInfo | null>) {
if (payload) {
state.voicing = { ...(state.voicing ?? {}), ...payload };
// 更新登录用户在member list的状态
const loginUid = localStorage.getItem(KEY_UID) ?? 0;
const idx = state.voicingMembers.ids.findIndex((uid) => uid == loginUid);
if (idx > -1) {
// state.voicingMembers.byId[+loginUid] = payload;
Object.keys(payload).forEach((key) => {
switch (key) {
case "video":
state.voicingMembers.byId[+loginUid].video = payload.video;
break;
case "muted":
state.voicingMembers.byId[+loginUid].muted = payload.muted;
if (!payload.muted) {
state.voicingMembers.byId[+loginUid].deafen = false;
if (state.voicing) {
state.voicing.deafen = false;
}
}
break;
case "deafen":
if (state.voicing) {
state.voicing.muted = payload.deafen;
}
state.voicingMembers.byId[+loginUid].deafen = payload.deafen;
state.voicingMembers.byId[+loginUid].muted = payload.deafen;
break;
case "shareScreen":
state.voicingMembers.byId[+loginUid].shareScreen = payload.shareScreen;
break;
default:
break;
}
});
}
} else {
// reset
state.voicing = payload;
state.voicingMembers = {
ids: [],
byId: {}
};
}
},
updateConnectionState(state, { payload }: PayloadAction<ConnectionState>) {
if (state.voicing) {
state.voicing.connectionState = payload;
}
},
updateVoicingNetworkQuality(state, { payload }: PayloadAction<number>) {
if (state.voicing) {
state.voicing.downlinkNetworkQuality = payload;
}
},
upsertVoiceList(state, { payload }: PayloadAction<VoiceInfo[] | VoiceInfo>) {
if (Array.isArray(payload)) {
state.list = payload;
} else {
const { id, context } = payload;
const idx = state.list.findIndex(v => v.id == id && v.context == context);
if (idx > -1) {
state.list.splice(idx, 1, payload);
} else {
state.list.push(payload);
}
}
},
addVoiceMember(state, { payload }: PayloadAction<number>) {
const notExisted = !state.voicingMembers.ids.includes(payload);
console.log("add voice member", payload, notExisted, state.voicingMembers.ids);
if (notExisted) {
state.voicingMembers.ids = [...state.voicingMembers.ids, payload];
state.voicingMembers.byId[payload] = {
speakingVolume: 0,
muted: false
};
}
},
removeVoiceMember(state, { payload }: PayloadAction<number>) {
const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload);
if (idx > -1) {
state.voicingMembers.ids.splice(idx, 1);
delete state.voicingMembers.byId[payload];
}
},
updateVoicingMember(state, { payload }: PayloadAction<{ uid: number, info: VoicingMemberInfo }>) {
const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload.uid);
if (idx > -1) {
const { uid, info } = payload;
state.voicingMembers.byId[uid] = { ...state.voicingMembers.byId[uid], ...info };
}
},
updatePin(state, { payload }: PayloadAction<{ uid: number, action: "pin" | "unpin" }>) {
const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload.uid);
if (idx > -1) {
state.voicingMembers.pin = payload.action == "pin" ? payload.uid : undefined;
}
}
},
extraReducers: (builder) => {
builder.addCase(resetAuthData, (state) => {
// reset voicing info
if (window.VOICE_CLIENT) {
window.VOICE_CLIENT.leave();
Object.entries(window.VOICE_TRACK_MAP).forEach(([uid, track]) => {
if (track && 'close' in track) {
track.close();
}
delete window.VOICE_TRACK_MAP[+uid];
});
Object.entries(window.VIDEO_TRACK_MAP).forEach(([uid, track]) => {
if (track && 'close' in track) {
track?.close();
}
delete window.VOICE_TRACK_MAP[+uid];
});
}
state.voicing = null;
state.calling = false;
state.voicingMembers = {
ids: [],
byId: {}
};
});
}
});
export const {
updateSelectDeviceId,
updateDevices,
updateCalling,
updateCallInfo,
updatePin,
updateConnectionState,
addVoiceMember,
removeVoiceMember,
upsertVoiceList,
updateVoicingInfo,
updateVoicingNetworkQuality,
updateVoicingMember
} = voiceSlice.actions;
export default voiceSlice.reducer;