import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { LightAdjustmentProcessor } from '@shiguredo/light-adjustment' import { NoiseSuppressionProcessor } from '@shiguredo/noise-suppression' import { VirtualBackgroundProcessor } from '@shiguredo/virtual-background' import type { ConnectionPublisher, ConnectionSubscriber, DataChannelConfiguration, Role, } from 'sora-js-sdk' import { WORKER_SCRIPT } from '@/constants' import type { AlertMessage, DataChannelMessage, DebugType, LogMessage, NotifyMessage, PushMessage, SignalingMessage, SoraDevtoolsState, TimelineMessage, } from '@/types' import packageJSON from '../../package.json' const initialState: SoraDevtoolsState = { alertMessages: [], audio: true, audioBitRate: '', audioCodecType: '', audioContentHint: '', audioInput: '', audioInputDevices: [], audioOutput: '', audioOutputDevices: [], autoGainControl: '', blurRadius: '', bundleId: '', enabledBundleId: false, clientId: '', channelId: 'sora', googCpuOveruseDetection: null, timelineMessages: [], debug: false, debugFilterText: '', debugType: 'timeline', dataChannelSignaling: '', dataChannels: '', dataChannelMessages: [], displayResolution: '', e2ee: false, echoCancellation: '', echoCancellationType: '', enabledClientId: false, enabledDataChannel: false, enabledDataChannels: false, enabledForwardingFilter: false, enabledMetadata: false, enabledSignalingNotifyMetadata: false, enabledSignalingUrlCandidates: false, enabledVideoVP9Params: false, enabledVideoH264Params: false, enabledVideoAV1Params: false, audioStreamingLanguageCode: '', enabledAudioStreamingLanguageCode: false, audioLyraParamsBitrate: '', fakeVolume: '0', fakeContents: { worker: null, colorCode: 0, gainNode: null, }, frameRate: '', soraContents: { connectionStatus: 'initializing', reconnecting: false, reconnectingTrials: 0, sora: null, connectionId: null, clientId: null, localMediaStream: null, remoteMediaStreams: [], prevStatsReport: [], statsReport: [], datachannels: [], }, ignoreDisconnectWebSocket: '', logMessages: [], mediaProcessorsNoiseSuppression: false, mediaType: 'getUserMedia', metadata: '', multistream: 'true', mute: false, noiseSuppression: '', notifyMessages: [], pushMessages: [], resolution: '', showStats: false, simulcast: '', spotlight: '', signalingMessages: [], signalingNotifyMetadata: '', signalingUrlCandidates: [], forwardingFilter: '', simulcastRid: '', spotlightNumber: '', spotlightFocusRid: '', spotlightUnfocusRid: '', focusedSpotlightConnectionIds: {}, video: true, videoBitRate: '', videoCodecType: '', videoContentHint: '', videoInput: '', videoInputDevices: [], videoVP9Params: '', videoH264Params: '', videoAV1Params: '', version: packageJSON.version, cameraDevice: true, videoTrack: true, micDevice: true, audioTrack: true, role: 'sendrecv', reconnect: false, apiUrl: null, aspectRatio: '', resizeMode: '', lightAdjustment: '', lightAdjustmentProcessor: null, noiseSuppressionProcessor: null, virtualBackgroundProcessor: null, facingMode: '', } export const slice = createSlice({ name: 'soraDevtools', initialState, reducers: { resetState: (state) => { Object.assign(state, initialState) }, setAudio: (state, action: PayloadAction) => { state.audio = action.payload }, setAudioInput: (state, action: PayloadAction) => { state.audioInput = action.payload }, setAudioOutput: (state, action: PayloadAction) => { state.audioOutput = action.payload }, setAudioBitRate: (state, action: PayloadAction) => { state.audioBitRate = action.payload }, setAudioCodecType: (state, action: PayloadAction) => { state.audioCodecType = action.payload }, setAudioContentHint: (state, action: PayloadAction) => { state.audioContentHint = action.payload if (state.soraContents.localMediaStream) { for (const track of state.soraContents.localMediaStream.getAudioTracks()) { track.contentHint = state.audioContentHint } } }, setAutoGainControl: (state, action: PayloadAction) => { state.autoGainControl = action.payload }, setClientId: (state, action: PayloadAction) => { state.clientId = action.payload }, setChannelId: (state, action: PayloadAction) => { state.channelId = action.payload }, setTimelineMessage: (state, action: PayloadAction) => { state.timelineMessages.push(action.payload) }, setDataChannelSignaling: ( state, action: PayloadAction, ) => { state.dataChannelSignaling = action.payload }, setDataChannels: (state, action: PayloadAction) => { state.dataChannels = action.payload }, setDataChannelMessage: (state, action: PayloadAction) => { state.dataChannelMessages.push(action.payload) }, setGoogCpuOveruseDetection: (state, action: PayloadAction) => { state.googCpuOveruseDetection = action.payload }, setDisplayResolution: ( state, action: PayloadAction, ) => { state.displayResolution = action.payload }, setE2EE: (state, action: PayloadAction) => { state.e2ee = action.payload }, setEchoCancellation: (state, action: PayloadAction) => { state.echoCancellation = action.payload }, setEchoCancellationType: ( state, action: PayloadAction, ) => { state.echoCancellationType = action.payload }, setEnabledClientId: (state, action: PayloadAction) => { state.enabledClientId = action.payload }, setEnabledDataChannels: (state, action: PayloadAction) => { state.enabledDataChannels = action.payload }, setEnabledDataChannel: (state, action: PayloadAction) => { state.enabledDataChannel = action.payload }, setEnabledMetadata: (state, action: PayloadAction) => { state.enabledMetadata = action.payload }, setIgnoreDisconnectWebSocket: ( state, action: PayloadAction, ) => { state.ignoreDisconnectWebSocket = action.payload }, setSignalingMessage: (state, action: PayloadAction) => { state.signalingMessages.push(action.payload) }, setEnabledForwardingFilter: (state, action: PayloadAction) => { state.enabledForwardingFilter = action.payload }, setEnabledSignalingNotifyMetadata: (state, action: PayloadAction) => { state.enabledSignalingNotifyMetadata = action.payload }, setEnabledSignalingUrlCandidates: (state, action: PayloadAction) => { state.enabledSignalingUrlCandidates = action.payload }, setEnabledVideoVP9Params: (state, action: PayloadAction) => { state.enabledVideoVP9Params = action.payload }, setEnabledVideoH264Params: (state, action: PayloadAction) => { state.enabledVideoH264Params = action.payload }, setEnabledVideoAV1Params: (state, action: PayloadAction) => { state.enabledVideoAV1Params = action.payload }, setFakeVolume: (state, action: PayloadAction) => { const volume = parseFloat(action.payload) if (isNaN(volume)) { state.fakeVolume = '0' } else if (1 < volume) { state.fakeVolume = '1' } else { state.fakeVolume = String(volume) } if (state.fakeContents.gainNode) { state.fakeContents.gainNode.gain.setValueAtTime(parseFloat(state.fakeVolume), 0) } }, setFakeContentsGainNode: (state, action: PayloadAction) => { state.fakeContents.gainNode = action.payload }, setInitialFakeContents: (state) => { // Fake canvas の背景色で使う color code を生成 state.fakeContents.colorCode = Math.floor(Math.random() * 0xffffff) // Fake canvas を表示しているブラウザタブがバックグラウンドへ移動しても canvas のレンダリングを続けるために worker を生成 if (URL.createObjectURL) { const url = URL.createObjectURL( new Blob([WORKER_SCRIPT], { type: 'application/javascript' }), ) state.fakeContents.worker = new Worker(url) } }, setFrameRate: (state, action: PayloadAction) => { state.frameRate = action.payload }, setMute: (state, action: PayloadAction) => { state.mute = action.payload }, setNoiseSuppression: (state, action: PayloadAction) => { state.noiseSuppression = action.payload }, setMediaType: (state, action: PayloadAction) => { // NOTE(yuito): 現時点で window.CropTarget は正式リリースではないので、API がない場合は使用できないようにする if ( action.payload === 'mediacaptureRegion' && (typeof window === 'undefined' || window.CropTarget === undefined) ) { state.mediaType = 'getUserMedia' } else { state.mediaType = action.payload } }, setMetadata: (state, action: PayloadAction) => { state.metadata = action.payload }, setResolution: (state, action: PayloadAction) => { state.resolution = action.payload }, setSignalingNotifyMetadata: (state, action: PayloadAction) => { state.signalingNotifyMetadata = action.payload }, setSignalingUrlCandidates: (state, action: PayloadAction) => { state.signalingUrlCandidates = action.payload }, setForwardingFilter: (state, action: PayloadAction) => { state.forwardingFilter = action.payload }, setSimulcastRid: (state, action: PayloadAction) => { state.simulcastRid = action.payload }, setSpotlightNumber: (state, action: PayloadAction) => { state.spotlightNumber = action.payload }, setSpotlightFocusRid: ( state, action: PayloadAction, ) => { state.spotlightFocusRid = action.payload }, setSpotlightUnfocusRid: ( state, action: PayloadAction, ) => { state.spotlightUnfocusRid = action.payload }, setVideo: (state, action: PayloadAction) => { state.video = action.payload }, setVideoInput: (state, action: PayloadAction) => { state.videoInput = action.payload }, setVideoBitRate: (state, action: PayloadAction) => { state.videoBitRate = action.payload }, setVideoCodecType: (state, action: PayloadAction) => { state.videoCodecType = action.payload }, setVideoContentHint: (state, action: PayloadAction) => { state.videoContentHint = action.payload if (state.soraContents.localMediaStream) { for (const track of state.soraContents.localMediaStream.getVideoTracks()) { track.contentHint = state.videoContentHint } } }, setVideoVP9Params: (state, action: PayloadAction) => { state.videoVP9Params = action.payload }, setVideoH264Params: (state, action: PayloadAction) => { state.videoH264Params = action.payload }, setVideoAV1Params: (state, action: PayloadAction) => { state.videoAV1Params = action.payload }, setSora: (state, action: PayloadAction) => { // `Type instantiation is excessively deep and possibly infinite` エラーが出るので any に type casting する // eslint-disable-next-line @typescript-eslint/no-explicit-any state.soraContents.sora = action.payload if (state.soraContents.sora) { state.soraContents.connectionId = state.soraContents.sora.connectionId state.soraContents.clientId = state.soraContents.sora.clientId } else { state.soraContents.connectionId = null state.soraContents.clientId = null state.soraContents.datachannels = [] } }, setSoraConnectionStatus: ( state, action: PayloadAction, ) => { state.soraContents.connectionStatus = action.payload }, setSoraReconnecting: ( state, action: PayloadAction, ) => { state.soraContents.reconnecting = action.payload if (state.soraContents.reconnecting === false) { state.soraContents.reconnectingTrials = 0 } }, setSoraReconnectingTrials: ( state, action: PayloadAction, ) => { state.soraContents.reconnectingTrials = action.payload }, setSoraDataChannels: (state, action: PayloadAction) => { state.soraContents.datachannels.push(action.payload) }, setLocalMediaStream: (state, action: PayloadAction) => { if (state.soraContents.localMediaStream) { state.soraContents.localMediaStream.getTracks().forEach((track) => { track.stop() }) } state.soraContents.localMediaStream = action.payload }, setRemoteMediaStream: (state, action: PayloadAction) => { state.soraContents.remoteMediaStreams.push(action.payload) }, setStatsReport: (state, action: PayloadAction) => { state.soraContents.prevStatsReport = state.soraContents.statsReport state.soraContents.statsReport = action.payload }, removeRemoteMediaStream: (state, action: PayloadAction) => { const remoteMediaStreams = state.soraContents.remoteMediaStreams.filter( (stream) => stream.id !== action.payload, ) state.soraContents.remoteMediaStreams = remoteMediaStreams }, removeAllRemoteMediaStreams: (state) => { state.soraContents.remoteMediaStreams = [] }, setAudioInputDevices: (state, action: PayloadAction) => { state.audioInputDevices = action.payload }, setVideoInputDevices: (state, action: PayloadAction) => { state.videoInputDevices = action.payload }, setAudioOutputDevices: (state, action: PayloadAction) => { state.audioOutputDevices = action.payload }, setSoraInfoAlertMessage: (state, action: PayloadAction) => { const alertMessage: AlertMessage = { title: 'Sora info', type: 'info', message: action.payload, timestamp: new Date().getTime(), } setAlertMessagesAndLogMessages(state.alertMessages, state.logMessages, alertMessage) }, setSoraErrorAlertMessage: (state, action: PayloadAction) => { const alertMessage: AlertMessage = { title: 'Sora error', type: 'error', message: action.payload, timestamp: new Date().getTime(), } setAlertMessagesAndLogMessages(state.alertMessages, state.logMessages, alertMessage) }, setAPIInfoAlertMessage: (state, action: PayloadAction) => { const alertMessage: AlertMessage = { title: 'API info', type: 'info', message: action.payload, timestamp: new Date().getTime(), } setAlertMessagesAndLogMessages(state.alertMessages, state.logMessages, alertMessage) }, setAPIErrorAlertMessage: (state, action: PayloadAction) => { const alertMessage: AlertMessage = { title: 'API error', type: 'error', message: action.payload, timestamp: new Date().getTime(), } setAlertMessagesAndLogMessages(state.alertMessages, state.logMessages, alertMessage) }, deleteAlertMessage: (state, action: PayloadAction) => { const filterdAlertMessages = state.alertMessages.filter( (alertMessage) => alertMessage.timestamp !== action.payload, ) state.alertMessages = filterdAlertMessages }, setDebug: (state, action: PayloadAction) => { state.debug = action.payload }, setDebugFilterText: (state, action: PayloadAction) => { state.debugFilterText = action.payload }, setDebugType: (state, action: PayloadAction) => { state.debugFilterText = '' state.debugType = action.payload }, setLogMessages: (state, action: PayloadAction) => { state.logMessages.push({ timestamp: new Date().getTime(), message: { title: action.payload.title, description: action.payload.description, }, }) }, setNotifyMessages: (state, action: PayloadAction) => { state.notifyMessages.push(action.payload) }, setPushMessages: (state, action: PayloadAction) => { state.pushMessages.push(action.payload) }, setFocusedSpotlightConnectionId: (state, action: PayloadAction) => { state.focusedSpotlightConnectionIds[action.payload] = true }, setUnFocusedSpotlightConnectionId: (state, action: PayloadAction) => { state.focusedSpotlightConnectionIds[action.payload] = false }, deleteFocusedSpotlightConnectionId: (state, action: PayloadAction) => { delete state.focusedSpotlightConnectionIds[action.payload] }, setShowStats: (state, action: PayloadAction) => { state.showStats = action.payload }, setCameraDevice: (state, action: PayloadAction) => { state.cameraDevice = action.payload }, setMicDevice: (state, action: PayloadAction) => { state.micDevice = action.payload }, setAudioTrack: (state, action: PayloadAction) => { state.audioTrack = action.payload if (state.soraContents.localMediaStream) { for (const track of state.soraContents.localMediaStream.getAudioTracks()) { track.enabled = state.audioTrack } } }, setVideoTrack: (state, action: PayloadAction) => { state.videoTrack = action.payload if (state.soraContents.localMediaStream) { for (const track of state.soraContents.localMediaStream.getVideoTracks()) { track.enabled = state.videoTrack } } }, setRole: (state, action: PayloadAction) => { state.role = action.payload }, setMultistream: (state, action: PayloadAction) => { state.multistream = action.payload }, setSimulcast: (state, action: PayloadAction) => { state.simulcast = action.payload }, setSpotlight: (state, action: PayloadAction) => { state.spotlight = action.payload }, setReconnect: (state, action: PayloadAction) => { state.reconnect = action.payload }, setApiUrl: (state, action: PayloadAction) => { state.apiUrl = action.payload }, clearDataChannelMessages: (state) => { state.dataChannelMessages = [] }, setAspectRatio: (state, action: PayloadAction) => { state.aspectRatio = action.payload }, setResizeMode: (state, action: PayloadAction) => { state.resizeMode = action.payload }, setLightAdjustment: (state, action: PayloadAction) => { if (action.payload !== '' && state.lightAdjustmentProcessor === null) { const processor = new LightAdjustmentProcessor() state.lightAdjustmentProcessor = processor } state.lightAdjustment = action.payload }, setBlurRadius: (state, action: PayloadAction) => { if (action.payload !== '' && state.virtualBackgroundProcessor === null) { const assetsPath = process.env.NEXT_PUBLIC_VIRTUAL_BACKGROUND_ASSETS_PATH || '' const processor = new VirtualBackgroundProcessor(assetsPath) state.virtualBackgroundProcessor = processor } state.blurRadius = action.payload }, setMediaProcessorsNoiseSuppression: ( state, action: PayloadAction, ) => { if (action.payload && state.noiseSuppressionProcessor === null) { const assetsPath = process.env.NEXT_PUBLIC_NOISE_SUPPRESSION_ASSETS_PATH || '' const processor = new NoiseSuppressionProcessor(assetsPath) state.noiseSuppressionProcessor = processor } state.mediaProcessorsNoiseSuppression = action.payload }, setBundleId: (state, action: PayloadAction) => { state.bundleId = action.payload }, setEnabledBundleId: (state, action: PayloadAction) => { state.enabledBundleId = action.payload }, setFacingMode: (state, action: PayloadAction) => { state.facingMode = action.payload }, setAudioStreamingLanguageCode: ( state, action: PayloadAction, ) => { state.audioStreamingLanguageCode = action.payload }, setEnabledAudioStreamingLanguageCode: ( state, action: PayloadAction, ) => { state.enabledAudioStreamingLanguageCode = action.payload }, setAudioLyraParamsBitrate: ( state, action: PayloadAction, ) => { state.audioLyraParamsBitrate = action.payload }, }, }) function setAlertMessagesAndLogMessages( alertMessages: SoraDevtoolsState['alertMessages'], logMessages: SoraDevtoolsState['logMessages'], alertMessage: AlertMessage, ): void { if (10 <= alertMessages.length) { for (let i = 0; i <= alertMessages.length - 5; i++) { alertMessages.pop() } } alertMessages.unshift(alertMessage) logMessages.push({ timestamp: alertMessage.timestamp, message: { title: `ALERT MESSAGE ${alertMessage.title}`, description: JSON.stringify({ title: alertMessage.title, type: alertMessage.type, message: alertMessage.message, }), }, }) }