{"version":3,"sources":["src/common.browser/MicAudioSource.ts"],"names":[],"mappings":"AAGA,OAAO,EACH,iBAAiB,EAEpB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAEH,wBAAwB,EAE3B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAEH,gBAAgB,EAYhB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAEhB,OAAO,EAIV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AASxC,eAAO,MAAM,iCAAiC,gCAAgC,CAAC;AAE/E,qBAAa,cAAe,YAAW,YAAY;IAqB3C,OAAO,CAAC,QAAQ,CAAC,YAAY;IAG7B,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,CAAoB;IAElD,OAAO,CAAC,eAAe,CAAc;IAErC,OAAO,CAAC,WAAW,CAAe;IAElC,OAAO,CAAC,mBAAmB,CAAS;IAEpC,OAAO,CAAC,mBAAmB,CAAS;gBAGf,YAAY,EAAE,SAAS,EACxC,eAAe,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,MAAM,EACL,QAAQ,CAAC,EAAE,MAAM;aAO3B,MAAM,EAAI,iBAAiB;IAI/B,MAAM,yBAmEZ;IAEM,EAAE,eAER;IAEM,MAAM,qDAqBZ;IAEM,MAAM,gCAMZ;IAEM,OAAO,yBAgBb;aAEU,MAAM,EAAI,WAAW,CAAC,gBAAgB,CAAC;aAIvC,UAAU,EAAI,OAAO,CAAC,wBAAwB,CAAC;IAcnD,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQrD,OAAO,CAAC,kBAAkB;IAyC1B,OAAO,CAAC,MAAM,CAeb;IAED,OAAO,CAAC,OAAO,CAGd;IAED,OAAO,CAAC,kBAAkB,CAezB;IAED,OAAO,CAAC,mBAAmB,CA2B1B;CACJ","file":"MicAudioSource.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\n// Licensed under the MIT license.\n\nimport {\n    AudioStreamFormat,\n    AudioStreamFormatImpl,\n} from \"../../src/sdk/Audio/AudioStreamFormat\";\nimport {\n    connectivity,\n    ISpeechConfigAudioDevice,\n    type\n} from \"../common.speech/Exports\";\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    Promise,\n    PromiseHelper,\n    Stream,\n    StreamReader,\n} from \"../common/Exports\";\nimport { IRecorder } from \"./IRecorder\";\n\n// Extending the default definition with browser specific definitions for backward compatibility\ninterface INavigatorUserMedia extends NavigatorUserMedia {\n    webkitGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback) => void;\n    mozGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback) => void;\n    msGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback) => void;\n}\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<boolean>;\n\n    private privMediaStream: MediaStream;\n\n    private privContext: AudioContext;\n\n    private privMicrophoneLabel: string;\n\n    private privOutputChunkSize: number;\n\n    public constructor(\n        private readonly privRecorder: IRecorder,\n        outputChunkSize: number,\n        audioSourceId?: string,\n        private readonly deviceId?: string) {\n\n        this.privOutputChunkSize = outputChunkSize;\n        this.privId = audioSourceId ? audioSourceId : createNoDashGuid();\n        this.privEvents = new EventSource<AudioSourceEvent>();\n    }\n\n    public get format(): AudioStreamFormat {\n        return MicAudioSource.AUDIOFORMAT;\n    }\n\n    public turnOn = (): Promise<boolean> => {\n        if (this.privInitializeDeferral) {\n            return this.privInitializeDeferral.promise();\n        }\n\n        this.privInitializeDeferral = new Deferred<boolean>();\n\n        this.createAudioContext();\n\n        const nav = window.navigator as INavigatorUserMedia;\n\n        let getUserMedia = (\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 = () => {\n                this.onEvent(new AudioSourceInitializingEvent(this.privId)); // no stream id\n                getUserMedia(\n                    { audio: this.deviceId ? { deviceId: this.deviceId } : true, video: false },\n                    (mediaStream: MediaStream) => {\n                        this.privMediaStream = mediaStream;\n                        this.onEvent(new AudioSourceReadyEvent(this.privId));\n                        this.privInitializeDeferral.resolve(true);\n                    }, (error: MediaStreamError) => {\n                        const errorMsg = `Error occurred during microphone initialization: ${error}`;\n                        const tmp = this.privInitializeDeferral;\n                        // HACK: this should be handled through onError callbacks of all promises up the stack.\n                        // Unfortunately, the current implementation does not provide an easy way to reject promises\n                        // without a lot of code replication.\n                        // TODO: fix promise implementation, allow for a graceful reject chaining.\n                        this.privInitializeDeferral = null;\n                        tmp.reject(errorMsg); // this will bubble up through the whole chain of promises,\n                        // with each new level adding extra \"Unhandled callback error\" prefix to the error message.\n                        // The following line is not guaranteed to be executed.\n                        this.onEvent(new AudioSourceErrorEvent(this.privId, errorMsg));\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().then(next, (reason: any) => {\n                    this.privInitializeDeferral.reject(`Failed to initialize audio context: ${reason}`);\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).onSuccessContinueWith<IAudioStreamNode>(\n            (streamReader: StreamReader<ArrayBuffer>) => {\n                this.onEvent(new AudioStreamNodeAttachedEvent(this.privId, audioNodeId));\n                return {\n                    detach: () => {\n                        streamReader.close();\n                        delete this.privStreams[audioNodeId];\n                        this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));\n                        this.turnOff();\n                    },\n                    id: () => {\n                        return audioNodeId;\n                    },\n                    read: () => {\n                        return streamReader.read();\n                    },\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 turnOff = (): Promise<boolean> => {\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        this.privInitializeDeferral = null;\n\n        this.destroyAudioContext();\n\n        return PromiseHelper.fromResult(true);\n    }\n\n    public get events(): EventSource<AudioSourceEvent> {\n        return this.privEvents;\n    }\n\n    public get deviceInfo(): Promise<ISpeechConfigAudioDevice> {\n        return this.getMicrophoneLabel().onSuccessContinueWith((label: string) => {\n            return {\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 PromiseHelper.fromResult(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 PromiseHelper.fromResult(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 PromiseHelper.fromResult(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[]) => {\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.resolve(this.privMicrophoneLabel));\n\n        return deferred.promise();\n    }\n\n    private listen = (audioNodeId: string): Promise<StreamReader<ArrayBuffer>> => {\n        return this.turnOn()\n            .onSuccessContinueWith<StreamReader<ArrayBuffer>>((_: boolean) => {\n                const stream = new ChunkedArrayBufferStream(this.privOutputChunkSize, audioNodeId);\n                this.privStreams[audioNodeId] = stream;\n\n                try {\n                    this.privRecorder.record(this.privContext, this.privMediaStream, stream);\n                } catch (error) {\n                    this.onEvent(new AudioStreamNodeErrorEvent(this.privId, audioNodeId, error));\n                    throw error;\n                }\n\n                return stream.getReader();\n            });\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        // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext\n        const AudioContext = ((window as any).AudioContext)\n            || ((window as any).webkitAudioContext)\n            || false;\n\n        if (!AudioContext) {\n            throw new Error(\"Browser does not support Web Audio API (AudioContext is not available).\");\n        }\n\n        this.privContext = new AudioContext();\n    }\n\n    private destroyAudioContext = (): 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            this.privContext.close();\n            this.privContext = null;\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            this.privContext.suspend();\n        }\n    }\n}\n"]}