1- import {
2- useLocalParticipant ,
3- useParticipants ,
4- } from "@livekit/components-react" ;
1+ import { useLocalParticipant , useParticipants } from "@livekit/components-react" ;
52import { motion } from "framer-motion" ;
6- import { useCallback , useEffect , useRef , useState , MouseEvent as ReactMouseEvent } from "react" ;
3+ import { MouseEvent as ReactMouseEvent , useCallback , useEffect , useRef , useState } from "react" ;
74
85export function PushToTalkButton ( ) {
96 const { localParticipant } = useLocalParticipant ( ) ;
@@ -13,9 +10,7 @@ export function PushToTalkButton() {
1310 const lastActionTime = useRef ( 0 ) ;
1411
1512 // find agent participant that supports PTT
16- const agent = participants . find (
17- ( p ) => p . attributes ?. [ "push-to-talk" ] === "1"
18- ) ;
13+ const agent = participants . find ( ( p ) => p . attributes ?. [ "push-to-talk" ] === "1" ) ;
1914
2015 useEffect ( ( ) => {
2116 // start with microphone disabled for PTT agents
@@ -25,25 +20,28 @@ export function PushToTalkButton() {
2520 } , [ localParticipant , agent ] ) ;
2621
2722 // when user presses the button
28- const handleMouseDown = useCallback ( async ( e : ReactMouseEvent < HTMLButtonElement > ) => {
29- e . preventDefault ( ) ; // prevent default browser behavior
30-
31- if ( ! agent || ! localParticipant ) return ;
32-
33- console . log ( "starting turn" ) ;
34- try {
35- // await localParticipant.setMicrophoneEnabled(true);
36- await localParticipant . performRpc ( {
37- destinationIdentity : agent . identity ,
38- method : "start_turn" ,
39- payload : "" ,
40- } ) ;
41- setIsPressed ( true ) ;
42- setIsOutside ( false ) ;
43- } catch ( error ) {
44- console . error ( "Failed to start turn:" , error ) ;
45- }
46- } , [ agent , localParticipant ] ) ;
23+ const handleMouseDown = useCallback (
24+ async ( e : ReactMouseEvent < HTMLButtonElement > ) => {
25+ e . preventDefault ( ) ; // prevent default browser behavior
26+
27+ if ( ! agent || ! localParticipant ) return ;
28+
29+ console . log ( "starting turn" ) ;
30+ try {
31+ // await localParticipant.setMicrophoneEnabled(true);
32+ await localParticipant . performRpc ( {
33+ destinationIdentity : agent . identity ,
34+ method : "start_turn" ,
35+ payload : "" ,
36+ } ) ;
37+ setIsPressed ( true ) ;
38+ setIsOutside ( false ) ;
39+ } catch ( error ) {
40+ console . error ( "Failed to start turn:" , error ) ;
41+ }
42+ } ,
43+ [ agent , localParticipant ]
44+ ) ;
4745
4846 // when mouse leaves the button area while pressed
4947 const handleMouseLeave = useCallback ( ( ) => {
@@ -62,41 +60,47 @@ export function PushToTalkButton() {
6260 } , [ isPressed , isOutside ] ) ;
6361
6462 // shared function to end turn with specified method
65- const endTurnWithMethod = useCallback ( async ( method : string ) => {
66- if ( ! agent || ! localParticipant || ! isPressed ) return ;
67-
68- // Prevent multiple actions within 100ms
69- const now = Date . now ( ) ;
70- if ( now - lastActionTime . current < 100 ) return ;
71- lastActionTime . current = now ;
72-
73- console . log ( `ending turn with method: ${ method } ` ) ;
74- try {
75- // await localParticipant.setMicrophoneEnabled(false);
76- await localParticipant . performRpc ( {
77- destinationIdentity : agent . identity ,
78- method : method ,
79- payload : "" ,
80- } ) ;
81- } catch ( error ) {
82- console . error ( `Failed to ${ method } :` , error ) ;
83- } finally {
84- setIsPressed ( false ) ;
85- setIsOutside ( false ) ;
86- }
87- } , [ agent , localParticipant , isPressed ] ) ;
63+ const endTurnWithMethod = useCallback (
64+ async ( method : string ) => {
65+ if ( ! agent || ! localParticipant || ! isPressed ) return ;
66+
67+ // Prevent multiple actions within 100ms
68+ const now = Date . now ( ) ;
69+ if ( now - lastActionTime . current < 100 ) return ;
70+ lastActionTime . current = now ;
71+
72+ console . log ( `ending turn with method: ${ method } ` ) ;
73+ try {
74+ // await localParticipant.setMicrophoneEnabled(false);
75+ await localParticipant . performRpc ( {
76+ destinationIdentity : agent . identity ,
77+ method : method ,
78+ payload : "" ,
79+ } ) ;
80+ } catch ( error ) {
81+ console . error ( `Failed to ${ method } :` , error ) ;
82+ } finally {
83+ setIsPressed ( false ) ;
84+ setIsOutside ( false ) ;
85+ }
86+ } ,
87+ [ agent , localParticipant , isPressed ]
88+ ) ;
8889
8990 // when user releases the mouse anywhere
90- const handleMouseUp = useCallback ( ( e : ReactMouseEvent ) => {
91- e . preventDefault ( ) ; // prevent default browser behavior
92-
93- if ( ! isPressed ) return ;
94-
95- // if mouse is outside the button on release, cancel the turn
96- // otherwise, end the turn normally
97- const method = isOutside ? "cancel_turn" : "end_turn" ;
98- endTurnWithMethod ( method ) ;
99- } , [ isPressed , isOutside , endTurnWithMethod ] ) ;
91+ const handleMouseUp = useCallback (
92+ ( e : ReactMouseEvent ) => {
93+ e . preventDefault ( ) ; // prevent default browser behavior
94+
95+ if ( ! isPressed ) return ;
96+
97+ // if mouse is outside the button on release, cancel the turn
98+ // otherwise, end the turn normally
99+ const method = isOutside ? "cancel_turn" : "end_turn" ;
100+ endTurnWithMethod ( method ) ;
101+ } ,
102+ [ isPressed , isOutside , endTurnWithMethod ]
103+ ) ;
100104
101105 // ensure turn is ended when component unmounts
102106 useEffect ( ( ) => {
@@ -114,9 +118,9 @@ export function PushToTalkButton() {
114118 handleMouseUp ( e as unknown as ReactMouseEvent ) ;
115119 } ;
116120
117- window . addEventListener ( ' mouseup' , handleGlobalMouseUp ) ;
121+ window . addEventListener ( " mouseup" , handleGlobalMouseUp ) ;
118122 return ( ) => {
119- window . removeEventListener ( ' mouseup' , handleGlobalMouseUp ) ;
123+ window . removeEventListener ( " mouseup" , handleGlobalMouseUp ) ;
120124 } ;
121125 }
122126 } , [ isPressed , handleMouseUp ] ) ;
@@ -139,16 +143,13 @@ export function PushToTalkButton() {
139143 : "#004085" // blue when speaking normally
140144 : "#007bff" , // default blue
141145 scale : isPressed ? 0.95 : 1 ,
142- boxShadow : isOutside && isPressed
143- ? "0 0 0 3px rgba(217, 83, 79, 0.5)"
144- : "0 4px 6px rgba(0, 0, 0, 0.1)" ,
146+ boxShadow :
147+ isOutside && isPressed
148+ ? "0 0 0 3px rgba(217, 83, 79, 0.5)"
149+ : "0 4px 6px rgba(0, 0, 0, 0.1)" ,
145150 } }
146151 >
147- { isPressed
148- ? isOutside
149- ? "Release to Cancel"
150- : "Speaking..."
151- : "Press to Talk" }
152+ { isPressed ? ( isOutside ? "Release to Cancel" : "Speaking..." ) : "Press to Talk" }
152153 </ motion . button >
153154 ) ;
154155}
0 commit comments