Skip to content

Commit aa1d47d

Browse files
committed
wip: remove after review. This is only for testing
Signed-off-by: Dorra Jaouad <dorra.jaoued7@gmail.com>
1 parent 714338d commit aa1d47d

2 files changed

Lines changed: 205 additions & 0 deletions

File tree

src/components/CallView/CallView.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ import { callParticipantCollection, localCallParticipantModel, localMediaModel }
197197
import RemoteVideoBlocker from '../../utils/webrtc/RemoteVideoBlocker.js'
198198
import { placeholderImage, placeholderModel, placeholderName, placeholderSharedData } from './Grid/gridPlaceholders.ts'
199199
import { animateTilePromotion } from './Grid/tilePromotionTransition.ts'
200+
// TODO: development only, remove before opening a pull request
201+
import { setUpSpeakerSimulation, tearDownSpeakerSimulation } from './speakerSimulation.ts'
200202
import { useActiveSpeakers } from './useActiveSpeakers.ts'
201203
import { useWakeLock } from './useWakeLock.ts'
202204
@@ -629,9 +631,15 @@ export default {
629631
callParticipantCollection.on('remove', this._lowerHandWhenParticipantLeaves)
630632
631633
subscribe('switch-screen-to-id', this._switchScreenToId)
634+
635+
// TODO: development only, remove before opening a pull request
636+
setUpSpeakerSimulation(this.token)
632637
},
633638
634639
beforeUnmount() {
640+
// TODO: development only, remove before opening a pull request
641+
tearDownSpeakerSimulation()
642+
635643
this.debounceFetchPeers.clear?.()
636644
this.debounceHandleMovement.clear?.()
637645
this.callViewStore.setIsEmptyCallView(true)
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
/**
7+
* Development helper: fakes a conversation between six participants so the
8+
* multi-speaker layout can be exercised without a room full of people.
9+
*
10+
* NOT MEANT TO BE MERGED. Delete this file and its call in `CallView.vue`
11+
* before opening a pull request.
12+
*
13+
* Automatically starts when joining the call of `SIMULATION_TOKEN`, and can be
14+
* driven by hand from the console:
15+
*
16+
* OCA.Talk.speakerSimulation.start() // (re)start the timeline
17+
* OCA.Talk.speakerSimulation.start(4) // 4x faster
18+
* OCA.Talk.speakerSimulation.stop()
19+
* OCA.Talk.speakerSimulation.speak(2, 5) // make participant 2 speak 5s
20+
*/
21+
22+
import { reactive } from 'vue'
23+
import { callParticipantCollection } from '../../utils/webrtc/index.js'
24+
import { ConnectionState } from '../../utils/webrtc/models/CallParticipantModel.js'
25+
import { placeholderName } from './Grid/gridPlaceholders.ts'
26+
27+
/** Conversation the simulation starts in on its own. */
28+
export const SIMULATION_TOKEN = 'mmhc6xg4'
29+
30+
const PARTICIPANT_COUNT = 6
31+
32+
/**
33+
* One turn of speech: who speaks, when they start (in seconds from the
34+
* beginning of the timeline) and for how long.
35+
*
36+
* The timeline is built to exercise every rule of the layout, and loops.
37+
*/
38+
const TIMELINE: { speaker: number, at: number, duration: number }[] = [
39+
// A holds the floor: promoted after 3s, alone in the main area
40+
{ speaker: 0, at: 0, duration: 20 },
41+
// B asks a question: the main area splits in two
42+
{ speaker: 1, at: 6, duration: 6 },
43+
// Too short to count, the layout must not move
44+
{ speaker: 2, at: 14, duration: 2 },
45+
// C answers properly: three tiles, the last one centered
46+
{ speaker: 2, at: 18, duration: 8 },
47+
// D joins in: the main area is now full
48+
{ speaker: 3, at: 24, duration: 10 },
49+
// E speaks while all four spots are taken: queued, not promoted
50+
{ speaker: 4, at: 38, duration: 8 },
51+
// F too, behind E in the queue
52+
{ speaker: 5, at: 48, duration: 6 },
53+
// Nobody says anything for a while, so the spots are given up one by one:
54+
// B was silent from 12s and is demoted at 72s, which lets E in, then A
55+
// (silent from 20s) is demoted at 80s and lets F in
56+
{ speaker: 1, at: 100, duration: 6 },
57+
{ speaker: 4, at: 108, duration: 10 },
58+
]
59+
60+
/** Length of a full run, in seconds. */
61+
const TIMELINE_DURATION = 130
62+
63+
type SimulatedModel = ReturnType<typeof createModel>
64+
65+
/**
66+
* Build an object with the surface of a `CallParticipantModel` that the call
67+
* view actually reads. No peer connection is involved, so the tiles show the
68+
* participant name over an avatar rather than a video.
69+
*
70+
* @param index - position of the participant in the simulation
71+
*/
72+
function createModel(index: number) {
73+
return {
74+
attributes: reactive({
75+
peerId: `simulated-peer-${index}`,
76+
nextcloudSessionId: `simulated-session-${index}`,
77+
peer: null,
78+
screenPeer: null,
79+
actorType: 'users',
80+
actorId: `simulated-user-${index}`,
81+
userId: `simulated-user-${index}`,
82+
name: placeholderName(index),
83+
internal: false,
84+
connectionState: ConnectionState.CONNECTED,
85+
negotiating: false,
86+
connecting: false,
87+
initialConnection: false,
88+
connectedAtLeastOnce: true,
89+
stream: null,
90+
audioAvailable: true,
91+
speaking: false,
92+
videoBlocked: false,
93+
videoAvailable: false,
94+
screen: null,
95+
raisedHand: { state: false, timestamp: null },
96+
}),
97+
// Methods the call view calls on a participant model
98+
on: () => {},
99+
off: () => {},
100+
forceMute: () => {},
101+
setVideoBlocked: () => {},
102+
setSimulcastVideoQuality: () => {},
103+
getWebRtc: () => ({ connection: { getSendVideoIfAvailable: () => {} } }),
104+
destroy: () => {},
105+
}
106+
}
107+
108+
let models: SimulatedModel[] = []
109+
let timers: ReturnType<typeof setTimeout>[] = []
110+
111+
/**
112+
* Remove the simulated participants and cancel the timeline.
113+
*/
114+
function stop() {
115+
timers.forEach((timer) => clearTimeout(timer))
116+
timers = []
117+
118+
models.forEach((model) => {
119+
const index = callParticipantCollection.callParticipantModels.indexOf(model as never)
120+
if (index !== -1) {
121+
callParticipantCollection.callParticipantModels.splice(index, 1)
122+
}
123+
})
124+
models = []
125+
126+
console.info('[speaker simulation] stopped')
127+
}
128+
129+
/**
130+
* Make one of the simulated participants speak.
131+
*
132+
* @param index - position of the participant in the simulation
133+
* @param duration - how long they speak, in seconds
134+
*/
135+
function speak(index: number, duration: number) {
136+
const model = models[index]
137+
if (!model) {
138+
console.warn('[speaker simulation] no participant', index)
139+
return
140+
}
141+
142+
model.attributes.speaking = true
143+
timers.push(setTimeout(() => {
144+
model.attributes.speaking = false
145+
}, duration * 1000))
146+
}
147+
148+
/**
149+
* Add the simulated participants and play the timeline in a loop.
150+
*
151+
* @param speed - how much faster than real time to play, 1 by default. The
152+
* promotion and demotion delays are NOT scaled, so a high speed shows what
153+
* the layout does when everybody talks over each other.
154+
*/
155+
function start(speed = 1) {
156+
stop()
157+
158+
models = Array.from({ length: PARTICIPANT_COUNT }, (_, index) => createModel(index))
159+
callParticipantCollection.callParticipantModels.push(...(models as never[]))
160+
161+
const scheduleRun = () => {
162+
TIMELINE.forEach(({ speaker, at, duration }) => {
163+
timers.push(setTimeout(() => speak(speaker, duration / speed), (at / speed) * 1000))
164+
})
165+
timers.push(setTimeout(scheduleRun, (TIMELINE_DURATION / speed) * 1000))
166+
}
167+
scheduleRun()
168+
169+
console.info(`[speaker simulation] started with ${PARTICIPANT_COUNT} participants at ${speed}x`)
170+
}
171+
172+
/**
173+
* Expose the controls and start the simulation if the call is the one it was
174+
* written for.
175+
*
176+
* @param token - token of the conversation being joined
177+
*/
178+
export function setUpSpeakerSimulation(token: string) {
179+
if (window.OCA?.Talk) {
180+
// @ts-expect-error: OCA is not typed
181+
window.OCA.Talk.speakerSimulation = { start, stop, speak }
182+
}
183+
184+
if (token === SIMULATION_TOKEN) {
185+
start()
186+
}
187+
}
188+
189+
/**
190+
* Counterpart of {@link setUpSpeakerSimulation}, to be called when the call
191+
* view goes away.
192+
*/
193+
export function tearDownSpeakerSimulation() {
194+
stop()
195+
// @ts-expect-error: OCA is not typed
196+
delete window.OCA?.Talk?.speakerSimulation
197+
}

0 commit comments

Comments
 (0)