Media APIs
Controlling audio and video in the browser — HTMLMediaElement, Web Audio API, MediaStream, getUserMedia, and the MediaRecorder API.
play() returns a Promise, and browsers reject it when a page tries to start media with sound before the user has interacted with the page (autoplay policy). Code that calls play() and carries on as if it worked gets an unhandled NotAllowedError. The player UI then lies to the user: the button shows "pause" while nothing is playing.
// <audio src="tone.wav"></audio> <button id="play">▶</button>
const media = document.querySelector("audio");
const playBtn = document.querySelector("#play");
media.play(); // start on page load, before any click
playBtn.textContent = "⏸"; // assumes it worked
console.log("button shows:", playBtn.textContent, "| paused:", media.paused);
// button shows: ⏸ | paused: true
// Uncaught (in promise) NotAllowedError: play() failed because the user didn't interact with the document first.Playing and controlling audio and video
HTMLMediaElement — the video and audio API
const video = document.querySelector("video");
// Playback control:
video.play(); // returns a Promise
video.pause();
video.currentTime = 0; // seek to start (in seconds)
video.volume = 0.7; // 0 to 1
video.muted = true;
video.playbackRate = 2; // 2x speed
// State:
video.paused; // true/false
video.ended; // true/false
video.duration; // total seconds (NaN until metadata loaded)
video.readyState; // 0-4 (4 = can play through)
// Events:
video.addEventListener("play", () => console.log("Playing"));
video.addEventListener("pause", () => console.log("Paused"));
video.addEventListener("ended", () => console.log("Ended"));
video.addEventListener("timeupdate", () => {
const pct = (video.currentTime / video.duration * 100).toFixed(1);
progressBar.style.width = pct + "%";
});
video.addEventListener("error", (e) => console.error("Error:", e.target.error));Building a custom video player
const video = document.querySelector("video");
const playBtn = document.querySelector("#play");
playBtn.addEventListener("click", () => {
if (video.paused) {
video.play();
playBtn.textContent = "⏸";
} else {
video.pause();
playBtn.textContent = "▶";
}
});
// Keyboard controls:
document.addEventListener("keydown", (e) => {
if (e.key === " ") video.paused ? video.play() : video.pause();
if (e.key === "ArrowLeft") video.currentTime -= 10;
if (e.key === "ArrowRight") video.currentTime += 10;
if (e.key === "ArrowUp") video.volume = Math.min(1, video.volume + 0.1);
if (e.key === "ArrowDown") video.volume = Math.max(0, video.volume - 0.1);
});The custom player above sets the button right after calling play(), so it has the same weakness whenever playback fails. Await the Promise and handle rejection. Better still, let the element's own play and pause events drive the button, so it always shows what really happened. (Muted media is generally allowed to autoplay, which is why background videos use muted.)
const media = document.querySelector("audio");
const playBtn = document.querySelector("#play");
media.addEventListener("play", () => { playBtn.textContent = "⏸"; }); // the button follows
media.addEventListener("pause", () => { playBtn.textContent = "▶"; }); // what really happened
async function tryPlay() {
try {
await media.play(); // play() returns a Promise: wait for it
} catch (err) {
console.log("blocked:", err.name); // NotAllowedError until the user interacts
}
console.log("button shows:", playBtn.textContent, "| paused:", media.paused);
}
tryPlay();
// blocked: NotAllowedError
// button shows: ▶ | paused: trueOfficial Sources
- MDN Web Docs — HTMLMediaElement
- MDN Web Docs — Web Audio API
- MDN Web Docs — MediaDevices.getUserMedia()
- MDN Web Docs — MediaRecorder
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.