snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
251
tests/specs/jaas/transcriptions.spec.ts
Normal file
251
tests/specs/jaas/transcriptions.spec.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import type WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
const asyncTranscriptionValues = [
|
||||
...expectations.jaas.transcription.jigasiTranscription ? [ false ] : [],
|
||||
...expectations.jaas.transcription.asyncTranscription ? [ true ] : []
|
||||
];
|
||||
|
||||
for (const asyncTranscriptions of asyncTranscriptionValues) {
|
||||
describe(`Transcription (async=${asyncTranscriptions})`, () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
let webhooksProxy: WebhookProxy;
|
||||
|
||||
async function clearTranscriptionStatusChange() {
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until a transcribingStatusChanged iFrame event is received for both p1 and p2, with a specific value
|
||||
* for the "on" field.
|
||||
* Note that addEventListener for 'transcriptionChunkReceived' should have been called prior to this function.
|
||||
*/
|
||||
async function waitForTranscriptionStatusChange(expectedOn: boolean) {
|
||||
for (const p of [ p1, p2 ]) {
|
||||
const event = await p.driver.waitUntil(
|
||||
() => p.getIframeAPI().getEventResult('transcribingStatusChanged'),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: `transcribingStatusChanged event not received on ${p.name}`
|
||||
});
|
||||
|
||||
if (event.on !== expectedOn) {
|
||||
throw new Error(`Expected transcribing to be ${expectedOn} for ${p.name}, got ${event.on}`);
|
||||
}
|
||||
if (!expectedOn && !asyncTranscriptions) {
|
||||
// The "stopped" event is sometimes fired before the jigasi participant leaves. If we re-start
|
||||
// transcription before jigasi has left, jicofo will reject the request.
|
||||
await p.switchToIFrame();
|
||||
await p.waitForParticipants(
|
||||
1,
|
||||
'Unexpected number of participants. Jigasi failed to leave?');
|
||||
await p.switchToMainFrame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
webhooksProxy.defaultMeetingSettings = { asyncTranscriptions };
|
||||
|
||||
p1 = await joinJaasMuc({
|
||||
name: 'p1',
|
||||
token: t({ room, moderator: true }),
|
||||
iFrameApi: true
|
||||
});
|
||||
|
||||
const transcriptionEnabled = await p1.execute(() => config.transcription?.enabled);
|
||||
|
||||
expect(transcriptionEnabled).toBe(expectations.jaas.transcriptionEnabled);
|
||||
|
||||
const roomMetadata = await p1.getRoomMetadata();
|
||||
|
||||
if (asyncTranscriptions) {
|
||||
expect(roomMetadata.asyncTranscription).toBe(true);
|
||||
} else {
|
||||
expect(roomMetadata.asyncTranscription).toBeFalsy();
|
||||
}
|
||||
|
||||
p2 = await joinJaasMuc({
|
||||
name: 'p2',
|
||||
token: t({ room }),
|
||||
iFrameApi: true
|
||||
}, {
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
p1.switchToMainFrame(),
|
||||
p2.switchToMainFrame(),
|
||||
]);
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
|
||||
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
|
||||
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
|
||||
});
|
||||
|
||||
it('toggle subtitles', async () => {
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
await waitForTranscriptionStatusChange(true);
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
|
||||
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
await waitForTranscriptionStatusChange(false);
|
||||
});
|
||||
|
||||
it('set subtitles on and off', async () => {
|
||||
// we need to clear results or the last one will be used, from the previous time subtitles were on
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
webhooksProxy.clearCache();
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', true, true);
|
||||
|
||||
await waitForTranscriptionStatusChange(true);
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
|
||||
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', false);
|
||||
|
||||
await waitForTranscriptionStatusChange(false);
|
||||
});
|
||||
|
||||
it('start/stop transcriptions via recording', async () => {
|
||||
// we need to clear results or the last one will be used, from the previous time subtitles were on
|
||||
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await clearTranscriptionStatusChange();
|
||||
webhooksProxy.clearCache();
|
||||
|
||||
await p1.getIframeAPI().executeCommand('startRecording', { transcription: true });
|
||||
|
||||
await waitForTranscriptionStatusChange(true);
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
|
||||
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().executeCommand('stopRecording', 'file', true);
|
||||
|
||||
await waitForTranscriptionStatusChange(false);
|
||||
|
||||
await p1.getIframeAPI().executeCommand('hangup');
|
||||
await p2.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
// sometimes events are not immediately received,
|
||||
// let's wait for destroy event before waiting for those that depends on it
|
||||
await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
if (!asyncTranscriptions || expectations.jaas.transcription.asyncTranscriptionWebhook) {
|
||||
const event: {
|
||||
data: {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
|
||||
|
||||
expect(event.eventType).toBe('TRANSCRIPTION_UPLOADED');
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param p1
|
||||
* @param p2
|
||||
* @param webhooksProxy
|
||||
* @param asyncTranscription Whether async transciptions are used.
|
||||
*/
|
||||
async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksProxy: WebhookProxy, asyncTranscription = false) {
|
||||
const p1Promise = p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
.getEventResult('transcriptionChunkReceived'), {
|
||||
timeout: 60000,
|
||||
timeoutMsg: 'transcriptionChunkReceived event not received on p1 side'
|
||||
});
|
||||
|
||||
const p2Promise = p2.driver.waitUntil(() => p2.getIframeAPI()
|
||||
.getEventResult('transcriptionChunkReceived'), {
|
||||
timeout: 60000,
|
||||
timeoutMsg: 'transcriptionChunkReceived event not received on p2 side'
|
||||
});
|
||||
|
||||
const [ p1Event, p2Event ] = await Promise.all([ p1Promise, p2Promise ]);
|
||||
const p1Id = await p1.getEndpointId();
|
||||
|
||||
const p1Transcript = p1Event.data.stable || p1Event.data.final;
|
||||
const p2Transcript = p2Event.data.stable || p2Event.data.final;
|
||||
|
||||
expect(p2Transcript).toPartiallyMatch(p1Transcript, 'p2 transcript', 'p1 transcript');
|
||||
expect(p2Event.data.language).toBe(p1Event.data.language);
|
||||
expect(p2Event.data.messageID).toBe(p1Event.data.messageID);
|
||||
expect(p1Event.data.participant.id).toBe(p1Id);
|
||||
expect(p2Event.data.participant.id).toBe(p1Id);
|
||||
|
||||
if (!asyncTranscription) {
|
||||
expect(p1Event.data.participant.name).toBe(p1.name);
|
||||
expect(p2Event.data.participant.name).toBe(p1.name);
|
||||
}
|
||||
|
||||
if (!asyncTranscription || expectations.jaas.transcription.asyncTranscriptionWebhook) {
|
||||
// In async mode the backend re-IDs and re-segments chunks after post-processing, so live (iframe) and
|
||||
// async (webhook) ID spaces don't overlap and transcript text won't correlate. Match by participant only
|
||||
// and skip the content comparison.
|
||||
const event: {
|
||||
data: {
|
||||
final: string;
|
||||
language: string;
|
||||
messageID: string;
|
||||
participant: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
stable: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent(
|
||||
'TRANSCRIPTION_CHUNK_RECEIVED',
|
||||
asyncTranscription
|
||||
? e => e.data.participant.id === p1Id
|
||||
: e => e.data.messageID === p1Event.data.messageID
|
||||
);
|
||||
|
||||
expect(event.eventType).toBe('TRANSCRIPTION_CHUNK_RECEIVED');
|
||||
|
||||
if (!asyncTranscription) {
|
||||
const webhookTranscript = event.data.final;
|
||||
|
||||
expect(webhookTranscript).toPartiallyMatch(p1Transcript, 'webhook transcript', 'p1 transcript');
|
||||
}
|
||||
if (p1Event.data.language) {
|
||||
expect(event.data.language).toBe(p1Event.data.language);
|
||||
}
|
||||
expect(event.data.participant.id).toBe(p1Id);
|
||||
if (!asyncTranscription) {
|
||||
expect(event.data.participant.name).toBe(p1.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user