AI-translated from English; not yet reviewed by a fluent editor.
# Crea una conversación de voz local en el navegador con OpenAI Realtime
> Crea una conversación local con el micrófono del navegador mediante OpenAI Realtime y WebRTC. El código completo del servidor y la página mantiene la clave API en el servidor y permite comprobar la reproducción, detener el micrófono y tratar fallos.
By BIG CHANGE Editorial
Published: 2026-10-19T19:20:34.863Z
Updated: 2026-10-19T19:20:34.863Z
Canonical: https://bigchange.ai/blog/openai-realtime-browser-voice-webrtc-guide

AI-generated conceptual illustration by BIG CHANGE; no actual OpenAI interface, browser test or heard reply is depicted.
La API Realtime de OpenAI permite enviar audio del micrófono y reproducir una respuesta hablada mediante WebRTC. Esta guía crea una conversación local con `gpt-realtime-2.1`. La clave API permanece en un servidor Node del ordenador. Necesitas un proyecto de API OpenAI con acceso al modelo y facturación; la suscripción de ChatGPT no paga el uso de API. BIG CHANGE comprobó la documentación y el código, pero no abrió una sesión de pago ni escuchó una respuesta.
## El gran cambio
- **Qué cambió:** OpenAI documenta una vía WebRTC del navegador para conversar con Realtime: un servidor local conserva la clave e intercambia la oferta de conexión del navegador por una respuesta.
- **Por qué importa:** El desarrollador puede comprobar todo el recorrido del micrófono a la respuesta hablada en una página local antes de diseñar un producto mayor. El propietario del proyecto controla la clave y paga la API.
- **Qué vigilar:** Un intercambio local correcto no demuestra preparación para producción. Una versión compartida necesita autenticación y control de gasto; una solicitud cancelada o agotada puede haber llegado ya a OpenAI.
## Antes de empezar
Instala Node.js 22.6 o posterior y usa un navegador actual con micrófono y altavoz funcionales. El acceso al micrófono exige un contexto seguro; `http://localhost` sirve para este ejemplo local. Prepara una clave de proyecto API OpenAI con acceso a `gpt-realtime-2.1` y facturación habilitada. Comprueba primero los límites de velocidad y gasto. La página del modelo indica que el nivel Free no está admitido. Conserva la clave en el entorno del proceso servidor con tu gestor de secretos o configuración local habitual. No la incluyas en `index.html`, no la guardes en un archivo `.env` del repositorio ni la pegues en una transcripción. [La guía WebRTC de OpenAI](https://developers.openai.com/api/docs/guides/voice-webrtc) documenta el intercambio con un servidor de confianza; [la página del modelo](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) enumera acceso y precios actuales.
Esta es la ruta unificada `/v1/realtime/calls` de la API Realtime. La guía WebRTC también describe GPT-Live mediante `/v1/live/sessions`, que usa otro protocolo de sesión. Mantén juntos la ruta y el contenido siguientes. El ejemplo se limita a tu ordenador; los controles locales Host y Origin no autentican usuarios de un servicio compartido.
1. Crea un directorio vacío para la demostración e instala Express desde él:
```bash
npm init -y
npm install express
```
La instalación crea `package.json` y añade la dependencia del servidor web. Guarda los dos archivos siguientes en ese directorio. El código utiliza los componentes integrados de Node `fetch`, `FormData` y `AbortSignal.timeout`; la configuración revisada necesita Node.js 22.6 o posterior.
2. Guarda lo siguiente como `server.mjs`. Recibe la oferta SDP del navegador (descripción de la conexión WebRTC propuesta) y la envía a OpenAI con la configuración Realtime. Solo este servidor lee la clave API estándar.
```javascript
import express from "express";
const app = express();
// This demo accepts only the local page, including protection against DNS rebinding.
app.use((req, res, next) => {
const hosts = ["localhost:3000", "127.0.0.1:3000"];
if (!hosts.includes(req.get("host"))) return res.status(403).send("Local host required");
if (req.method === "POST" && req.get("origin") !== `http://${req.get("host")}`) {
return res.status(403).send("Same-origin local page required");
}
next();
});
app.use(express.text({ type: "application/sdp", limit: "64kb" }));
app.get("/", (_req, res) => res.sendFile("index.html", { root: process.cwd() }));
app.post("/session", async (req, res) => {
if (!process.env.OPENAI_API_KEY) return res.status(500).send("Server API key missing");
if (typeof req.body !== "string" || !req.body.startsWith("v=")) {
return res.status(400).send("Expected an SDP offer");
}
const form = new FormData();
form.set("sdp", req.body);
form.set("session", JSON.stringify({
type: "realtime",
model: "gpt-realtime-2.1",
output_modalities: ["audio"],
audio: { output: { voice: "marin" } },
instructions: "Be a concise, helpful voice assistant. Do not claim access to external tools."
}));
try {
const upstream = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
body: form,
signal: AbortSignal.timeout(25000)
});
const answer = await upstream.text();
if (!upstream.ok) {
console.error("Realtime session rejected", upstream.status);
return res.status(502).send(`OpenAI session request failed (${upstream.status})`);
}
res.type("application/sdp").send(answer);
} catch (error) {
console.error("Realtime session network failure", error.name);
res.status(502).send("Could not reach the Realtime API");
}
});
app.listen(3000, "127.0.0.1", () => console.log("Open http://localhost:3000"));
```
El servidor escucha en `127.0.0.1:3000`, acepta únicamente el host y el origen POST de la página local y devuelve la respuesta SDP. El límite de 25 segundos para la solicitud al proveedor es una elección del ejemplo. Un tiempo agotado o una desconexión no prueba que OpenAI rechazara la solicitud; inspecciona el uso antes de repetir un inicio incierto.
3. Guarda lo siguiente como `index.html`. La página solicita permiso del micrófono después de **Start voice**, envía la oferta al servidor local y reproduce el audio entrante. **Stop** cierra la conexión y detiene las pistas del micrófono incluso durante el inicio. El plazo de 30 segundos también es una decisión del ejemplo, no una garantía de API.
```html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Local Realtime voice check</title>
<button id="start">Start voice</button>
<button id="stop" disabled>Stop</button>
<p id="status" role="status">Idle</p>
<audio id="reply" autoplay controls></audio>
<script type="module">
const start = document.querySelector("#start");
const stop = document.querySelector("#stop");
const status = document.querySelector("#status");
const reply = document.querySelector("#reply");
let current;
function closeSession(session = current, message = "Stopped") {
if (!session) return;
session.closed = true;
clearTimeout(session.timer);
session.abort.abort();
session.mic?.getTracks().forEach(track => track.stop());
if (session.pc) session.pc.onconnectionstatechange = null;
session.dc?.close();
session.pc?.close();
if (current === session) {
current = null;
reply.srcObject = null;
start.disabled = false;
stop.disabled = true;
status.textContent = message;
}
}
start.onclick = async () => {
const session = { abort: new AbortController(), closed: false };
current = session;
start.disabled = true;
stop.disabled = false;
status.textContent = "Requesting microphone";
session.timer = setTimeout(() => closeSession(session, "Setup timed out; microphone stopped"), 30000);
try {
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
if (session.closed) { mic.getTracks().forEach(track => track.stop()); return; }
session.mic = mic;
const pc = session.pc = new RTCPeerConnection();
pc.ontrack = event => { if (!session.closed) reply.srcObject = event.streams[0]; };
pc.onconnectionstatechange = () => {
if (session.closed) return;
status.textContent = `Connection: ${pc.connectionState}`;
if (pc.connectionState === "connected") clearTimeout(session.timer);
if (["failed", "closed", "disconnected"].includes(pc.connectionState)) {
closeSession(session, "Connection ended; microphone stopped. Start a new session.");
}
};
pc.addTrack(mic.getAudioTracks()[0], mic);
const dc = session.dc = pc.createDataChannel("oai-events");
dc.onmessage = message => {
if (session.closed) return;
try {
const event = JSON.parse(message.data);
if (event.type === "session.created") status.textContent = "Session ready: speak when connected";
if (event.type === "response.done") {
status.textContent = `Response status: ${event.response?.status || "unknown"}; verify audible playback`;
}
if (event.type === "error") closeSession(session, `Realtime error: ${event.error?.message || "unknown"}; stopped`);
} catch { closeSession(session, "Invalid event; stopped"); }
};
dc.onclose = () => { if (!session.closed) closeSession(session, "Event channel closed; stopped"); };
dc.onerror = () => closeSession(session, "Event channel failed; stopped");
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
if (session.closed) return;
// The official WebRTC example submits the generated offer directly.
const response = await fetch("/session", {
method: "POST",
headers: { "Content-Type": "application/sdp" },
body: offer.sdp,
signal: session.abort.signal
});
if (!response.ok) throw new Error(await response.text());
const answer = await response.text();
if (session.closed) return;
await pc.setRemoteDescription({ type: "answer", sdp: answer });
if (!session.closed && pc.connectionState !== "connected") status.textContent = "Signaling complete; waiting for connection";
} catch (error) {
if (!session.closed) closeSession(session, `Start failed: ${error.message}`);
}
};
stop.onclick = () => closeSession();
window.addEventListener("pagehide", () => closeSession());
</script>
</html>
```
El navegador cierra además la sesión si falla el par o el canal de eventos y cuando se abandona la página. El permiso puede llegar después de detener o agotar el plazo; la pista tardía se detiene. La línea de estado muestra `response.done` con su estado declarado. El evento por sí solo no demuestra que la respuesta fuera audible ni que una respuesta cancelada o fallida terminara bien.
4. Inicia el servidor desde el directorio de los dos archivos, con `OPENAI_API_KEY` ya definido en el entorno de ese proceso:
```bash
node server.mjs
```
Abre `http://localhost:3000`. Debes ver **Start voice**, **Stop**, el estado **Idle** y un reproductor. Pulsa **Start voice**, permite el micrófono y formula una pregunta breve sin información sensible. La [guía de conversaciones Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) dice que la detección de actividad de voz predeterminada puede convertir una pausa tras hablar en una respuesta. Para comprobar tu ejecución, verifica que el navegador muestra un micrófono activo, que la conexión llega a `connected`, que aparece un evento `session.created`, que oyes voz y que `response.done` indica una respuesta completada. Pulsa **Stop** y comprueba que se apaga el indicador del micrófono. Estas pruebas te corresponden; BIG CHANGE no realizó esta sesión de pago.
## Si falla la comprobación
Si se deniega el permiso o no aparece dispositivo, revisa permisos y micrófono y usa la URL local `localhost` o HTTPS. Si la página comunica un fallo `/session`, mira el estado HTTP del servidor y verifica clave, acceso al modelo, facturación, límites y endpoint. El servidor no imprime la clave ni el cuerpo de respuesta de OpenAI. Un 502 también puede ser fallo de red o el plazo de 25 segundos; no revela si se aceptó una solicitud incierta.
Si la señalización funciona pero falla la conexión, mira su estado, las restricciones de red y la respuesta SDP. Si conecta sin sonido, revisa entrada de micrófono y salida elegida. El navegador puede bloquear reproducción automática: pulsa **Play** en el control de audio y, si hace falta, detén e inicia otra sesión. Ante cualquier fallo, usa **Stop** y confirma que el indicador se apaga. Comprueba el uso antes de repetir inicios con resultado incierto.
## Coste, privacidad y límites
OpenAI publica para `gpt-realtime-2.1` texto no almacenado en caché de entrada/salida a **$4/$24 por millón de tokens** y audio de entrada/salida a **$32/$64 por millón de tokens**, comprobado el 19 de octubre de 2026. El coste depende de tokens consumidos, incluidos turnos posteriores que pueden incorporar contexto previo. Consulta [precios y límites del modelo](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) y el uso del proyecto antes de empezar; la duración sola no permite estimar la factura. La [guía de conversaciones Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) fija una duración máxima de 60 minutos.
El audio del micrófono se envía a OpenAI durante la sesión. Utiliza voz que tengas permiso para transmitir y evita audio privado o ajeno. Esta página local no tiene inicio de sesión ni presupuesto por usuario. Un servicio compartido necesitaría acceso `/session` autenticado y limitado por tasa, y un identificador de seguridad generado por el servidor; los controles Host y Origin locales no bastan. El ejemplo no llama herramientas externas ni hace llamadas telefónicas. No se midieron latencia, fiabilidad, calidad de voz, ahorro ni resultados de despliegue.
## Fuentes y lecturas adicionales
- [Guía WebRTC de OpenAI](https://developers.openai.com/api/docs/guides/voice-webrtc): intercambio unificado Realtime, audio del navegador y protocolo GPT-Live separado.
- [Guía de conversaciones OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations): eventos, turnos, estado de respuesta y límite de duración.
- [OpenAI `gpt-realtime-2.1`: página del modelo](https://developers.openai.com/api/docs/models/gpt-realtime-2.1): acceso, precios de tokens y límites. Vuelve a comprobar precios y acceso al ejecutar el ejemplo.
## Sources
- [Guía WebRTC de OpenAI](https://developers.openai.com/api/docs/guides/voice-webrtc) — Interfaz unificada Realtime calls, clave de confianza, SDP, audio del navegador y protocolo GPT-Live separado.
- [Conversaciones OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) — Eventos, VAD, estado de respuesta y duración de 60 minutos.
- [Modelo OpenAI gpt-realtime-2.1](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) — Precios de tokens y acceso del modelo.
- [Optimización de costes de voz OpenAI](https://developers.openai.com/api/docs/guides/voice-latency-cost) — Contexto y uso por respuesta.
- [Facturación de API OpenAI y ChatGPT](https://help.openai.com/en/articles/9039756-managing-billing-for-chatgpt-and-the-api-platform) — Facturación separada.
El boletín de BIG CHANGE
La visión global, a tu ritmo.
Historias recientes sobre IA y robótica, cambios que vale la pena observar e ideas prácticas para usar. Elige un informe diario, un resumen semanal o una perspectiva mensual.
Next scheduled send (UTC): . Your first edition arrives at the next scheduled send after you confirm.
Tu privacidad, tu elección.
El almacenamiento necesario ayuda a proteger el sitio y a recordar tus preferencias. Google Analytics opcional permanece desactivado hasta que lo autorices. Puedes leer todas las historias usando solo el almacenamiento necesario. Detalles de privacidad