apphouse
Version:
Component library for React that uses observable state management and theme-able components.
356 lines (327 loc) • 9.62 kB
text/typescript
import {
action,
computed,
makeObservable,
observable,
runInAction
} from 'mobx';
import { audioLog } from './AudioLog';
import { translate } from './translate';
export interface MicrophoneOption {
value: string;
label: string;
}
export interface MicrophoneInfo {
channelCount: {
min: number | undefined;
max: number | undefined;
value: number | undefined;
};
sampleRate: {
min: number | undefined;
max: number | undefined;
value: number | undefined;
};
sampleSize: {
min: number | undefined;
max: number | undefined;
value: number | undefined;
};
latency: {
min: number | undefined;
max: number | undefined;
};
autoGainControl: boolean | undefined;
echoCancellation: boolean | undefined;
noiseSuppression: boolean | undefined;
}
export default class Microphone {
devices: MediaDeviceInfo[];
selectedDevice?: MediaStream;
selectedDeviceId?: string; // selected microphone
stream: MediaStream | undefined;
track: MediaStreamTrack | undefined;
constructor() {
this.stream = undefined;
this.devices = [];
this.selectedDevice = undefined;
this.selectedDeviceId = undefined;
this.track = undefined;
makeObservable(this, {
currentMicrophone: computed,
currentTrack: computed,
devices: observable,
init: action,
isReady: computed,
loadDefault: action,
loadSystemDevices: action,
microphoneDetails: computed,
microphoneOptions: computed,
onMicrophoneChange: action,
resetMicData: action,
selectedDevice: observable,
selectedDeviceId: observable,
setMediaStreamTrack: action,
setSelectedDevice: action,
stopStreaming: action,
pauseStreaming: action,
resumeStreaming: action,
stream: observable,
systemInputs: computed,
track: observable,
streamingStream: computed
});
}
get isReady() {
const isMicrophoneReady = this.stream !== undefined;
isMicrophoneReady && audioLog.logMicrophone('Ready');
return isMicrophoneReady;
}
get currentTrack() {
return this.track;
}
get currentMicrophone() {
const selectedDeviceId = this.selectedDeviceId;
const selectedInput = this.microphoneOptions.filter(
(option) => option.value === selectedDeviceId
);
if (selectedInput.length > 0) {
return selectedInput[0];
}
return undefined;
}
get streamingStream(): MediaStream | undefined {
if (this.track && this.track.enabled === true) {
return this.stream;
}
return undefined;
}
get microphoneOptions(): MicrophoneOption[] {
if (this.devices.length === 0) {
return [
{
value: 'none',
label: translate('noMic.label')
}
];
}
return this.devices.map((d) => ({
value: d.deviceId,
label: d.label
}));
}
get systemInputs() {
return this.devices;
}
get microphoneDetails(): MicrophoneInfo {
const track = this.stream?.getAudioTracks()[0];
if (!track) {
return {
channelCount: {
min: undefined,
max: undefined,
value: undefined
},
sampleRate: {
min: undefined,
max: undefined,
value: undefined
},
sampleSize: {
min: undefined,
max: undefined,
value: undefined
},
latency: {
min: undefined,
max: undefined
},
autoGainControl: false,
echoCancellation: false,
noiseSuppression: false
};
}
const settings = track.getSettings();
const capabilities = track.getCapabilities();
return {
channelCount: {
min: capabilities.channelCount?.min,
max: capabilities.channelCount?.max,
value: settings.channelCount
},
sampleRate: {
min: capabilities.sampleRate?.min,
max: capabilities.sampleRate?.max,
value: settings.sampleRate
},
sampleSize: {
min: capabilities.sampleSize?.min,
max: capabilities.sampleSize?.max,
value: settings.sampleSize
},
latency: {
// @ts-ignore
min: capabilities.latency?.min,
// @ts-ignore
max: capabilities.latency?.max
},
autoGainControl: settings.autoGainControl,
echoCancellation: settings.echoCancellation,
noiseSuppression: settings.noiseSuppression
};
}
// Loads all available input devices in the user's computer
// and sets the default stream to the user's computer's default
// selected input device (audio input/microphone)
init = async (): Promise<boolean> => {
const defaultLoaded = await this.loadDefault();
const systemDevicesLoaded = await this.loadSystemDevices();
if (defaultLoaded && systemDevicesLoaded) {
return true; // default microphone and system devices loaded successfully
}
return false;
};
// Load default device (microphone) and set default
// Promise will return true once the default device has been selected
loadDefault = async (): Promise<boolean> => {
if (typeof window !== 'undefined') {
if (window.navigator.mediaDevices) {
return window.navigator.mediaDevices
.getUserMedia({ audio: true })
.then((device: any) => {
runInAction(() => {
this.setSelectedDevice(device, 'default');
});
return true;
})
.catch((err: any) => {
console.error(err);
return false;
});
}
}
return new Promise(() => false);
};
// Load all microphone options in the system
loadSystemDevices = async (): Promise<boolean> => {
if (typeof window !== 'undefined') {
return window.navigator.mediaDevices
.enumerateDevices()
.then((devices: any) => {
runInAction(() => {
this.devices = devices.filter((d: any) => d.kind === 'audioinput');
});
audioLog.logMicrophone(
'Input device list loaded and set successfully'
);
return true;
})
.catch((err: AsyncGenerator) => {
console.error(err);
return false;
});
}
return new Promise(() => false);
};
setMediaStreamTrack = (stream: MediaStream | undefined): boolean => {
if (stream) {
const audioTracks = stream.getAudioTracks();
this.track = audioTracks[0];
this.stream = stream;
audioLog.logMicrophone('Set MediaStream', stream);
this.resumeStreaming();
return true;
} else {
console.warn('Attempted to set media stream but stream was undefined');
}
return false;
};
onMicrophoneChange = async (
newValue: MicrophoneOption | null | undefined
): Promise<boolean> => {
if (!newValue) {
return false;
}
const selectedDeviceId = newValue.value;
if (typeof window !== 'undefined') {
return window.navigator.mediaDevices
.getUserMedia({ audio: { deviceId: newValue?.value ?? '' } })
.then((device: MediaStream) => {
this.setSelectedDevice(device, selectedDeviceId);
return true;
})
.catch((err: any) => {
console.error(err);
return false;
});
}
return false;
};
setSelectedDevice = (device: MediaStream, deviceId: string): void => {
this.selectedDevice = device;
this.selectedDeviceId = deviceId;
audioLog.logMicrophone('Set selected device', device, deviceId);
};
startStream = async (): Promise<boolean> => {
if (this.selectedDevice) {
return new Promise((resolve) =>
resolve(this.setMediaStreamTrack(this.selectedDevice))
);
} else {
return this.loadDefault();
}
};
stopStreaming = () => {
if (this.track) {
this.track.stop();
audioLog.logMicrophone(
'Stopped streaming microphone - To reuse it again, it needs to be reloaded'
);
} else {
console.warn('Attempted to stop track but track was undefined');
}
};
pauseStreaming = () => {
// The enabled property on the MediaStreamTrack interface is a Boolean value which is true
// if the track is allowed to render the source stream or false if it is not. This can be
// used to intentionally mute a track. When enabled, a track's data is output from the
// source to the destination; otherwise, empty frames are output.
if (this.track) {
this.track.enabled = false;
}
};
resumeStreaming = () => {
if (this.track) {
this.track.enabled = true;
}
};
resetMicData = () => {
this.stream = undefined;
this.track = undefined;
audioLog.logMicrophone('Stream reset');
};
printStreamInfo = () => {
if (this.stream) {
const audioTracks = this.stream.getAudioTracks();
this.track = audioTracks[0];
console.log('Num Audio Tracks', audioTracks.length);
console.log('Capabilities', audioTracks[0].getCapabilities());
console.log('Contraints', audioTracks[0].getConstraints());
console.log('Settings', audioTracks[0].getSettings());
const mimeTypes = [
'audio/wav',
'audio/mpeg',
'audio/ogg',
'audio/opus',
'audio/webm',
'audio/webm;codecs=opus',
'audio/webm;codecs=pcm'
];
mimeTypes.forEach((mimeType) =>
console.log('format', mimeType, MediaRecorder.isTypeSupported(mimeType))
);
} else {
console.log('Stream is undefined');
}
};
}