Send and receive additional MediaStreamTracks beyond camera and microphone — including canvas-based video, pre-processed audio, and multiple simultaneous media streams.
Custom tracks let you send any MediaStreamTrack as an additional stream alongside your camera and microphone. Common use cases include:
Streaming additional camera angles (e.g., a document camera)
startCustomTrack() takes a StartCustomTrackOptions object and returns a promise that resolves to the track name — which is used to identify the track in the participant’s track list and used for stopping it later.
const trackName = await call.startCustomTrack({ track: myMediaStreamTrack, // MediaStreamTrack — required trackName: 'doc-cam', // optional; auto-generated if omitted mode: 'speech', // optional audio mode hint ignoreAudioLevel: false, // optional});console.log('Custom track started with name:', trackName);
A name for the track. Must be unique within the call. If omitted, Daily
generates a UUID. The resolved promise value is the final track name.
Track names become part of the participant’s tracks object for the lifetime
of the call. Choose descriptive, stable names — especially if remote
participants need to subscribe to them by name.
For audio tracks: sets the encoding mode. Use 'music' for music or
wideband audio, 'speech' for voice. For fine-grained control, pass a
DailyMicAudioModeSettings object.
When true, this track’s audio level is excluded from the local audio
level observer. Useful for background music that should not trigger
speaking indicators.
Passing the track name used in or returned by startCustomTrack() stops sending that track. The underlying MediaStreamTrack is not automatically stopped — call .stop() on it yourself if needed.
A common pattern is to draw content onto an HTML <canvas> element and stream it as a custom track:
async function startCanvasTrack(call) { const canvas = document.createElement('canvas'); canvas.width = 1280; canvas.height = 720; const ctx = canvas.getContext('2d'); // Use a Web Worker for the draw loop. Both requestAnimationFrame and // main-thread setInterval are throttled (or paused) when the sender's tab is // in the background, causing remote participants to see frozen or choppy // video. Web Workers run off the main thread and are not subject to the same // throttling. let frame = 0; const tickWorker = new Worker( URL.createObjectURL( new Blob(['setInterval(() => postMessage("tick"), 1000 / 30)'], { type: 'application/javascript' }) ) ); tickWorker.onmessage = () => { ctx.fillStyle = '#1a1a2e'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.font = '48px monospace'; ctx.fillStyle = '#e0e0e0'; ctx.fillText(`Frame: ${frame++}`, 50, canvas.height / 2); }; // Capture a 30 fps stream from the canvas const stream = canvas.captureStream(30); const [videoTrack] = stream.getVideoTracks(); const trackName = await call.startCustomTrack({ track: videoTrack, trackName: 'canvas-viz', }); return { trackName, stop: () => { tickWorker.terminate(); call.stopCustomTrack(trackName); }, };}
When subscribeToTracksAutomatically is enabled (the default), remote custom tracks are subscribed to automatically — no extra setup needed. Listen for track-started with the custom track name as type to render them.If you’ve disabled automatic subscriptions, use updateParticipant() with setSubscribedTracks to opt in. See the startCustomTrack() reference for the full custom subscription options.
Custom track events arrive on the standard track-started / track-stopped events. The type field holds the custom track name:
call.on('track-started', ({ participant, track, type }) => { // type is 'video' | 'audio' | 'screenVideo' | 'screenAudio' | <custom-name> if (type === 'canvas-viz') { console.log( `Custom track '${type}' started from`, participant?.user_name ); const video = document.createElement('video'); video.srcObject = new MediaStream([track]); video.autoplay = true; document.getElementById('custom-tracks').appendChild(video); }});call.on('track-stopped', ({ participant, type }) => { if (type === 'canvas-viz') { console.log(`Custom track '${type}' stopped.`); // Clean up your video element here }});