1/* eslint-env browser */ 2/** 3 * A web-only module for ponyfilling the UserMedia API. 4 */ 5import { Platform } from 'expo-modules-core'; 6 7export const userMediaRequested: boolean = false; 8 9export const mountedInstances: any[] = []; 10 11async function requestLegacyUserMediaAsync(props): Promise<any[]> { 12 const optionalSource = (id) => ({ optional: [{ sourceId: id }] }); 13 14 const constraintToSourceId = (constraint) => { 15 const { deviceId } = constraint; 16 17 if (typeof deviceId === 'string') { 18 return deviceId; 19 } 20 21 if (Array.isArray(deviceId) && deviceId.length > 0) { 22 return deviceId[0]; 23 } 24 25 if (typeof deviceId === 'object' && deviceId.ideal) { 26 return deviceId.ideal; 27 } 28 29 return null; 30 }; 31 32 const sources: any[] = await new Promise((resolve) => 33 // @ts-ignore: https://caniuse.com/#search=getSources Chrome for Android (78) & Samsung Internet (10.1) use this 34 MediaStreamTrack.getSources((sources) => resolve(sources)) 35 ); 36 37 let audioSource = null; 38 let videoSource = null; 39 40 sources.forEach((source) => { 41 if (source.kind === 'audio') { 42 audioSource = source.id; 43 } else if (source.kind === 'video') { 44 videoSource = source.id; 45 } 46 }); 47 48 const audioSourceId = constraintToSourceId(props.audioConstraints); 49 if (audioSourceId) { 50 audioSource = audioSourceId; 51 } 52 53 const videoSourceId = constraintToSourceId(props.videoConstraints); 54 if (videoSourceId) { 55 videoSource = videoSourceId; 56 } 57 58 return [optionalSource(audioSource), optionalSource(videoSource)]; 59} 60 61async function sourceSelectedAsync( 62 isMuted: boolean, 63 audioConstraints?: MediaTrackConstraints | boolean, 64 videoConstraints?: MediaTrackConstraints | boolean 65): Promise<MediaStream> { 66 const constraints: MediaStreamConstraints = { 67 video: typeof videoConstraints !== 'undefined' ? videoConstraints : true, 68 }; 69 70 if (!isMuted) { 71 constraints.audio = typeof audioConstraints !== 'undefined' ? audioConstraints : true; 72 } 73 74 return await getAnyUserMediaAsync(constraints); 75} 76 77export async function requestUserMediaAsync( 78 props: { audio?: any; video?: any }, 79 isMuted: boolean = true 80): Promise<MediaStream> { 81 if (canGetUserMedia()) { 82 return await sourceSelectedAsync(isMuted, props.audio, props.video); 83 } 84 const [audio, video] = await requestLegacyUserMediaAsync(props); 85 return await sourceSelectedAsync(isMuted, audio, video); 86} 87 88export async function getAnyUserMediaAsync( 89 constraints: MediaStreamConstraints, 90 ignoreConstraints: boolean = false 91): Promise<MediaStream> { 92 try { 93 return await getUserMediaAsync({ 94 ...constraints, 95 video: ignoreConstraints || constraints.video, 96 }); 97 } catch (error) { 98 if (!ignoreConstraints && error.name === 'ConstraintNotSatisfiedError') { 99 return await getAnyUserMediaAsync(constraints, true); 100 } 101 throw error; 102 } 103} 104 105export async function getUserMediaAsync(constraints: MediaStreamConstraints): Promise<MediaStream> { 106 if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 107 return navigator.mediaDevices.getUserMedia(constraints); 108 } 109 110 const _getUserMedia = 111 navigator['mozGetUserMedia'] || navigator['webkitGetUserMedia'] || navigator['msGetUserMedia']; 112 return new Promise((resolve, reject) => 113 _getUserMedia.call(navigator, constraints, resolve, reject) 114 ); 115} 116 117export function canGetUserMedia(): boolean { 118 return ( 119 // SSR 120 Platform.isDOMAvailable && 121 // Has any form of media API 122 !!( 123 (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) || 124 navigator['mozGetUserMedia'] || 125 navigator['webkitGetUserMedia'] || 126 navigator['msGetUserMedia'] 127 ) 128 ); 129} 130 131export async function isFrontCameraAvailableAsync( 132 devices?: MediaDeviceInfo[] 133): Promise<null | string> { 134 return await supportsCameraType(['front', 'user', 'facetime'], 'user', devices); 135} 136 137export async function isBackCameraAvailableAsync( 138 devices?: MediaDeviceInfo[] 139): Promise<null | string> { 140 return await supportsCameraType(['back', 'rear'], 'environment', devices); 141} 142 143async function supportsCameraType( 144 labels: string[], 145 type: string, 146 devices?: MediaDeviceInfo[] 147): Promise<null | string> { 148 if (!devices) { 149 if (!navigator.mediaDevices.enumerateDevices) { 150 return null; 151 } 152 devices = await navigator.mediaDevices.enumerateDevices(); 153 } 154 const cameras = devices.filter((t) => t.kind === 'videoinput'); 155 const [hasCamera] = cameras.filter((camera) => 156 labels.some((label) => camera.label.toLowerCase().includes(label)) 157 ); 158 const [isCapable] = cameras.filter((camera) => { 159 if (!('getCapabilities' in camera)) { 160 return null; 161 } 162 163 const capabilities = (camera as any).getCapabilities(); 164 if (!capabilities.facingMode) { 165 return null; 166 } 167 168 return capabilities.facingMode.find((_: string) => type); 169 }); 170 171 return isCapable?.deviceId || hasCamera?.deviceId || null; 172} 173