26
26
</template >
27
27
28
28
<div v-if =" participants" class =" participants-container" >
29
- <template v-for =" p in participants " :key =" p .session_id " >
30
- <video-tile
31
- :participant =" p"
32
- :handle-video-click =" handleVideoClick"
33
- :handle-audio-click =" handleAudioClick"
34
- :handle-screenshare-click =" handleScreenshareClick"
35
- :leave-call =" leaveAndCleanUp"
36
- :disable-screen-share =" screen && !screen?.local"
37
- />
38
- </template >
29
+ <div id =" video-call" ></div >
39
30
40
31
<template v-if =" count === 1 " >
41
32
<waiting-card :url =" roomUrl" />
50
41
</main >
51
42
</template >
52
43
53
- <script >
54
- import daily from " @daily-co/daily-js" ;
44
+ <script lang="ts">
45
+ import { defineComponent } from " vue" ;
46
+
47
+ import daily , {
48
+ type DailyCall ,
49
+ type DailyParticipant ,
50
+ } from " @daily-co/daily-js" ;
55
51
56
52
import WaitingCard from " ./WaitingCard.vue" ;
57
53
import ChatTile from " ./ChatTile.vue" ;
@@ -60,7 +56,33 @@ import ScreenshareTile from "./ScreenshareTile.vue";
60
56
import LoadingTile from " ./LoadingTile.vue" ;
61
57
import PermissionsErrorMsg from " ./PermissionsErrorMsg.vue" ;
62
58
63
- export default {
59
+ interface Participant {
60
+ session_id: string ;
61
+ user_id: string ;
62
+ user_name: string ;
63
+ local: boolean ;
64
+ video: boolean ;
65
+ audio: boolean ;
66
+ screen: boolean ;
67
+ }
68
+
69
+ interface CallTileData {
70
+ callObject: null | DailyCall ;
71
+ loading: boolean ;
72
+ error: boolean ;
73
+ showPermissionsError: boolean ;
74
+ participants: Participant [];
75
+ screen: unknown ; // video track
76
+ messages: string [];
77
+ count: number ;
78
+ }
79
+
80
+ type Tracks = {
81
+ videoTrack: MediaStreamTrack | null ;
82
+ audioTrack: MediaStreamTrack | null ;
83
+ };
84
+
85
+ export default defineComponent ({
64
86
name: " CallTile" ,
65
87
components: {
66
88
VideoTile ,
@@ -70,11 +92,24 @@ export default {
70
92
LoadingTile ,
71
93
PermissionsErrorMsg ,
72
94
},
73
- props: [" leaveCall" , " name" , " roomUrl" ],
74
- data () {
95
+ props: {
96
+ leaveCall: {
97
+ type: Function ,
98
+ required: true ,
99
+ },
100
+ name: {
101
+ type: String ,
102
+ required: true ,
103
+ },
104
+ roomUrl: {
105
+ type: String ,
106
+ required: true ,
107
+ },
108
+ },
109
+ data(): CallTileData {
75
110
return {
76
111
callObject: null ,
77
- participants: null ,
112
+ participants: [] ,
78
113
count: 0 ,
79
114
messages: [],
80
115
error: false ,
@@ -98,31 +133,219 @@ export default {
98
133
99
134
// Add call and participant event handler
100
135
// Visit https://docs.daily.co/reference/daily-js/events for more event info
101
- co .on (" joining-meeting" , this .handleJoiningMeeting )
102
- .on (" joined-meeting" , this .updateParticpants )
103
- .on (" participant-joined" , this .updateParticpants )
104
- .on (" participant-updated" , this .updateParticpants )
105
- .on (" participant-left" , this .updateParticpants )
136
+ co
137
+ // .on("joining-meeting", this.handleJoiningMeeting)
138
+ // .on("joined-meeting", this.updateParticpants)
139
+ // .on("participant-joined", this.updateParticpants)
140
+ // .on("participant-updated", this.updateParticpants)
141
+ // .on("participant-left", this.updateParticpants)
106
142
.on (" error" , this .handleError )
107
143
// camera-error = device permissions issue
108
144
.on (" camera-error" , this .handleDeviceError )
109
145
// app-message handles receiving remote chat messages
110
- .on (" app-message" , this .updateMessages );
146
+ .on (" app-message" , this .updateMessages )
147
+ .on (" track-started" , (p ) => {
148
+ if (! p ?.participant ) return ;
149
+ const tracks = this .getParticipantTracks (p .participant );
150
+ try {
151
+ this .updateMedia (p .participant .session_id , tracks );
152
+ } catch (e ) {
153
+ console .warn (e );
154
+ }
155
+ })
156
+ .on (" track-stopped" , (p ) => {
157
+ if (! p ?.participant ) return ;
158
+ const tracks = this .getParticipantTracks (p .participant );
159
+ try {
160
+ this .updateMedia (p .participant .session_id , tracks );
161
+ } catch (e ) {
162
+ console .warn (e );
163
+ }
164
+ });
111
165
},
112
166
unmounted() {
113
167
if (! this .callObject ) return ;
114
168
// Clean-up event handlers
115
169
this .callObject
116
- .off (" joining-meeting" , this .handleJoiningMeeting )
117
- .off (" joined-meeting" , this .updateParticpants )
118
- .off (" participant-joined" , this .updateParticpants )
119
- .off (" participant-updated" , this .updateParticpants )
120
- .off (" participant-left" , this .updateParticpants )
170
+ // .off("joining-meeting", this.handleJoiningMeeting)
171
+ // .off("joined-meeting", this.updateParticpants)
172
+ // .off("participant-joined", this.updateParticpants)
173
+ // .off("participant-updated", this.updateParticpants)
174
+ // .off("participant-left", this.updateParticpants)
121
175
.off (" error" , this .handleError )
122
176
.off (" camera-error" , this .handleDeviceError )
123
- .off (" app-message" , this .updateMessages );
177
+ .off (" app-message" , this .updateMessages )
178
+ .off (" track-started" , (p ) => {
179
+ if (! p ?.participant ) return ;
180
+ const tracks = this .getParticipantTracks (p .participant );
181
+ try {
182
+ this .updateMedia (p .participant .session_id , tracks );
183
+ } catch (e ) {
184
+ console .warn (e );
185
+ }
186
+ })
187
+ .off (" track-stopped" , (p ) => {
188
+ if (! p ?.participant ) return ;
189
+ const tracks = this .getParticipantTracks (p .participant );
190
+ try {
191
+ this .updateMedia (p .participant .session_id , tracks );
192
+ } catch (e ) {
193
+ console .warn (e );
194
+ }
195
+ });
124
196
},
125
197
methods: {
198
+ updateMedia(participantID : string , newTracks : Tracks ) {
199
+ // Get the video tag.
200
+ let videoTile = document .getElementById (
201
+ participantID
202
+ ) as HTMLVideoElement | null ;
203
+ if (! videoTile ) {
204
+ const videoCall = document .getElementById (
205
+ " video-call"
206
+ ) as HTMLDivElement ;
207
+ const newVideoTile = document .createElement (" video" );
208
+ newVideoTile .id = participantID ;
209
+ videoCall .appendChild (newVideoTile );
210
+ videoTile = newVideoTile ;
211
+ }
212
+
213
+ const video = videoTile ;
214
+
215
+ // Get existing MediaStream from the video tag source object.
216
+ const existingStream = video .srcObject as MediaStream ;
217
+
218
+ const newVideo = newTracks .videoTrack ;
219
+ const newAudio = newTracks .audioTrack ;
220
+
221
+ // If there is no existing stream or it contains no tracks,
222
+ // Just create a new media stream using our new tracks.
223
+ // This will happen if this is the first time we're
224
+ // setting the tracks.
225
+ if (! existingStream || existingStream .getTracks ().length === 0 ) {
226
+ const tracks: MediaStreamTrack [] = [];
227
+ if (newVideo ) tracks .push (newVideo );
228
+ if (newAudio ) tracks .push (newAudio );
229
+ const newStream = new MediaStream (tracks );
230
+ video .srcObject = newStream ;
231
+ video .playsInline = true ;
232
+ video .autoplay = true ;
233
+ video .muted = true ;
234
+ this .playMedia (video );
235
+ return ;
236
+ }
237
+
238
+ // This boolean will define whether we play the video element again
239
+ // This should be `true` if any of the tracks have changed.
240
+ let needsPlay = false ;
241
+ needsPlay = this .refreshAudioTrack (existingStream , newAudio );
242
+
243
+ // We have an extra if check here compared to the audio track
244
+ // handling above, because the video track also dictates
245
+ // whether we should hide the video DOM element.
246
+ if (newVideo ) {
247
+ if (this .refreshVideoTrack (existingStream , newVideo ) && ! needsPlay ) {
248
+ needsPlay = true ;
249
+ }
250
+
251
+ video .classList .remove (" hidden" );
252
+ } else {
253
+ // If there's no video to be played, hide the element.
254
+ video .classList .add (" hidden" );
255
+ }
256
+ if (needsPlay ) {
257
+ this .playMedia (video );
258
+ }
259
+ },
260
+ refreshAudioTrack(
261
+ existingStream : MediaStream ,
262
+ newAudioTrack : MediaStreamTrack | null
263
+ ): boolean {
264
+ // If there is no new track, just early out
265
+ // and keep the old track on the stream as-is.
266
+ if (! newAudioTrack ) return false ;
267
+ const existingTracks = existingStream .getAudioTracks ();
268
+ return this .refreshTrack (existingStream , existingTracks , newAudioTrack );
269
+ },
270
+ refreshVideoTrack(
271
+ existingStream : MediaStream ,
272
+ newVideoTrack : MediaStreamTrack | null
273
+ ): boolean {
274
+ // If there is no new track, just early out
275
+ // and keep the old track on the stream as-is.
276
+ if (! newVideoTrack ) return false ;
277
+ const existingTracks = existingStream .getVideoTracks ();
278
+ return this .refreshTrack (existingStream , existingTracks , newVideoTrack );
279
+ },
280
+ refreshTrack(
281
+ existingStream : MediaStream ,
282
+ oldTracks : MediaStreamTrack [],
283
+ newTrack : MediaStreamTrack
284
+ ): boolean {
285
+ const trackCount = oldTracks .length ;
286
+ // If there is no matching old track,
287
+ // just add the new track.
288
+ if (trackCount === 0 ) {
289
+ existingStream .addTrack (newTrack );
290
+ return true ;
291
+ }
292
+ if (trackCount > 1 ) {
293
+ console .warn (
294
+ ` expected up to 1 media track, but got ${trackCount }. Only using the first one. `
295
+ );
296
+ }
297
+ const oldTrack = oldTracks [0 ];
298
+ // If the IDs of the old and new track don't match,
299
+ // replace the old track with the new one.
300
+ if (oldTrack .id !== newTrack .id ) {
301
+ existingStream .removeTrack (oldTrack );
302
+ existingStream .addTrack (newTrack );
303
+ return true ;
304
+ }
305
+ return false ;
306
+ },
307
+ playMedia(video : HTMLVideoElement ) {
308
+ const isPlaying =
309
+ ! video .paused &&
310
+ ! video .ended &&
311
+ video .currentTime > 0 &&
312
+ video .readyState > video .HAVE_CURRENT_DATA ;
313
+
314
+ if (isPlaying ) return ;
315
+
316
+ video .play ().catch ((e ) => {
317
+ if (e instanceof Error && e .name === " NotAllowedError" ) {
318
+ throw new Error (" Autoplay error" );
319
+ }
320
+
321
+ console .warn (" Failed to play media." , e );
322
+ });
323
+ },
324
+ getParticipantTracks(p : DailyParticipant ) {
325
+ const mediaTracks: Tracks = {
326
+ videoTrack: null ,
327
+ audioTrack: null ,
328
+ };
329
+
330
+ const tracks = p ?.tracks ;
331
+ if (! tracks ) return mediaTracks ;
332
+
333
+ const vt = tracks .video ;
334
+ const vs = vt ?.state ;
335
+ if (vt .persistentTrack && ! (vs === " off" || vs === " blocked" )) {
336
+ mediaTracks .videoTrack = vt .persistentTrack ;
337
+ }
338
+
339
+ // Only get audio track if this is a remote participant
340
+ if (! p .local ) {
341
+ const at = tracks .audio ;
342
+ const as = at ?.state ;
343
+ if (at .persistentTrack && ! (as === " off" || as === " blocked" )) {
344
+ mediaTracks .audioTrack = at .persistentTrack ;
345
+ }
346
+ }
347
+ return mediaTracks ;
348
+ },
126
349
/**
127
350
* This is called any time a participant update registers.
128
351
* In large calls, this should be optimized to avoid re-renders.
@@ -163,13 +386,13 @@ export default {
163
386
},
164
387
// Toggle local microphone in use (on/off)
165
388
handleAudioClick() {
166
- const audioOn = this .callObject .localAudio ();
167
- this .callObject .setLocalAudio (! audioOn);
389
+ const audioOn = this .callObject ? .localAudio ();
390
+ this .callObject ? .setLocalAudio (! audioOn );
168
391
},
169
392
// Toggle local camera in use (on/off)
170
393
handleVideoClick() {
171
- const videoOn = this .callObject .localVideo ();
172
- this .callObject .setLocalVideo (! videoOn);
394
+ const videoOn = this .callObject ? .localVideo ();
395
+ this .callObject ? .setLocalVideo (! videoOn );
173
396
},
174
397
// Show permissions error in UI to alert local participant
175
398
handleDeviceError() {
@@ -178,10 +401,10 @@ export default {
178
401
// Toggle screen share
179
402
handleScreenshareClick() {
180
403
if (this .screen ?.local ) {
181
- this .callObject .stopScreenShare ();
404
+ this .callObject ? .stopScreenShare ();
182
405
this .screen = null ;
183
406
} else {
184
- this .callObject .startScreenShare ();
407
+ this .callObject ? .startScreenShare ();
185
408
}
186
409
},
187
410
/**
@@ -192,7 +415,7 @@ export default {
192
415
*/
193
416
sendMessage(text ) {
194
417
// Attach the local participant's username to the message to be displayed in ChatTile.vue
195
- const local = this .callObject .participants ().local ;
418
+ const local = this .callObject ? .participants ().local ;
196
419
const message = { message: text , name: local ?.user_name || " Guest" };
197
420
this .messages .push (message );
198
421
this .callObject .sendAppMessage (message , " *" );
@@ -211,7 +434,7 @@ export default {
211
434
});
212
435
},
213
436
},
214
- };
437
+ }) ;
215
438
</script >
216
439
217
440
<style scoped>
0 commit comments