TeaWeb/shared/js/media/Stream.ts

109 lines
4.3 KiB
TypeScript

import {MediaStreamRequestResult} from "tc-shared/voice/RecorderBase";
import * as log from "tc-shared/log";
import {LogCategory, logWarn} from "tc-shared/log";
import { tr } from "tc-shared/i18n/localize";
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<MediaStreamEvents>();
*/
export async function requestMediaStreamWithConstraints(constraints: MediaTrackConstraints, type: MediaStreamType) : Promise<MediaStreamRequestResult | MediaStream> {
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 });
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<MediaStream | MediaStreamRequestResult>;
export async function requestMediaStream(deviceId: string | undefined, groupId: string | undefined, type: MediaStreamType) : Promise<MediaStream | MediaStreamRequestResult> {
/* 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 if(deviceId !== undefined) {
constrains.deviceId = deviceId;
constrains.groupId = groupId;
} else {
/* Nothing to select. Let the browser select the right device */
}
constrains.echoCancellation = true;
constrains.autoGainControl = true;
constrains.noiseSuppression = true;
const promise = (currentMediaStreamRequest = requestMediaStreamWithConstraints(constrains, type));
try {
return await currentMediaStreamRequest;
} finally {
if(currentMediaStreamRequest === promise)
currentMediaStreamRequest = undefined;
}
}
export async function queryMediaPermissions(type: MediaStreamType, changeListener?: (value: PermissionState) => void) : Promise<PermissionState> {
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 as any).stop();
}
}