(feat): add sounds
Some checks failed
/ release (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled

This commit is contained in:
Alois 2026-07-30 23:51:38 +02:00
commit 184b85190b
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
19 changed files with 207 additions and 16 deletions

View file

@ -4,6 +4,7 @@ import { useLocation, useNavigate } from "@tanstack/react-router";
import { useMTP } from "@tensamin/mtp";
import { log, toast } from "@tensamin/shared/log";
import { mtp } from "@tensamin/shared/data";
import { playSound, stopSound } from "@tensamin/shared/sounds";
import { bytesToBase64 } from "mtp";
import {
deriveCallSecretId,
@ -154,6 +155,8 @@ export function getRoom(): Room {
}
const remoteAudioElements = new Map<string, HTMLMediaElement>();
let outgoingJingle: HTMLAudioElement | null = null;
let outgoingJingleGeneration = 0;
const CALL_SECRET_VERSION = 1;
const SCREEN_SHARE_PREVIEW_MAX_WIDTH = 320;
@ -161,6 +164,34 @@ const SCREEN_SHARE_PREVIEW_MAX_HEIGHT = 180;
const SCREEN_SHARE_PREVIEW_QUALITY = 0.7;
const SCREEN_SHARE_PREVIEW_TIMEOUT_MS = 5000;
async function startOutgoingJingle() {
const generation = ++outgoingJingleGeneration;
stopSound(outgoingJingle);
outgoingJingle = null;
const jingle = await requireRuntime(useCall.getState().runtime).load(
"settings.call_jingle",
);
if (
generation !== outgoingJingleGeneration ||
useCall.getState().invitedUserId == null
) {
return;
}
outgoingJingle = playSound(
jingle === "jingle_2" ? "call_jingle_2" : "call_jingle_1",
true,
);
}
function stopOutgoingJingle() {
outgoingJingleGeneration += 1;
stopSound(outgoingJingle);
outgoingJingle = null;
}
function protocolBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
return new Uint8Array(bytes);
}
@ -695,6 +726,16 @@ export async function sendCallInvite(userId: number) {
// Start tracking a participant's shared screen in the call UI.
export function startWatchingStream(participantId: number) {
const trackReady = getScreenShareTrackForParticipant(participantId) != null;
const alreadyWatching = useCall
.getState()
.watchedStreamParticipantIds.includes(participantId);
const localParticipantId = getParticipantId(
getRoom().localParticipant.identity,
);
if (!alreadyWatching && participantId !== localParticipantId) {
playSound("stream_watch_start");
}
setParticipantTrackSubscribed(participantId, Track.Source.ScreenShare);
setParticipantTrackSubscribed(participantId, Track.Source.ScreenShareAudio);
@ -745,6 +786,17 @@ export function focusParticipant(
// Stop tracking a participant's shared screen and clean up related UI state.
export function stopWatchingStream(participantId: number) {
const wasWatching = useCall
.getState()
.watchedStreamParticipantIds.includes(participantId);
const localParticipantId = getParticipantId(
getRoom().localParticipant.identity,
);
if (wasWatching && participantId !== localParticipantId) {
playSound("stream_watch_end");
}
setParticipantTrackSubscribed(participantId, Track.Source.ScreenShare, false);
setParticipantTrackSubscribed(
participantId,
@ -866,6 +918,7 @@ export async function connect(callId: string) {
// Tear down the active call session and return the store to a closed state.
export async function disconnect() {
stopOutgoingJingle();
disposeSpeakingDetector();
await clearScreenSharePreview();
@ -939,6 +992,10 @@ export async function joinCall(
ownCallSecretInvitePending: isNewCall,
});
if (sendInvite && !existingCallId) {
void startOutgoingJingle();
}
if (callSecret) {
try {
if (!existingCallId) {
@ -1303,6 +1360,7 @@ export function useInitializeCall() {
const onConnected = async () => {
useCall.setState({ state: "open" });
playSound("call_join");
syncParticipantState();
const detector = getSpeakingDetector();
@ -1376,6 +1434,8 @@ export function useInitializeCall() {
};
const onDisconnected = () => {
stopOutgoingJingle();
playSound("call_leave");
useCall.setState({ state: "closed" });
syncParticipantState();
log(2, "call", "purple", "Disconnected from call", {
@ -1385,11 +1445,14 @@ export function useInitializeCall() {
};
const onParticipantConnected = () => {
stopOutgoingJingle();
playSound("call_join");
syncAllRemoteTrackSubscriptions();
syncParticipantState();
};
const onParticipantDisconnected = (participant: Participant) => {
playSound("call_leave");
const participantId = getParticipantId(participant.identity);
if (participantId != null) {
@ -1416,6 +1479,10 @@ export function useInitializeCall() {
};
const onLocalTrackPublished = (publication: LocalTrackPublication) => {
if (publication.source === Track.Source.ScreenShare) {
playSound("stream_start_self");
}
if (
publication.kind === Track.Kind.Audio &&
publication.source === Track.Source.Microphone &&
@ -1437,6 +1504,10 @@ export function useInitializeCall() {
};
const onLocalTrackUnpublished = (publication: LocalTrackPublication) => {
if (publication.source === Track.Source.ScreenShare) {
playSound("stream_end_self");
}
if (
publication.kind === Track.Kind.Audio &&
publication.source === Track.Source.Microphone
@ -1453,6 +1524,10 @@ export function useInitializeCall() {
publication: RemoteTrackPublication,
participant: RemoteParticipant,
) => {
if (publication.source === Track.Source.ScreenShare) {
playSound("stream_start_other");
}
const participantId = getParticipantId(participant.identity);
if (participantId != null) {
@ -1469,6 +1544,14 @@ export function useInitializeCall() {
onParticipantStateChange();
};
const onTrackUnpublished = (publication: RemoteTrackPublication) => {
if (publication.source === Track.Source.ScreenShare) {
playSound("stream_end_other");
}
onParticipantStateChange();
};
const onTrackSubscribed = (
track: RemoteTrack,
publication: RemoteTrackPublication,
@ -1526,7 +1609,7 @@ export function useInitializeCall() {
room.on(RoomEvent.TrackSubscribed, onTrackSubscribed);
room.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed);
room.on(RoomEvent.TrackPublished, onTrackPublished);
room.on(RoomEvent.TrackUnpublished, onParticipantStateChange);
room.on(RoomEvent.TrackUnpublished, onTrackUnpublished);
room.on(RoomEvent.ParticipantConnected, onParticipantConnected);
room.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected);
room.on(RoomEvent.TrackMuted, onParticipantStateChange);
@ -1547,7 +1630,7 @@ export function useInitializeCall() {
room.off(RoomEvent.TrackSubscribed, onTrackSubscribed);
room.off(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed);
room.off(RoomEvent.TrackPublished, onTrackPublished);
room.off(RoomEvent.TrackUnpublished, onParticipantStateChange);
room.off(RoomEvent.TrackUnpublished, onTrackUnpublished);
room.off(RoomEvent.ParticipantConnected, onParticipantConnected);
room.off(RoomEvent.ParticipantDisconnected, onParticipantDisconnected);
room.off(RoomEvent.TrackMuted, onParticipantStateChange);