Skip to content

Commit 0d3afaf

Browse files
Merge pull request #69 from VapiAI/bryant/add-video-tracks
feat: add video tracks
2 parents 2d8415d + de830fe commit 0d3afaf

File tree

2 files changed

+109
-54
lines changed

2 files changed

+109
-54
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vapi.ts

Lines changed: 107 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,65 @@
1-
import { Call, CreateAssistantDTO, CreateSquadDTO, AssistantOverrides } from './api';
1+
import type { ChatCompletionMessageParam } from 'openai/resources';
2+
23
import DailyIframe, {
3-
DailyAdvancedConfig,
44
DailyCall,
5+
DailyAdvancedConfig,
6+
DailyFactoryOptions,
57
DailyEventObjectAppMessage,
68
DailyEventObjectParticipant,
79
DailyEventObjectRemoteParticipantsAudioLevel,
8-
DailyFactoryOptions,
910
} from '@daily-co/daily-js';
10-
11-
import type { ChatCompletionMessageParam } from 'openai/resources';
1211
import EventEmitter from 'events';
12+
13+
import {
14+
Call,
15+
CreateSquadDTO,
16+
CreateAssistantDTO,
17+
AssistantOverrides,
18+
} from './api';
1319
import { client } from './client';
1420

15-
function destroyAudioPlayer(participantId: string) {
16-
const player = document.querySelector(`audio[data-participant-id="${participantId}"]`);
17-
player?.remove();
18-
}
19-
async function startPlayer(player: HTMLAudioElement, track: any) {
21+
async function startAudioPlayer(
22+
player: HTMLAudioElement,
23+
track: MediaStreamTrack,
24+
) {
2025
player.muted = false;
2126
player.autoplay = true;
2227
if (track != null) {
2328
player.srcObject = new MediaStream([track]);
2429
await player.play();
2530
}
2631
}
27-
async function buildAudioPlayer(track: any, participantId: string) {
32+
33+
async function buildAudioPlayer(
34+
track: MediaStreamTrack,
35+
participantId: string,
36+
) {
2837
const player = document.createElement('audio');
2938
player.dataset.participantId = participantId;
3039
document.body.appendChild(player);
31-
await startPlayer(player, track);
40+
await startAudioPlayer(player, track);
3241
return player;
3342
}
43+
44+
function destroyAudioPlayer(participantId: string) {
45+
const player = document.querySelector(
46+
`audio[data-participant-id="${participantId}"]`,
47+
);
48+
player?.remove();
49+
}
50+
3451
function subscribeToTracks(
3552
e: DailyEventObjectParticipant,
3653
call: DailyCall,
3754
isVideoRecordingEnabled?: boolean,
55+
isVideoEnabled?: boolean,
3856
) {
3957
if (e.participant.local) return;
4058

4159
call.updateParticipant(e.participant.session_id, {
4260
setSubscribedTracks: {
4361
audio: true,
44-
video: isVideoRecordingEnabled,
62+
video: isVideoRecordingEnabled || isVideoEnabled,
4563
},
4664
});
4765
}
@@ -63,7 +81,10 @@ export interface SayMessage {
6381
endCallAfterSpoken?: boolean;
6482
}
6583

66-
type VapiClientToServerMessage = AddMessageMessage | ControlMessages | SayMessage;
84+
type VapiClientToServerMessage =
85+
| AddMessageMessage
86+
| ControlMessages
87+
| SayMessage;
6788

6889
type VapiEventNames =
6990
| 'call-end'
@@ -72,6 +93,7 @@ type VapiEventNames =
7293
| 'speech-start'
7394
| 'speech-end'
7495
| 'message'
96+
| 'video'
7597
| 'error';
7698

7799
type VapiEventListeners = {
@@ -80,23 +102,36 @@ type VapiEventListeners = {
80102
'volume-level': (volume: number) => void;
81103
'speech-start': () => void;
82104
'speech-end': () => void;
105+
video: (track: MediaStreamTrack) => void;
83106
message: (message: any) => void;
84107
error: (error: any) => void;
85108
};
86109

87110
class VapiEventEmitter extends EventEmitter {
88-
on<E extends VapiEventNames>(event: E, listener: VapiEventListeners[E]): this {
111+
on<E extends VapiEventNames>(
112+
event: E,
113+
listener: VapiEventListeners[E],
114+
): this {
89115
super.on(event, listener);
90116
return this;
91117
}
92-
once<E extends VapiEventNames>(event: E, listener: VapiEventListeners[E]): this {
118+
once<E extends VapiEventNames>(
119+
event: E,
120+
listener: VapiEventListeners[E],
121+
): this {
93122
super.once(event, listener);
94123
return this;
95124
}
96-
emit<E extends VapiEventNames>(event: E, ...args: Parameters<VapiEventListeners[E]>): boolean {
125+
emit<E extends VapiEventNames>(
126+
event: E,
127+
...args: Parameters<VapiEventListeners[E]>
128+
): boolean {
97129
return super.emit(event, ...args);
98130
}
99-
removeListener<E extends VapiEventNames>(event: E, listener: VapiEventListeners[E]): this {
131+
removeListener<E extends VapiEventNames>(
132+
event: E,
133+
listener: VapiEventListeners[E],
134+
): this {
100135
super.removeListener(event, listener);
101136
return this;
102137
}
@@ -110,20 +145,23 @@ export default class Vapi extends VapiEventEmitter {
110145
private started: boolean = false;
111146
private call: DailyCall | null = null;
112147
private speakingTimeout: NodeJS.Timeout | null = null;
113-
private dailyCallConfig: DailyAdvancedConfig = {}
114-
private dailyCallObject: DailyFactoryOptions = {}
148+
private dailyCallConfig: DailyAdvancedConfig = {};
149+
private dailyCallObject: DailyFactoryOptions = {};
115150

116151
constructor(
117-
apiToken: string,
118-
apiBaseUrl?: string,
119-
dailyCallConfig?: Pick<DailyAdvancedConfig, 'avoidEval' | 'alwaysIncludeMicInPermissionPrompt'>,
120-
dailyCallObject?: Pick<DailyFactoryOptions, 'audioSource'>
152+
apiToken: string,
153+
apiBaseUrl?: string,
154+
dailyCallConfig?: Pick<
155+
DailyAdvancedConfig,
156+
'avoidEval' | 'alwaysIncludeMicInPermissionPrompt'
157+
>,
158+
dailyCallObject?: Pick<DailyFactoryOptions, 'audioSource'>,
121159
) {
122160
super();
123161
client.baseUrl = apiBaseUrl ?? 'https://api.vapi.ai';
124162
client.setSecurityData(apiToken);
125-
this.dailyCallConfig = dailyCallConfig ?? {}
126-
this.dailyCallObject = dailyCallObject ?? {}
163+
this.dailyCallConfig = dailyCallConfig ?? {};
164+
this.dailyCallObject = dailyCallObject ?? {};
127165
}
128166

129167
private cleanup() {
@@ -161,12 +199,17 @@ export default class Vapi extends VapiEventEmitter {
161199
if (this.call) {
162200
this.cleanup();
163201
}
164-
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
202+
203+
const isVideoRecordingEnabled =
204+
webCall?.artifactPlan?.videoRecordingEnabled ?? false;
205+
206+
// @ts-expect-error Tavus voice exists
207+
const isVideoEnabled = webCall.assistant?.voice?.provider === 'tavus';
165208

166209
this.call = DailyIframe.createCallObject({
167210
audioSource: this.dailyCallObject.audioSource ?? true,
168-
videoSource: isVideoRecordingEnabled,
169-
dailyConfig: this.dailyCallConfig
211+
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
212+
dailyConfig: this.dailyCallConfig,
170213
});
171214
this.call.iframe()?.style.setProperty('display', 'none');
172215

@@ -197,17 +240,27 @@ export default class Vapi extends VapiEventEmitter {
197240
this.call.on('track-started', async (e) => {
198241
if (!e || !e.participant) return;
199242
if (e.participant?.local) return;
200-
if (e.track.kind !== 'audio') return;
243+
if (e.participant?.user_name !== 'Vapi Speaker') return;
244+
245+
if (e.track.kind === 'video') {
246+
this.emit('video', e.track);
247+
}
201248

202-
await buildAudioPlayer(e.track, e.participant.session_id);
249+
if (e.track.kind === 'audio') {
250+
await buildAudioPlayer(e.track, e.participant.session_id);
251+
}
203252

204-
if (e?.participant?.user_name !== 'Vapi Speaker') return;
205253
this.call?.sendAppMessage('playable');
206254
});
207255

208256
this.call.on('participant-joined', (e) => {
209257
if (!e || !this.call) return;
210-
subscribeToTracks(e, this.call, isVideoRecordingEnabled);
258+
subscribeToTracks(
259+
e,
260+
this.call,
261+
isVideoRecordingEnabled,
262+
isVideoEnabled,
263+
);
211264
});
212265

213266
await this.call.join({
@@ -231,7 +284,8 @@ export default class Vapi extends VapiEventEmitter {
231284
this.send({
232285
type: 'control',
233286
control: 'say-first-message',
234-
videoRecordingStartDelaySeconds: (new Date().getTime() - recordingRequestedTime) / 1000,
287+
videoRecordingStartDelaySeconds:
288+
(new Date().getTime() - recordingRequestedTime) / 1000,
235289
});
236290
});
237291
}
@@ -296,8 +350,13 @@ export default class Vapi extends VapiEventEmitter {
296350
}
297351
}
298352

299-
private handleRemoteParticipantsAudioLevel(e: DailyEventObjectRemoteParticipantsAudioLevel) {
300-
const speechLevel = Object.values(e.participantsAudioLevel).reduce((a, b) => a + b, 0);
353+
private handleRemoteParticipantsAudioLevel(
354+
e: DailyEventObjectRemoteParticipantsAudioLevel,
355+
) {
356+
const speechLevel = Object.values(e.participantsAudioLevel).reduce(
357+
(a, b) => a + b,
358+
0,
359+
);
301360

302361
this.emit('volume-level', Math.min(1, speechLevel / 0.15));
303362

@@ -329,25 +388,17 @@ export default class Vapi extends VapiEventEmitter {
329388
}
330389

331390
public setMuted(mute: boolean) {
332-
try {
333-
if (!this.call) {
334-
throw new Error('Call object is not available.');
335-
}
336-
this.call.setLocalAudio(!mute);
337-
} catch (error) {
338-
throw error;
391+
if (!this.call) {
392+
throw new Error('Call object is not available.');
339393
}
394+
this.call.setLocalAudio(!mute);
340395
}
341396

342397
public isMuted() {
343-
try {
344-
if (!this.call) {
345-
return false;
346-
}
347-
return this.call.localAudio() === false;
348-
} catch (error) {
349-
throw error;
398+
if (!this.call) {
399+
return false;
350400
}
401+
return this.call.localAudio() === false;
351402
}
352403

353404
public say(message: string, endCallAfterSpoken?: boolean) {
@@ -358,11 +409,15 @@ export default class Vapi extends VapiEventEmitter {
358409
});
359410
}
360411

361-
public setInputDevicesAsync(options: Parameters<DailyCall['setInputDevicesAsync']>[0]) {
412+
public setInputDevicesAsync(
413+
options: Parameters<DailyCall['setInputDevicesAsync']>[0],
414+
) {
362415
this.call?.setInputDevicesAsync(options);
363416
}
364417

365-
public setOutputDeviceAsync(options: Parameters<DailyCall['setOutputDeviceAsync']>[0]) {
418+
public setOutputDeviceAsync(
419+
options: Parameters<DailyCall['setOutputDeviceAsync']>[0],
420+
) {
366421
this.call?.setOutputDeviceAsync(options);
367422
}
368423

0 commit comments

Comments
 (0)