---
title: 'Media and Device APIs'
source: 'https://academia.sh/en/courses/browser-platform/media-and-device-apis'
course: 'The Browser and the Web Platform'
language: en
updated: '2026-08-17T18:09:11+00:00'
license: 'CC BY-SA 4.0'
---

# Media and Device APIs

Access to capabilities that require permission; permission's states and preconditions, the secure-context and user-gesture requirements, feature detection and graceful degradation, media tracks' lifecycle, and the privacy balance of location access.

Locale, time zone, and language preference get read from the user's
device without asking permission; these are information the page
already knows. There are other things the measurement station page
will want to ask for: the coordinates of the point a measurement got
taken at, a photo to attach to a fault report, a voice note.

None of these get given without asking. What they have in common is
that they **carry information out of the user's device**. The
browser puts these capabilities behind a common permission mechanism
that, once learned, applies to all of them.

## Capabilities That Require Permission

Capabilities split into three sets.

**Those that require no permission** are the device's general
properties that do not identify the user: viewport dimensions,
language preference, time zone, whether the connection is online.
These get read without asking.

**Those that require explicit permission** are the ones that collect
data from the user's surroundings: camera, microphone, location,
screen sharing, motion and orientation sensors, reading the
clipboard, sending notifications.

**Those limited by user gesture** are capabilities that do not ask
permission but cannot get triggered on their own: going fullscreen,
prompting a file selection, keeping the screen awake, writing to the
clipboard. These only run while an event arising from a user action
gets processed.

A capability can also be closed from the very start. The
permissions-policy declaration defined in the Embedded Content lesson
determines which capabilities get delegated to a frame; an
undelegated capability does not work even with user consent.

## Permission's States

Permission sits in one of three states: not yet asked, granted,
denied. When a request gets made, the browser checks the
preconditions first, then looks at the state.

```js
// permission.mjs — the permission state machine and a request's preconditions
function request(state, env) {
  if (env.secureContext === false)
    return { state, result: "error: no secure context, not asked" };
  if (state === "granted") return { state, result: "granted directly, not asked" };
  if (state === "denied") return { state, result: "denied, not asked again" };
  if (env.userGesture === false)
    return { state, result: "error: no user gesture, not asked" };
  return { state: env.response, result: `asked the user -> ${env.response}` };
}

const scenarios = [
  ["insecure context", { secureContext: false, userGesture: true, response: "granted" }],
  ["gestureless call", { secureContext: true, userGesture: false, response: "granted" }],
  ["user dismissed", { secureContext: true, userGesture: true, response: "not-asked" }],
  ["user granted", { secureContext: true, userGesture: true, response: "granted" }],
  ["second request", { secureContext: true, userGesture: true, response: "granted" }],
];

let state = "not-asked";
console.log("initial state:", state);
for (const [label, env] of scenarios) {
  const result = request(state, env);
  state = result.state;
  console.log(label.padEnd(18), "->", result.result.padEnd(38), "| state:", state);
}

// When the user revokes permission from settings, the state changes; the page only learns this by asking.
state = "denied";
console.log("permission revoked ->", request(state, { secureContext: true, userGesture: true, response: "granted" }).result);
```

```
initial state: not-asked
insecure context   -> error: no secure context, not asked    | state: not-asked
gestureless call   -> error: no user gesture, not asked      | state: not-asked
user dismissed     -> asked the user -> not-asked            | state: not-asked
user granted       -> asked the user -> granted              | state: granted
second request     -> granted directly, not asked            | state: granted
permission revoked -> denied, not asked again
```

Five rules come together in this output.

**A secure context is required.** A page served unencrypted cannot
request these capabilities; without the privacy
guarantee defined in the Role of HTTPS lesson, data coming from the
camera would travel exposed over the network. The local development
address is a defined exception to this rule.

**A user gesture is required.** Code that asks for permission the
moment the page loads can get rejected by the browser. The request
gets made while processing the event started by the user pressing a
button.

**Dismissing is not denying.** When the user closes the prompt
without answering it, the state stays "not asked"; the next request
can ask again. This also opens the door to insistent asking, which
the browser blocks on its own once abused.

**Granted permission does not get asked again.** The second request
resolves without bothering the user.

**Denied permission does not get asked again either.** The request
fails directly; what gets shown to the user is not a question, but
an explanation of how to enable it in the browser's settings.

Permission state can also get **queried**: what the state is can get
learned without making a request, and a notification can get
received when the state changes. This makes it possible to update
the interface when the user revokes permission from settings.

## Feature Detection and Graceful Degradation

A capability being absent and being denied are separate situations
and get handled separately.

**Feature detection** is testing at runtime whether the relevant
interface exists. Code that checks the browser's name or version is
the wrong path: testing the name does not test the capability. The
test checks for the presence of the object or method that will get
used; if negative, the interface hides that option entirely.

These three situations have different responses. If the capability
is absent, the option does not get shown. If it exists but permission
got denied, the option shows with an explanation of how to enable
it. If the capability exists, permission exists, but
the operation failed, an error gets reported and can get retried.

