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