Skip to content

Commit cdb51af

Browse files
add content downloader (Azure#25348)
### Describe the problem that is addressed by this PR Can now use the downloadStreaming and downloadTo, and delete recording methods.
1 parent 4e96139 commit cdb51af

File tree

5 files changed

+230
-3
lines changed

5 files changed

+230
-3
lines changed

sdk/communication/communication-call-automation/review/communication-call-automation.api.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
55
```ts
66

7+
/// <reference types="node" />
8+
79
import { CommonClientOptions } from '@azure/core-client';
810
import { CommunicationIdentifier } from '@azure/communication-common';
911
import { CommunicationUserIdentifier } from '@azure/communication-common';
1012
import * as coreClient from '@azure/core-client';
1113
import { KeyCredential } from '@azure/core-auth';
1214
import { OperationOptions } from '@azure/core-client';
1315
import { PhoneNumberIdentifier } from '@azure/communication-common';
16+
import { PipelineResponse } from '@azure/core-rest-pipeline';
1417
import { TokenCredential } from '@azure/core-auth';
1518

1619
// @public
@@ -205,7 +208,11 @@ export interface CallParticipant {
205208
// @public
206209
export class CallRecording {
207210
// Warning: (ae-forgotten-export) The symbol "CallRecordingImpl" needs to be exported by the entry point index.d.ts
208-
constructor(callRecordingImpl: CallRecordingImpl);
211+
// Warning: (ae-forgotten-export) The symbol "ContentDownloaderImpl" needs to be exported by the entry point index.d.ts
212+
constructor(callRecordingImpl: CallRecordingImpl, contentDownloader: ContentDownloaderImpl);
213+
deleteRecording(recordingLocation: string, options?: DeleteRecordingOptions): Promise<void>;
214+
downloadStreaming(sourceLocation: string, options?: DownloadRecordingOptions): Promise<NodeJS.ReadableStream>;
215+
downloadTo(sourceLocation: string, destinationPath: string, options?: DownloadRecordingOptions): Promise<void>;
209216
getRecordingState(recordingId: string, options?: GetRecordingPropertiesOptions): Promise<RecordingStateResult>;
210217
pauseRecording(recordingId: string, options?: PauseRecordingOptions): Promise<void>;
211218
resumeRecording(recordingId: string, options?: ResumeRecordingOptions): Promise<void>;
@@ -238,6 +245,15 @@ export interface CreateCallOptions extends OperationOptions {
238245
// @public
239246
export type CreateCallResult = CallResult;
240247

248+
// @public
249+
export type DeleteRecordingOptions = OperationOptions;
250+
251+
// @public
252+
export interface DownloadRecordingOptions extends OperationOptions {
253+
length?: number;
254+
offset?: number;
255+
}
256+
241257
// @public
242258
export enum DtmfTone {
243259
// (undocumented)

sdk/communication/communication-call-automation/src/callAutomationClient.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
phoneNumberIdentifierConverter,
3838
PhoneNumberIdentifierModelConverter,
3939
} from "./utli/converters";
40+
import { ContentDownloaderImpl } from "./contentDownloader";
4041

4142
/**
4243
* Client options used to configure CallAutomation Client API requests.
@@ -63,6 +64,7 @@ export class CallAutomationClient {
6364
private readonly callAutomationApiClient: CallAutomationApiClient;
6465
private readonly callConnectionImpl: CallConnectionImpl;
6566
private readonly callRecordingImpl: CallRecordingImpl;
67+
private readonly contentDownloaderImpl: ContentDownloaderImpl;
6668
private readonly callMediaImpl: CallMediaImpl;
6769
private readonly sourceIdentity?: CommunicationIdentifierModel;
6870

@@ -127,6 +129,7 @@ export class CallAutomationClient {
127129
this.callConnectionImpl = new CallConnectionImpl(this.callAutomationApiClient);
128130
this.callMediaImpl = new CallMediaImpl(this.callAutomationApiClient);
129131
this.callRecordingImpl = new CallRecordingImpl(this.callAutomationApiClient);
132+
this.contentDownloaderImpl = new ContentDownloaderImpl(this.callAutomationApiClient);
130133
this.sourceIdentity = options.sourceIdentity
131134
? communicationIdentifierModelConverter(options.sourceIdentity)
132135
: undefined;
@@ -144,7 +147,7 @@ export class CallAutomationClient {
144147
* Initializes a new instance of CallRecording.
145148
*/
146149
public getCallRecording(): CallRecording {
147-
return new CallRecording(this.callRecordingImpl);
150+
return new CallRecording(this.callRecordingImpl, this.contentDownloaderImpl);
148151
}
149152

150153
/**

sdk/communication/communication-call-automation/src/callRecording.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,23 @@ import {
99
PauseRecordingOptions,
1010
GetRecordingPropertiesOptions,
1111
ResumeRecordingOptions,
12+
DeleteRecordingOptions,
13+
DownloadRecordingOptions,
1214
} from "./models/options";
1315
import { communicationIdentifierModelConverter } from "./utli/converters";
16+
import { ContentDownloaderImpl } from "./contentDownloader";
17+
import * as fs from "fs";
1418

1519
/**
1620
* CallRecording class represents call recording related APIs.
1721
*/
1822
export class CallRecording {
1923
private readonly callRecordingImpl: CallRecordingImpl;
24+
private readonly contentDownloader: ContentDownloaderImpl;
2025

21-
constructor(callRecordingImpl: CallRecordingImpl) {
26+
constructor(callRecordingImpl: CallRecordingImpl, contentDownloader: ContentDownloaderImpl) {
2227
this.callRecordingImpl = callRecordingImpl;
28+
this.contentDownloader = contentDownloader;
2329
}
2430

2531
/**
@@ -111,4 +117,55 @@ export class CallRecording {
111117
): Promise<void> {
112118
return this.callRecordingImpl.resumeRecording(recordingId, options);
113119
}
120+
121+
/**
122+
* Deletes a recording.
123+
* @param recordingLocation - The recording location uri. Required.
124+
* @param options - Additional request options contains deleteRecording api options.
125+
*/
126+
public async deleteRecording(
127+
recordingLocation: string,
128+
options: DeleteRecordingOptions = {}
129+
): Promise<void> {
130+
await this.contentDownloader.deleteRecording(recordingLocation, options);
131+
}
132+
133+
/**
134+
* Returns a stream with a call recording.
135+
* @param sourceLocation - The source location uri. Required.
136+
* @param options - Additional request options contains downloadRecording api options.
137+
*/
138+
public async downloadStreaming(
139+
sourceLocation: string,
140+
options: DownloadRecordingOptions = {}
141+
): Promise<NodeJS.ReadableStream> {
142+
const result = this.contentDownloader.download(sourceLocation, options);
143+
const recordingStream = (await result).readableStreamBody;
144+
if (recordingStream) {
145+
return recordingStream;
146+
} else {
147+
throw Error("failed to get stream");
148+
}
149+
}
150+
151+
/**
152+
* Downloads a call recording file to the specified path.
153+
* @param sourceLocation - The source location uri. Required.
154+
* @param destinationPath - The destination path. Required.
155+
* @param options - Additional request options contains downloadRecording api options.
156+
*/
157+
public async downloadTo(
158+
sourceLocation: string,
159+
destinationPath: string,
160+
options: DownloadRecordingOptions = {}
161+
): Promise<void> {
162+
console.log(destinationPath);
163+
const result = this.contentDownloader.download(sourceLocation, options);
164+
const recordingStream = (await result).readableStreamBody;
165+
if (recordingStream) {
166+
recordingStream.pipe(fs.createWriteStream(destinationPath));
167+
} else {
168+
throw Error("failed to get stream");
169+
}
170+
}
114171
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { CallAutomationApiClient } from "./generated/src/callAutomationApiClient";
5+
import {
6+
AddPipelineOptions,
7+
createHttpHeaders,
8+
createPipelineRequest,
9+
PipelineRequest,
10+
PipelineRequestOptions,
11+
PipelineResponse,
12+
SendRequest,
13+
} from "@azure/core-rest-pipeline";
14+
import { DeleteRecordingOptions, DownloadRecordingOptions } from "./models/options";
15+
16+
/** Class containing ContentDownloading operations. */
17+
export class ContentDownloaderImpl {
18+
private readonly client: CallAutomationApiClient;
19+
20+
/**
21+
* Initialize a new instance of the class ContentDownloader class.
22+
* @param client - Reference to the service client
23+
*/
24+
constructor(client: CallAutomationApiClient) {
25+
this.client = client;
26+
this.addCustomSignUrlPolicy();
27+
}
28+
29+
private addCustomSignUrlPolicy() {
30+
const signUrlPolicy = {
31+
name: "CustomSignUrlPolicy",
32+
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
33+
if (request.headers.has("OriginalUrl")) {
34+
request.url = `${request.headers.get("OriginalUrl")}`;
35+
const originalRequest = new URL(request.url);
36+
request.headers.set("Host", originalRequest.host);
37+
}
38+
return next(request);
39+
},
40+
};
41+
42+
const pipelineOptions: AddPipelineOptions = {};
43+
pipelineOptions.afterPhase = "Sign";
44+
this.client.pipeline.addPolicy(signUrlPolicy, pipelineOptions);
45+
}
46+
47+
/**
48+
* Deletes a recording.
49+
* @param recordingLocation - The recording location uri. Required.
50+
*/
51+
async deleteRecording(
52+
recordingLocation: string,
53+
options?: DeleteRecordingOptions
54+
): Promise<void> {
55+
const fileLocation = new URL(recordingLocation);
56+
const endpoint = new URL(this.client.endpoint);
57+
const modifiedUrlForSigning = endpoint.origin + fileLocation.pathname;
58+
59+
const opt: PipelineRequestOptions = {
60+
url: modifiedUrlForSigning,
61+
method: "DELETE",
62+
headers: createHttpHeaders(),
63+
body: "",
64+
abortSignal: options?.abortSignal,
65+
tracingOptions: options?.tracingOptions,
66+
};
67+
68+
opt.headers?.set("OriginalUrl", recordingLocation);
69+
opt.headers?.set("x-ms-host", endpoint.host);
70+
opt.headers?.set("accept", "application/json");
71+
72+
const req = createPipelineRequest(opt);
73+
74+
const results = await this.client.sendRequest(req);
75+
76+
if (results.status !== 200) {
77+
if (results.bodyAsText) {
78+
const jsonBody = JSON.parse(results.bodyAsText);
79+
throw { status: jsonBody.status, message: jsonBody.message };
80+
}
81+
throw { status: results.status };
82+
}
83+
}
84+
85+
/**
86+
* Returns a stream with a call recording.
87+
* @param sourceLocation - The source location uri. Required.
88+
* @param offset - Offset byte. Not required.
89+
* @param length - how many bytes. Not required.
90+
*/
91+
async download(
92+
sourceLocation: string,
93+
options: DownloadRecordingOptions
94+
): Promise<PipelineResponse> {
95+
const fileLocation = new URL(sourceLocation);
96+
const endpoint = new URL(this.client.endpoint);
97+
const modifiedUrlForSigning = endpoint.origin + fileLocation.pathname;
98+
99+
const opt: PipelineRequestOptions = {
100+
url: modifiedUrlForSigning,
101+
method: "GET",
102+
headers: createHttpHeaders(),
103+
body: "",
104+
streamResponseStatusCodes: new Set([200, 206]),
105+
abortSignal: options.abortSignal,
106+
tracingOptions: options?.tracingOptions,
107+
};
108+
109+
if (options.length && !options.offset) {
110+
throw Error("Download offset value must not be empty if length is set.");
111+
} else if (options.length && options.offset) {
112+
options.length = options.offset + options.length - 1;
113+
}
114+
115+
let rangeHeader = "bytes=" + options.offset;
116+
if (options.length) rangeHeader += "-" + options.length;
117+
118+
opt.headers?.set("OriginalUrl", sourceLocation);
119+
opt.headers?.set("x-ms-host", endpoint.host);
120+
opt.headers?.set("accept", "application/json");
121+
opt.headers?.set("Range", rangeHeader);
122+
123+
const req = createPipelineRequest(opt);
124+
125+
const results = await this.client.sendRequest(req);
126+
127+
if (results.status !== 200 && results.status !== 206) {
128+
if (results.bodyAsText) {
129+
const jsonBody = JSON.parse(results.bodyAsText);
130+
throw { status: jsonBody.status, message: jsonBody.message };
131+
}
132+
throw { status: results.status };
133+
}
134+
return results;
135+
}
136+
}

sdk/communication/communication-call-automation/src/models/options.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,18 @@ export type GetRecordingPropertiesOptions = OperationOptions;
169169
* Options to resume recording.
170170
*/
171171
export type ResumeRecordingOptions = OperationOptions;
172+
173+
/**
174+
* Options to delete recording.
175+
*/
176+
export type DeleteRecordingOptions = OperationOptions;
177+
178+
/**
179+
* Options to download recording.
180+
*/
181+
export interface DownloadRecordingOptions extends OperationOptions {
182+
/** Offset byte to start download from. */
183+
offset?: number;
184+
/** Max content length in bytes. */
185+
length?: number;
186+
}

0 commit comments

Comments
 (0)