The common rule in every case is this: the page has to do its job
even without the capability. If it cannot read location, it asks the
user to type the coordinate; if it cannot open the camera, it
prompts a file selection. A design with only a capability-dependent
path leaves a declining user outside the page.

## Media Tracks and Releasing the Device

A camera or microphone request returns a **stream**. A stream
consists of **tracks**, each tied to a single source, and releasing
the source depends on stopping the tracks.

```js
// stream.mjs — media track lifecycle and releasing the device
const devices = { camera: 0, microphone: 0 };   // how many tracks are using the device

const track = (kind, device, settings) => {
  devices[device] += 1;
  return {
    kind, device, settings, live: true,
    stop() { if (this.live) { this.live = false; devices[device] -= 1; } },
  };
};

function getStream(constraints) {
  const tracks = [];
  if (constraints.video) tracks.push(track("video", "camera", { width: 1280, height: 720 }));
  if (constraints.audio) tracks.push(track("audio", "microphone", { echoCancellation: true }));
  return { tracks, stop() { for (const t of this.tracks) t.stop(); } };
}

const log = (label) =>
  console.log(label.padEnd(24), "camera:", devices.camera, "| microphone:", devices.microphone);

log("start");
const stream = getStream({ video: true, audio: true });
log("stream acquired");
console.log("tracks:", stream.tracks.map((t) => `${t.kind}(${JSON.stringify(t.settings)})`).join(" "));

stream.tracks[0].stop();                        // only the video track gets stopped
log("video track stopped");
stream.tracks[0].stop();                        // a second stop has no effect
log("same track stopped again");
stream.stop();
log("stream stopped");
console.log("track states:", stream.tracks.map((t) => `${t.kind}=${t.live ? "live" : "ended"}`).join(" "));
```

```
start                    camera: 0 | microphone: 0
stream acquired          camera: 1 | microphone: 1
tracks: video({"width":1280,"height":720}) audio({"echoCancellation":true})
video track stopped      camera: 0 | microphone: 1
same track stopped again camera: 0 | microphone: 1
stream stopped           camera: 0 | microphone: 0
track states: video=ended audio=ended
```

The fourth line shows the most common mistake. Clearing a video
element's source or removing the element from the tree does not stop
the track; the recording indicator keeps glowing, and the user
thinks the page is still listening. The device only gets released
once **every track gets stopped individually**. Stopping is
idempotent: a second call does not break the count.

The **constraints** given during the request declare settings like
the requested resolution and audio processing. These are a wish, not
a guarantee: the actual settings the device provides get read back
from the track and may differ. Giving a
constraint in mandatory form means the request fails entirely if it
cannot get met.

## Location and Sensors

Location access has two forms: a one-time read and continuous
watching. Watching produces a notification as the location changes
and continues until it gets explicitly released; watching that does
not get released drains the battery.

A **high-accuracy** request declared in the request can bring
satellite-based positioning into play; this is slower and more
expensive. The accuracy needed to know which valley a point is in
differs from the accuracy needed to know which side of a sidewalk
someone stands on; accuracy that is not needed does not get
requested.

The incoming location has an **accuracy radius**, and this value has
to get reflected in the interface; showing a single point claims a
precision that is not available. Timeout and cached-reading age also
get declared in the request: whether a stale reading is acceptable,
and how long to wait.

Motion and orientation sensors sit within a similar framework, and
also require permission on some platforms. A use like recording the
device's tilt while taking a measurement has to get weighed against
sensor data's potential to identify the user.

## The Privacy Contract

These capabilities' shared contract has four articles. Permission
gets requested **in context**: the moment the user understands why,
when they press the relevant button. The data collected is **minimal**: if location gets attached
to a measurement, the station name may be enough instead of the
exact coordinate. Access is **visible**: the interface shows it
while the capability is in use. And access gets **released**: once
the job is done, tracks get stopped and watches get ended.

## Summary

- Capabilities split into sets that require no permission, require
  explicit permission, and get limited by user gesture; a capability
  not delegated to frames does not open even with permission.
- A permission request depends on the secure-context and
  user-gesture preconditions; granted and denied permissions do not
  get asked again, while a dismissed prompt does not change the
  state.
- Feature detection checks for the interface's presence, not the
  browser's name; capability absence, permission denial, and
  operation failure each require a different response.
- The page has to do its job even without the capability; a design
  that leaves only a path dependent on the capability leaves a
  declining user outside.
- The device only gets released once every track in the stream gets
  stopped; removing the element from the tree does not stop the
  track.
- Requesting accuracy for location has a cost, and the incoming
  reading's accuracy radius gets reflected in the interface; watching
  gets ended explicitly.

## Next Step

Most of this lesson's capabilities produce heavy data: frames coming
from the camera, sensor readings, thousands of rows in a selected
measurement file. Processing these on the main thread exhausts the
Timing and the Paint Cycle lesson's frame budget in a single call,
and the interface stops responding.
Splitting the work into chunks spreads out the delay but does not
shorten the total time. The next lesson takes up web workers, which
move the computation outside the main thread, message passing
between them, and the cost of this separation.
