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