Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ The `say(message: string, endCallAfterSpoken?: boolean)` can be used to invoke s
vapi.say("Our time's up, goodbye!", true)
```

## Recording

When video recording is enabled on the call's artifact plan, Vapi starts a recording automatically when the call begins. Recording start is asynchronous: success is signaled by the `recording-started` event and failure by the `recording-error` event.

A failed recording start is not retried automatically. Use `startRecording()` and `stopRecording()` to control the recording yourself. For example, to retry after a failure:

```javascript
vapi.on('recording-error', (e) => {
console.error('Recording failed', e);
// Retry after a short delay
setTimeout(() => vapi.startRecording(), 5000);
});
```

## Events

You can listen to the following events:
Expand Down
28 changes: 28 additions & 0 deletions __tests__/vapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ describe("Vapi", () => {
mockCall = {
setLocalAudio: jest.fn(),
localAudio: jest.fn(),
startRecording: jest.fn(),
stopRecording: jest.fn(),
};
// Initialize Vapi instance and inject the mock
vapi = new Vapi("dummy_token");
Expand Down Expand Up @@ -53,4 +55,30 @@ describe("Vapi", () => {
expect(res).toBe(false);
});
});

describe("startRecording", () => {
it("should start a recording", () => {
vapi.startRecording();
expect(mockCall.startRecording).toHaveBeenCalled();
});

it("should throw when call object is not available", () => {
vapi["call"] = null; // Simulate call object not being available
expect(() => vapi.startRecording()).toThrow(
"Call object is not available."
);
});
});

describe("stopRecording", () => {
it("should stop the recording", () => {
vapi.stopRecording();
expect(mockCall.stopRecording).toHaveBeenCalled();
});

it("should not throw when call object is not available", () => {
vapi["call"] = null; // Simulate call object not being available
expect(() => vapi.stopRecording()).not.toThrow();
});
});
});
53 changes: 35 additions & 18 deletions vapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,14 +705,7 @@ export default class Vapi extends VapiEventEmitter {
const recordingStartTime = Date.now();

try {
this.call.startRecording({
width: 1280,
height: 720,
backgroundColor: '#FF1F2D3D',
layout: {
preset: 'default',
},
});
this.startRecording();

const recordingSetupDuration = Date.now() - recordingStartTime;
this.emit('call-start-progress', {
Expand All @@ -722,7 +715,7 @@ export default class Vapi extends VapiEventEmitter {
timestamp: new Date().toISOString()
});

this.call.on('recording-started', () => {
this.call.once('recording-started', () => {
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
this.emit('call-start-progress', {
stage: 'video-recording-started',
Expand Down Expand Up @@ -1140,6 +1133,37 @@ export default class Vapi extends VapiEventEmitter {
this.call?.stopScreenShare();
}

/**
* Starts (or retries) a cloud recording of the call.
*
* Recording start is asynchronous: success is signaled by the
* 'recording-started' event and failure by 'recording-error'. Daily does not
* retry automatically, so if you receive 'recording-error' (e.g. the
* recording start timed out), you can call this method again a few seconds
* later to retry.
*/
public startRecording(): void {
if (!this.call) {
throw new Error('Call object is not available.');
}
this.call.startRecording({
width: 1280,
height: 720,
backgroundColor: '#FF1F2D3D',
layout: {
preset: 'default',
},
});
}

/**
* Stops the in-progress recording. Completion is signaled by the
* 'recording-stopped' event.
*/
public stopRecording(): void {
this.call?.stopRecording();
}

/**
* Reconnects to an active call.
*
Expand Down Expand Up @@ -1418,14 +1442,7 @@ export default class Vapi extends VapiEventEmitter {
const recordingRequestedTime = new Date().getTime();

try {
this.call.startRecording({
width: 1280,
height: 720,
backgroundColor: '#FF1F2D3D',
layout: {
preset: 'default',
},
});
this.startRecording();

const recordingSetupDuration = Date.now() - recordingStartTime;
this.emit('call-start-progress', {
Expand All @@ -1435,7 +1452,7 @@ export default class Vapi extends VapiEventEmitter {
timestamp: new Date().toISOString()
});

this.call.on('recording-started', () => {
this.call.once('recording-started', () => {
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
this.emit('call-start-progress', {
stage: 'video-recording-started',
Expand Down
Loading