{"version":3,"sources":["src/common.browser/MicAudioSource.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,wBAAwB,EAE3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAEH,gBAAgB,EAYhB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAGnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAEH,qBAAqB,EACxB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAK3C,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;IAEM,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAqGvB,EAAE,IAAI,MAAM;IAIZ,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAmBtD,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAQ3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuBrC,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;YAyCZ,MAAM;IAcpB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,kBAAkB;YAQZ,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.js\";\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.js\";\r\nimport { IStreamChunk } from \"../common/Stream.js\";\r\nimport {\r\n    AudioStreamFormat,\r\n    AudioStreamFormatImpl,\r\n} from \"../sdk/Audio/AudioStreamFormat.js\";\r\nimport { IRecorder } from \"./IRecorder.js\";\r\n\r\ntype NavigatorUserMediaSuccessCallback = (stream: MediaStream) => void;\r\ntype NavigatorUserMediaErrorCallback = (error: DOMException) => void;\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 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;\r\n                this.privInitializeDeferral.reject(typedError.name + \": \" + typedError.message);\r\n            } else {\r\n                this.privInitializeDeferral.reject(error as string);\r\n            }\r\n            return this.privInitializeDeferral.promise;\r\n        }\r\n\r\n        const nav = window.navigator as unknown as {\r\n            webkitGetUserMedia?: (\r\n                constraints: MediaStreamConstraints,\r\n                successCallback: (stream: MediaStream) => void,\r\n                errorCallback: (error: any) => void\r\n            ) => void;\r\n            mozGetUserMedia?: (\r\n                constraints: MediaStreamConstraints,\r\n                successCallback: (stream: MediaStream) => void,\r\n                errorCallback: (error: any) => void\r\n            ) => void;\r\n            msGetUserMedia?: (\r\n                constraints: MediaStreamConstraints,\r\n                successCallback: (stream: MediaStream) => void,\r\n                errorCallback: (error: any) => void\r\n            ) => void;\r\n            getUserMedia?: (\r\n                constraints: MediaStreamConstraints,\r\n                successCallback: (stream: MediaStream) => void,\r\n                errorCallback: (error: any) => void\r\n            ) => void;\r\n            mediaDevices?: MediaDevices;\r\n        };\r\n\r\n        let getUserMedia = (\r\n            // eslint-disable-next-line\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 = (): void => {\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): void => {\r\n                            this.privMediaStream = mediaStream;\r\n                            this.onEvent(new AudioSourceReadyEvent(this.privId));\r\n                            this.privInitializeDeferral.resolve();\r\n                        }, (error: any): void => {\r\n                            const errorMsg = `Error occurred during microphone initialization: ${error as string}`;\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): void => {\r\n                        this.privInitializeDeferral.reject(`Failed to initialize audio context: ${reason as string}`);\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>): IAudioStreamNode => {\r\n                this.onEvent(new AudioStreamNodeAttachedEvent(this.privId, audioNodeId));\r\n                return {\r\n                    detach: async (): Promise<void> => {\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: (): string => audioNodeId,\r\n                    read: (): Promise<IStreamChunk<ArrayBuffer>> => stream.read(),\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            // eslint-disable-next-line @typescript-eslint/await-thenable\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): ISpeechConfigAudioDevice => (\r\n            {\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[]): void => {\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<string> => deferred.resolve(this.privMicrophoneLabel));\r\n\r\n        return deferred.promise;\r\n    }\r\n\r\n    private async listen(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 as string));\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"]}