1 | import { BrowserCompatibility } from "./browserCompatibility";
|
2 | import { BrowserHelper } from "./browserHelper";
|
3 | import { Camera } from "./camera";
|
4 | import { UnsupportedBrowserError } from "./unsupportedBrowserError";
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | export namespace CameraAccess {
|
10 | |
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | const backCameraKeywords: string[] = [
|
20 | "rear",
|
21 | "back",
|
22 | "rück",
|
23 | "arrière",
|
24 | "trasera",
|
25 | "trás",
|
26 | "traseira",
|
27 | "posteriore",
|
28 | "后面",
|
29 | "後面",
|
30 | "背面",
|
31 | "后置",
|
32 | "後置",
|
33 | "背置",
|
34 | "задней",
|
35 | "الخلفية",
|
36 | "후",
|
37 | "arka",
|
38 | "achterzijde",
|
39 | "หลัง",
|
40 | "baksidan",
|
41 | "bagside",
|
42 | "sau",
|
43 | "bak",
|
44 | "tylny",
|
45 | "takakamera",
|
46 | "belakang",
|
47 | "אחורית",
|
48 | "πίσω",
|
49 | "spate",
|
50 | "hátsó",
|
51 | "zadní",
|
52 | "darrere",
|
53 | "zadná",
|
54 | "задня",
|
55 | "stražnja",
|
56 | "belakang",
|
57 | "बैक"
|
58 | ];
|
59 |
|
60 | |
61 |
|
62 |
|
63 | const cameraObjects: Map<string, Camera> = new Map<string, Camera>();
|
64 |
|
65 | |
66 |
|
67 |
|
68 | let getCamerasPromise: Promise<Camera[]> | undefined;
|
69 |
|
70 | |
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | function isBackCameraLabel(label: string): boolean {
|
77 | const lowercaseLabel: string = label.toLowerCase();
|
78 |
|
79 | return backCameraKeywords.some(keyword => {
|
80 | return lowercaseLabel.includes(keyword);
|
81 | });
|
82 | }
|
83 |
|
84 | |
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | export function adjustCamerasFromMainCameraStream(
|
98 | mediaStreamTrack: MediaStreamTrack,
|
99 | cameras: Camera[]
|
100 | ): Camera | undefined {
|
101 | let mediaTrackSettings: MediaTrackSettings | undefined;
|
102 | if (typeof mediaStreamTrack.getSettings === "function") {
|
103 | mediaTrackSettings = mediaStreamTrack.getSettings();
|
104 | }
|
105 | const activeCamera: Camera | undefined = cameras.find(camera => {
|
106 | return (
|
107 | (mediaTrackSettings != null && camera.deviceId === mediaTrackSettings.deviceId) ||
|
108 | camera.label === mediaStreamTrack.label
|
109 | );
|
110 | });
|
111 | if (activeCamera !== undefined) {
|
112 | const activeCameraIsBackFacing: boolean =
|
113 | (mediaTrackSettings != null && mediaTrackSettings.facingMode === "environment") ||
|
114 | isBackCameraLabel(mediaStreamTrack.label);
|
115 | let activeCameraIsMainBackCamera: boolean = activeCameraIsBackFacing;
|
116 |
|
117 | if (activeCameraIsBackFacing && cameras.length > 1) {
|
118 |
|
119 | cameras.forEach(camera => {
|
120 | if (camera.deviceId === activeCamera.deviceId) {
|
121 |
|
122 | (<any>camera).cameraType = Camera.Type.BACK;
|
123 | } else if (!isBackCameraLabel(camera.label)) {
|
124 |
|
125 | (<any>camera).cameraType = Camera.Type.FRONT;
|
126 | }
|
127 | });
|
128 |
|
129 | const mainBackCamera: Camera = cameras
|
130 | .filter(camera => {
|
131 | return camera.cameraType === Camera.Type.BACK;
|
132 | })
|
133 | .sort((camera1, camera2) => {
|
134 | return camera1.label.localeCompare(camera2.label);
|
135 | })[0];
|
136 | activeCameraIsMainBackCamera = activeCamera.deviceId === mainBackCamera.deviceId;
|
137 | }
|
138 |
|
139 | if (cameras.length === 1 || activeCameraIsMainBackCamera) {
|
140 | return activeCamera;
|
141 | }
|
142 | }
|
143 |
|
144 | return undefined;
|
145 | }
|
146 |
|
147 | |
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | function extractCamerasFromDevices(devices: MediaDeviceInfo[]): Camera[] {
|
154 | const cameras: Camera[] = devices
|
155 | .filter(device => {
|
156 | return device.kind === "videoinput";
|
157 | })
|
158 | .map(videoDevice => {
|
159 | if (cameraObjects.has(videoDevice.deviceId)) {
|
160 | return <Camera>cameraObjects.get(videoDevice.deviceId);
|
161 | }
|
162 |
|
163 | const label: string = videoDevice.label != null ? videoDevice.label : "";
|
164 | const camera: Camera = {
|
165 | deviceId: videoDevice.deviceId,
|
166 | label,
|
167 | cameraType: isBackCameraLabel(label) ? Camera.Type.BACK : Camera.Type.FRONT
|
168 | };
|
169 |
|
170 | if (label !== "") {
|
171 | cameraObjects.set(videoDevice.deviceId, camera);
|
172 | }
|
173 |
|
174 | return camera;
|
175 | });
|
176 | if (
|
177 | cameras.length > 1 &&
|
178 | !cameras.some(camera => {
|
179 | return camera.cameraType === Camera.Type.BACK;
|
180 | })
|
181 | ) {
|
182 |
|
183 |
|
184 | let backCameraIndex: number = cameras.length - 1;
|
185 |
|
186 | const cameraResolutions: number[] = cameras.map(camera => {
|
187 | const match: RegExpMatchArray | null = camera.label.match(/\b([0-9]+)MP?\b/i);
|
188 | if (match != null) {
|
189 | return parseInt(match[1], 10);
|
190 | }
|
191 |
|
192 | return NaN;
|
193 | });
|
194 | if (
|
195 | !cameraResolutions.some(cameraResolution => {
|
196 | return isNaN(cameraResolution);
|
197 | })
|
198 | ) {
|
199 | backCameraIndex = cameraResolutions.lastIndexOf(Math.max(...cameraResolutions));
|
200 | }
|
201 |
|
202 |
|
203 | (<any>cameras[backCameraIndex]).cameraType = Camera.Type.BACK;
|
204 | }
|
205 |
|
206 | return cameras;
|
207 | }
|
208 |
|
209 | |
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 | export function getCameras(): Promise<Camera[]> {
|
226 | if (getCamerasPromise != null) {
|
227 | return getCamerasPromise;
|
228 | }
|
229 |
|
230 | const browserCompatibility: BrowserCompatibility = BrowserHelper.checkBrowserCompatibility();
|
231 | if (!browserCompatibility.fullSupport) {
|
232 | return Promise.reject(new UnsupportedBrowserError(browserCompatibility));
|
233 | }
|
234 |
|
235 | const accessPermissionPromise: Promise<void | MediaStream> = new Promise((resolve, reject) => {
|
236 | return enumerateDevices()
|
237 | .then(devices => {
|
238 | if (
|
239 | devices
|
240 | .filter(device => {
|
241 | return device.kind === "videoinput";
|
242 | })
|
243 | .every(device => {
|
244 | return device.label === "";
|
245 | })
|
246 | ) {
|
247 | resolve(
|
248 | navigator.mediaDevices.getUserMedia({
|
249 | video: true,
|
250 | audio: false
|
251 | })
|
252 | );
|
253 | } else {
|
254 | resolve();
|
255 | }
|
256 | })
|
257 | .catch(reject);
|
258 | });
|
259 |
|
260 | getCamerasPromise = new Promise(async (resolve, reject) => {
|
261 | let stream!: void | MediaStream;
|
262 | try {
|
263 | stream = await accessPermissionPromise;
|
264 | const devices: MediaDeviceInfo[] = await enumerateDevices();
|
265 | const cameras: Camera[] = extractCamerasFromDevices(devices);
|
266 |
|
267 | console.debug("Camera list: ", ...cameras);
|
268 |
|
269 | return resolve(cameras);
|
270 | } catch (error) {
|
271 |
|
272 | if (error.name === "SourceUnavailableError") {
|
273 | error.name = "NotReadableError";
|
274 | }
|
275 |
|
276 | return reject(error);
|
277 | } finally {
|
278 |
|
279 | if (stream != null) {
|
280 | stream.getVideoTracks().forEach(track => {
|
281 | track.stop();
|
282 | });
|
283 | }
|
284 | getCamerasPromise = undefined;
|
285 | }
|
286 | });
|
287 |
|
288 | return getCamerasPromise;
|
289 | }
|
290 |
|
291 | |
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 | function getUserMediaDelayed(getUserMediaParams: MediaStreamConstraints): Promise<MediaStream> {
|
300 | console.debug("Camera access:", getUserMediaParams.video);
|
301 |
|
302 | return new Promise((resolve, reject) => {
|
303 | window.setTimeout(() => {
|
304 | navigator.mediaDevices
|
305 | .getUserMedia(getUserMediaParams)
|
306 | .then(resolve)
|
307 | .catch(reject);
|
308 | }, 0);
|
309 | });
|
310 | }
|
311 |
|
312 | |
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 | function getUserMediaVideoParams(resolutionFallbackLevel: number, isSafariBrowser: boolean): MediaTrackConstraints {
|
323 | switch (resolutionFallbackLevel) {
|
324 | case 0:
|
325 | if (isSafariBrowser) {
|
326 | return {
|
327 | width: { min: 1400, ideal: 1920, max: 1920 },
|
328 | height: { min: 900, ideal: 1080, max: 1440 }
|
329 | };
|
330 | } else {
|
331 | return {
|
332 | width: { min: 1400, ideal: 1920, max: 1920 },
|
333 | height: { min: 900, ideal: 1440, max: 1440 }
|
334 | };
|
335 | }
|
336 | case 1:
|
337 | if (isSafariBrowser) {
|
338 | return {
|
339 | width: { min: 1200, ideal: 1600, max: 1920 },
|
340 | height: { min: 900, ideal: 1080, max: 1200 }
|
341 | };
|
342 | } else {
|
343 | return {
|
344 | width: { min: 1200, ideal: 1920, max: 1920 },
|
345 | height: { min: 900, ideal: 1200, max: 1200 }
|
346 | };
|
347 | }
|
348 | case 2:
|
349 | if (isSafariBrowser) {
|
350 | return {
|
351 | width: { min: 1080, ideal: 1600, max: 1920 },
|
352 | height: { min: 900, ideal: 900, max: 1080 }
|
353 | };
|
354 | } else {
|
355 | return {
|
356 | width: { min: 1080, ideal: 1920, max: 1920 },
|
357 | height: { min: 900, ideal: 1080, max: 1080 }
|
358 | };
|
359 | }
|
360 | case 3:
|
361 | if (isSafariBrowser) {
|
362 | return {
|
363 | width: { min: 960, ideal: 1280, max: 1440 },
|
364 | height: { min: 480, ideal: 720, max: 960 }
|
365 | };
|
366 | } else {
|
367 | return {
|
368 | width: { min: 960, ideal: 1280, max: 1440 },
|
369 | height: { min: 480, ideal: 960, max: 960 }
|
370 | };
|
371 | }
|
372 | case 4:
|
373 | if (isSafariBrowser) {
|
374 | return {
|
375 | width: { min: 720, ideal: 1024, max: 1440 },
|
376 | height: { min: 480, ideal: 768, max: 768 }
|
377 | };
|
378 | } else {
|
379 | return {
|
380 | width: { min: 720, ideal: 1280, max: 1440 },
|
381 | height: { min: 480, ideal: 720, max: 768 }
|
382 | };
|
383 | }
|
384 | case 5:
|
385 | if (isSafariBrowser) {
|
386 | return {
|
387 | width: { min: 640, ideal: 800, max: 1440 },
|
388 | height: { min: 480, ideal: 600, max: 720 }
|
389 | };
|
390 | } else {
|
391 | return {
|
392 | width: { min: 640, ideal: 960, max: 1440 },
|
393 | height: { min: 480, ideal: 720, max: 720 }
|
394 | };
|
395 | }
|
396 | default:
|
397 | return {};
|
398 | }
|
399 | }
|
400 |
|
401 | |
402 |
|
403 |
|
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 | export function accessCameraStream(resolutionFallbackLevel: number, camera: Camera): Promise<MediaStream> {
|
412 | const browserName: string | undefined = BrowserHelper.userAgentInfo.getBrowser().name;
|
413 | const getUserMediaParams: MediaStreamConstraints = {
|
414 | audio: false,
|
415 | video: getUserMediaVideoParams(resolutionFallbackLevel, browserName != null && browserName.includes("Safari"))
|
416 | };
|
417 |
|
418 | if (camera.deviceId === "") {
|
419 | (<MediaTrackConstraints>getUserMediaParams.video).facingMode = {
|
420 | ideal: camera.cameraType === Camera.Type.BACK ? "environment" : "user"
|
421 | };
|
422 | } else {
|
423 | (<MediaTrackConstraints>getUserMediaParams.video).deviceId = {
|
424 | exact: camera.deviceId
|
425 | };
|
426 | }
|
427 |
|
428 | return getUserMediaDelayed(getUserMediaParams);
|
429 | }
|
430 |
|
431 | |
432 |
|
433 |
|
434 |
|
435 |
|
436 |
|
437 |
|
438 | function enumerateDevices(): Promise<MediaDeviceInfo[]> {
|
439 | if (typeof navigator.enumerateDevices === "function") {
|
440 | return navigator.enumerateDevices();
|
441 | } else if (
|
442 | typeof navigator.mediaDevices === "object" &&
|
443 | typeof navigator.mediaDevices.enumerateDevices === "function"
|
444 | ) {
|
445 | return navigator.mediaDevices.enumerateDevices();
|
446 | } else {
|
447 | return new Promise((resolve, reject) => {
|
448 | try {
|
449 | if (window.MediaStreamTrack == null || window.MediaStreamTrack.getSources == null) {
|
450 | throw new Error();
|
451 | }
|
452 | window.MediaStreamTrack.getSources((devices: MediaDeviceInfo[]) => {
|
453 | resolve(
|
454 | devices
|
455 | .filter(device => {
|
456 | return device.kind.toLowerCase() === "video" || device.kind.toLowerCase() === "videoinput";
|
457 | })
|
458 | .map(device => {
|
459 | return {
|
460 | deviceId: device.deviceId != null ? device.deviceId : "",
|
461 | groupId: device.groupId,
|
462 | kind: <MediaDeviceKind>"videoinput",
|
463 | label: device.label,
|
464 | toJSON: function(): MediaDeviceInfo {
|
465 | return this;
|
466 | }
|
467 | };
|
468 | })
|
469 | );
|
470 | });
|
471 | } catch {
|
472 | const browserCompatibility: BrowserCompatibility = {
|
473 | fullSupport: false,
|
474 | scannerSupport: true,
|
475 | missingFeatures: [BrowserCompatibility.Feature.MEDIA_DEVICES]
|
476 | };
|
477 |
|
478 | return reject(new UnsupportedBrowserError(browserCompatibility));
|
479 | }
|
480 | });
|
481 | }
|
482 | }
|
483 | }
|