Using the microphone to create unique games
This guide is for game developers using airconsole-1.11.0.js inside their game.
Use airconsole.getUserMedia() when a controller needs microphone access, for example for voice volume, singing, blowing, rhythm, or other audio-reactive gameplay.
Quick Start
Add a button to your controller HTML:
<button id="request-microphone" type="button">Enable microphone</button>
<p id="microphone-status"></p>Then wait until AirConsole is ready and request microphone access from that button. In most games, requesting from an intentional player action is clearer than asking immediately on page load and various browsers require user interaction for permission requests to work:
var micStream = null;
function setMicrophoneStatus(message) {
document.getElementById("microphone-status").textContent = message;
}
airconsole.onReady = function() {
document.getElementById("request-microphone").onclick = function() {
airconsole.getUserMedia({ audio: true })
.then(function(stream) {
micStream = stream;
setMicrophoneStatus("Microphone enabled");
// Use the stream, for example with the Web Audio API.
})
.catch(function(error) {
// Show a useful message or keep the game playable without microphone input.
setMicrophoneStatus("Microphone is not available");
console.error("Microphone access failed:", error);
});
};
};Stop the microphone when you no longer need it:
if (micStream) {
micStream.getTracks().forEach(function(track) {
track.stop();
});
micStream = null;
}Stopping tracks matters. It releases the microphone for the browser or app and lets privacy indicators turn off when nothing else is using the microphone.
Current limits
As of 2026-05-19, the AirConsole API 1.11.0 supports getUserMedia only on controllers and it only supports the audio constraint, not video
// Rejects in AirConsole 1.11.0.
airconsole.getUserMedia({ video: true });
// Rejects in AirConsole 1.11.0 on the screen.
airconsole.getUserMedia({ audio: true });What You Can Request
In AirConsole 1.11.0, you can call
airconsole.getUserMedia({ audio: true });to request access to the users audio stream on the controller.
You can also request user media constraints for audio:
airconsole.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});Where It Works
After airconsole.onReady has fired, call getUserMedia() from a controller, not from the screen. Calling before onReady, before the controller has a device id, rejects getUserMedia() with NotReady.
The screen can learn whether one of the controllers was granted or denied microphone access through callbacks:
airconsole.onUserMediaAccessGranted = function(deviceId) {
console.log("Controller " + deviceId + " has microphone access");
};
airconsole.onUserMediaAccessDenied = function(deviceId) {
console.log("Controller " + deviceId + " was denied microphone access");
};The controller that requested access should use the returned Promise to know its own result:
airconsole.getUserMedia({ audio: true })
.then(function(stream) {
// This controller got access.
})
.catch(function(error) {
// This controller did not get access.
});Handling Errors
getUserMedia() returns a Promise. It rejects when AirConsole cannot start the request or when the browser/app cannot grant microphone access.
AirConsole-specific errors extend AirConsoleUserMediaError
error.name === "AirConsole.UserMediaError"Common AirConsole-specific messages:
- NotSupportedOnScreen: the screen called getUserMedia().
- NotReady: AirConsole is not ready yet.
- AlreadyPending: another microphone request is already in progress on this controller.
- InvalidConstraints: the constraints are missing, false, or unsupported by AirConsole 1.11.0.
- Timeout: the AirConsole permission flow timed out.
- PermissionDenied: AirConsole/platform permission flow denied the request.
Browser-originated errors use the browser's own error names. Common ones are:
- NotAllowedError: the user, browser, page, iframe policy, or app settings denied microphone access.
- NotFoundError: no matching microphone was found.
- NotReadableError: the microphone exists, but the browser/OS could not open it.
- OverconstrainedError: your audio constraints could not be satisfied.
- AbortError: the browser could not use the device after starting the request.
A practical handler:
function showMessage(message) {
document.getElementById("microphone-status").textContent = message;
}
function handleMicrophoneError(error) {
if (error instanceof AirConsoleUserMediaError) {
if (error.message === AirConsole.USER_MEDIA_ERROR_TYPE.notSupportedOnScreen) {
showMessage("Microphone input is only available on controllers.");
} else if (error.message === AirConsole.USER_MEDIA_ERROR_TYPE.alreadyPending) {
showMessage("Microphone permission is already being requested.");
} else if (error.message === AirConsole.USER_MEDIA_ERROR_TYPE.timeout) {
showMessage("Microphone permission timed out. Try again.");
} else {
showMessage("Microphone is not available right now.");
}
return;
}
if (error instanceof DOMException) {
if (error.name === "NotAllowedError") {
showMessage("Allow microphone access in your browser or app settings.");
} else if (error.name === "NotFoundError") {
showMessage("No microphone was found on this device.");
} else if (error.name === "OverconstrainedError") {
showMessage("This microphone does not support the requested audio settings.");
} else {
showMessage("Could not start the microphone.");
}
return;
}
showMessage("Could not start the microphone.");
}Recommended Game Flow
Ask for microphone access only when the player intentionally enables an audio feature. A button in the controller lobby is usually better than requesting immediately on page load as certain browsers will reject permission requests without user interaction. If you use the request-microphone button from the quick-start HTML above, the flow is:
airconsole.onReady = function() {
document.getElementById("request-microphone").onclick = function() {
airconsole.getUserMedia({ audio: true })
.then(startMicrophoneGameplay)
.catch(handleMicrophoneError);
};
};Keep the game playable when microphone access fails. For example, keep button controls available or let the player retry.
Do not call getUserMedia() repeatedly while one request is pending. AirConsole rejects concurrent requests with AlreadyPending.
Do as much of the audio processing as possible on the controller. The message rate limit and the users network bandwidth can impact the users game experience otherwise.
Example: Send Audio Level To The Screen
This example measures microphone volume on the controller and sends a simple level value to the screen.
Controller HTML:
<button id="request-microphone" type="button">Enable microphone</button>
<button id="stop-microphone" type="button">Stop microphone</button>
<p id="microphone-status"></p>Controller JavaScript:
var micStream = null;
var audioContext = null;
var analyser = null;
var animationFrame = null;
var lastSendTime = 0;
var source = null;
airconsole.onReady = function() {
document.getElementById("request-microphone").onclick = startMicLevelInput;
document.getElementById("stop-microphone").onclick = stopMicLevelInput;
};
function setMicrophoneStatus(message) {
document.getElementById("microphone-status").textContent = message;
}
function startMicLevelInput() {
airconsole.getUserMedia({ audio: true })
.then(function(stream) {
var AudioContextClass = window.AudioContext || window.webkitAudioContext;
micStream = stream;
audioContext = new AudioContextClass();
source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
setMicrophoneStatus("Microphone enabled");
sendMicLevelLoop();
})
.catch(handleMicrophoneError);
}
function sendMicLevelLoop() {
var data = new Uint8Array(analyser.frequencyBinCount);
function tick() {
animationFrame = requestAnimationFrame(tick);
var now = Date.now();
// We want to send the information only 10 times per second of our 25 messages / second limit.
if (now - lastSendTime < 100) {
return;
}
lastSendTime = now;
analyser.getByteFrequencyData(data);
var sum = 0;
for (var i = 0; i < data.length; i++) {
sum += data[i] * data[i];
}
var level = Math.sqrt(sum / data.length) / 255;
airconsole.message(AirConsole.SCREEN, {
type: "audio-level",
level: level
});
}
tick();
}
function stopMicLevelInput() {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
if (micStream) {
micStream.getTracks().forEach(function(track) {
track.stop();
});
micStream = null;
}
if (source) {
source.disconnect();
source = null;
}
if (analyser) {
analyser.disconnect();
analyser = null;
}
if (audioContext) {
audioContext.close();
audioContext = null;
}
setMicrophoneStatus("Microphone stopped");
}
function handleMicrophoneError(error) {
setMicrophoneStatus("Microphone is not available");
console.error("Microphone access failed:", error);
}Screen:
airconsole.onMessage = function(deviceId, data) {
if (data && data.type === "audio-level") {
var level = data.level;
// Use level for gameplay or visualization.
}
};The example sends at most 10 audio-level messages per second. Keep audio telemetry small and rate-limited; games should not stream raw microphone data through AirConsole messages.
For a complete game structure, start from the AirConsole Pong example repository:
In this game you will find controller.html which implements the getUserMedia requesting.
Additionally you will find screen.html that subscribes to the airconsole.onUserMediaPermissionGranted and airconsole.onUserMediaPermissionDenied callbacks.
Use those as full-context examples for where controller setup, onReady, lobby UI, screen messages, and microphone cleanup live in an actual game.
Browser And App Behavior To Expect
Browsers and mobile apps do not expose microphone permission exactly the same way.
On web controllers:
- the browser may show its own microphone prompt.
- the Promise can stay pending if the player ignores the prompt.
- AirConsole rejects with Timeout after its own timeout period.
- browser failures such as NotAllowedError or NotFoundError may be returned.
On native AirConsole controller apps:
- the app may show native permission UI.
- if permission was denied permanently, the player may need to open system settings. AirConsole will inform the user about that.
- older app versions may not support the feature. AirConsole will in this case inform the user about the need to update the application.
Build your game UI around outcomes, not platform assumptions:
- granted: start audio gameplay.
- denied: When the user denies microphone access, the platform will be handling explain how to allow microphone access or offer a retry.
Checklist
- Call getUserMedia() only from controllers.
- Request microphone with { audio: true } unless you have a reason for advanced audio constraints.
- Handle both AirConsole.UserMediaError and browser error names.
- Keep a non-microphone fallback.
- Stop all tracks when leaving the audio feature, lobby, or game.
- Rate-limit any audio-derived messages sent to the screen.
- Do not request video through AirConsole 1.11.0.
References
- MDN MediaDevices.getUserMedia(): https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
- MDN MediaTrackConstraints: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
- MDN MediaStreamTrack.stop(): https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop