import {MediaStreamRequestResult} from "tc-shared/voice/RecorderBase"; import * as log from "tc-shared/log"; import {LogCategory, logWarn} from "tc-shared/log"; import {inputDeviceList} from "tc-backend/web/audio/RecorderDeviceList"; import {Registry} from "tc-shared/events"; export type MediaStreamType = "audio" | "video"; export enum MediaPermissionStatus { Unknown, Granted, Denied } export interface MediaStreamEvents { notify_permissions_changed: { type: MediaStreamType, newState: MediaPermissionStatus }, } export const mediaStreamEvents = new Registry(); async function requestMediaStream0(constraints: MediaTrackConstraints, type: MediaStreamType, updateDeviceList: boolean) : Promise { const beginTimestamp = Date.now(); try { log.info(LogCategory.AUDIO, tr("Requesting a %s stream for device %s in group %s"), type, constraints.deviceId, constraints.groupId); const stream = await navigator.mediaDevices.getUserMedia(type === "audio" ? { audio: constraints } : { video: constraints }); if(updateDeviceList && inputDeviceList.getStatus() === "no-permissions") { inputDeviceList.refresh().then(() => {}); /* added the then body to avoid a inspection warning... */ } return stream; } catch(error) { if('name' in error) { if(error.name === "NotAllowedError") { if(Date.now() - beginTimestamp < 250) { log.warn(LogCategory.AUDIO, tr("Media stream request failed (System denied). Browser message: %o"), error.message); return MediaStreamRequestResult.ESYSTEMDENIED; } else { log.warn(LogCategory.AUDIO, tr("Media stream request failed (No permissions). Browser message: %o"), error.message); return MediaStreamRequestResult.ENOTALLOWED; } } else { log.warn(LogCategory.AUDIO, tr("Media stream request failed. Request resulted in error: %o: %o"), error.name, error); } } else { log.warn(LogCategory.AUDIO, tr("Failed to initialize media stream (%o)"), error); } return MediaStreamRequestResult.EUNKNOWN; } } /* request permission for devices only one per time! */ let currentMediaStreamRequest: Promise; export async function requestMediaStream(deviceId: string, groupId: string | undefined, type: MediaStreamType) : Promise { /* wait for the current media stream requests to finish */ while(currentMediaStreamRequest) { try { await currentMediaStreamRequest; } catch(error) { } } const constrains: MediaTrackConstraints = {}; if(window.detectedBrowser?.name === "firefox") { /* * Firefox only allows to open one mic/video as well deciding whats the input device it. * It does not respect the deviceId nor the groupId */ } else { constrains.deviceId = deviceId; constrains.groupId = groupId; } constrains.echoCancellation = true; constrains.autoGainControl = true; constrains.noiseSuppression = true; const promise = (currentMediaStreamRequest = requestMediaStream0(constrains, type, true)); try { return await currentMediaStreamRequest; } finally { if(currentMediaStreamRequest === promise) currentMediaStreamRequest = undefined; } } export async function queryMediaPermissions(type: MediaStreamType, changeListener?: (value: PermissionState) => void) : Promise { if('permissions' in navigator && 'query' in navigator.permissions) { try { const result = await navigator.permissions.query({ name: type === "audio" ? "microphone" : "camera" }); if(changeListener) { result.addEventListener("change", () => { changeListener(result.state); }); } return result.state; } catch (error) { logWarn(LogCategory.GENERAL, tr("Failed to query for %s permissions: %s"), type, error); } } return "prompt"; } export function stopMediaStream(stream: MediaStream) { stream.getVideoTracks().forEach(track => track.stop()); stream.getAudioTracks().forEach(track => track.stop()); if('stop' in stream) { stream.stop(); } }