E5F1 Fix unneeded messages when sending initial state by danxuliu · Pull Request #16420 · nextcloud/spreed · GitHub
[go: up one dir, main page]

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/CallView/shared/VideoVue.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createStore } from 'vuex'
import VideoVue from './VideoVue.vue'
import storeConfig from '../../../store/storeConfig.js'
import EmitterMixin from '../../../utils/EmitterMixin.js'
import CallParticipantModel from '../../../utils/webrtc/models/CallParticipantModel.js'
import { CallParticipantModel } from '../../../utils/webrtc/models/CallParticipantModel.js'

describe('VideoVue.vue', () => {
let store
Expand Down
29 changes: 29 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,33 @@ declare global {
let __webpack_public_path__: string
}

// Augment models with the public methods added to their prototype by the
// EmitterMixin.
/* eslint-disable @typescript-eslint/no-explicit-any --
* Arguments of function types are contravariant in strict mode, so the
* "any" type is required here, as the "unknown" type would prevent
* assigning a function type with narrower argument types.
*/
declare module './utils/webrtc/models/CallParticipantCollection.js' {
interface CallParticipantCollection {
on(event: string, handler: (callParticipantCollection: CallParticipantCollection, ...args: any[]) => void): void
off(event: string, handler: (callParticipantCollection: CallParticipantCollection, ...args: any[]) => void): void
}
}

declare module './utils/webrtc/models/CallParticipantModel.js' {
interface CallParticipantModel {
on(event: string, handler: (callParticipantModel: CallParticipantModel, ...args: any[]) => void): void
off(event: string, handler: (callParticipantModel: CallParticipantModel, ...args: any[]) => void): void
}
}

declare module './utils/webrtc/models/LocalCallParticipantModel.js' {
interface LocalCallParticipantModel {
on(event: string, handler: (localCallParticipantModel: LocalCallParticipantModel, ...args: any[]) => void): void
off(event: string, handler: (localCallParticipantModel: LocalCallParticipantModel, ...args: any[]) => void): void
}
}
/* eslint-enable @typescript-eslint/no-explicit-any */

export {}
77 changes: 77 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,83 @@ export type JoinRoomFullResponse = {
export type fetchPeersResponse = ApiResponse<operations['call-get-peers-for-call']['responses'][200]['content']['application/json']>
export type callSIPDialOutResponse = ApiResponse<operations['call-sip-dial-out']['responses'][201]['content']['application/json']>

export type CallParticipantCollection = {
callParticipantModels: Array<CallParticipantModel>

/* eslint-disable @typescript-eslint/no-explicit-any --
* Arguments of function types are contravariant in strict mode, so the
* "any" type is required here, as the "unknown" type would prevent
* assigning a function type with narrower argument types.
*/
on(event: string, handler: (callParticipantCollection: CallParticipantCollection, ...args: any[]) => void): void
off(event: string, handler: (callParticipantCollection: CallParticipantCollection, ...args: any[]) => void): void
/* eslint-enable @typescript-eslint/no-explicit-any */

add(options: CallParticipantModelOptions): CallParticipantModel
get(peerId: string): CallParticipantModel | undefined
remove(peerId: string): boolean
}

export type CallParticipantModelOptions = {
peerId: string
webRtc: WebRtc
}

export type CallParticipantModel = {
/* eslint-disable @typescript-eslint/no-explicit-any --
* Arguments of function types are contravariant in strict mode, so the
* "any" type is required here, as the "unknown" type would prevent
* assigning a function type with narrower argument types.
*/
on(event: string, handler: (callParticipantModel: CallParticipantModel, ...args: any[]) => void): void
off(event: string, handler: (callParticipantModel: CallParticipantModel, ...args: any[]) => void): void
/* eslint-enable @typescript-eslint/no-explicit-any */

get(key: string): unknown
set(key: string, value: unknown): void
}

export type LocalCallParticipantModel = {
/* eslint-disable @typescript-eslint/no-explicit-any --
* Arguments of function types are contravariant in strict mode, so the
* "any" type is required here, as the "unknown" type would prevent
* assigning a function type with narrower argument types.
*/
on(event: string, handler: (localCallParticipantModel: LocalCallParticipantModel, ...args: any[]) => void): void
off(event: string, handler: (localCallParticipantModel: LocalCallParticipantModel, ...args: any[]) => void): void
/* eslint-enable @typescript-eslint/no-explicit-any */

get(key: string): unknown
set(key: string, value: unknown): void
}

export type Signaling = {
settings: {
userId: string | null
}
}

export type InternalWebRtc = {
isAudioEnabled(): boolean
isVideoEnabled(): boolean
isSpeaking(): boolean
}

export type WebRtc = {
on(event: string, handler: () => void): void
off(event: string, handler: () => void): void
emit(event: string): void

sendDataChannelToAll(channel: string, message: string, payload?: string | object): void
sendToAll(message: string, payload: object): void

sendDataChannelTo(peerId: string, channel: string, message: string, payload?: string | object): void
sendTo(peerId: string, messageType: string, payload: object): void

connection: Signaling
webrtc: InternalWebRtc
}

// Participants
export type ParticipantStatus = {
status?: string | null
Expand Down
6 changes: 6 additions & 0 deletions src/types/vendor/wildemitter.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare module 'wildemitter'
187 changes: 187 additions & 0 deletions src/utils/webrtc/LocalStateBroadcas 9B9E ter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type {
CallParticipantModel as CallParticipantModelType,
InternalWebRtc,
WebRtc,
} from '../../types/index.ts'

import {
afterEach,
beforeEach,
describe,
expect,
test,
vi,
} from 'vitest'
import WildEmitter from 'wildemitter'
import { LocalStateBroadcaster } from './LocalStateBroadcaster.ts'
import { CallParticipantCollection } from './models/CallParticipantCollection.js'
import { LocalCallParticipantModel } from './models/LocalCallParticipantModel.js'

class BaseLocalStateBroadcaster extends LocalStateBroadcaster {
protected _handleAddCallParticipantModel(callParticipantCollection: CallParticipantCollection, callParticipantModel: CallParticipantModelType): void {
// Not used in base class tests
}

protected _handleRemoveCallParticipantModel(callParticipantCollection: CallParticipantCollection, callParticipantModel: CallParticipantModelType): void {
// Not used in base class tests
}
}

describe('LocalStateBroadcaster', () => {
let webRtc: WebRtc
let internalWebRtc: InternalWebRtc
let callParticipantCollection: CallParticipantCollection
let localCallParticipantModel: LocalCallParticipantModel

let localStateBroadcaster: LocalStateBroadcaster

beforeEach(() => {
internalWebRtc = new (function(this: InternalWebRtc) {
this.isAudioEnabled = vi.fn()
this.isSpeaking = vi.fn()
this.isVideoEnabled = vi.fn()
} as any)()

const signaling = {
settings: {
userId: null,
},
}

webRtc = new (function(this: WebRtc) {
WildEmitter.mixin(this)

this.connection = signaling
this.webrtc = internalWebRtc

this.sendDataChannelToAll = vi.fn()
this.sendToAll = vi.fn()

this.sendDataChannelTo = vi.fn()
this.sendTo = vi.fn()
} as any)()

callParticipantCollection = new CallParticipantCollection()

localCallParticipantModel = new LocalCallParticipantModel()
})

afterEach(() => {
vi.clearAllMocks()
})

test('enable audio', () => {
localStateBroadcaster = new BaseLocalStateBroadcaster(webRtc, callParticipantCollection, localCallParticipantModel)

webRtc.emit('audioOn')

expect(webRtc.sendDataChannelToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendDataChannelToAll).toHaveBeenCalledWith('status', 'audioOn')

expect(webRtc.sendToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendToAll).toHaveBeenCalledWith('unmute', { name: 'audio' })
})

test('disable audio', () => {
localStateBroadcaster = new BaseLocalStateBroadcaster(webRtc, callParticipantCollection, localCallParticipantModel)

webRtc.emit('audioOff')

expect(webRtc.sendDataChannelToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendDataChannelToAll).toHaveBeenCalledWith('status', 'audioOff')

expect(webRtc.sendToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendToAll).toHaveBeenCalledWith('mute', { name: 'audio' })
})

test('enable speaking', () => {
localStateBroadcaster = new BaseLocalStateBroadcaster(webRtc, callParticipantCollection, localCallParticipantModel)

webRtc.emit('speaking')

expect(webRtc.sendDataChannelToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendDataChannelToAll).toHaveBeenCalledWith('status', 'speaking')
})

test('disable speaking', () => {
localStateBroadcaster = new BaseLocalStateBroadcaster(webRtc, callParticipantCollection, localCallParticipantModel)

webRtc.emit('stoppedSpeaking')

expect(webRtc.sendDataChannelToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendDataChannelToAll).toHaveBeenCalledWith('status', 'stoppedSpeaking')
})

test('enable video', () => {
localStateBroadcaster = new BaseLocalStateBroadcaster(webRtc, callParticipantCollection, localCallParticipantModel)

webRtc.emit('videoOn')

expect(webRtc.sendDataChannelToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendDataChannelToAll).toHaveBeenCalledWith('status', 'videoOn')

expect(webRtc.sendToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendToAll).toHaveBeenCalledWith('unmute', { name: 'video' })
})

test('disable video', () => {
localStateBroadcaster = new BaseLocalStateBroadcaster(webRtc, callParticipantCollection, localCallParticipantModel)

webRtc.emit('videoOff')

expect(webRtc.sendDataChannelToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendDataChannelToAll).toHaveBeenCalledWith('status', 'videoOff')

expect(webRtc.sendToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendToAll).toHaveBeenCalledWith('mute', { name: 'video' })
})

test('set nick as user', () => {
webRtc.connection.settings.userId = 'theUserId'

localStateBroadcaster = new BaseLocalStateBroadcaster(webRtc, callParticipantCollection, localCallParticipantModel)

localCallParticipantModel.set('name', 'theName')

expect(webRtc.sendDataChannelToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendDataChannelToAll).toHaveBeenCalledWith('status', 'nickChanged', { name: 'theName', userid: 'theUserId' })

expect(webRtc.sendToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendToAll).toHaveBeenCalledWith('nickChanged', { name: 'theName' })
})

test('set nick as guest', () => {
localStateBroadcaster = new BaseLocalStateBroadcaster(webRtc, callParticipantCollection, localCallParticipantModel)

localCallParticipantModel.set('name', 'theName')

expect(webRtc.sendDataChannelToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendDataChannelToAll).toHaveBeenCalledWith('status', 'nickChanged', 'theName')

expect(webRtc.sendToAll).toHaveBeenCalledTimes(1)
expect(webRtc.sendToAll).toHaveBeenCalledWith('nickChanged', { name: 'theName' })
})

test('change state after destroying', () => {
localStateBroadcaster = new BaseLocalStateBroadcaster(webRtc, callParticipantCollection, localCallParticipantModel)

localStateBroadcaster.destroy()

webRtc.emit('audioOn')
webRtc.emit('audioOff')
webRtc.emit('speaking')
webRtc.emit('stoppedSpeaking')
webRtc.emit('videoOn')
webRtc.emit('videoOff')

localCallParticipantModel.set('name', 'theName')

expect(webRtc.sendDataChannelToAll).toHaveBeenCalledTimes(0)
expect(webRtc.sendToAll).toHaveBeenCalledTimes(0)
})
})
Loading
Loading
0