Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | 159x 62x 1x 1x 6x 6x 6x 6x 6x 67x 31x 31x 31x 6x 31x 37x 37x 7x 37x 4x 4x 3x 3x 7x 85x 85x 37x 85x 121x 121x 73x 73x | import { UserMediaError, type UserMediaErrorReason } from "./types";
/** Whether this environment can open a media stream at all. */
export function isUserMediaSupported(): boolean {
return (
typeof navigator !== "undefined" &&
typeof navigator.mediaDevices !== "undefined" &&
typeof navigator.mediaDevices.getUserMedia === "function"
);
}
/** Whether device enumeration is available (it can be missing where capture is not). */
export function isEnumerationSupported(): boolean {
return (
typeof navigator !== "undefined" &&
typeof navigator.mediaDevices !== "undefined" &&
typeof navigator.mediaDevices.enumerateDevices === "function"
);
}
const REASONS: Readonly<Record<string, UserMediaErrorReason>> = {
NotAllowedError: "denied",
PermissionDeniedError: "denied",
SecurityError: "denied",
NotFoundError: "not-found",
DevicesNotFoundError: "not-found",
NotReadableError: "in-use",
TrackStartError: "in-use",
OverconstrainedError: "over-constrained",
ConstraintNotSatisfiedError: "over-constrained",
};
const MESSAGES: Readonly<Record<UserMediaErrorReason, string>> = {
denied: "Access was denied. Allow camera and microphone permission for this site and try again.",
"not-found": "No matching camera or microphone was found on this device.",
"in-use": "The device is already in use by another application.",
"over-constrained": "No device matches the requested constraints.",
unsupported:
"Media capture is unavailable here. It needs a secure context (HTTPS or localhost) and a browser with mediaDevices.",
unknown: "The media stream could not be started.",
};
/** Turn a `getUserMedia` rejection into a typed error with an actionable message. */
export function toUserMediaError(error: unknown): UserMediaError {
Iif (error instanceof UserMediaError) return error;
const name =
typeof error === "object" && error !== null && "name" in error
? String((error as { name: unknown }).name)
: "";
const reason = REASONS[name] ?? "unknown";
const detail =
reason === "unknown" && error instanceof Error && error.message
? `${MESSAGES.unknown} ${error.message}`
: MESSAGES[reason];
return new UserMediaError(reason, detail, error);
}
/**
* Stop every track on a stream, turning the torch off first.
*
* Called from more places than seems necessary — unmount, `stop()`, replacing
* a stream, a failed device switch — because a track left running keeps the
* camera light on, and users read that as spyware, not as a bug.
*
* The torch needs its own instruction: on several Android devices the LED
* survives a bare `track.stop()`, leaving a torch burning with no app on screen
* to turn it off. `applyConstraints` is asynchronous and this function is not —
* deliberately, because `stop()` must be safe to call from a cleanup function.
* The request is fired and the track is stopped immediately after; the LED goes
* out either way, and waiting would risk the stop never happening at all.
*/
export function stopStream(stream: MediaStream | null): void {
if (!stream) return;
for (const track of stream.getTracks()) {
try {
if (track.kind === "video" && supportsTorch(stream)) {
void (track as MediaStreamTrack)
.applyConstraints({ advanced: [{ torch: false }] } as unknown as MediaTrackConstraints)
.catch(() => {
// A camera that refuses is no worse off than one that was never asked.
});
}
track.stop();
} catch {
// A track already ended by the OS throws on some browsers; that is the
// outcome we wanted anyway.
}
}
}
/** Merge `facingMode`/`deviceId` into a constraints object without mutating it. */
export function withVideoPreferences(
constraints: MediaStreamConstraints,
preferences: { facingMode?: "user" | "environment"; deviceId?: string },
): MediaStreamConstraints {
Iif (constraints.video === false || constraints.video === undefined) return constraints;
if (preferences.deviceId === undefined && preferences.facingMode === undefined) return constraints;
const base: MediaTrackConstraints = constraints.video === true ? {} : { ...constraints.video };
if (preferences.deviceId !== undefined) {
// `exact` on purpose: a device the user explicitly picked is not a
// suggestion, and silently opening a different camera is worse than failing.
base.deviceId = { exact: preferences.deviceId };
// A specific device and a facing mode can contradict each other, and the
// explicit choice wins.
delete base.facingMode;
E} else if (preferences.facingMode !== undefined) {
// Not `exact`: a laptop with only a front camera should still open it
// rather than refuse, and "prefer the back one" is what callers mean.
base.facingMode = preferences.facingMode;
}
return { ...constraints, video: base };
}
/** The `deviceId` a stream's video track came from, when it reports one. */
export function activeDeviceIdOf(stream: MediaStream | null): string | null {
const track = stream?.getVideoTracks()[0];
if (!track) return null;
const settings = typeof track.getSettings === "function" ? track.getSettings() : undefined;
return settings?.deviceId ?? null;
}
/** Whether a stream's video track advertises torch support. */
export function supportsTorch(stream: MediaStream | null): boolean {
const track = stream?.getVideoTracks()[0];
if (!track || typeof track.getCapabilities !== "function") return false;
const capabilities = track.getCapabilities() as MediaTrackCapabilities & { torch?: boolean };
return capabilities.torch === true;
}
|