WebSocket enables real-time communication between frontend and backend for Speaking Practice. This section guides you through integrating WebSocket into Next.js.
User speaks
↓
Frontend records → sends audio chunks via WebSocket
↓
Lambda receives audio → Transcribe (STT)
↓
Lambda sends text → Bedrock (AI response)
↓
Lambda receives AI response → Polly (TTS)
↓
Lambda sends audio response to frontend
↓
Frontend plays audio for user
Create lib/websocket.ts:
export class WebSocketClient {
private ws: WebSocket | null = null;
private url: string;
public onOpen: (() => void) | null = null;
public onMessage: ((data: any) => void) | null = null;
public onClose: (() => void) | null = null;
public onError: ((error: Event) => void) | null = null;
constructor(url: string) {
this.url = url;
}
connect(token?: string) {
const wsUrl = token ? `${this.url}?token=${token}` : this.url;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.onOpen?.();
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.onMessage?.(data);
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.onClose?.();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.onError?.(error);
};
}
send(data: any) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
close() {
this.ws?.close();
}
}
Create hooks/useAudioRecorder.ts:
import { useState, useRef, useCallback } from 'react';
export const useAudioRecorder = () => {
const [isRecording, setIsRecording] = useState(false);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const startRecording = useCallback(async () => {
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true }
});
const mediaRecorder = new MediaRecorder(stream);
mediaRecorderRef.current = mediaRecorder;
mediaRecorder.start(100);
setIsRecording(true);
}, []);
const stopRecording = useCallback((): Promise<Blob> => {
return new Promise((resolve) => {
if (!mediaRecorderRef.current) return resolve(new Blob());
mediaRecorderRef.current.onstop = () => {
const audioBlob = new Blob([], { type: 'audio/webm' });
setIsRecording(false);
resolve(audioBlob);
};
mediaRecorderRef.current.stop();
});
}, []);
return { isRecording, startRecording, stopRecording };
};
Create app/(app)/speaking/page.tsx:
'use client';
import { useEffect, useRef, useState } from 'react';
import { WebSocketClient } from '@/lib/websocket';
import { useAudioRecorder } from '@/hooks/useAudioRecorder';
export default function SpeakingPage() {
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef<WebSocketClient | null>(null);
const { isRecording, startRecording, stopRecording } = useAudioRecorder();
useEffect(() => {
const ws = new WebSocketClient(process.env.NEXT_PUBLIC_WEBSOCKET_URL || '');
ws.onOpen = () => setIsConnected(true);
ws.connect();
wsRef.current = ws;
return () => ws.close();
}, []);
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Speaking Practice</h1>
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
</div>
);
}
.env.localFrontend complete! Continue to CI/CD Pipeline for automation.