@@ -3,87 +3,152 @@ import {
33 useParticipants ,
44} from "@livekit/components-react" ;
55import { motion } from "framer-motion" ;
6- import { useCallback , useEffect , useRef , useState } from "react" ;
6+ import { useCallback , useEffect , useRef , useState , MouseEvent } from "react" ;
77
88export function PushToTalkButton ( ) {
99 const { localParticipant } = useLocalParticipant ( ) ;
1010 const participants = useParticipants ( ) ;
1111 const [ isPressed , setIsPressed ] = useState ( false ) ;
12- const lastReleaseTime = useRef ( 0 ) ;
12+ const [ isOutside , setIsOutside ] = useState ( false ) ;
13+ const lastActionTime = useRef ( 0 ) ;
1314
14- // Find agent participant that supports PTT
15+ // find agent participant that supports PTT
1516 const agent = participants . find (
16- ( p ) => p . attributes ?. [ "supports-ptt " ] === "1"
17+ ( p ) => p . attributes ?. [ "push-to-talk " ] === "1"
1718 ) ;
1819
1920 useEffect ( ( ) => {
20- // start with microphone enabled for PTT agents
21+ // start with microphone disabled for PTT agents
2122 if ( agent && localParticipant ) {
2223 localParticipant . setMicrophoneEnabled ( false ) ;
2324 }
2425 } , [ localParticipant , agent ] ) ;
2526
26- const handlePushStart = useCallback ( async ( ) => {
27+ // when user presses the button
28+ const handleMouseDown = useCallback ( async ( e : MouseEvent < HTMLButtonElement > ) => {
29+ e . preventDefault ( ) ; // prevent default browser behavior
30+
2731 if ( ! agent || ! localParticipant ) return ;
2832
33+ console . log ( "starting turn" ) ;
2934 try {
3035 await localParticipant . setMicrophoneEnabled ( true ) ;
3136 await localParticipant . performRpc ( {
3237 destinationIdentity : agent . identity ,
33- method : "ptt.start" ,
38+ method : "start_turn" ,
39+ payload : "" ,
3440 } ) ;
3541 setIsPressed ( true ) ;
42+ setIsOutside ( false ) ;
3643 } catch ( error ) {
37- console . error ( "Failed to send PTT push :" , error ) ;
44+ console . error ( "Failed to start turn :" , error ) ;
3845 }
3946 } , [ agent , localParticipant ] ) ;
4047
41- const handlePushEnd = useCallback ( async ( ) => {
48+ // when mouse leaves the button area while pressed
49+ const handleMouseLeave = useCallback ( ( ) => {
50+ if ( isPressed ) {
51+ console . log ( "mouse left button while pressed" ) ;
52+ setIsOutside ( true ) ;
53+ }
54+ } , [ isPressed ] ) ;
55+
56+ // when mouse re-enters the button area while pressed
57+ const handleMouseEnter = useCallback ( ( ) => {
58+ if ( isPressed && isOutside ) {
59+ console . log ( "mouse re-entered button while pressed" ) ;
60+ setIsOutside ( false ) ;
61+ }
62+ } , [ isPressed , isOutside ] ) ;
63+
64+ // shared function to end turn with specified method
65+ const endTurnWithMethod = useCallback ( async ( method : string ) => {
4266 if ( ! agent || ! localParticipant || ! isPressed ) return ;
4367
44- // Prevent multiple releases within 100ms
68+ // Prevent multiple actions within 100ms
4569 const now = Date . now ( ) ;
46- if ( now - lastReleaseTime . current < 100 ) {
47- return ;
48- }
49- lastReleaseTime . current = now ;
70+ if ( now - lastActionTime . current < 100 ) return ;
71+ lastActionTime . current = now ;
5072
73+ console . log ( `ending turn with method: ${ method } ` ) ;
5174 try {
5275 await localParticipant . setMicrophoneEnabled ( false ) ;
5376 await localParticipant . performRpc ( {
5477 destinationIdentity : agent . identity ,
55- method : "ptt.end" ,
78+ method : method ,
79+ payload : "" ,
5680 } ) ;
5781 } catch ( error ) {
58- console . error ( " Failed to send PTT release:" , error ) ;
82+ console . error ( ` Failed to ${ method } :` , error ) ;
5983 } finally {
6084 setIsPressed ( false ) ;
85+ setIsOutside ( false ) ;
6186 }
6287 } , [ agent , localParticipant , isPressed ] ) ;
6388
64- // Clean up pressed state when component unmounts
89+ // when user releases the mouse anywhere
90+ const handleMouseUp = useCallback ( ( e : MouseEvent ) => {
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 ] ) ;
100+
101+ // ensure turn is ended when component unmounts
65102 useEffect ( ( ) => {
66103 return ( ) => {
67104 if ( isPressed ) {
68- handlePushEnd ( ) ;
105+ endTurnWithMethod ( "end_turn" ) ;
69106 }
70107 } ;
71- } , [ isPressed , handlePushEnd ] ) ;
108+ } , [ isPressed , endTurnWithMethod ] ) ;
109+
110+ // add global mouse-up handler to catch events outside the button
111+ useEffect ( ( ) => {
112+ if ( isPressed ) {
113+ const handleGlobalMouseUp = ( e : MouseEvent ) => {
114+ handleMouseUp ( e ) ;
115+ } ;
116+
117+ window . addEventListener ( 'mouseup' , handleGlobalMouseUp as any ) ;
118+ return ( ) => {
119+ window . removeEventListener ( 'mouseup' , handleGlobalMouseUp as any ) ;
120+ } ;
121+ }
122+ } , [ isPressed , handleMouseUp ] ) ;
72123
73124 if ( ! agent ) return null ;
74125
75126 return (
76127 < motion . button
77128 className = "ptt-button"
78- onMouseDown = { handlePushStart }
79- onMouseUp = { handlePushEnd }
129+ onMouseDown = { handleMouseDown }
130+ onMouseLeave = { handleMouseLeave }
131+ onMouseEnter = { handleMouseEnter }
132+ // we handle mouseup at the window level to catch all cases
133+ // onTouchStart/End would be implemented similarly
80134 initial = { false }
81135 animate = { {
82- backgroundColor : isPressed ? "#004085" : "#007bff" ,
136+ backgroundColor : isPressed
137+ ? isOutside
138+ ? "#d9534f" // red when outside and pressed (about to cancel)
139+ : "#004085" // blue when speaking normally
140+ : "#007bff" , // default blue
83141 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)" ,
84145 } }
85146 >
86- { isPressed ? "Speaking..." : "Press to Talk" }
147+ { isPressed
148+ ? isOutside
149+ ? "Release to Cancel"
150+ : "Speaking..."
151+ : "Press to Talk" }
87152 </ motion . button >
88153 ) ;
89154}
0 commit comments