Thanks for tuning in to Google I/O. Watch the Chrome content on-demand.
Media patterns
A collection of common patterns for working with media.
On this page
Welcome to our collection of media patterns. These intend to help you create modern media experiences on the web for great audio and video playback.
How to add effects to audio
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔉</text></svg>"
/>
<title>How to add effects to audio</title>
</head>
<body>
<h1>How to add effects to audio</h1>
<button type="button">Press to hear audio effect</button>
</body>
</html>
const button = document.querySelector('button');
button.addEventListener('click', () => {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
oscillator.type = 'sine';
// Value is in hertz.
oscillator.frequency.setValueAtTime(420, audioCtx.currentTime);
const biquadFilter = audioCtx.createBiquadFilter();
biquadFilter.type = 'lowpass';
biquadFilter.frequency.setValueAtTime(200, audioCtx.currentTime + 1);
oscillator.connect(biquadFilter);
biquadFilter.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(2);
});
:root {
color-scheme: dark light;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 1rem;
font-family: system-ui, sans-serif;
}
Learn how to use the Web Audio API to add effects to audio sources.
How to customize media notifications
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📺</text></svg>"
/>
<title>How to customize media notifications</title>
</head>
<body>
<h1>How to customize media notifications</h1>
<video controls playsinline src="https://storage.googleapis.com/media-session/caminandes/short.mp4"></video>
<p>Credits: Media files are © copyright Blender Foundation | <a href="https://www.blender.org">www.blender.org</a>.</p>
</body>
</html>
const video = document.querySelector("video");
navigator.mediaSession.metadata = new MediaMetadata({
title: "Caminandes 2: Gran Dillama - Blender Animated Short",
artist: "Blender Foundation",
artwork: [
{ src: "https://storage.googleapis.com/media-session/caminandes/artwork-96.png", sizes: "96x96" },
{ src: "https://storage.googleapis.com/media-session/caminandes/artwork-128.png", sizes: "128x128" },
{ src: "https://storage.googleapis.com/media-session/caminandes/artwork-256.png", sizes: "256x256" },
{ src: "https://storage.googleapis.com/media-session/caminandes/artwork-512.png", sizes: "512x512" },
],
});
navigator.mediaSession.setActionHandler("play", async () => {
// Resume playback
try {
await video.play();
} catch (err) {
console.error(err.name, err.message);
}
});
navigator.mediaSession.setActionHandler("pause", () => {
// Pause active playback
video.pause();
});
video.addEventListener("play", () => {
navigator.mediaSession.playbackState = "playing";
});
video.addEventListener("pause", () => {
navigator.mediaSession.playbackState = "paused";
});
:root {
color-scheme: dark light;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 1rem;
font-family: system-ui, sans-serif;
}
video {
display: block;
margin-top: 10px;
max-width: 100%;
}
Learn how to let the user control media playback on the lock screen, in a widget, or with hardware keys.
How to process audio from the user's microphone
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎙️</text></svg>"
/>
<title>How to process audio from the user's microphone</title>
</head>
<body>
<h1>How to process audio from the user's microphone</h1>
<button id="startMicrophoneButton">Start using microphone</button>
<button id="stopMicrophoneButton" disabled>Stop using microphone</button>
<pre id="logs"></pre>
</body>
</html>
// This file is evaluated in the audio rendering thread
// upon context.audioWorklet.addModule() call.
class WorkletProcessor extends AudioWorkletProcessor {
process([input], [output]) {
// Copy inputs to outputs.
output[0].set(input[0]);
return true;
}
}
registerProcessor("processor", WorkletProcessor);
const startMicrophoneButton = document.querySelector('#startMicrophoneButton');
const stopMicrophoneButton = document.querySelector('#stopMicrophoneButton');
let stream;
startMicrophoneButton.addEventListener("click", async () => {
// Prompt the user to use their microphone.
stream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
const context = new AudioContext();
const source = context.createMediaStreamSource(stream);
// Load and execute the module script.
await context.audioWorklet.addModule("processor.js");
// Create an AudioWorkletNode. The name of the processor is the
// one passed to registerProcessor() in the module script.
const processor = new AudioWorkletNode(context, "processor");
source.connect(processor).connect(context.destination);
stopMicrophoneButton.disabled = false;
log("Your microphone audio is being used.");
});
stopMicrophoneButton.addEventListener("click", () => {
// Stop the stream.
stream.getTracks().forEach(track => track.stop());
log("Your microphone audio is not used anymore.");
});
:root {
color-scheme: dark light;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 1rem;
font-family: system-ui, sans-serif;
}
button {
display: block;
margin-bottom: 4px;
}
pre {
color: red;
white-space: pre-line;
}
Learn how to process audio from the user's microphone.
How to record audio from the user's microphone
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎙️</text></svg>"
/>
<title>How to record audio from the user's microphone</title>
</head>
<body>
<h1>How to record audio from the user's microphone</h1>
<button id="startMicrophoneButton">Start using microphone</button>
<button id="stopMicrophoneButton" disabled>Stop using microphone</button>
<button id="startRecordButton" disabled>Start recording</button>
<button id="stopRecordButton" disabled>Stop recording</button>
<pre id="logs"></pre>
</body>
</html>
const startMicrophoneButton = document.querySelector('#startMicrophoneButton');
const stopMicrophoneButton = document.querySelector('#stopMicrophoneButton');
const startRecordButton = document.querySelector('#startRecordButton');
const stopRecordButton = document.querySelector('#stopRecordButton');
let stream;
let recorder;
startMicrophoneButton.addEventListener("click", async () => {
// Prompt the user to use their microphone.
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
recorder = new MediaRecorder(stream);
stopMicrophoneButton.disabled = false;
startRecordButton.disabled = false;
log("Your microphone audio is being used.");
});
stopMicrophoneButton.addEventListener("click", () => {
// Stop the stream.
stream.getTracks().forEach(track => track.stop());
startRecordButton.disabled = true;
stopRecordButton.disabled = true;
log("Your microphone audio is not used anymore.");
});
startRecordButton.addEventListener("click", async () => {
// For the sake of more legible code, this sample only uses the
// `showSaveFilePicker()` method. In production, you need to
// cater for browsers that don't support this method, as
// outlined in https://web.dev/patterns/files/save-a-file/.
// Prompt the user to choose where to save the recording file.
const suggestedName = "microphone-recording.webm";
const handle = await window.showSaveFilePicker({ suggestedName });
const writable = await handle.createWritable();
// Start recording.
recorder.start();
recorder.addEventListener("dataavailable", async (event) => {
// Write chunks to the file.
await writable.write(event.data);
if (recorder.state === "inactive") {
// Close the file when the recording stops.
await writable.close();
}
});
stopRecordButton.disabled = false;
log("Your microphone audio is being recorded locally.");
});
stopRecordButton.addEventListener("click", () => {
// Stop the recording.
recorder.stop();
stopRecordButton.disabled = true;
log("Your microphone audio has been successfully recorded locally.");
});
:root {
color-scheme: dark light;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 1rem;
font-family: system-ui, sans-serif;
}
button {
display: block;
margin-bottom: 4px;
}
pre {
color: red;
white-space: pre-line;
}
Learn how to record audio from the user's microphone.
How to add Picture-in-Picture to video controls
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📺</text></svg>"
/>
<title>How to add Picture-in-Picture to video controls</title>
</head>
<body>
<h1>How to add Picture-in-Picture to video controls</h1>
<button id="togglePipButton">Picture-in-Picture</button>
<button id="toggleFakePipButton">Fake Picture-in-Picture</button>
<video autoplay muted playsinline loop src="https://storage.googleapis.com/media-session/caminandes/short.mp4"></video>
</body>
</html>
const video = document.querySelector('video');
const togglePipButton = document.querySelector('#togglePipButton');
const toggleFakePipButton = document.querySelector('#toggleFakePipButton');
togglePipButton.addEventListener("click", async (event) => {
togglePipButton.disabled = true;
try {
if (video !== document.pictureInPictureElement) {
await video.requestPictureInPicture();
} else {
await document.exitPictureInPicture();
}
} finally {
togglePipButton.disabled = false;
}
});
video.addEventListener("enterpictureinpicture", (event) => {
togglePipButton.classList.add("on");
});
video.addEventListener("leavepictureinpicture", (event) => {
togglePipButton.classList.remove("on");
});
/* Feature detection */
if ("pictureInPictureEnabled" in document) {
// Hide fake PiP button if Picture-in-Picture is supported.
toggleFakePipButton.hidden = true;
// Set button ability depending on whether Picture-in-Picture can be used.
setPipButton();
video.addEventListener("loadedmetadata", setPipButton);
video.addEventListener("emptied", setPipButton);
} else {
// Hide button if Picture-in-Picture is not supported.
togglePipButton.hidden = true;
}
function setPipButton() {
togglePipButton.disabled =
video.readyState === 0 ||
!document.pictureInPictureEnabled ||
video.disablePictureInPicture;
}
/* Fake Picture-in-Picture */
toggleFakePipButton.addEventListener("click", (event) => {
toggleFakePipButton.classList.toggle("on");
video.classList.toggle("fake-pip");
});
:root {
color-scheme: dark light;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 1rem;
font-family: system-ui, sans-serif;
}
button:before {
content: "Enter ";
}
button.on:before {
content: "Leave ";
}
video {
display: block;
margin-top: 10px;
max-width: 100%;
}
video.fake-pip {
position: fixed;
z-index: 1000;
bottom: 10px;
right: 10px;
}
Learn how to create a custom Picture-in-Picture button for your video player.
How to record the user's screen
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🖥️</text></svg>"
/>
<title>How to record the user's screen</title>
</head>
<body>
<h1>How to record the user's screen</h1>
<button id="startShareScreenButton">Start sharing screen</button>
<button id="stopShareScreenButton" disabled>Stop sharing screen</button>
<button id="startRecordButton" disabled>Start recording</button>
<button id="stopRecordButton" disabled>Stop recording</button>
<video autoplay muted playsinline></video>
<pre id="logs"></pre>
</body>
</html>
const video = document.querySelector('video');
const startShareScreenButton = document.querySelector('#startShareScreenButton');
const stopShareScreenButton = document.querySelector('#stopShareScreenButton');
const startRecordButton = document.querySelector('#startRecordButton');
const stopRecordButton = document.querySelector('#stopRecordButton');
let stream;
let recorder;
startShareScreenButton.addEventListener("click", async () => {
// Prompt the user to share their screen.
stream = await navigator.mediaDevices.getDisplayMedia();
recorder = new MediaRecorder(stream);
// Preview the screen locally.
video.srcObject = stream;
startRecordButton.disabled = false;
stopShareScreenButton.disabled = false;
log("Your screen is being shared.");
});
stopShareScreenButton.addEventListener("click", () => {
// Stop the stream.
stream.getTracks().forEach(track => track.stop());
video.srcObject = null;
stopShareScreenButton.disabled = true;
startRecordButton.disabled = true;
stopRecordButton.disabled = true;
log("Your screen is not shared anymore.");
});
startRecordButton.addEventListener("click", async () => {
// For the sake of more legible code, this sample only uses the
// `showSaveFilePicker()` method. In production, you need to
// cater for browsers that don't support this method, as
// outlined in https://web.dev/patterns/files/save-a-file/.
// Prompt the user to choose where to save the recording file.
const suggestedName = "screen-recording.webm";
const handle = await window.showSaveFilePicker({ suggestedName });
const writable = await handle.createWritable();
// Start recording.
recorder.start();
recorder.addEventListener("dataavailable", async (event) => {
// Write chunks to the file.
await writable.write(event.data);
if (recorder.state === "inactive") {
// Close the file when the recording stops.
await writable.close();
}
});
stopRecordButton.disabled = false;
log("Your screen is being recorded locally.");
});
stopRecordButton.addEventListener("click", () => {
// Stop the recording.
recorder.stop();
stopRecordButton.disabled = true;
log("Your screen has been successfully recorded locally.");
});
:root {
color-scheme: dark light;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 1rem;
font-family: system-ui, sans-serif;
}
button {
display: block;
margin-bottom: 4px;
}
video {
display: block;
margin-top: 10px;
max-width: 100%;
background: black;
max-height: 480px;
}
pre {
color: red;
white-space: pre-line;
}
Learn how to record the user's screen.