From 54f342dbeb3448dc672565b118907c587e6d521a Mon Sep 17 00:00:00 2001 From: Adam Tazi <89811028+atazimsft@users.noreply.github.com> Date: Thu, 15 Jun 2023 11:32:19 -0700 Subject: [PATCH] Update SDK with V4 Media Features (#26196) This pull request updates the SDK with the remaining V4 Media features, including: - Media Streaming - Play TTS - Play SSML - Recognize choices - Recognize speech --- .vscode/cspell.json | 13 +- .../communication-call-automation.api.md | 67 ++++++++- .../src/callMedia.ts | 129 ++++++++++++++++-- .../src/generated/src/models/index.ts | 8 +- .../src/generated/src/models/mappers.ts | 12 +- .../src/models/models.ts | 32 +++++ .../src/models/options.ts | 28 ++++ .../swagger/README.md | 6 + .../test/callMediaClient.spec.ts | 100 +++++++++++++- 9 files changed, 363 insertions(+), 32 deletions(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 229a9a11e407..bd72f2c8985e 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -110,7 +110,7 @@ "Rollup", "rrggbb", "Rtsp", - "reoffer", + "reoffer", "rushx", "soundex", "southcentralus", @@ -246,7 +246,9 @@ }, { "filename": "sdk/formrecognizer/ai-form-recognizer/README.md", - "words": ["iddocument"] + "words": [ + "iddocument" + ] }, { "filename": "sdk/formrecognizer/ai-form-recognizer/review/**/*.md", @@ -278,6 +280,13 @@ "riscv" ] }, + { + "filename": "sdk/communication/communication-call-automation/review/**/*.md", + "words": [ + "ssml", + "Ssml" + ] + }, { "filename": "sdk/communication/communication-common/review/**/*.md", "words": [ diff --git a/sdk/communication/communication-call-automation/review/communication-call-automation.api.md b/sdk/communication/communication-call-automation/review/communication-call-automation.api.md index 79c11b48a3cd..65b62337254c 100644 --- a/sdk/communication/communication-call-automation/review/communication-call-automation.api.md +++ b/sdk/communication/communication-call-automation/review/communication-call-automation.api.md @@ -158,22 +158,27 @@ export type CallLocatorType = "serverCallLocator" | "groupCallLocator"; export class CallMedia { constructor(callConnectionId: string, endpoint: string, credential: KeyCredential | TokenCredential, options?: CallAutomationApiClientOptionalParams); cancelAllOperations(): Promise; - play(playSource: FileSource, playTo: CommunicationIdentifier[], playOptions?: PlayOptions): Promise; - playToAll(playSource: FileSource, playOptions?: PlayOptions): Promise; + play(playSource: FileSource | TextSource | SsmlSource, playTo: CommunicationIdentifier[], playOptions?: PlayOptions): Promise; + playToAll(playSource: FileSource | TextSource | SsmlSource, playOptions?: PlayOptions): Promise; // Warning: (ae-forgotten-export) The symbol "Tone" needs to be exported by the entry point index.d.ts sendDtmf(tones: Tone[], targetParticipant: CommunicationIdentifier, sendDtmfOptions?: SendDtmfOptions): Promise; startContinuousDtmfRecognition(targetParticipant: CommunicationIdentifier, continuousDtmfRecognitionOptions?: ContinuousDtmfRecognitionOptions): Promise; - startRecognizing(targetParticipant: CommunicationIdentifier, maxTonesToCollect: number, recognizeOptions: CallMediaRecognizeDtmfOptions): Promise; + startRecognizing(targetParticipant: CommunicationIdentifier, maxTonesToCollect: number, recognizeOptions: CallMediaRecognizeDtmfOptions | CallMediaRecognizeChoiceOptions | CallMediaRecognizeSpeechOptions | CallMediaRecognizeSpeechOrDtmfOptions): Promise; stopContinuousDtmfRecognition(targetParticipant: CommunicationIdentifier, continuousDtmfRecognitionOptions?: ContinuousDtmfRecognitionOptions): Promise; } // @public -export interface CallMediaRecognizeDtmfOptions extends CallMediaRecognizeOptions { +export interface CallMediaRecognizeChoiceOptions extends CallMediaRecognizeOptions { + choices: Choice[]; // (undocumented) + readonly kind: "callMediaRecognizeChoiceOptions"; +} + +// @public +export interface CallMediaRecognizeDtmfOptions extends CallMediaRecognizeOptions { interToneTimeoutInSeconds?: number; // (undocumented) readonly kind: "callMediaRecognizeDtmfOptions"; - // (undocumented) stopDtmfTones?: DtmfTone[]; } @@ -193,6 +198,22 @@ export interface CallMediaRecognizeOptions extends OperationOptions { stopCurrentOperations?: boolean; } +// @public +export interface CallMediaRecognizeSpeechOptions extends CallMediaRecognizeOptions { + endSilenceTimeoutInMs?: number; + // (undocumented) + readonly kind: "callMediaRecognizeSpeechOptions"; +} + +// @public +export interface CallMediaRecognizeSpeechOrDtmfOptions extends CallMediaRecognizeOptions { + endSilenceTimeoutInMs?: number; + interToneTimeoutInSeconds?: number; + // (undocumented) + readonly kind: "callMediaRecognizeSpeechOrDtmfOptions"; + stopDtmfTones?: DtmfTone[]; +} + // @public export interface CallParticipant { identifier?: CommunicationIdentifier; @@ -240,6 +261,14 @@ export interface ChannelAffinity { targetParticipant: CommunicationIdentifier; } +// @public +export interface Choice { + label: string; + phrases: string[]; + // (undocumented) + tone?: DtmfTone; +} + // @public export interface ContinuousDtmfRecognitionOptions extends OperationOptions { operationContext?: string; @@ -335,6 +364,12 @@ export interface FileSource extends PlaySource { url: string; } +// @public +export enum Gender { + Female = "female", + Male = "male" +} + // @public export type GetCallConnectionPropertiesOptions = OperationOptions; @@ -811,6 +846,14 @@ export interface SendDtmfOptions extends OperationOptions { operationContext?: string; } +// @public +export interface SsmlSource extends PlaySource { + // (undocumented) + readonly kind: "ssmlSource"; + // (undocumented) + ssmlText: string; +} + // @public export interface StartRecordingOptions extends OperationOptions { audioChannelParticipantOrdering?: CommunicationIdentifier[]; @@ -825,6 +868,20 @@ export interface StartRecordingOptions extends OperationOptions { // @public export type StopRecordingOptions = OperationOptions; +// @public +export interface TextSource extends PlaySource { + // (undocumented) + readonly kind: "textSource"; + // (undocumented) + sourceLocale?: string; + // (undocumented) + text: string; + // (undocumented) + voiceGender?: Gender; + // (undocumented) + voiceName?: string; +} + // @public export interface ToneInfo extends Omit { sequenceId: number; diff --git a/sdk/communication/communication-call-automation/src/callMedia.ts b/sdk/communication/communication-call-automation/src/callMedia.ts index a4066c7a9dcb..abf1474206a6 100644 --- a/sdk/communication/communication-call-automation/src/callMedia.ts +++ b/sdk/communication/communication-call-automation/src/callMedia.ts @@ -5,6 +5,8 @@ import { PlayRequest, PlaySourceInternal, FileSourceInternal, + TextSourceInternal, + SsmlSourceInternal, KnownPlaySourceType, RecognizeRequest, KnownRecognizeInputType, @@ -15,6 +17,7 @@ import { ContinuousDtmfRecognitionRequest, SendDtmfRequest, Tone, + SpeechOptions, } from "./generated/src"; import { CallMediaImpl } from "./generated/src/operations"; @@ -25,13 +28,16 @@ import { serializeCommunicationIdentifier, } from "@azure/communication-common"; -import { FileSource } from "./models/models"; +import { FileSource, TextSource, SsmlSource } from "./models/models"; import { PlayOptions, CallMediaRecognizeDtmfOptions, + CallMediaRecognizeChoiceOptions, ContinuousDtmfRecognitionOptions, SendDtmfOptions, + CallMediaRecognizeSpeechOptions, + CallMediaRecognizeSpeechOrDtmfOptions, } from "./models/options"; import { KeyCredential, TokenCredential } from "@azure/core-auth"; @@ -55,8 +61,10 @@ export class CallMedia { this.callMedia = new CallMediaImpl(this.callAutomationApiClient); } - private createPlaySourceInternal(playSource: FileSource): PlaySourceInternal { - if (playSource.kind === "fileSource" || playSource.kind === undefined) { + private createPlaySourceInternal( + playSource: FileSource | TextSource | SsmlSource + ): PlaySourceInternal { + if (playSource.kind === "fileSource") { const fileSource: FileSourceInternal = { uri: playSource.url, }; @@ -65,6 +73,27 @@ export class CallMedia { fileSource: fileSource, playSourceId: playSource.playSourceId, }; + } else if (playSource.kind === "textSource") { + const textSource: TextSourceInternal = { + text: playSource.text, + sourceLocale: playSource.sourceLocale, + voiceGender: playSource.voiceGender, + voiceName: playSource.voiceName, + }; + return { + sourceType: KnownPlaySourceType.Text, + textSource: textSource, + playSourceId: playSource.playSourceId, + }; + } else if (playSource.kind === "ssmlSource") { + const ssmlSource: SsmlSourceInternal = { + ssmlText: playSource.ssmlText, + }; + return { + sourceType: KnownPlaySourceType.Ssml, + ssmlSource: ssmlSource, + playSourceId: playSource.playSourceId, + }; } throw new Error("Invalid play source"); } @@ -77,7 +106,7 @@ export class CallMedia { * @param playOptions - Additional attributes for play. */ public async play( - playSource: FileSource, + playSource: FileSource | TextSource | SsmlSource, playTo: CommunicationIdentifier[], playOptions: PlayOptions = { loop: false } ): Promise { @@ -103,7 +132,7 @@ export class CallMedia { * @param playOptions - Additional attributes for play. */ public async playToAll( - playSource: FileSource, + playSource: FileSource | TextSource | SsmlSource, playOptions: PlayOptions = { loop: false } ): Promise { const playRequest: PlayRequest = { @@ -124,12 +153,13 @@ export class CallMedia { private createRecognizeRequest( targetParticipant: CommunicationIdentifier, maxTonesToCollect: number, - recognizeOptions: CallMediaRecognizeDtmfOptions + recognizeOptions: + | CallMediaRecognizeDtmfOptions + | CallMediaRecognizeChoiceOptions + | CallMediaRecognizeSpeechOptions + | CallMediaRecognizeSpeechOrDtmfOptions ): RecognizeRequest { - if ( - recognizeOptions.kind === "callMediaRecognizeDtmfOptions" || - recognizeOptions.kind === undefined - ) { + if (recognizeOptions.kind === "callMediaRecognizeDtmfOptions") { const dtmfOptionsInternal: DtmfOptions = { interToneTimeoutInSeconds: recognizeOptions.interToneTimeoutInSeconds ? recognizeOptions.interToneTimeoutInSeconds @@ -154,8 +184,79 @@ export class CallMedia { recognizeOptions: recognizeOptionsInternal, operationContext: recognizeOptions.operationContext, }; + } else if (recognizeOptions.kind === "callMediaRecognizeChoiceOptions") { + const recognizeOptionsInternal: RecognizeOptions = { + interruptPrompt: recognizeOptions.interruptPrompt, + initialSilenceTimeoutInSeconds: recognizeOptions.initialSilenceTimeoutInSeconds + ? recognizeOptions.initialSilenceTimeoutInSeconds + : 5, + targetParticipant: serializeCommunicationIdentifier(targetParticipant), + choices: recognizeOptions.choices, + }; + return { + recognizeInputType: KnownRecognizeInputType.Choices, + playPrompt: recognizeOptions.playPrompt + ? this.createPlaySourceInternal(recognizeOptions.playPrompt) + : undefined, + interruptCallMediaOperation: recognizeOptions.interruptCallMediaOperation, + recognizeOptions: recognizeOptionsInternal, + operationContext: recognizeOptions.operationContext, + }; + } else if (recognizeOptions.kind === "callMediaRecognizeSpeechOptions") { + const speechOptions: SpeechOptions = { + endSilenceTimeoutInMs: recognizeOptions.endSilenceTimeoutInMs + ? recognizeOptions.endSilenceTimeoutInMs + : 2, + }; + const recognizeOptionsInternal: RecognizeOptions = { + interruptPrompt: recognizeOptions.interruptPrompt, + initialSilenceTimeoutInSeconds: recognizeOptions.initialSilenceTimeoutInSeconds + ? recognizeOptions.initialSilenceTimeoutInSeconds + : 5, + targetParticipant: serializeCommunicationIdentifier(targetParticipant), + speechOptions: speechOptions, + }; + return { + recognizeInputType: KnownRecognizeInputType.Speech, + playPrompt: recognizeOptions.playPrompt + ? this.createPlaySourceInternal(recognizeOptions.playPrompt) + : undefined, + interruptCallMediaOperation: recognizeOptions.interruptCallMediaOperation, + recognizeOptions: recognizeOptionsInternal, + operationContext: recognizeOptions.operationContext, + }; + } else if (recognizeOptions.kind === "callMediaRecognizeSpeechOrDtmfOptions") { + const dtmfOptionsInternal: DtmfOptions = { + interToneTimeoutInSeconds: recognizeOptions.interToneTimeoutInSeconds + ? recognizeOptions.interToneTimeoutInSeconds + : 2, + maxTonesToCollect: maxTonesToCollect, + stopTones: recognizeOptions.stopDtmfTones, + }; + const speechOptions: SpeechOptions = { + endSilenceTimeoutInMs: recognizeOptions.endSilenceTimeoutInMs + ? recognizeOptions.endSilenceTimeoutInMs + : 2, + }; + const recognizeOptionsInternal: RecognizeOptions = { + interruptPrompt: recognizeOptions.interruptPrompt, + initialSilenceTimeoutInSeconds: recognizeOptions.initialSilenceTimeoutInSeconds + ? recognizeOptions.initialSilenceTimeoutInSeconds + : 5, + targetParticipant: serializeCommunicationIdentifier(targetParticipant), + speechOptions: speechOptions, + dtmfOptions: dtmfOptionsInternal, + }; + return { + recognizeInputType: KnownRecognizeInputType.Speech, + playPrompt: recognizeOptions.playPrompt + ? this.createPlaySourceInternal(recognizeOptions.playPrompt) + : undefined, + interruptCallMediaOperation: recognizeOptions.interruptCallMediaOperation, + recognizeOptions: recognizeOptionsInternal, + operationContext: recognizeOptions.operationContext, + }; } - throw new Error("Invalid recognizeOptions"); } @@ -166,7 +267,11 @@ export class CallMedia { public async startRecognizing( targetParticipant: CommunicationIdentifier, maxTonesToCollect: number, - recognizeOptions: CallMediaRecognizeDtmfOptions + recognizeOptions: + | CallMediaRecognizeDtmfOptions + | CallMediaRecognizeChoiceOptions + | CallMediaRecognizeSpeechOptions + | CallMediaRecognizeSpeechOrDtmfOptions ): Promise { return this.callMedia.recognize( this.callConnectionId, diff --git a/sdk/communication/communication-call-automation/src/generated/src/models/index.ts b/sdk/communication/communication-call-automation/src/generated/src/models/index.ts index deb4468c3e71..206acb54cb3c 100644 --- a/sdk/communication/communication-call-automation/src/generated/src/models/index.ts +++ b/sdk/communication/communication-call-automation/src/generated/src/models/index.ts @@ -193,9 +193,9 @@ export interface PlaySourceInternal { /** Defines the file source info to be used for play */ fileSource?: FileSourceInternal; /** Defines the text source info to be used for play */ - textSource?: TextSource; + textSource?: TextSourceInternal; /** Defines the ssml(Speech Synthesis Markup Language) source info to be used for play */ - ssmlSource?: SsmlSource; + ssmlSource?: SsmlSourceInternal; } export interface FileSourceInternal { @@ -203,7 +203,7 @@ export interface FileSourceInternal { uri: string; } -export interface TextSource { +export interface TextSourceInternal { /** Text for the cognitive service to be played */ text: string; /** @@ -220,7 +220,7 @@ export interface TextSource { voiceName?: string; } -export interface SsmlSource { +export interface SsmlSourceInternal { /** Ssml string for the cognitive service to be played */ ssmlText: string; } diff --git a/sdk/communication/communication-call-automation/src/generated/src/models/mappers.ts b/sdk/communication/communication-call-automation/src/generated/src/models/mappers.ts index 8725ff859516..5c33798eee5e 100644 --- a/sdk/communication/communication-call-automation/src/generated/src/models/mappers.ts +++ b/sdk/communication/communication-call-automation/src/generated/src/models/mappers.ts @@ -606,14 +606,14 @@ export const PlaySourceInternal: coreClient.CompositeMapper = { serializedName: "textSource", type: { name: "Composite", - className: "TextSource" + className: "TextSourceInternal" } }, ssmlSource: { serializedName: "ssmlSource", type: { name: "Composite", - className: "SsmlSource" + className: "SsmlSourceInternal" } } } @@ -636,10 +636,10 @@ export const FileSourceInternal: coreClient.CompositeMapper = { } }; -export const TextSource: coreClient.CompositeMapper = { +export const TextSourceInternal: coreClient.CompositeMapper = { type: { name: "Composite", - className: "TextSource", + className: "TextSourceInternal", modelProperties: { text: { serializedName: "text", @@ -670,10 +670,10 @@ export const TextSource: coreClient.CompositeMapper = { } }; -export const SsmlSource: coreClient.CompositeMapper = { +export const SsmlSourceInternal: coreClient.CompositeMapper = { type: { name: "Composite", - className: "SsmlSource", + className: "SsmlSourceInternal", modelProperties: { ssmlText: { serializedName: "ssmlText", diff --git a/sdk/communication/communication-call-automation/src/models/models.ts b/sdk/communication/communication-call-automation/src/models/models.ts index 9d1fc80fd42c..109bc222f125 100644 --- a/sdk/communication/communication-call-automation/src/models/models.ts +++ b/sdk/communication/communication-call-automation/src/models/models.ts @@ -64,6 +64,14 @@ export interface CallLocator { kind: CallLocatorType; } +/** Defines values for Gender that the service accepts. */ +export enum Gender { + /** Male */ + Male = "male", + /** Female */ + Female = "female", +} + /** The PlaySource model. */ export interface PlaySource { playSourceId?: string; @@ -75,6 +83,21 @@ export interface FileSource extends PlaySource { readonly kind: "fileSource"; } +/** The TextSource model. */ +export interface TextSource extends PlaySource { + text: string; + sourceLocale?: string; + voiceGender?: Gender; + voiceName?: string; + readonly kind: "textSource"; +} + +/** The SsmlSource model. */ +export interface SsmlSource extends PlaySource { + ssmlText: string; + readonly kind: "ssmlSource"; +} + /** A Dtmf Tone. */ export enum DtmfTone { /** Zero */ @@ -111,6 +134,15 @@ export enum DtmfTone { Asterisk = "asterisk", } +/** A Recognize Choice */ +export interface Choice { + /** Identifier for a given choice */ + label: string; + /** List of phrases to recognize */ + phrases: string[]; + tone?: DtmfTone; +} + /** The type of the recognition that the service accepts. */ export enum RecognizeInputType { /** Dtmf */ diff --git a/sdk/communication/communication-call-automation/src/models/options.ts b/sdk/communication/communication-call-automation/src/models/options.ts index 8810cc1d448f..b55aff86a6ad 100644 --- a/sdk/communication/communication-call-automation/src/models/options.ts +++ b/sdk/communication/communication-call-automation/src/models/options.ts @@ -8,6 +8,7 @@ import { CallRejectReason, FileSource, DtmfTone, + Choice, RecordingContent, RecordingChannel, RecordingFormat, @@ -27,11 +28,38 @@ export interface CallMediaRecognizeOptions extends OperationOptions { /** The recognize configuration specific to Dtmf. */ export interface CallMediaRecognizeDtmfOptions extends CallMediaRecognizeOptions { + /** Time to wait between DTMF inputs to stop recognizing. */ interToneTimeoutInSeconds?: number; + /** List of tones that will stop recognizing. */ stopDtmfTones?: DtmfTone[]; readonly kind: "callMediaRecognizeDtmfOptions"; } +/** The recognize configuration specific to Choices. */ +export interface CallMediaRecognizeChoiceOptions extends CallMediaRecognizeOptions { + /** The IvR choices for recognize. */ + choices: Choice[]; + readonly kind: "callMediaRecognizeChoiceOptions"; +} + +/** The recognize configuration specific to Speech. */ +export interface CallMediaRecognizeSpeechOptions extends CallMediaRecognizeOptions { + /** The length of end silence when user stops speaking and cogservice send response. */ + endSilenceTimeoutInMs?: number; + readonly kind: "callMediaRecognizeSpeechOptions"; +} + +/** The recognize configuration for Speech or Dtmf */ +export interface CallMediaRecognizeSpeechOrDtmfOptions extends CallMediaRecognizeOptions { + /** The length of end silence when user stops speaking and cogservice send response. */ + endSilenceTimeoutInMs?: number; + /** Time to wait between DTMF inputs to stop recognizing. */ + interToneTimeoutInSeconds?: number; + /** List of tones that will stop recognizing. */ + stopDtmfTones?: DtmfTone[]; + readonly kind: "callMediaRecognizeSpeechOrDtmfOptions"; +} + /** * Options to create a call. */ diff --git a/sdk/communication/communication-call-automation/swagger/README.md b/sdk/communication/communication-call-automation/swagger/README.md index c196688b5444..d743f3b79208 100644 --- a/sdk/communication/communication-call-automation/swagger/README.md +++ b/sdk/communication/communication-call-automation/swagger/README.md @@ -49,4 +49,10 @@ directive: - rename-model: from: RecognizeInputType to: RecognizeInputTypeInternal +- rename-model: + from: TextSource + to: TextSourceInternal +- rename-model: + from: SsmlSource + to: SsmlSourceInternal ``` diff --git a/sdk/communication/communication-call-automation/test/callMediaClient.spec.ts b/sdk/communication/communication-call-automation/test/callMediaClient.spec.ts index 19fdebc2eb2f..7839b7d4756c 100644 --- a/sdk/communication/communication-call-automation/test/callMediaClient.spec.ts +++ b/sdk/communication/communication-call-automation/test/callMediaClient.spec.ts @@ -13,9 +13,11 @@ import { // Parent directory imports import { CallMedia } from "../src/callMedia"; -import { FileSource } from "../src/models/models"; +import { FileSource, TextSource, SsmlSource, Choice } from "../src/models/models"; import { CallMediaRecognizeDtmfOptions, + CallMediaRecognizeChoiceOptions, + CallMediaRecognizeSpeechOptions, CallAutomationClient, CallConnection, CallInvite, @@ -62,7 +64,7 @@ describe("CallMedia Unit Tests", async function () { new CallMedia(CALL_CONNECTION_ID, baseUri, { key: generateToken() }); }); - it("makes successful Play request", async function () { + it("makes successful Play file request", async function () { const mockHttpClient = generateHttpClient(202); callMedia = createMediaClient(mockHttpClient); @@ -85,6 +87,53 @@ describe("CallMedia Unit Tests", async function () { assert.equal(request.method, "POST"); }); + it("makes successful Play TTS request", async function () { + const mockHttpClient = generateHttpClient(202); + + callMedia = createMediaClient(mockHttpClient); + const spy = sinon.spy(mockHttpClient, "sendRequest"); + + const playSource: TextSource = { + text: "test test test", + kind: "textSource", + }; + + const playTo: CommunicationIdentifier[] = [{ communicationUserId: CALL_TARGET_ID }]; + + await callMedia.play(playSource, playTo); + const request = spy.getCall(0).args[0]; + const data = JSON.parse(request.body?.toString() || ""); + + assert.equal(data.playTo[0].rawId, CALL_TARGET_ID); + assert.equal(data.playSourceInfo.sourceType, "text"); + assert.equal(data.playSourceInfo.textSource.text, playSource.text); + assert.equal(request.method, "POST"); + }); + + it("makes successful Play SSML request", async function () { + const mockHttpClient = generateHttpClient(202); + + callMedia = createMediaClient(mockHttpClient); + const spy = sinon.spy(mockHttpClient, "sendRequest"); + + const playSource: SsmlSource = { + ssmlText: + 'Recognize Choice Completed, played through SSML source.', + kind: "ssmlSource", + }; + + const playTo: CommunicationIdentifier[] = [{ communicationUserId: CALL_TARGET_ID }]; + + await callMedia.play(playSource, playTo); + const request = spy.getCall(0).args[0]; + const data = JSON.parse(request.body?.toString() || ""); + + assert.equal(data.playTo[0].rawId, CALL_TARGET_ID); + assert.equal(data.playSourceInfo.sourceType, "ssml"); + assert.equal(data.playSourceInfo.ssmlSource.ssmlText, playSource.ssmlText); + assert.equal(request.method, "POST"); + }); + it("makes successful PlayToAll request", async function () { const mockHttpClient = generateHttpClient(202); @@ -107,7 +156,7 @@ describe("CallMedia Unit Tests", async function () { assert.equal(request.method, "POST"); }); - it("makes successful StartRecognizing request", async function () { + it("makes successful StartRecognizing DTMF request", async function () { const mockHttpClient = generateHttpClient(202); callMedia = createMediaClient(mockHttpClient); @@ -127,6 +176,51 @@ describe("CallMedia Unit Tests", async function () { assert.equal(request.method, "POST"); }); + it("makes successful StartRecognizing Choices request", async function () { + const mockHttpClient = generateHttpClient(202); + + callMedia = createMediaClient(mockHttpClient); + const spy = sinon.spy(mockHttpClient, "sendRequest"); + const targetParticipant: CommunicationIdentifier = { communicationUserId: CALL_TARGET_ID }; + const choice: Choice = { + label: "choice", + phrases: ["test"], + }; + const recognizeOptions: CallMediaRecognizeChoiceOptions = { + choices: [choice], + kind: "callMediaRecognizeChoiceOptions", + }; + const maxTonesToCollect = 5; + + await callMedia.startRecognizing(targetParticipant, maxTonesToCollect, recognizeOptions); + const request = spy.getCall(0).args[0]; + const data = JSON.parse(request.body?.toString() || ""); + + assert.equal(data.recognizeInputType, "choices"); + assert.equal(data.recognizeOptions.choices[0].phrases[0], "test"); + assert.equal(request.method, "POST"); + }); + + it("makes successful StartRecognizing Speech request", async function () { + const mockHttpClient = generateHttpClient(202); + + callMedia = createMediaClient(mockHttpClient); + const spy = sinon.spy(mockHttpClient, "sendRequest"); + const targetParticipant: CommunicationIdentifier = { communicationUserId: CALL_TARGET_ID }; + const recognizeOptions: CallMediaRecognizeSpeechOptions = { + kind: "callMediaRecognizeSpeechOptions", + }; + const maxTonesToCollect = 5; + + await callMedia.startRecognizing(targetParticipant, maxTonesToCollect, recognizeOptions); + const request = spy.getCall(0).args[0]; + const data = JSON.parse(request.body?.toString() || ""); + + assert.equal(data.recognizeInputType, "speech"); + assert.equal(data.recognizeOptions.speechOptions.endSilenceTimeoutInMs, 2); + assert.equal(request.method, "POST"); + }); + it("makes successful CancelAllMediaOperations request", async function () { const mockHttpClient = generateHttpClient(202);