UNPKG

livekit-client

Version:

JavaScript/TypeScript client SDK for LiveKit

228 lines (210 loc) 8.1 kB
import DeviceManager from '../DeviceManager'; import { audioDefaults, videoDefaults } from '../defaults'; import { DeviceUnsupportedError, TrackInvalidError } from '../errors'; import { mediaTrackToLocalTrack } from '../participant/publishUtils'; import type { LoggerOptions } from '../types'; import { isAudioTrack, isSafari17Based, isVideoTrack, unwrapConstraint } from '../utils'; import LocalAudioTrack from './LocalAudioTrack'; import type LocalTrack from './LocalTrack'; import LocalVideoTrack from './LocalVideoTrack'; import { Track } from './Track'; import type { AudioCaptureOptions, CreateLocalTracksOptions, ScreenShareCaptureOptions, VideoCaptureOptions, } from './options'; import { ScreenSharePresets } from './options'; import { constraintsForOptions, extractProcessorsFromOptions, mergeDefaultOptions, screenCaptureToDisplayMediaStreamOptions, } from './utils'; /** * Creates a local video and audio track at the same time. When acquiring both * audio and video tracks together, it'll display a single permission prompt to * the user instead of two separate ones. * @param options */ export async function createLocalTracks( options?: CreateLocalTracksOptions, loggerOptions?: LoggerOptions, ): Promise<Array<LocalTrack>> { options ??= {}; let attemptExactMatch = false; const { audioProcessor, videoProcessor, optionsWithoutProcessor: internalOptions, } = extractProcessorsFromOptions(options); let retryAudioOptions: AudioCaptureOptions | undefined | boolean = internalOptions.audio; let retryVideoOptions: VideoCaptureOptions | undefined | boolean = internalOptions.video; if (audioProcessor && typeof internalOptions.audio === 'object') { internalOptions.audio.processor = audioProcessor; } if (videoProcessor && typeof internalOptions.video === 'object') { internalOptions.video.processor = videoProcessor; } // if the user passes a device id as a string, we default to exact match if ( options.audio && typeof internalOptions.audio === 'object' && typeof internalOptions.audio.deviceId === 'string' ) { const deviceId: string = internalOptions.audio.deviceId; internalOptions.audio.deviceId = { exact: deviceId }; attemptExactMatch = true; retryAudioOptions = { ...internalOptions.audio, deviceId: { ideal: deviceId }, }; } if ( internalOptions.video && typeof internalOptions.video === 'object' && typeof internalOptions.video.deviceId === 'string' ) { const deviceId: string = internalOptions.video.deviceId; internalOptions.video.deviceId = { exact: deviceId }; attemptExactMatch = true; retryVideoOptions = { ...internalOptions.video, deviceId: { ideal: deviceId }, }; } if (internalOptions.audio === true) { internalOptions.audio = { deviceId: 'default' }; } else if (typeof internalOptions.audio === 'object' && internalOptions.audio !== null) { internalOptions.audio = { ...internalOptions.audio, deviceId: internalOptions.audio.deviceId || 'default', }; } if (internalOptions.video === true) { internalOptions.video = { deviceId: 'default' }; } else if (typeof internalOptions.video === 'object' && !internalOptions.video.deviceId) { internalOptions.video.deviceId = 'default'; } const opts = mergeDefaultOptions(internalOptions, audioDefaults, videoDefaults); const constraints = constraintsForOptions(opts); // Keep a reference to the promise on DeviceManager and await it in getLocalDevices() // works around iOS Safari Bug https://bugs.webkit.org/show_bug.cgi?id=179363 const mediaPromise = navigator.mediaDevices.getUserMedia(constraints); if (internalOptions.audio) { DeviceManager.userMediaPromiseMap.set('audioinput', mediaPromise); mediaPromise.catch(() => DeviceManager.userMediaPromiseMap.delete('audioinput')); } if (internalOptions.video) { DeviceManager.userMediaPromiseMap.set('videoinput', mediaPromise); mediaPromise.catch(() => DeviceManager.userMediaPromiseMap.delete('videoinput')); } try { const stream = await mediaPromise; return await Promise.all( stream.getTracks().map(async (mediaStreamTrack) => { const isAudio = mediaStreamTrack.kind === 'audio'; let trackOptions = isAudio ? opts!.audio : opts!.video; if (typeof trackOptions === 'boolean' || !trackOptions) { trackOptions = {}; } let trackConstraints: MediaTrackConstraints | undefined; const conOrBool = isAudio ? constraints.audio : constraints.video; if (typeof conOrBool !== 'boolean') { trackConstraints = conOrBool; } // update the constraints with the device id the user gave permissions to in the permission prompt // otherwise each track restart (e.g. mute - unmute) will try to initialize the device again -> causing additional permission prompts const newDeviceId = mediaStreamTrack.getSettings().deviceId; if ( trackConstraints?.deviceId && unwrapConstraint(trackConstraints.deviceId) !== newDeviceId ) { trackConstraints.deviceId = newDeviceId; } else if (!trackConstraints) { trackConstraints = { deviceId: newDeviceId }; } const track = mediaTrackToLocalTrack(mediaStreamTrack, trackConstraints, loggerOptions); if (track.kind === Track.Kind.Video) { track.source = Track.Source.Camera; } else if (track.kind === Track.Kind.Audio) { track.source = Track.Source.Microphone; } track.mediaStream = stream; if (isAudioTrack(track) && audioProcessor) { await track.setProcessor(audioProcessor); } else if (isVideoTrack(track) && videoProcessor) { await track.setProcessor(videoProcessor); } return track; }), ); } catch (e) { if (!attemptExactMatch) { throw e; } return createLocalTracks( { ...options, audio: retryAudioOptions, video: retryVideoOptions, }, loggerOptions, ); } } /** * Creates a [[LocalVideoTrack]] with getUserMedia() * @param options */ export async function createLocalVideoTrack( options?: VideoCaptureOptions, ): Promise<LocalVideoTrack> { const tracks = await createLocalTracks({ audio: false, video: options ?? true, }); return <LocalVideoTrack>tracks[0]; } export async function createLocalAudioTrack( options?: AudioCaptureOptions, ): Promise<LocalAudioTrack> { const tracks = await createLocalTracks({ audio: options ?? true, video: false, }); return <LocalAudioTrack>tracks[0]; } /** * Creates a screen capture tracks with getDisplayMedia(). * A LocalVideoTrack is always created and returned. * If { audio: true }, and the browser supports audio capture, a LocalAudioTrack is also created. */ export async function createLocalScreenTracks( options?: ScreenShareCaptureOptions, ): Promise<Array<LocalTrack>> { if (options === undefined) { options = {}; } if (options.resolution === undefined && !isSafari17Based()) { options.resolution = ScreenSharePresets.h1080fps30.resolution; } if (navigator.mediaDevices.getDisplayMedia === undefined) { throw new DeviceUnsupportedError('getDisplayMedia not supported'); } const constraints = screenCaptureToDisplayMediaStreamOptions(options); const stream: MediaStream = await navigator.mediaDevices.getDisplayMedia(constraints); const tracks = stream.getVideoTracks(); if (tracks.length === 0) { throw new TrackInvalidError('no video track found'); } const screenVideo = new LocalVideoTrack(tracks[0], undefined, false); screenVideo.source = Track.Source.ScreenShare; const localTracks: Array<LocalTrack> = [screenVideo]; if (stream.getAudioTracks().length > 0) { const screenAudio = new LocalAudioTrack(stream.getAudioTracks()[0], undefined, false); screenAudio.source = Track.Source.ScreenShareAudio; localTracks.push(screenAudio); } return localTracks; }