From a0e4de73cc8d1c40c008741053456f6555174fb3 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Tue, 26 Apr 2022 15:20:06 -0700 Subject: [PATCH 1/9] Add support for to-device messages via OLM --- package.json | 1 + public/index.html | 1 + src/IndexedDBWorker.js | 5 +++++ src/matrix-utils.js | 45 ++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 4 ++++ 5 files changed, 56 insertions(+) create mode 100644 src/IndexedDBWorker.js diff --git a/package.json b/package.json index 9471442..947b255 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", + "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "pako": "^2.0.4", "postcss-preset-env": "^6.7.0", "re-resizable": "^6.9.0", diff --git a/public/index.html b/public/index.html index c6ddea7..50f5fc9 100644 --- a/public/index.html +++ b/public/index.html @@ -15,6 +15,7 @@
+ \ No newline at end of file diff --git a/src/IndexedDBWorker.js b/src/IndexedDBWorker.js new file mode 100644 index 0000000..0e373ca --- /dev/null +++ b/src/IndexedDBWorker.js @@ -0,0 +1,5 @@ +import { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker"; + +const remoteWorker = new IndexedDBStoreWorker(self.postMessage); + +self.onmessage = remoteWorker.onMessage; diff --git a/src/matrix-utils.js b/src/matrix-utils.js index 04ce3df..b1201f9 100644 --- a/src/matrix-utils.js +++ b/src/matrix-utils.js @@ -3,6 +3,7 @@ import { GroupCallIntent, GroupCallType, } from "matrix-js-sdk/src/browser-index"; +import IndexedDBWorker from "./IndexedDBWorker?worker"; export const defaultHomeserver = import.meta.env.VITE_DEFAULT_HOMESERVER || @@ -26,11 +27,55 @@ function waitForSync(client) { } export async function initClient(clientOptions) { + let indexedDB; + + try { + indexedDB = window.indexedDB; + } catch (e) {} + + const storeOpts = {}; + + if (indexedDB && localStorage && !import.meta.env.DEV) { + storeOpts.store = new matrix.IndexedDBStore({ + indexedDB: window.indexedDB, + localStorage: window.localStorage, + dbName: "element-call-sync", + workerFactory: () => new IndexedDBWorker(), + }); + } + + if (localStorage) { + storeOpts.sessionStore = new matrix.WebStorageSessionStore(localStorage); + } + + if (indexedDB) { + storeOpts.cryptoStore = new matrix.IndexedDBCryptoStore( + indexedDB, + "matrix-js-sdk:crypto" + ); + } + const client = matrix.createClient({ + ...storeOpts, ...clientOptions, useAuthorizationHeader: true, }); + try { + await client.store.startup(); + } catch (error) { + console.error( + "Error starting matrix client store. Falling back to memory store.", + error + ); + client.store = new matrix.MemoryStore({ localStorage }); + await client.store.startup(); + } + + if (client.initCrypto) { + await client.initCrypto(); + } + await client.startClient({ // dirty hack to reduce chance of gappy syncs // should be fixed by spotting gaps and backpaginating diff --git a/yarn.lock b/yarn.lock index 4c92108..1749475 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8631,6 +8631,10 @@ objectorarray@^1.0.5: resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== +"olm@https://packages.matrix.org/npm/olm/olm-3.2.1.tgz": + version "3.2.1" + resolved "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz#d623d76f99c3518dde68be8c86618d68bc7b004a" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" From 44486aa62dcfd33f372f6f83b06025319513baeb Mon Sep 17 00:00:00 2001 From: Robert Long Date: Tue, 26 Apr 2022 16:11:32 -0700 Subject: [PATCH 2/9] Fix building olm library in production --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 50f5fc9..0368b6a 100644 --- a/public/index.html +++ b/public/index.html @@ -15,7 +15,7 @@
- + \ No newline at end of file From e2aee0be819669ff4bf08313fcaac506eeed42c7 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Tue, 26 Apr 2022 16:28:21 -0700 Subject: [PATCH 3/9] Fix olm import --- src/matrix-utils.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/matrix-utils.js b/src/matrix-utils.js index b1201f9..a5a0683 100644 --- a/src/matrix-utils.js +++ b/src/matrix-utils.js @@ -4,6 +4,8 @@ import { GroupCallType, } from "matrix-js-sdk/src/browser-index"; import IndexedDBWorker from "./IndexedDBWorker?worker"; +import olmJsPath from "olm/olm.js?url"; +import olmWasmPath from "olm/olm.wasm?url"; export const defaultHomeserver = import.meta.env.VITE_DEFAULT_HOMESERVER || @@ -26,7 +28,20 @@ function waitForSync(client) { }); } +function addScript(src) { + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.setAttribute("src", src); + script.onload = resolve; + script.onerror = reject; + document.body.appendChild(script); + }); +} + export async function initClient(clientOptions) { + await addScript(olmJsPath); + await window.Olm.init({ locateFile: () => olmWasmPath }); + let indexedDB; try { From 3d54047f8783590e99c26a6206fbdd5d2b9456e5 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Wed, 27 Apr 2022 13:38:16 -0700 Subject: [PATCH 4/9] Fix Olm import --- package.json | 2 +- public/index.html | 1 - src/matrix-utils.js | 18 ++++-------------- yarn.lock | 8 ++++---- 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 947b255..11f33c4 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@juggle/resize-observer": "^3.3.1", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz", "@react-aria/button": "^3.3.4", "@react-aria/dialog": "^3.1.4", "@react-aria/focus": "^3.5.0", @@ -34,7 +35,6 @@ "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", - "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "pako": "^2.0.4", "postcss-preset-env": "^6.7.0", "re-resizable": "^6.9.0", diff --git a/public/index.html b/public/index.html index 0368b6a..c6ddea7 100644 --- a/public/index.html +++ b/public/index.html @@ -15,7 +15,6 @@
- \ No newline at end of file diff --git a/src/matrix-utils.js b/src/matrix-utils.js index a5a0683..07e1616 100644 --- a/src/matrix-utils.js +++ b/src/matrix-utils.js @@ -4,8 +4,8 @@ import { GroupCallType, } from "matrix-js-sdk/src/browser-index"; import IndexedDBWorker from "./IndexedDBWorker?worker"; -import olmJsPath from "olm/olm.js?url"; -import olmWasmPath from "olm/olm.wasm?url"; +import Olm from "@matrix-org/olm"; +import olmWasmPath from "@matrix-org/olm/olm.wasm?url"; export const defaultHomeserver = import.meta.env.VITE_DEFAULT_HOMESERVER || @@ -28,19 +28,9 @@ function waitForSync(client) { }); } -function addScript(src) { - return new Promise((resolve, reject) => { - const script = document.createElement("script"); - script.setAttribute("src", src); - script.onload = resolve; - script.onerror = reject; - document.body.appendChild(script); - }); -} - export async function initClient(clientOptions) { - await addScript(olmJsPath); - await window.Olm.init({ locateFile: () => olmWasmPath }); + window.OLM_OPTIONS = {}; + await Olm.init({ locateFile: () => olmWasmPath }); let indexedDB; diff --git a/yarn.lock b/yarn.lock index 1749475..985937a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1320,6 +1320,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz": + version "3.2.8" + resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz#8d53636d045e1776e2a2ec6613e57330dd9ce856" + "@mdx-js/mdx@^1.6.22": version "1.6.22" resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" @@ -8631,10 +8635,6 @@ objectorarray@^1.0.5: resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== -"olm@https://packages.matrix.org/npm/olm/olm-3.2.1.tgz": - version "3.2.1" - resolved "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz#d623d76f99c3518dde68be8c86618d68bc7b004a" - on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" From 7a9ff98550c7b60f9fefd97523b9e53e0a489f80 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Wed, 27 Apr 2022 13:51:08 -0700 Subject: [PATCH 5/9] Add OLM_OPTIONS global TODO --- src/matrix-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix-utils.js b/src/matrix-utils.js index 07e1616..6132b7d 100644 --- a/src/matrix-utils.js +++ b/src/matrix-utils.js @@ -29,6 +29,7 @@ function waitForSync(client) { } export async function initClient(clientOptions) { + // TODO: https://gitlab.matrix.org/matrix-org/olm/-/issues/10 window.OLM_OPTIONS = {}; await Olm.init({ locateFile: () => olmWasmPath }); From 0f687fb8b8d9fdb1efc731985d365fa2be251671 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 May 2022 17:58:59 +0100 Subject: [PATCH 6/9] Fix races on mute / unmute By serialising everything on a promise chain --- src/room/usePTT.ts | 135 ++++++++++++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 44 deletions(-) diff --git a/src/room/usePTT.ts b/src/room/usePTT.ts index d84907b..9c4bac3 100644 --- a/src/room/usePTT.ts +++ b/src/room/usePTT.ts @@ -22,6 +22,30 @@ import { logger } from "matrix-js-sdk/src/logger"; import { PlayClipFunction, PTTClipID } from "../sound/usePttSounds"; +// Works out who the active speaker should be given what feeds are active and +// the power level of each user. +function getActiveSpeakerFeed( + feeds: CallFeed[], + groupCall: GroupCall +): CallFeed { + const activeSpeakerFeeds = feeds.filter((f) => !f.isAudioMuted()); + + let activeSpeakerFeed; + let highestPowerLevel; + for (const feed of activeSpeakerFeeds) { + const member = groupCall.room.getMember(feed.userId); + if ( + highestPowerLevel === undefined || + member.powerLevel > highestPowerLevel + ) { + highestPowerLevel = member.powerLevel; + activeSpeakerFeed = feed; + } + } + + return activeSpeakerFeed; +} + export interface PTTState { pttButtonHeld: boolean; isAdmin: boolean; @@ -39,6 +63,24 @@ export const usePTT = ( userMediaFeeds: CallFeed[], playClip: PlayClipFunction ): PTTState => { + // Used to serialise all the mute calls so they don't race. It has + // its own state as its always set separately from anything else. + const [mutePromise, setMutePromise] = useState(Promise.resolve()); + + // Wrapper to serialise all the mute operations on the promise + const setMicMuteWrapper = useCallback( + (muted) => { + setMutePromise( + mutePromise.then(() => { + groupCall.setMicrophoneMuted(muted).catch((e) => { + logger.error("Failed to unmute microphone", e); + }); + }) + ); + }, + [groupCall, mutePromise] + ); + const [ { pttButtonHeld, @@ -51,7 +93,7 @@ export const usePTT = ( ] = useState(() => { const roomMember = groupCall.room.getMember(client.getUserId()); - const activeSpeakerFeed = userMediaFeeds.find((f) => !f.isAudioMuted()); + const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall); return { isAdmin: roomMember.powerLevel >= 100, @@ -62,38 +104,52 @@ export const usePTT = ( }; }); - useEffect(() => { - function onMuteStateChanged(...args): void { - const activeSpeakerFeed = userMediaFeeds.find((f) => !f.isAudioMuted()); + const onMuteStateChanged = useCallback(() => { + const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall); - if (activeSpeakerUserId === null && activeSpeakerFeed.userId !== null) { - if (activeSpeakerFeed.userId === client.getUserId()) { - playClip(PTTClipID.START_TALKING_LOCAL); - } else { - playClip(PTTClipID.START_TALKING_REMOTE); - } - } else if ( - pttButtonHeld && - activeSpeakerUserId === client.getUserId() && - activeSpeakerFeed?.userId !== client.getUserId() - ) { - // We were talking but we've been cut off - playClip(PTTClipID.BLOCKED); + let blocked = false; + if (activeSpeakerUserId === null && activeSpeakerFeed.userId !== null) { + if (activeSpeakerFeed.userId === client.getUserId()) { + playClip(PTTClipID.START_TALKING_LOCAL); + } else { + playClip(PTTClipID.START_TALKING_REMOTE); } + } else if ( + pttButtonHeld && + activeSpeakerUserId === client.getUserId() && + activeSpeakerFeed?.userId !== client.getUserId() + ) { + // We were talking but we've been cut off + setMicMuteWrapper(true); + blocked = true; + playClip(PTTClipID.BLOCKED); + } - setState((prevState) => ({ + setState((prevState) => { + return { ...prevState, activeSpeakerUserId: activeSpeakerFeed ? activeSpeakerFeed.userId : null, - })); - } + transmitBlocked: blocked, + }; + }); + }, [ + playClip, + groupCall, + pttButtonHeld, + activeSpeakerUserId, + client, + userMediaFeeds, + setMicMuteWrapper, + ]); + useEffect(() => { for (const callFeed of userMediaFeeds) { callFeed.addListener(CallFeedEvent.MuteStateChanged, onMuteStateChanged); } - const activeSpeakerFeed = userMediaFeeds.find((f) => !f.isAudioMuted()); + const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall); setState((prevState) => ({ ...prevState, @@ -108,29 +164,26 @@ export const usePTT = ( ); } }; - }, [userMediaFeeds, activeSpeakerUserId, client, playClip, pttButtonHeld]); + }, [userMediaFeeds, onMuteStateChanged, groupCall]); const startTalking = useCallback(async () => { if (pttButtonHeld) return; let blocked = false; - if (!activeSpeakerUserId || (isAdmin && talkOverEnabled)) { - if (groupCall.isMicrophoneMuted()) { - try { - await groupCall.setMicrophoneMuted(false); - } catch (e) { - logger.error("Failed to unmute microphone", e); - } - } - } else { + if (activeSpeakerUserId && !(isAdmin && talkOverEnabled)) { playClip(PTTClipID.BLOCKED); blocked = true; } + // setstate before doing the async call to mute / unmute the mic setState((prevState) => ({ ...prevState, pttButtonHeld: true, transmitBlocked: blocked, })); + + if (!blocked && groupCall.isMicrophoneMuted()) { + setMicMuteWrapper(false); + } }, [ pttButtonHeld, groupCall, @@ -139,25 +192,18 @@ export const usePTT = ( talkOverEnabled, setState, playClip, + setMicMuteWrapper, ]); - const stopTalking = useCallback(() => { - setState((prevState) => ({ - ...prevState, - pttButtonHeld: false, - unmuteError: null, - })); - - if (!groupCall.isMicrophoneMuted()) { - groupCall.setMicrophoneMuted(true); - } - + const stopTalking = useCallback(async () => { setState((prevState) => ({ ...prevState, pttButtonHeld: false, transmitBlocked: false, })); - }, [groupCall]); + + setMicMuteWrapper(true); + }, [setMicMuteWrapper]); useEffect(() => { function onKeyDown(event: KeyboardEvent): void { @@ -181,7 +227,7 @@ export const usePTT = ( function onBlur(): void { // TODO: We will need to disable this for a global PTT hotkey to work if (!groupCall.isMicrophoneMuted()) { - groupCall.setMicrophoneMuted(true); + setMicMuteWrapper(true); } setState((prevState) => ({ ...prevState, pttButtonHeld: false })); @@ -204,6 +250,7 @@ export const usePTT = ( isAdmin, talkOverEnabled, pttButtonHeld, + setMicMuteWrapper, ]); const setTalkOverEnabled = useCallback((talkOverEnabled) => { From d34c8d08a4e2d0497439fb6c47b6849e437bfe37 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 May 2022 18:09:45 +0100 Subject: [PATCH 7/9] Add comment --- src/room/usePTT.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/room/usePTT.ts b/src/room/usePTT.ts index 9c4bac3..5d4682e 100644 --- a/src/room/usePTT.ts +++ b/src/room/usePTT.ts @@ -119,7 +119,11 @@ export const usePTT = ( activeSpeakerUserId === client.getUserId() && activeSpeakerFeed?.userId !== client.getUserId() ) { - // We were talking but we've been cut off + // We were talking but we've been cut off: mute our own mic + // (this is the easier way of cutting other speakers off if an + // admin barges in: we could also mute the non-admin speaker + // on all receivers, but we'd have to make sure we unmuted them + // correctly.) setMicMuteWrapper(true); blocked = true; playClip(PTTClipID.BLOCKED); From f6f0c20b08f434d88a39bb75efc5a6dfc02f530b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 May 2022 20:39:21 +0100 Subject: [PATCH 8/9] Chain promises correctly --- src/room/usePTT.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/room/usePTT.ts b/src/room/usePTT.ts index 5d4682e..3b80f8c 100644 --- a/src/room/usePTT.ts +++ b/src/room/usePTT.ts @@ -65,14 +65,16 @@ export const usePTT = ( ): PTTState => { // Used to serialise all the mute calls so they don't race. It has // its own state as its always set separately from anything else. - const [mutePromise, setMutePromise] = useState(Promise.resolve()); + const [mutePromise, setMutePromise] = useState( + Promise.resolve(false) + ); // Wrapper to serialise all the mute operations on the promise const setMicMuteWrapper = useCallback( - (muted) => { + (muted: boolean) => { setMutePromise( mutePromise.then(() => { - groupCall.setMicrophoneMuted(muted).catch((e) => { + return groupCall.setMicrophoneMuted(muted).catch((e) => { logger.error("Failed to unmute microphone", e); }); }) From 9fd7329554f7f596bd8b1224a9549d8cc4bee7f7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 May 2022 21:00:14 +0100 Subject: [PATCH 9/9] Add sound when speaker stops speaking And also a slightly nicer blocked sound (ok, I couldn't let it go). --- src/room/PTTCallView.tsx | 10 ++++++++-- src/room/usePTT.ts | 15 +++++++-------- src/sound/PTTClips.tsx | 8 ++++++++ src/sound/blocked.mp3 | Bin 8403 -> 8403 bytes src/sound/blocked.ogg | Bin 6490 -> 6863 bytes src/sound/end_talk.mp3 | Bin 0 -> 6313 bytes src/sound/end_talk.ogg | Bin 0 -> 4694 bytes src/sound/usePttSounds.ts | 9 ++++++++- 8 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 src/sound/end_talk.mp3 create mode 100644 src/sound/end_talk.ogg diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index 4acd7a6..3e699bd 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -109,8 +109,13 @@ export const PTTCallView: React.FC = ({ const { audioOutput } = useMediaHandler(); - const { startTalkingLocalRef, startTalkingRemoteRef, blockedRef, playClip } = - usePTTSounds(); + const { + startTalkingLocalRef, + startTalkingRemoteRef, + blockedRef, + endTalkingRef, + playClip, + } = usePTTSounds(); const { pttButtonHeld, @@ -146,6 +151,7 @@ export const PTTCallView: React.FC = ({
diff --git a/src/room/usePTT.ts b/src/room/usePTT.ts index 3b80f8c..673da76 100644 --- a/src/room/usePTT.ts +++ b/src/room/usePTT.ts @@ -27,17 +27,14 @@ import { PlayClipFunction, PTTClipID } from "../sound/usePttSounds"; function getActiveSpeakerFeed( feeds: CallFeed[], groupCall: GroupCall -): CallFeed { +): CallFeed | null { const activeSpeakerFeeds = feeds.filter((f) => !f.isAudioMuted()); - let activeSpeakerFeed; - let highestPowerLevel; + let activeSpeakerFeed = null; + let highestPowerLevel = null; for (const feed of activeSpeakerFeeds) { const member = groupCall.room.getMember(feed.userId); - if ( - highestPowerLevel === undefined || - member.powerLevel > highestPowerLevel - ) { + if (highestPowerLevel === null || member.powerLevel > highestPowerLevel) { highestPowerLevel = member.powerLevel; activeSpeakerFeed = feed; } @@ -110,12 +107,14 @@ export const usePTT = ( const activeSpeakerFeed = getActiveSpeakerFeed(userMediaFeeds, groupCall); let blocked = false; - if (activeSpeakerUserId === null && activeSpeakerFeed.userId !== null) { + if (activeSpeakerUserId === null && activeSpeakerFeed !== null) { if (activeSpeakerFeed.userId === client.getUserId()) { playClip(PTTClipID.START_TALKING_LOCAL); } else { playClip(PTTClipID.START_TALKING_REMOTE); } + } else if (activeSpeakerUserId !== null && activeSpeakerFeed === null) { + playClip(PTTClipID.END_TALKING); } else if ( pttButtonHeld && activeSpeakerUserId === client.getUserId() && diff --git a/src/sound/PTTClips.tsx b/src/sound/PTTClips.tsx index e13acb5..90958af 100644 --- a/src/sound/PTTClips.tsx +++ b/src/sound/PTTClips.tsx @@ -20,6 +20,8 @@ import startTalkLocalOggUrl from "./start_talk_local.ogg"; import startTalkLocalMp3Url from "./start_talk_local.mp3"; import startTalkRemoteOggUrl from "./start_talk_remote.ogg"; import startTalkRemoteMp3Url from "./start_talk_remote.mp3"; +import endTalkOggUrl from "./end_talk.ogg"; +import endTalkMp3Url from "./end_talk.mp3"; import blockedOggUrl from "./blocked.ogg"; import blockedMp3Url from "./blocked.mp3"; import styles from "./PTTClips.module.css"; @@ -27,12 +29,14 @@ import styles from "./PTTClips.module.css"; interface Props { startTalkingLocalRef: React.RefObject; startTalkingRemoteRef: React.RefObject; + endTalkingRef: React.RefObject; blockedRef: React.RefObject; } export const PTTClips: React.FC = ({ startTalkingLocalRef, startTalkingRemoteRef, + endTalkingRef, blockedRef, }) => { return ( @@ -53,6 +57,10 @@ export const PTTClips: React.FC = ({ +