{"version":3,"sources":["src/common.browser/MicAudioSource.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,wBAAwB,EAE3B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAEH,gBAAgB,EAYhB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAGnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEH,qBAAqB,EACxB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AASxC,eAAO,MAAM,iCAAiC,gCAAgC,CAAC;AAE/E,qBAAa,cAAe,YAAW,YAAY;IAuB3C,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAtB9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAA6F;IAEhI,OAAO,CAAC,WAAW,CAA8C;IAEjE,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,UAAU,CAAgC;IAElD,OAAO,CAAC,sBAAsB,CAAiB;IAE/C,OAAO,CAAC,eAAe,CAAc;IAErC,OAAO,CAAC,WAAW,CAAe;IAElC,OAAO,CAAC,mBAAmB,CAAS;IAEpC,OAAO,CAAC,mBAAmB,CAAS;IAEpC,OAAO,CAAC,aAAa,CAAU;gBAGV,YAAY,EAAE,SAAS,EACvB,QAAQ,CAAC,EAAE,MAAM,EAClC,aAAa,CAAC,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,WAAW;IAU7B,IAAW,MAAM,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAElD;IAED,IAAW,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/B;IAEM,MAAM,QAAO,OAAO,CAAC,IAAI,CAAC,CA4EhC;IAEM,EAAE,QAAO,MAAM,CAErB;IAEM,MAAM,gBAAiB,MAAM,KAAG,OAAO,CAAC,gBAAgB,CAAC,CAqB/D;IAEM,MAAM,gBAAiB,MAAM,KAAG,IAAI,CAM1C;IAEY,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBrC,IAAW,MAAM,IAAI,WAAW,CAAC,gBAAgB,CAAC,CAEjD;IAED,IAAW,UAAU,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAYzD;IAEM,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQrD,OAAO,CAAC,kBAAkB;IAyC1B,OAAO,CAAC,MAAM,CAYb;IAED,OAAO,CAAC,OAAO,CAGd;IAED,OAAO,CAAC,kBAAkB,CAMzB;YAEa,mBAAmB;CAiCpC","file":"MicAudioSource.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\r\n// Licensed under the MIT license.\r\n\r\nimport {\r\n    connectivity,\r\n    ISpeechConfigAudioDevice,\r\n    type\r\n} from \"../common.speech/Exports\";\r\nimport {\r\n    AudioSourceErrorEvent,\r\n    AudioSourceEvent,\r\n    AudioSourceInitializingEvent,\r\n    AudioSourceOffEvent,\r\n    AudioSourceReadyEvent,\r\n    AudioStreamNodeAttachedEvent,\r\n    AudioStreamNodeAttachingEvent,\r\n    AudioStreamNodeDetachedEvent,\r\n    AudioStreamNodeErrorEvent,\r\n    ChunkedArrayBufferStream,\r\n    createNoDashGuid,\r\n    Deferred,\r\n    Events,\r\n    EventSource,\r\n    IAudioSource,\r\n    IAudioStreamNode,\r\n    IStringDictionary,\r\n    Stream,\r\n} from \"../common/Exports\";\r\nimport {\r\n    AudioStreamFormat,\r\n    AudioStreamFormatImpl,\r\n} from \"../sdk/Audio/AudioStreamFormat\";\r\nimport { IRecorder } from \"./IRecorder\";\r\n\r\n// Extending the default definition with browser specific definitions for backward compatibility\r\ninterface INavigator extends Navigator {\r\n    webkitGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback) => void;\r\n    mozGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback) => void;\r\n    msGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback) => void;\r\n}\r\n\r\nexport const AudioWorkletSourceURLPropertyName = \"MICROPHONE-WorkletSourceUrl\";\r\n\r\nexport class MicAudioSource implements IAudioSource {\r\n\r\n    private static readonly AUDIOFORMAT: AudioStreamFormatImpl = AudioStreamFormat.getDefaultInputFormat() as AudioStreamFormatImpl;\r\n\r\n    private privStreams: IStringDictionary<Stream<ArrayBuffer>> = {};\r\n\r\n    private privId: string;\r\n\r\n    private privEvents: EventSource<AudioSourceEvent>;\r\n\r\n    private privInitializeDeferral: Deferred<void>;\r\n\r\n    private privMediaStream: MediaStream;\r\n\r\n    private privContext: AudioContext;\r\n\r\n    private privMicrophoneLabel: string;\r\n\r\n    private privOutputChunkSize: number;\r\n\r\n    private privIsClosing: boolean;\r\n\r\n    public constructor(\r\n        private readonly privRecorder: IRecorder,\r\n        private readonly deviceId?: string,\r\n        audioSourceId?: string,\r\n        mediaStream?: MediaStream\r\n        ) {\r\n\r\n        this.privOutputChunkSize = MicAudioSource.AUDIOFORMAT.avgBytesPerSec / 10;\r\n        this.privId = audioSourceId ? audioSourceId : createNoDashGuid();\r\n        this.privEvents = new EventSource<AudioSourceEvent>();\r\n        this.privMediaStream = mediaStream || null;\r\n        this.privIsClosing = false;\r\n    }\r\n\r\n    public get format(): Promise<AudioStreamFormatImpl> {\r\n        return Promise.resolve(MicAudioSource.AUDIOFORMAT);\r\n    }\r\n\r\n    public get blob(): Promise<Blob> {\r\n        return Promise.reject(\"Not implemented for Mic input\");\r\n    }\r\n\r\n    public turnOn = (): Promise<void> => {\r\n        if (this.privInitializeDeferral) {\r\n            return this.privInitializeDeferral.promise;\r\n        }\r\n\r\n        this.privInitializeDeferral = new Deferred<void>();\r\n\r\n        try {\r\n            this.createAudioContext();\r\n        } catch (error) {\r\n            if (error instanceof Error) {\r\n                const typedError: Error = error as Error;\r\n                this.privInitializeDeferral.reject(typedError.name + \": \" + typedError.message);\r\n            } else {\r\n                this.privInitializeDeferral.reject(error);\r\n            }\r\n            return this.privInitializeDeferral.promise;\r\n        }\r\n\r\n        const nav = window.navigator as INavigator;\r\n\r\n        let getUserMedia = (\r\n            nav.getUserMedia ||\r\n            nav.webkitGetUserMedia ||\r\n            nav.mozGetUserMedia ||\r\n            nav.msGetUserMedia\r\n        );\r\n\r\n        if (!!nav.mediaDevices) {\r\n            getUserMedia = (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void => {\r\n                nav.mediaDevices\r\n                    .getUserMedia(constraints)\r\n                    .then(successCallback)\r\n                    .catch(errorCallback);\r\n            };\r\n        }\r\n\r\n        if (!getUserMedia) {\r\n            const errorMsg = \"Browser does not support getUserMedia.\";\r\n            this.privInitializeDeferral.reject(errorMsg);\r\n            this.onEvent(new AudioSourceErrorEvent(errorMsg, \"\")); // mic initialized error - no streamid at this point\r\n        } else {\r\n            const next = () => {\r\n                this.onEvent(new AudioSourceInitializingEvent(this.privId)); // no stream id\r\n                if (this.privMediaStream && this.privMediaStream.active) {\r\n                    this.onEvent(new AudioSourceReadyEvent(this.privId));\r\n                    this.privInitializeDeferral.resolve();\r\n                } else {\r\n                    getUserMedia(\r\n                        { audio: this.deviceId ? { deviceId: this.deviceId } : true, video: false },\r\n                        (mediaStream: MediaStream) => {\r\n                            this.privMediaStream = mediaStream;\r\n                            this.onEvent(new AudioSourceReadyEvent(this.privId));\r\n                            this.privInitializeDeferral.resolve();\r\n                        }, (error: MediaStreamError) => {\r\n                            const errorMsg = `Error occurred during microphone initialization: ${error}`;\r\n                            this.privInitializeDeferral.reject(errorMsg);\r\n                            this.onEvent(new AudioSourceErrorEvent(this.privId, errorMsg));\r\n                        });\r\n                }\r\n            };\r\n\r\n            if (this.privContext.state === \"suspended\") {\r\n                // NOTE: On iOS, the Web Audio API requires sounds to be triggered from an explicit user action.\r\n                // https://github.com/WebAudio/web-audio-api/issues/790\r\n                this.privContext.resume()\r\n                    .then(next)\r\n                    .catch((reason: any) => {\r\n                        this.privInitializeDeferral.reject(`Failed to initialize audio context: ${reason}`);\r\n                    });\r\n            } else {\r\n                next();\r\n            }\r\n        }\r\n\r\n        return this.privInitializeDeferral.promise;\r\n    }\r\n\r\n    public id = (): string => {\r\n        return this.privId;\r\n    }\r\n\r\n    public attach = (audioNodeId: string): Promise<IAudioStreamNode> => {\r\n        this.onEvent(new AudioStreamNodeAttachingEvent(this.privId, audioNodeId));\r\n\r\n        return this.listen(audioNodeId).then<IAudioStreamNode>(\r\n            (stream: Stream<ArrayBuffer>) => {\r\n                this.onEvent(new AudioStreamNodeAttachedEvent(this.privId, audioNodeId));\r\n                return {\r\n                    detach: async () => {\r\n                        stream.readEnded();\r\n                        delete this.privStreams[audioNodeId];\r\n                        this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));\r\n                        return this.turnOff();\r\n                    },\r\n                    id: () => {\r\n                        return audioNodeId;\r\n                    },\r\n                    read: () => {\r\n                        return stream.read();\r\n                    },\r\n                };\r\n            });\r\n    }\r\n\r\n    public detach = (audioNodeId: string): void => {\r\n        if (audioNodeId && this.privStreams[audioNodeId]) {\r\n            this.privStreams[audioNodeId].close();\r\n            delete this.privStreams[audioNodeId];\r\n            this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));\r\n        }\r\n    }\r\n\r\n    public async turnOff(): Promise<void> {\r\n        for (const streamId in this.privStreams) {\r\n            if (streamId) {\r\n                const stream = this.privStreams[streamId];\r\n                if (stream) {\r\n                    stream.close();\r\n                }\r\n            }\r\n        }\r\n\r\n        this.onEvent(new AudioSourceOffEvent(this.privId)); // no stream now\r\n        if (this.privInitializeDeferral) {\r\n            // Correctly handle when browser forces mic off before turnOn() completes\r\n            await this.privInitializeDeferral;\r\n            this.privInitializeDeferral = null;\r\n        }\r\n\r\n        await this.destroyAudioContext();\r\n\r\n        return;\r\n    }\r\n\r\n    public get events(): EventSource<AudioSourceEvent> {\r\n        return this.privEvents;\r\n    }\r\n\r\n    public get deviceInfo(): Promise<ISpeechConfigAudioDevice> {\r\n        return this.getMicrophoneLabel().then((label: string) => {\r\n            return {\r\n                bitspersample: MicAudioSource.AUDIOFORMAT.bitsPerSample,\r\n                channelcount: MicAudioSource.AUDIOFORMAT.channels,\r\n                connectivity: connectivity.Unknown,\r\n                manufacturer: \"Speech SDK\",\r\n                model: label,\r\n                samplerate: MicAudioSource.AUDIOFORMAT.samplesPerSec,\r\n                type: type.Microphones,\r\n            };\r\n        });\r\n    }\r\n\r\n    public setProperty(name: string, value: string): void {\r\n        if (name === AudioWorkletSourceURLPropertyName) {\r\n            this.privRecorder.setWorkletUrl(value);\r\n        } else {\r\n            throw new Error(\"Property '\" + name + \"' is not supported on Microphone.\");\r\n        }\r\n    }\r\n\r\n    private getMicrophoneLabel(): Promise<string> {\r\n        const defaultMicrophoneName: string = \"microphone\";\r\n\r\n        // If we did this already, return the value.\r\n        if (this.privMicrophoneLabel !== undefined) {\r\n            return Promise.resolve(this.privMicrophoneLabel);\r\n        }\r\n\r\n        // If the stream isn't currently running, we can't query devices because security.\r\n        if (this.privMediaStream === undefined || !this.privMediaStream.active) {\r\n            return Promise.resolve(defaultMicrophoneName);\r\n        }\r\n\r\n        // Setup a default\r\n        this.privMicrophoneLabel = defaultMicrophoneName;\r\n\r\n        // Get the id of the device running the audio track.\r\n        const microphoneDeviceId: string = this.privMediaStream.getTracks()[0].getSettings().deviceId;\r\n\r\n        // If the browser doesn't support getting the device ID, set a default and return.\r\n        if (undefined === microphoneDeviceId) {\r\n            return Promise.resolve(this.privMicrophoneLabel);\r\n        }\r\n\r\n        const deferred: Deferred<string> = new Deferred<string>();\r\n\r\n        // Enumerate the media devices.\r\n        navigator.mediaDevices.enumerateDevices().then((devices: MediaDeviceInfo[]) => {\r\n            for (const device of devices) {\r\n                if (device.deviceId === microphoneDeviceId) {\r\n                    // Found the device\r\n                    this.privMicrophoneLabel = device.label;\r\n                    break;\r\n                }\r\n            }\r\n            deferred.resolve(this.privMicrophoneLabel);\r\n        }, () => deferred.resolve(this.privMicrophoneLabel));\r\n\r\n        return deferred.promise;\r\n    }\r\n\r\n    private listen = async (audioNodeId: string): Promise<Stream<ArrayBuffer>> => {\r\n        await this.turnOn();\r\n        const stream = new ChunkedArrayBufferStream(this.privOutputChunkSize, audioNodeId);\r\n        this.privStreams[audioNodeId] = stream;\r\n        try {\r\n            this.privRecorder.record(this.privContext, this.privMediaStream, stream);\r\n        } catch (error) {\r\n            this.onEvent(new AudioStreamNodeErrorEvent(this.privId, audioNodeId, error));\r\n            throw error;\r\n        }\r\n        const result: Stream<ArrayBuffer> = stream;\r\n        return result;\r\n    }\r\n\r\n    private onEvent = (event: AudioSourceEvent): void => {\r\n        this.privEvents.onEvent(event);\r\n        Events.instance.onEvent(event);\r\n    }\r\n\r\n    private createAudioContext = (): void => {\r\n        if (!!this.privContext) {\r\n            return;\r\n        }\r\n\r\n        this.privContext = AudioStreamFormatImpl.getAudioContext(MicAudioSource.AUDIOFORMAT.samplesPerSec);\r\n    }\r\n\r\n    private async destroyAudioContext(): Promise<void> {\r\n        if (!this.privContext) {\r\n            return;\r\n        }\r\n\r\n        this.privRecorder.releaseMediaResources(this.privContext);\r\n\r\n        // This pattern brought to you by a bug in the TypeScript compiler where it\r\n        // confuses the (\"close\" in this.privContext) with this.privContext always being null as the alternate.\r\n        // https://github.com/Microsoft/TypeScript/issues/11498\r\n        let hasClose: boolean = false;\r\n        if (\"close\" in this.privContext) {\r\n            hasClose = true;\r\n        }\r\n\r\n        if (hasClose) {\r\n            if (!this.privIsClosing) {\r\n                // The audio context close may take enough time that the close is called twice\r\n                this.privIsClosing = true;\r\n                await this.privContext.close();\r\n                this.privContext = null;\r\n                this.privIsClosing = false;\r\n            }\r\n        } else if (null !== this.privContext && this.privContext.state === \"running\") {\r\n            // Suspend actually takes a callback, but analogous to the\r\n            // resume method, it'll be only fired if suspend is called\r\n            // in a direct response to a user action. The later is not always\r\n            // the case, as TurnOff is also called, when we receive an\r\n            // end-of-speech message from the service. So, doing a best effort\r\n            // fire-and-forget here.\r\n            await this.privContext.suspend();\r\n        }\r\n    }\r\n}\r\n"]}