microsoft-cognitiveservices-speech-sdk
Version:
Microsoft Cognitive Services Speech SDK for JavaScript
255 lines (253 loc) • 11 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
Object.defineProperty(exports, "__esModule", { value: true });
exports.MicAudioSource = exports.AudioWorkletSourceURLPropertyName = void 0;
const Exports_js_1 = require("../common.speech/Exports.js");
const Exports_js_2 = require("../common/Exports.js");
const AudioStreamFormat_js_1 = require("../sdk/Audio/AudioStreamFormat.js");
exports.AudioWorkletSourceURLPropertyName = "MICROPHONE-WorkletSourceUrl";
class MicAudioSource {
constructor(privRecorder, deviceId, audioSourceId, mediaStream) {
this.privRecorder = privRecorder;
this.deviceId = deviceId;
this.privStreams = {};
this.privOutputChunkSize = MicAudioSource.AUDIOFORMAT.avgBytesPerSec / 10;
this.privId = audioSourceId ? audioSourceId : (0, Exports_js_2.createNoDashGuid)();
this.privEvents = new Exports_js_2.EventSource();
this.privMediaStream = mediaStream || null;
this.privIsClosing = false;
}
get format() {
return Promise.resolve(MicAudioSource.AUDIOFORMAT);
}
turnOn() {
if (this.privInitializeDeferral) {
return this.privInitializeDeferral.promise;
}
this.privInitializeDeferral = new Exports_js_2.Deferred();
try {
this.createAudioContext();
}
catch (error) {
if (error instanceof Error) {
const typedError = error;
this.privInitializeDeferral.reject(typedError.name + ": " + typedError.message);
}
else {
this.privInitializeDeferral.reject(error);
}
return this.privInitializeDeferral.promise;
}
const nav = window.navigator;
let getUserMedia = (
// eslint-disable-next-line
nav.getUserMedia ||
nav.webkitGetUserMedia ||
nav.mozGetUserMedia ||
nav.msGetUserMedia);
if (!!nav.mediaDevices) {
getUserMedia = (constraints, successCallback, errorCallback) => {
nav.mediaDevices
.getUserMedia(constraints)
.then(successCallback)
.catch(errorCallback);
};
}
if (!getUserMedia) {
const errorMsg = "Browser does not support getUserMedia.";
this.privInitializeDeferral.reject(errorMsg);
this.onEvent(new Exports_js_2.AudioSourceErrorEvent(errorMsg, "")); // mic initialized error - no streamid at this point
}
else {
const next = () => {
this.onEvent(new Exports_js_2.AudioSourceInitializingEvent(this.privId)); // no stream id
if (this.privMediaStream && this.privMediaStream.active) {
this.onEvent(new Exports_js_2.AudioSourceReadyEvent(this.privId));
this.privInitializeDeferral.resolve();
}
else {
getUserMedia({ audio: this.deviceId ? { deviceId: this.deviceId } : true, video: false }, (mediaStream) => {
this.privMediaStream = mediaStream;
this.onEvent(new Exports_js_2.AudioSourceReadyEvent(this.privId));
this.privInitializeDeferral.resolve();
}, (error) => {
const errorMsg = `Error occurred during microphone initialization: ${error}`;
this.privInitializeDeferral.reject(errorMsg);
this.onEvent(new Exports_js_2.AudioSourceErrorEvent(this.privId, errorMsg));
});
}
};
if (this.privContext.state === "suspended") {
// NOTE: On iOS, the Web Audio API requires sounds to be triggered from an explicit user action.
// https://github.com/WebAudio/web-audio-api/issues/790
this.privContext.resume()
.then(next)
.catch((reason) => {
this.privInitializeDeferral.reject(`Failed to initialize audio context: ${reason}`);
});
}
else {
next();
}
}
return this.privInitializeDeferral.promise;
}
id() {
return this.privId;
}
attach(audioNodeId) {
this.onEvent(new Exports_js_2.AudioStreamNodeAttachingEvent(this.privId, audioNodeId));
return this.listen(audioNodeId).then((stream) => {
this.onEvent(new Exports_js_2.AudioStreamNodeAttachedEvent(this.privId, audioNodeId));
return {
detach: async () => {
stream.readEnded();
delete this.privStreams[audioNodeId];
this.onEvent(new Exports_js_2.AudioStreamNodeDetachedEvent(this.privId, audioNodeId));
return this.turnOff();
},
id: () => audioNodeId,
read: () => stream.read(),
};
});
}
detach(audioNodeId) {
if (audioNodeId && this.privStreams[audioNodeId]) {
this.privStreams[audioNodeId].close();
delete this.privStreams[audioNodeId];
this.onEvent(new Exports_js_2.AudioStreamNodeDetachedEvent(this.privId, audioNodeId));
}
}
async turnOff() {
for (const streamId in this.privStreams) {
if (streamId) {
const stream = this.privStreams[streamId];
if (stream) {
stream.close();
}
}
}
this.onEvent(new Exports_js_2.AudioSourceOffEvent(this.privId)); // no stream now
if (this.privInitializeDeferral) {
// Correctly handle when browser forces mic off before turnOn() completes
// eslint-disable-next-line @typescript-eslint/await-thenable
await this.privInitializeDeferral;
this.privInitializeDeferral = null;
}
await this.destroyAudioContext();
return;
}
get events() {
return this.privEvents;
}
get deviceInfo() {
return this.getMicrophoneLabel().then((label) => ({
bitspersample: MicAudioSource.AUDIOFORMAT.bitsPerSample,
channelcount: MicAudioSource.AUDIOFORMAT.channels,
connectivity: Exports_js_1.connectivity.Unknown,
manufacturer: "Speech SDK",
model: label,
samplerate: MicAudioSource.AUDIOFORMAT.samplesPerSec,
type: Exports_js_1.type.Microphones,
}));
}
setProperty(name, value) {
if (name === exports.AudioWorkletSourceURLPropertyName) {
this.privRecorder.setWorkletUrl(value);
}
else {
throw new Error("Property '" + name + "' is not supported on Microphone.");
}
}
getMicrophoneLabel() {
const defaultMicrophoneName = "microphone";
// If we did this already, return the value.
if (this.privMicrophoneLabel !== undefined) {
return Promise.resolve(this.privMicrophoneLabel);
}
// If the stream isn't currently running, we can't query devices because security.
if (this.privMediaStream === undefined || !this.privMediaStream.active) {
return Promise.resolve(defaultMicrophoneName);
}
// Setup a default
this.privMicrophoneLabel = defaultMicrophoneName;
// Get the id of the device running the audio track.
const microphoneDeviceId = this.privMediaStream.getTracks()[0].getSettings().deviceId;
// If the browser doesn't support getting the device ID, set a default and return.
if (undefined === microphoneDeviceId) {
return Promise.resolve(this.privMicrophoneLabel);
}
const deferred = new Exports_js_2.Deferred();
// Enumerate the media devices.
navigator.mediaDevices.enumerateDevices().then((devices) => {
for (const device of devices) {
if (device.deviceId === microphoneDeviceId) {
// Found the device
this.privMicrophoneLabel = device.label;
break;
}
}
deferred.resolve(this.privMicrophoneLabel);
}, () => deferred.resolve(this.privMicrophoneLabel));
return deferred.promise;
}
async listen(audioNodeId) {
await this.turnOn();
const stream = new Exports_js_2.ChunkedArrayBufferStream(this.privOutputChunkSize, audioNodeId);
this.privStreams[audioNodeId] = stream;
try {
this.privRecorder.record(this.privContext, this.privMediaStream, stream);
}
catch (error) {
this.onEvent(new Exports_js_2.AudioStreamNodeErrorEvent(this.privId, audioNodeId, error));
throw error;
}
const result = stream;
return result;
}
onEvent(event) {
this.privEvents.onEvent(event);
Exports_js_2.Events.instance.onEvent(event);
}
createAudioContext() {
if (!!this.privContext) {
return;
}
this.privContext = AudioStreamFormat_js_1.AudioStreamFormatImpl.getAudioContext(MicAudioSource.AUDIOFORMAT.samplesPerSec);
}
async destroyAudioContext() {
if (!this.privContext) {
return;
}
this.privRecorder.releaseMediaResources(this.privContext);
// This pattern brought to you by a bug in the TypeScript compiler where it
// confuses the ("close" in this.privContext) with this.privContext always being null as the alternate.
// https://github.com/Microsoft/TypeScript/issues/11498
let hasClose = false;
if ("close" in this.privContext) {
hasClose = true;
}
if (hasClose) {
if (!this.privIsClosing) {
// The audio context close may take enough time that the close is called twice
this.privIsClosing = true;
await this.privContext.close();
this.privContext = null;
this.privIsClosing = false;
}
}
else if (null !== this.privContext && this.privContext.state === "running") {
// Suspend actually takes a callback, but analogous to the
// resume method, it'll be only fired if suspend is called
// in a direct response to a user action. The later is not always
// the case, as TurnOff is also called, when we receive an
// end-of-speech message from the service. So, doing a best effort
// fire-and-forget here.
await this.privContext.suspend();
}
}
}
exports.MicAudioSource = MicAudioSource;
MicAudioSource.AUDIOFORMAT = AudioStreamFormat_js_1.AudioStreamFormat.getDefaultInputFormat();
//# sourceMappingURL=MicAudioSource.js.map