Live recording
Stream live microphone audio for real-time transcription in the browser
In this example, you capture microphone audio in the browser, stream it to the SubQ API over a WebSocket, and display the transcript in real time on the page. This builds on the same setup used in the pre-recorded example but introduces WebSocket streaming and microphone access.
Prerequisites
- Complete the browser setup (dependencies installed, HTTPS certificates generated)
- A SubQ API key
- A microphone connected to your machine
Run the example
Start the local HTTPS server:
npm run serve:liveOpen the page in your browser:
https://localhost:3001?key=org_YOUR_API_KEYReplace org_YOUR_API_KEY with your actual API key. Accept the self-signed certificate warning when prompted.
- Allow microphone access when the browser prompts
- Click Start — speak into your microphone; the transcript appears in real time on the page
- Click Stop — ends the recording and closes the WebSocket connection
How it works
The full source is in browser-live/index.html. Here are the key parts:
1. Create a client and open a WebSocket
The page loads the Deepgram SDK from a CDN, creates a client pointed at the SubQ API, and opens a persistent WebSocket connection:
const client = deepgram.createClient(apiKey, {
global: { url: "https://stt-api.subq.ai" }
});
connection = client.listen.live({});client.listen.live() opens a WebSocket to wss://stt-api.subq.ai/v1/listen and handles authentication automatically using the API key.
2. Capture microphone audio
The browser requests microphone access via getUserMedia and creates a MediaRecorder that emits audio chunks every second:
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
connection.send(event.data);
}
};Each chunk is sent directly to the SubQ API over the WebSocket with connection.send().
3. Display transcripts as they arrive
The SDK fires a Transcript event each time the API returns a result. The handler extracts the transcript text and displays it on the page:
connection.on(deepgram.LiveTranscriptionEvents.Transcript, (data) => {
transcriptEl.textContent = data.channel.alternatives[0].transcript;
});4. Start and stop
Clicking Start calls mediaRecorder.start(1000), which begins capturing audio in 1-second intervals. Clicking Stop calls mediaRecorder.stop() and connection.finish() to close the WebSocket gracefully:
startButton.onclick = () => {
if (!connection) {
setup().then(() => {
mediaRecorder.start(1000);
});
} else {
mediaRecorder.start(1000);
}
};
stopButton.onclick = () => {
mediaRecorder.stop();
connection.finish();
};The example also fetches a remote radio stream (icecast.omroep.nl/radio1-bb-mp3) on connection open and sends it alongside the microphone audio. This is a demo artifact included in the sample code. For a clean microphone-only implementation, remove the fetch block inside the Open event handler.
Full source
<!DOCTYPE html>
<html>
<head>
<title>SubQ Live Transcription</title>
</head>
<body>
<h1>Live Transcription</h1>
<button id="start">Start</button>
<button id="stop">Stop</button>
<pre id="transcript"></pre>
<script src="https://cdn.jsdelivr.net/npm/@deepgram/sdk@4/dist/umd/deepgram.js"></script>
<script>
const startButton = document.getElementById("start");
const stopButton = document.getElementById("stop");
const transcriptEl = document.getElementById("transcript");
let connection;
let mediaRecorder;
const setup = async () => {
const urlParams = new URLSearchParams(window.location.search);
const apiKey = urlParams.get("key");
if (!apiKey) {
alert("Please add your SubQ API key to the query string, e.g. ?key=org_xxx...");
return;
}
const client = deepgram.createClient(apiKey, {
global: {
url: 'https://stt-api.subq.ai'
}
});
connection = client.listen.live({});
connection.on(deepgram.LiveTranscriptionEvents.Open, () => {
console.log("Connection opened.");
fetch("https://icecast.omroep.nl/radio1-bb-mp3")
.then((response) => response.body)
.then((body) => {
const reader = body.getReader();
function read() {
reader
.read()
.then(({ done, value }) => {
if (done) {
console.log("Stream complete");
return;
}
connection.send(value);
read();
})
.catch((error) => {
console.error("Stream read error:", error);
});
}
read();
})
.catch((error) => {
console.error("Fetch error:", error);
});
});
connection.on(deepgram.LiveTranscriptionEvents.Transcript, (data) => {
transcriptEl.textContent = data.channel.alternatives[0].transcript;
});
connection.on(deepgram.LiveTranscriptionEvents.Close, () => {
console.log("Connection closed.");
});
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
connection.send(event.data);
}
};
};
startButton.onclick = () => {
if (!connection) {
setup().then(() => {
mediaRecorder.start(1000);
});
} else {
mediaRecorder.start(1000);
}
};
stopButton.onclick = () => {
mediaRecorder.stop();
connection.finish();
};
</script>
</body>
</html>Next steps
- Real-time streaming — server-side streaming with Python, Node.js, Go, or Rust
- Pre-recorded upload — transcribe a file without microphone access