From aca20adafb2291077d7cbb7c71df80f717929f3a Mon Sep 17 00:00:00 2001 From: zachm-vapi <276893101+zachm-vapi@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:58:59 -0700 Subject: [PATCH] Make it possible to retry video recording if it fails --- README.md | 14 +++++++++++ __tests__/vapi.test.ts | 28 ++++++++++++++++++++++ vapi.ts | 53 ++++++++++++++++++++++++++++-------------- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 308a15657..564bae095 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/__tests__/vapi.test.ts b/__tests__/vapi.test.ts index 63822586a..24a9548b4 100644 --- a/__tests__/vapi.test.ts +++ b/__tests__/vapi.test.ts @@ -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"); @@ -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(); + }); + }); }); diff --git a/vapi.ts b/vapi.ts index f34de5b5b..d92fca060 100644 --- a/vapi.ts +++ b/vapi.ts @@ -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', { @@ -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', @@ -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. * @@ -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', { @@ -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',