summaryrefslogtreecommitdiff
path: root/apps/web/src/voiceover/devices.ts
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-08-16 06:16:56 +0200
committercashmere <cashmere@autistici.org>2026-08-16 06:16:56 +0200
commitf1043734438c8518a302f698d9ae6583a75f0c14 (patch)
treed3e636a96e6349c1ad17afa7b28c9ffe5c882e37 /apps/web/src/voiceover/devices.ts
parent37d22b1e390bd6cea04bf8e15069281ee125cf10 (diff)
downloadopencut-f1043734438c8518a302f698d9ae6583a75f0c14.tar.gz
feat: add transitions, track mixing, and voiceover controls
Diffstat (limited to 'apps/web/src/voiceover/devices.ts')
-rw-r--r--apps/web/src/voiceover/devices.ts93
1 files changed, 93 insertions, 0 deletions
diff --git a/apps/web/src/voiceover/devices.ts b/apps/web/src/voiceover/devices.ts
new file mode 100644
index 00000000..dd5b680c
--- /dev/null
+++ b/apps/web/src/voiceover/devices.ts
@@ -0,0 +1,93 @@
+"use client";
+
+export const DEFAULT_AUDIO_INPUT_DEVICE_ID = "default";
+
+export interface VoiceOverInputDevice {
+ deviceId: string;
+ label: string;
+ isDefault: boolean;
+}
+
+export function buildVoiceOverAudioConstraints({
+ deviceId,
+}: {
+ deviceId?: string | null;
+}): MediaStreamConstraints {
+ const audio: MediaTrackConstraints = {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ };
+
+ if (deviceId && deviceId !== DEFAULT_AUDIO_INPUT_DEVICE_ID) {
+ audio.deviceId = { exact: deviceId };
+ }
+
+ return { audio };
+}
+
+export function normalizeSelectedDeviceId({
+ deviceId,
+ devices,
+}: {
+ deviceId: string | null;
+ devices: VoiceOverInputDevice[];
+}): string | null {
+ if (!deviceId) {
+ return null;
+ }
+
+ if (deviceId === DEFAULT_AUDIO_INPUT_DEVICE_ID) {
+ return deviceId;
+ }
+
+ return devices.some((device) => device.deviceId === deviceId)
+ ? deviceId
+ : null;
+}
+
+export async function listVoiceOverInputDevices({
+ requestPermission = false,
+}: {
+ requestPermission?: boolean;
+} = {}): Promise<VoiceOverInputDevice[]> {
+ const mediaDevices = navigator.mediaDevices;
+ if (!mediaDevices?.enumerateDevices) {
+ throw new Error("Microphone selection is not supported in this browser");
+ }
+
+ let permissionStream: MediaStream | null = null;
+ if (requestPermission) {
+ permissionStream = await mediaDevices.getUserMedia({
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ });
+ }
+
+ try {
+ const devices = await mediaDevices.enumerateDevices();
+ let inputIndex = 1;
+ return devices
+ .filter((device) => device.kind === "audioinput")
+ .map((device) => {
+ const isDefault = device.deviceId === DEFAULT_AUDIO_INPUT_DEVICE_ID;
+ const fallbackLabel = isDefault
+ ? "System default microphone"
+ : `Microphone ${inputIndex++}`;
+ return {
+ deviceId: device.deviceId,
+ label: device.label || fallbackLabel,
+ isDefault,
+ };
+ });
+ } finally {
+ if (permissionStream) {
+ for (const track of permissionStream.getTracks()) {
+ track.stop();
+ }
+ }
+ }
+}