File size: 5,713 Bytes
53ea588
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebRTC Voice Agent</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
        #status { font-size: 20px; margin: 20px; }
        button { padding: 10px 20px; font-size: 16px; }
    </style>
</head>
<body>
    <h1>WebRTC Voice Agent</h1>
    <p id="status">Disconnected</p>
    <button id="connect-btn">Connect</button>
    <audio id="audio-el" autoplay></audio>

    <script>
        const statusEl = document.getElementById("status")
        const buttonEl = document.getElementById("connect-btn")
        const audioEl = document.getElementById("audio-el")

        let connected = false
        let peerConnection = null

        const waitForIceGatheringComplete = async (pc, timeoutMs = 2000) => {
            if (pc.iceGatheringState === 'complete') return;
            console.log("Waiting for ICE gathering to complete. Current state:", pc.iceGatheringState);
            return new Promise((resolve) => {
                let timeoutId;
                const checkState = () => {
                    console.log("icegatheringstatechange:", pc.iceGatheringState);
                    if (pc.iceGatheringState === 'complete') {
                        cleanup();
                        resolve();
                    }
                };
                const onTimeout = () => {
                    console.warn(`ICE gathering timed out after ${timeoutMs} ms.`);
                    cleanup();
                    resolve();
                };
                const cleanup = () => {
                    pc.removeEventListener('icegatheringstatechange', checkState);
                    clearTimeout(timeoutId);
                };
                pc.addEventListener('icegatheringstatechange', checkState);
                timeoutId = setTimeout(onTimeout, timeoutMs);
                // Checking the state again to avoid any eventual race condition
                checkState();
            });
        };


        const createSmallWebRTCConnection = async (audioTrack) => {
            const config = {
              iceServers: [],
            };
            const pc = new RTCPeerConnection(config)
            addPeerConnectionEventListeners(pc)
            pc.ontrack = e => audioEl.srcObject = e.streams[0]
            // SmallWebRTCTransport expects to receive both transceivers
            pc.addTransceiver(audioTrack, { direction: 'sendrecv' })
            pc.addTransceiver('video', { direction: 'sendrecv' })
            await pc.setLocalDescription(await pc.createOffer())
            await waitForIceGatheringComplete(pc)
            const offer = pc.localDescription
            try {
                const response = await fetch('/api/offer', {
                    body: JSON.stringify({ sdp: offer.sdp, type: offer.type}),
                    headers: { 'Content-Type': 'application/json' },
                    method: 'POST',
                });
                
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                const answer = await response.json()
                await pc.setRemoteDescription(answer)
            } catch (error) {
                console.error('Error during WebRTC connection setup:', error);
                _onDisconnected();
                throw error;
            }
            return pc
        }

        const connect = async () => {
            _onConnecting()
            const audioStream = await navigator.mediaDevices.getUserMedia({audio: true})
            peerConnection= await createSmallWebRTCConnection(audioStream.getAudioTracks()[0])
        }

        const addPeerConnectionEventListeners = (pc) => {
            pc.oniceconnectionstatechange = () => {
                console.log("oniceconnectionstatechange", pc?.iceConnectionState)
            }
            pc.onconnectionstatechange = () => {
                console.log("onconnectionstatechange", pc?.connectionState)
                let connectionState = pc?.connectionState
                if (connectionState === 'connected') {
                    _onConnected()
                } else if (connectionState === 'disconnected') {
                    _onDisconnected()
                }
            }
            pc.onicecandidate = (event) => {
                if (event.candidate) {
                    console.log("New ICE candidate:", event.candidate);
                } else {
                    console.log("All ICE candidates have been sent.");
                }
            };
        }

        const _onConnecting = () => {
            statusEl.textContent = "Connecting"
            buttonEl.textContent = "Disconnect"
            connected = true
        }

        const _onConnected = () => {
            statusEl.textContent = "Connected"
            buttonEl.textContent = "Disconnect"
            connected = true
        }

        const _onDisconnected = () => {
            statusEl.textContent = "Disconnected"
            buttonEl.textContent = "Connect"
            connected = false
        }

        const disconnect = () => {
            if (!peerConnection) {
                return
            }
            peerConnection.close()
            peerConnection = null
            _onDisconnected()
        }

        buttonEl.addEventListener("click", async () => {
            if (!connected) {
                await connect()
            } else {
                disconnect()
            }
        });
    </script>
</body>
</html>