microsoft-cognitiveservices-speech-sdk
Version: 
Microsoft Cognitive Services Speech SDK for JavaScript
1 lines • 13.9 kB
Source Map (JSON)
{"version":3,"sources":["src/sdk/Audio/SpeakerAudioDestination.ts"],"names":[],"mappings":"AAGA,OAAO,EAIH,iBAAiB,EAEpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAmB3D;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,iBAAiB,EAAE,OAAO;IACtE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,UAAU,CAAwB;IAC1C,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,gBAAgB,CAAe;IACvC,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,eAAe,CAAgB;IACvC,OAAO,CAAC,qBAAqB,CAAkB;IAC/C,OAAO,CAAC,qBAAqB,CAAkB;IAC/C,OAAO,CAAC,YAAY,CAAU;IAC9B,OAAO,CAAC,YAAY,CAAU;IAC9B,OAAO,CAAC,qBAAqB,CAA4B;IACzD,OAAO,CAAC,iBAAiB,CAAa;gBAEnB,kBAAkB,CAAC,EAAE,MAAM;IAMvC,EAAE,IAAI,MAAM;IAIZ,KAAK,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE,MAAM,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAkBhF,KAAK,CAAC,EAAE,CAAC,EAAE,MAAM,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAiDlE,IAAW,MAAM,CAAC,MAAM,EAAE,iBAAiB,EA+C1C;IAED,IAAW,MAAM,IAAI,MAAM,CAE1B;IAED,IAAW,MAAM,CAAC,MAAM,EAAE,MAAM,EAI/B;IAEM,IAAI,IAAI,IAAI;IAMZ,MAAM,IAAI,IAAI;IAMrB,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAED,IAAW,WAAW,IAAI,MAAM,CAK/B;IAEM,KAAK,IAAI,IAAI;IAOb,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAe5D,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAExC,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAE7C,IAAW,aAAa,IAAI,gBAAgB,CAE3C;YAEa,kBAAkB;YAmBlB,2BAA2B;YAO3B,cAAc;IAiB5B,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,qBAAqB;CAGhC","file":"SpeakerAudioDestination.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\r\n// Licensed under the MIT license.\r\n\r\nimport {\r\n    BackgroundEvent,\r\n    createNoDashGuid,\r\n    Events,\r\n    IAudioDestination,\r\n    INumberDictionary\r\n} from \"../../common/Exports.js\";\r\nimport { AudioStreamFormat, IPlayer } from \"../Exports.js\";\r\nimport { AudioOutputFormatImpl } from \"./AudioOutputFormat.js\";\r\nimport { PullAudioOutputStreamImpl } from \"./AudioOutputStream.js\";\r\nimport { AudioFormatTag } from \"./AudioStreamFormat.js\";\r\n\r\nconst MediaDurationPlaceholderSeconds = 60 * 30;\r\n\r\nconst AudioFormatToMimeType: INumberDictionary<string> = {\r\n    [AudioFormatTag.PCM]: \"audio/wav\",\r\n    [AudioFormatTag.MuLaw]: \"audio/x-wav\",\r\n    [AudioFormatTag.MP3]: \"audio/mpeg\",\r\n    [AudioFormatTag.OGG_OPUS]: \"audio/ogg\",\r\n    [AudioFormatTag.WEBM_OPUS]: \"audio/webm; codecs=opus\",\r\n    [AudioFormatTag.ALaw]: \"audio/x-wav\",\r\n    [AudioFormatTag.FLAC]: \"audio/flac\",\r\n    [AudioFormatTag.AMR_WB]: \"audio/amr-wb\",\r\n    [AudioFormatTag.G722]: \"audio/G722\",\r\n};\r\n\r\n/**\r\n * Represents the speaker playback audio destination, which only works in browser.\r\n * Note: the SDK will try to use <a href=\"https://www.w3.org/TR/media-source/\">Media Source Extensions</a> to play audio.\r\n * Mp3 format has better supports on Microsoft Edge, Chrome and Safari (desktop), so, it's better to specify mp3 format for playback.\r\n * @class SpeakerAudioDestination\r\n * Updated in version 1.17.0\r\n */\r\nexport class SpeakerAudioDestination implements IAudioDestination, IPlayer {\r\n    private readonly privId: string;\r\n    private privFormat: AudioOutputFormatImpl;\r\n    private privAudio: HTMLAudioElement;\r\n    private privMediaSource: MediaSource;\r\n    private privSourceBuffer: SourceBuffer;\r\n    private privPlaybackStarted: boolean = false;\r\n    private privAudioBuffer: ArrayBuffer[];\r\n    private privAppendingToBuffer: boolean = false;\r\n    private privMediaSourceOpened: boolean = false;\r\n    private privIsClosed: boolean;\r\n    private privIsPaused: boolean;\r\n    private privAudioOutputStream: PullAudioOutputStreamImpl;\r\n    private privBytesReceived: number = 0;\r\n\r\n    public constructor(audioDestinationId?: string) {\r\n        this.privId = audioDestinationId ? audioDestinationId : createNoDashGuid();\r\n        this.privIsPaused = false;\r\n        this.privIsClosed = false;\r\n    }\r\n\r\n    public id(): string {\r\n        return this.privId;\r\n    }\r\n\r\n    public write(buffer: ArrayBuffer, cb?: () => void, err?: (error: string) => void): void {\r\n        if (this.privAudioBuffer !== undefined) {\r\n            this.privAudioBuffer.push(buffer);\r\n            this.updateSourceBuffer().then((): void => {\r\n                if (!!cb) {\r\n                    cb();\r\n                }\r\n            }, (error: string): void => {\r\n                if (!!err) {\r\n                    err(error);\r\n                }\r\n            });\r\n        } else if (this.privAudioOutputStream !== undefined) {\r\n            this.privAudioOutputStream.write(buffer);\r\n            this.privBytesReceived += buffer.byteLength;\r\n        }\r\n    }\r\n\r\n    public close(cb?: () => void, err?: (error: string) => void): void {\r\n        this.privIsClosed = true;\r\n        if (this.privSourceBuffer !== undefined) {\r\n            this.handleSourceBufferUpdateEnd().then((): void => {\r\n                if (!!cb) {\r\n                    cb();\r\n                }\r\n            }, (error: string): void => {\r\n                if (!!err) {\r\n                    err(error);\r\n                }\r\n            });\r\n        } else if (this.privAudioOutputStream !== undefined && typeof window !== \"undefined\") {\r\n            if ((this.privFormat.formatTag === AudioFormatTag.PCM || this.privFormat.formatTag === AudioFormatTag.MuLaw\r\n                || this.privFormat.formatTag === AudioFormatTag.ALaw) && this.privFormat.hasHeader === false) {\r\n                // eslint-disable-next-line no-console\r\n                console.warn(\"Play back is not supported for raw PCM, mulaw or alaw format without header.\");\r\n                if (!!this.onAudioEnd) {\r\n                    this.onAudioEnd(this);\r\n                }\r\n            } else {\r\n                let receivedAudio = new ArrayBuffer(this.privBytesReceived);\r\n                this.privAudioOutputStream.read(receivedAudio).then((): void => {\r\n                    receivedAudio = this.privFormat.addHeader(receivedAudio);\r\n                    const audioBlob = new Blob([receivedAudio], { type: AudioFormatToMimeType[this.privFormat.formatTag] });\r\n                    this.privAudio.src = window.URL.createObjectURL(audioBlob);\r\n                    this.notifyPlayback().then((): void => {\r\n                        if (!!cb) {\r\n                            cb();\r\n                        }\r\n                    }, (error: string): void => {\r\n                        if (!!err) {\r\n                            err(error);\r\n                        }\r\n                    });\r\n                }, (error: string): void => {\r\n                    if (!!err) {\r\n                        err(error);\r\n                    }\r\n                });\r\n            }\r\n        } else {\r\n            // unsupported format, call onAudioEnd directly.\r\n            if (!!this.onAudioEnd) {\r\n                this.onAudioEnd(this);\r\n            }\r\n        }\r\n    }\r\n\r\n    public set format(format: AudioStreamFormat) {\r\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\r\n        if (typeof (AudioContext) !== \"undefined\" || (typeof (window) !== \"undefined\" && typeof ((window as any).webkitAudioContext) !== \"undefined\")) {\r\n            this.privFormat = format as AudioOutputFormatImpl;\r\n            const mimeType: string = AudioFormatToMimeType[this.privFormat.formatTag];\r\n            if (mimeType === undefined) {\r\n                // eslint-disable-next-line no-console\r\n                console.warn(\r\n                    `Unknown mimeType for format ${AudioFormatTag[this.privFormat.formatTag]}; playback is not supported.`);\r\n\r\n            } else if (typeof (MediaSource) !== \"undefined\" && MediaSource.isTypeSupported(mimeType)) {\r\n                this.privAudio = new Audio();\r\n                this.privAudioBuffer = [];\r\n                this.privMediaSource = new MediaSource();\r\n                this.privAudio.src = URL.createObjectURL(this.privMediaSource);\r\n                this.privAudio.load();\r\n                this.privMediaSource.onsourceopen = (): void => {\r\n                    this.privMediaSourceOpened = true;\r\n                    this.privMediaSource.duration = MediaDurationPlaceholderSeconds;\r\n                    this.privSourceBuffer = this.privMediaSource.addSourceBuffer(mimeType);\r\n                    this.privSourceBuffer.onupdate = (): void => {\r\n                        this.updateSourceBuffer().catch((reason: string): void => {\r\n                            Events.instance.onEvent(new BackgroundEvent(reason));\r\n                        });\r\n                    };\r\n                    this.privSourceBuffer.onupdateend = (): void => {\r\n                        this.handleSourceBufferUpdateEnd().catch((reason: string): void => {\r\n                            Events.instance.onEvent(new BackgroundEvent(reason));\r\n                        });\r\n                    };\r\n                    this.privSourceBuffer.onupdatestart = (): void => {\r\n                        this.privAppendingToBuffer = false;\r\n                    };\r\n                };\r\n                this.updateSourceBuffer().catch((reason: string): void => {\r\n                    Events.instance.onEvent(new BackgroundEvent(reason));\r\n                });\r\n\r\n            } else {\r\n                // eslint-disable-next-line no-console\r\n                console.warn(\r\n                    `Format ${AudioFormatTag[this.privFormat.formatTag]} could not be played by MSE, streaming playback is not enabled.`);\r\n                this.privAudioOutputStream = new PullAudioOutputStreamImpl();\r\n                this.privAudioOutputStream.format = this.privFormat;\r\n                this.privAudio = new Audio();\r\n            }\r\n        }\r\n    }\r\n\r\n    public get volume(): number {\r\n        return this.privAudio?.volume ?? -1;\r\n    }\r\n\r\n    public set volume(volume: number) {\r\n        if (!!this.privAudio) {\r\n            this.privAudio.volume = volume;\r\n        }\r\n    }\r\n\r\n    public mute(): void {\r\n        if (!!this.privAudio) {\r\n            this.privAudio.muted = true;\r\n        }\r\n    }\r\n\r\n    public unmute(): void {\r\n        if (!!this.privAudio) {\r\n            this.privAudio.muted = false;\r\n        }\r\n    }\r\n\r\n    public get isClosed(): boolean {\r\n        return this.privIsClosed;\r\n    }\r\n\r\n    public get currentTime(): number {\r\n        if (this.privAudio !== undefined) {\r\n            return this.privAudio.currentTime;\r\n        }\r\n        return -1;\r\n    }\r\n\r\n    public pause(): void {\r\n        if (!this.privIsPaused && this.privAudio !== undefined) {\r\n            this.privAudio.pause();\r\n            this.privIsPaused = true;\r\n        }\r\n    }\r\n\r\n    public resume(cb?: () => void, err?: (error: string) => void): void {\r\n        if (this.privIsPaused && this.privAudio !== undefined) {\r\n            this.privAudio.play().then((): void => {\r\n                if (!!cb) {\r\n                    cb();\r\n                }\r\n            }, (error: string): void => {\r\n                if (!!err) {\r\n                    err(error);\r\n                }\r\n            });\r\n            this.privIsPaused = false;\r\n        }\r\n    }\r\n\r\n    public onAudioStart: (sender: IPlayer) => void;\r\n\r\n    public onAudioEnd: (sender: IPlayer) => void;\r\n\r\n    public get internalAudio(): HTMLAudioElement {\r\n        return this.privAudio;\r\n    }\r\n\r\n    private async updateSourceBuffer(): Promise<void> {\r\n        if (this.privAudioBuffer !== undefined && (this.privAudioBuffer.length > 0) && this.sourceBufferAvailable()) {\r\n            this.privAppendingToBuffer = true;\r\n            const binary = this.privAudioBuffer.shift();\r\n            try {\r\n                this.privSourceBuffer.appendBuffer(binary);\r\n            } catch (error) {\r\n                this.privAudioBuffer.unshift(binary);\r\n                // eslint-disable-next-line no-console\r\n                console.log(\r\n                    \"buffer filled, pausing addition of binaries until space is made\");\r\n                return;\r\n            }\r\n            await this.notifyPlayback();\r\n        } else if (this.canEndStream()) {\r\n            await this.handleSourceBufferUpdateEnd();\r\n        }\r\n    }\r\n\r\n    private async handleSourceBufferUpdateEnd(): Promise<void> {\r\n        if (this.canEndStream() && this.sourceBufferAvailable()) {\r\n            this.privMediaSource.endOfStream();\r\n            await this.notifyPlayback();\r\n        }\r\n    }\r\n\r\n    private async notifyPlayback(): Promise<void> {\r\n        if (!this.privPlaybackStarted && this.privAudio !== undefined) {\r\n            this.privPlaybackStarted = true;\r\n            if (!!this.onAudioStart) {\r\n                this.onAudioStart(this);\r\n            }\r\n            this.privAudio.onended = (): void => {\r\n                if (!!this.onAudioEnd) {\r\n                    this.onAudioEnd(this);\r\n                }\r\n            };\r\n            if (!this.privIsPaused) {\r\n                await this.privAudio.play();\r\n            }\r\n        }\r\n    }\r\n\r\n    private canEndStream(): boolean {\r\n        return (this.isClosed && this.privSourceBuffer !== undefined && (this.privAudioBuffer.length === 0)\r\n            && this.privMediaSourceOpened && !this.privAppendingToBuffer && this.privMediaSource.readyState === \"open\");\r\n    }\r\n\r\n    private sourceBufferAvailable(): boolean {\r\n        return (this.privSourceBuffer !== undefined && !this.privSourceBuffer.updating);\r\n    }\r\n}\r\n"]}