Skip to content

Commit 1be6c48

Browse files
authored
Merge pull request #93 from Resgrid/develop
RD-T39 Fixing PTT issue
2 parents a832654 + 0fba47c commit 1be6c48

File tree

3 files changed

+104
-25
lines changed

3 files changed

+104
-25
lines changed

.agent/rules/agent.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
trigger: always_on
3+
---
4+
15
You are an expert in TypeScript, React Native, Expo, and Mobile App Development.
26

37
Code Style and Structure:
@@ -71,3 +75,26 @@ Additional Rules:
7175
- Use `@rnmapbox/maps` for maps, mapping or vehicle navigation
7276
- Use `lucide-react-native` for icons and use those components directly in the markup and don't use the gluestack-ui icon component
7377
- Use ? : for conditional rendering and not &&
78+
79+
Be more strict about planning.
80+
81+
82+
Do not say things or provide incorrect information just to be polite; certainty is required.
83+
84+
85+
When solving problems, always analyze them through first principles thinking. Break every challenge down to its basic, fundamental truths and build your solutions from the ground up rather than relying on analogies or common practices.
86+
87+
88+
When debugging, always investigate whether legacy code or previous implementations are interfering with new logic before assuming the new code is inherently broken.
89+
90+
91+
**Anti-Repetition Protocol**
92+
: If a previously suggested fix is reported as failed, do not attempt to "patch" the broken logic or repeat the same suggestion. Instead, explicitly discard your previous assumptions, re-verify the data flow from first principles, and propose a fundamentally different architectural path. Avoid repetition bias at all costs.
93+
94+
95+
**Token Efficiency Protocol**
96+
: Be extremely concise. Prioritize code and technical facts over conversational filler.
97+
98+
99+
**Pre-Flight Verification**
100+
: Always verify the current state of relevant files, imports, and the specific environment (e.g., Windows paths, Node version) BEFORE proposing a change. The goal is to maximize the success rate of the first attempt.

