Skip to main content

no-floating-media-streams

Require captured MediaStream handles to be retained so their tracks can be stopped.

Rule catalog ID: R015

Targeted pattern scopeโ€‹

This rule targets browser media capture APIs:

  • navigator.mediaDevices.getUserMedia(...)
  • navigator.mediaDevices.getDisplayMedia(...)
  • window.navigator.mediaDevices.getUserMedia(...)
  • globalThis.navigator.mediaDevices.getDisplayMedia(...)

The rule reports capture requests whose resulting MediaStream is immediately discarded, including await expressions that do not store, return, or pass the stream to another owner.

What this rule reportsโ€‹

The rule reports:

  • standalone media capture requests
  • voided media capture requests
  • await navigator.mediaDevices.getUserMedia(...) used as a standalone expression
  • await navigator.mediaDevices.getDisplayMedia(...) used as a standalone expression

It intentionally allows promise chains and lifecycle-manager calls that receive the stream. The rule focuses on directly unowned stream handles, not on proving that every possible owner eventually stops every track.

Why this rule existsโ€‹

getUserMedia() and getDisplayMedia() return MediaStream objects backed by media tracks. Tracks can keep cameras, microphones, screen capture, indicators, or permission-sensitive resources active. MediaStreamTrack.stop() tells the browser that a track's source is no longer needed. If the stream handle is discarded, cleanup code cannot reliably stop its tracks.

Incorrectโ€‹

navigator.mediaDevices.getUserMedia({ video: true });
void navigator.mediaDevices.getDisplayMedia({ video: true });
async function openCamera() {
await navigator.mediaDevices.getUserMedia({ video: true });
}

Correctโ€‹

async function openCamera() {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });

stream.getTracks().forEach((track) => track.stop());
}
async function openScreen() {
return navigator.mediaDevices.getDisplayMedia({ video: true });
}
async function openCamera() {
registerStream(
await navigator.mediaDevices.getUserMedia({ audio: true }),
);
}

Behavior and migration notesโ€‹

Store the stream in the lifecycle owner that will stop its tracks. For UI code, that is usually a component cleanup hook, route cleanup hook, recording controller, or media session manager.

This rule does not autofix. Introducing a local variable without a matching track-stop lifecycle would hide the cleanup bug instead of solving it.

ESLint flat config exampleโ€‹

import runtimeCleanup from "eslint-plugin-runtime-cleanup";

export default [
runtimeCleanup.configs.recommended,
];

When not to use itโ€‹

Do not enable this rule for short demo snippets where the browser page lifetime is the intended cleanup boundary. Prefer narrow disable comments for those snippets rather than weakening the rule globally.

Further readingโ€‹