UNPKG

@euirim/microsoft-cognitiveservices-speech-sdk

Version:
239 lines (237 loc) 11.4 kB
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. import { AudioStreamFormat, } from "../../src/sdk/Audio/AudioStreamFormat"; import { connectivity, type } from "../common.speech/Exports"; import { AudioSourceErrorEvent, AudioSourceInitializingEvent, AudioSourceOffEvent, AudioSourceReadyEvent, AudioStreamNodeAttachedEvent, AudioStreamNodeAttachingEvent, AudioStreamNodeDetachedEvent, AudioStreamNodeErrorEvent, ChunkedArrayBufferStream, createNoDashGuid, Deferred, Events, EventSource, PromiseHelper, } from "../common/Exports"; export const AudioWorkletSourceURLPropertyName = "MICROPHONE-WorkletSourceUrl"; export class MicAudioSource { constructor(privRecorder, outputChunkSize, audioSourceId, deviceId) { this.privRecorder = privRecorder; this.deviceId = deviceId; this.privStreams = {}; this.turnOn = () => { if (this.privInitializeDeferral) { return this.privInitializeDeferral.promise(); } this.privInitializeDeferral = new Deferred(); this.createAudioContext(); const nav = window.navigator; let getUserMedia = (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 AudioSourceErrorEvent(errorMsg, "")); // mic initialized error - no streamid at this point } else { const next = () => { this.onEvent(new AudioSourceInitializingEvent(this.privId)); // no stream id getUserMedia({ audio: this.deviceId ? { deviceId: this.deviceId } : true, video: false }, (mediaStream) => { this.privMediaStream = mediaStream; this.onEvent(new AudioSourceReadyEvent(this.privId)); this.privInitializeDeferral.resolve(true); }, (error) => { const errorMsg = `Error occurred during microphone initialization: ${error}`; const tmp = this.privInitializeDeferral; // HACK: this should be handled through onError callbacks of all promises up the stack. // Unfortunately, the current implementation does not provide an easy way to reject promises // without a lot of code replication. // TODO: fix promise implementation, allow for a graceful reject chaining. this.privInitializeDeferral = null; tmp.reject(errorMsg); // this will bubble up through the whole chain of promises, // with each new level adding extra "Unhandled callback error" prefix to the error message. // The following line is not guaranteed to be executed. this.onEvent(new 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, (reason) => { this.privInitializeDeferral.reject(`Failed to initialize audio context: ${reason}`); }); } else { next(); } } return this.privInitializeDeferral.promise(); }; this.id = () => { return this.privId; }; this.attach = (audioNodeId) => { this.onEvent(new AudioStreamNodeAttachingEvent(this.privId, audioNodeId)); return this.listen(audioNodeId).onSuccessContinueWith((streamReader) => { this.onEvent(new AudioStreamNodeAttachedEvent(this.privId, audioNodeId)); return { detach: () => { streamReader.close(); delete this.privStreams[audioNodeId]; this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId)); this.turnOff(); }, id: () => { return audioNodeId; }, read: () => { return streamReader.read(); }, }; }); }; this.detach = (audioNodeId) => { if (audioNodeId && this.privStreams[audioNodeId]) { this.privStreams[audioNodeId].close(); delete this.privStreams[audioNodeId]; this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId)); } }; this.turnOff = () => { for (const streamId in this.privStreams) { if (streamId) { const stream = this.privStreams[streamId]; if (stream) { stream.close(); } } } this.onEvent(new AudioSourceOffEvent(this.privId)); // no stream now this.privInitializeDeferral = null; this.destroyAudioContext(); return PromiseHelper.fromResult(true); }; this.listen = (audioNodeId) => { return this.turnOn() .onSuccessContinueWith((_) => { const stream = new ChunkedArrayBufferStream(this.privOutputChunkSize, audioNodeId); this.privStreams[audioNodeId] = stream; try { this.privRecorder.record(this.privContext, this.privMediaStream, stream); } catch (error) { this.onEvent(new AudioStreamNodeErrorEvent(this.privId, audioNodeId, error)); throw error; } return stream.getReader(); }); }; this.onEvent = (event) => { this.privEvents.onEvent(event); Events.instance.onEvent(event); }; this.createAudioContext = () => { if (!!this.privContext) { return; } // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext const AudioContext = (window.AudioContext) || (window.webkitAudioContext) || false; if (!AudioContext) { throw new Error("Browser does not support Web Audio API (AudioContext is not available)."); } this.privContext = new AudioContext(); }; this.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) { this.privContext.close(); this.privContext = null; } 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. this.privContext.suspend(); } }; this.privOutputChunkSize = outputChunkSize; this.privId = audioSourceId ? audioSourceId : createNoDashGuid(); this.privEvents = new EventSource(); } get format() { return MicAudioSource.AUDIOFORMAT; } get events() { return this.privEvents; } get deviceInfo() { return this.getMicrophoneLabel().onSuccessContinueWith((label) => { return { bitspersample: MicAudioSource.AUDIOFORMAT.bitsPerSample, channelcount: MicAudioSource.AUDIOFORMAT.channels, connectivity: connectivity.Unknown, manufacturer: "Speech SDK", model: label, samplerate: MicAudioSource.AUDIOFORMAT.samplesPerSec, type: type.Microphones, }; }); } setProperty(name, value) { if (name === 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 PromiseHelper.fromResult(this.privMicrophoneLabel); } // If the stream isn't currently running, we can't query devices because security. if (this.privMediaStream === undefined || !this.privMediaStream.active) { return PromiseHelper.fromResult(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 PromiseHelper.fromResult(this.privMicrophoneLabel); } const deferred = new 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(); } } MicAudioSource.AUDIOFORMAT = AudioStreamFormat.getDefaultInputFormat(); //# sourceMappingURL=MicAudioSource.js.map