src/components/dispatch-console/ptt-interface.tsx

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Mic, MicOff, PhoneOff, Radio, Wifi, WifiOff } from 'lucide-react-native';
1+
import { PhoneOff, Radio, Wifi, WifiOff } from 'lucide-react-native';
22
import { useColorScheme } from 'nativewind';
33
import React, { useCallback, useEffect, useRef, useState } from 'react';
44
import { useTranslation } from 'react-i18next';
@@ -64,7 +64,6 @@ export const PTTInterface: React.FC<PTTInterfaceProps> = ({ onPTTPress, onPTTRel
6464
disconnect,
6565
startTransmitting,
6666
stopTransmitting,
67-
toggleMute,
6867
selectChannel,
6968
refreshVoiceSettings,
7069
} = usePTT({
@@ -78,7 +77,8 @@ export const PTTInterface: React.FC<PTTInterfaceProps> = ({ onPTTPress, onPTTRel
7877
const isTransmitting = isConnected ? pttTransmitting : externalTransmitting;
7978
const displayChannel = isConnected ? (pttChannel?.Name || externalChannel) : (pttChannel?.Name || t('dispatch.disconnected'));
8079

81-
const handlePTTPress = useCallback(async () => {
80+
// Toggle-style PTT handler - tap to start/stop transmitting
81+
const handlePTTToggle = useCallback(async () => {
8282
if (!isConnected) {
8383
// If not connected, try to connect first
8484
if (availableChannels.length > 0 && !pttChannel) {
@@ -92,18 +92,13 @@ export const PTTInterface: React.FC<PTTInterfaceProps> = ({ onPTTPress, onPTTRel
9292
return;
9393
}
9494

95-
await startTransmitting();
96-
}, [isConnected, availableChannels, pttChannel, selectChannel, connect, startTransmitting]);
97-
98-
const handlePTTRelease = useCallback(async () => {
99-
if (isConnected && pttTransmitting) {
95+
// Toggle transmitting state
96+
if (pttTransmitting) {
10097
await stopTransmitting();
98+
} else {
99+
await startTransmitting();
101100
}
102-
}, [isConnected, pttTransmitting, stopTransmitting]);
103-
104-
const handleMuteToggle = useCallback(async () => {
105-
await toggleMute();
106-
}, [toggleMute]);
101+
}, [isConnected, availableChannels, pttChannel, pttTransmitting, selectChannel, connect, startTransmitting, stopTransmitting]);
107102

108103
const handleChannelPress = useCallback(() => {
109104
if (isVoiceEnabled && availableChannels.length > 0) {
@@ -210,17 +205,8 @@ export const PTTInterface: React.FC<PTTInterfaceProps> = ({ onPTTPress, onPTTRel
210205
</Pressable>
211206
) : null}
212207

213-
{/* Mute Button */}
214-
<Pressable
215-
onPress={handleMuteToggle}
216-
style={StyleSheet.flatten([styles.compactControlButton, { backgroundColor: colorScheme === 'dark' ? '#374151' : '#e5e7eb' }, isMuted && styles.mutedButton])}
217-
disabled={!isConnected}
218-
>
219-
<Icon as={isMuted ? MicOff : Mic} size="sm" color={isMuted ? '#ef4444' : !isConnected ? '#9ca3af' : colorScheme === 'dark' ? '#fff' : '#374151'} />
220-
</Pressable>
221-
222-
{/* PTT Button */}
223-
<Pressable onPressIn={handlePTTPress} onPressOut={handlePTTRelease} style={StyleSheet.flatten(getPTTButtonStyle())} disabled={!isVoiceEnabled || isMuted}>
208+
{/* PTT Toggle Button - tap to start/stop transmitting */}
209+
<Pressable onPress={handlePTTToggle} style={StyleSheet.flatten(getPTTButtonStyle())} disabled={!isVoiceEnabled}>
224210
<Icon as={Radio} size="sm" color="#fff" />
225211
</Pressable>
226212
</HStack>
@@ -254,6 +240,7 @@ const styles = StyleSheet.create({
254240
alignItems: 'center',
255241
justifyContent: 'center',
256242
},
243+
// Note: mutedButton style kept for backwards compatibility but no longer used
257244
mutedButton: {
258245
backgroundColor: 'rgba(239, 68, 68, 0.1)',
259246
},

src/stores/app/livekit-store.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import notifee, { AndroidImportance } from '@notifee/react-native';
22
import { getRecordingPermissionsAsync, requestRecordingPermissionsAsync } from 'expo-audio';
3-
import { Room, RoomEvent } from 'livekit-client';
3+
import { Room, RoomEvent, Track } from 'livekit-client';
44
import { Platform } from 'react-native';
55
import { create } from 'zustand';
66

@@ -57,6 +57,8 @@ const setupAudioRouting = async (room: Room): Promise<void> => {
5757
}
5858
};
5959

60+
// Map to store web audio elements for cleanup (keyed by track SID)
61+
const webAudioElements = new Map<string, HTMLAudioElement>();
6062
interface LiveKitState {
6163
// Connection state
6264
isConnected: boolean;
@@ -243,6 +245,53 @@ export const useLiveKitStore = create<LiveKitState>((set, get) => ({
243245
set({ isTalking });
244246
});
245247

248+
// Handle remote audio tracks for web platform
249+
room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
250+
// On web, attach audio tracks to DOM elements for playback
251+
if (Platform.OS === 'web' && track.kind === Track.Kind.Audio) {
252+
try {
253+
const audioElement = track.attach();
254+
const trackSid = track.sid || publication.trackSid;
255+
if (trackSid) {
256+
audioElement.id = `livekit-audio-${trackSid}`;
257+
document.body.appendChild(audioElement);
258+
webAudioElements.set(trackSid, audioElement);
259+
}
260+
logger.debug({
261+
message: 'Attached audio track for web playback',
262+
context: { trackSid, participantIdentity: participant.identity },
263+
});
264+
} catch (err) {
265+
logger.error({
266+
message: 'Failed to attach audio track',
267+
context: { error: err, trackSid: track.sid },
268+
});
269+
}
270+
}
271+
});
272+
273+
room.on(RoomEvent.TrackUnsubscribed, (track, publication, participant) => {
274+
// On web, detach and remove audio elements
275+
if (Platform.OS === 'web' && track.kind === Track.Kind.Audio) {
276+
try {
277+
track.detach().forEach((el) => el.remove());
278+
const trackSid = track.sid || publication.trackSid;
279+
if (trackSid) {
280+
webAudioElements.delete(trackSid);
281+
}
282+
logger.debug({
283+
message: 'Detached audio track',
284+
context: { trackSid, participantIdentity: participant.identity },
285+
});
286+
} catch (err) {
287+
logger.error({
288+
message: 'Failed to detach audio track',
289+
context: { error: err, trackSid: track.sid },
290+
});
291+
}
292+
}
293+
});
294+
246295
// Connect to the room
247296
await room.connect(voipServerWebsocketSslAddress, token);
248297

@@ -294,6 +343,22 @@ export const useLiveKitStore = create<LiveKitState>((set, get) => ({
294343
disconnectFromRoom: async () => {
295344
const { currentRoom } = get();
296345
if (currentRoom) {
346+
// Clean up web audio elements before disconnecting
347+
if (Platform.OS === 'web') {
348+
webAudioElements.forEach((audioElement, trackSid) => {
349+
try {
350+
audioElement.pause();
351+
audioElement.remove();
352+
} catch (err) {
353+
logger.warn({
354+
message: 'Failed to clean up audio element',
355+
context: { error: err, trackSid },
356+
});
357+
}
358+
});
359+
webAudioElements.clear();
360+
}
361+
297362
await currentRoom.disconnect();
298363
await audioService.playDisconnectedFromAudioRoomSound();
299364

0 commit comments

Comments
 (0)