|
@@ -21,11 +21,10 @@
|
21
|
21
|
*/
|
22
|
22
|
|
23
|
23
|
import Speedy from 'speedy-vision';
|
24
|
|
-import { SpeedyMedia } from 'speedy-vision/types/core/speedy-media';
|
25
|
24
|
import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
|
26
|
25
|
import { Utils } from '../utils/utils';
|
27
|
26
|
import { Resolution } from '../utils/resolution';
|
28
|
|
-import { NotSupportedError, AccessDeniedError, IllegalOperationError } from '../utils/errors';
|
|
27
|
+import { NotSupportedError, AccessDeniedError, IllegalOperationError, IllegalArgumentError } from '../utils/errors';
|
29
|
28
|
import { VideoSource } from './video-source';
|
30
|
29
|
|
31
|
30
|
|
|
@@ -46,7 +45,14 @@ export interface CameraSourceOptions
|
46
|
45
|
|
47
|
46
|
/** Default options for camera sources */
|
48
|
47
|
const DEFAULT_CAMERA_OPTIONS: Readonly<Required<CameraSourceOptions>> = {
|
49
|
|
- resolution: 'md',
|
|
48
|
+ /*
|
|
49
|
+
|
|
50
|
+ we use well-known standards in landscape mode to ensure broad compatibility
|
|
51
|
+ the spec encourages User Agents to make landscape the primary orientation
|
|
52
|
+ https://w3c.github.io/mediacapture-main/#dfn-primary-orientation
|
|
53
|
+
|
|
54
|
+ */
|
|
55
|
+ resolution: '360p',
|
50
|
56
|
aspectRatio: 16/9,
|
51
|
57
|
constraints: { facingMode: 'environment' },
|
52
|
58
|
};
|
|
@@ -90,28 +96,41 @@ export class CameraSource extends VideoSource
|
90
|
96
|
*/
|
91
|
97
|
_init(): SpeedyPromise<void>
|
92
|
98
|
{
|
|
99
|
+ const options = this._options;
|
|
100
|
+
|
93
|
101
|
Utils.log('Accessing the webcam...');
|
94
|
102
|
|
95
|
103
|
// validate
|
96
|
104
|
if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia)
|
97
|
105
|
throw new NotSupportedError('Unsupported browser: no navigator.mediaDevices.getUserMedia()');
|
98
|
106
|
|
|
107
|
+ // for best compatibility, always request landscape resolutions,
|
|
108
|
+ // even when mobile devices are expected to be in portrait mode
|
|
109
|
+ if(options.aspectRatio < 1) {
|
|
110
|
+ if(options.aspectRatio > 0)
|
|
111
|
+ Utils.warning(`CameraSource: an aspectRatio of ${options.aspectRatio} was requested. Prefer standard landscape settings instead`);
|
|
112
|
+ else
|
|
113
|
+ throw new IllegalArgumentError(`Invalid aspect ratio: ${options.aspectRatio}`);
|
|
114
|
+ }
|
|
115
|
+
|
99
|
116
|
// set up media constraints
|
100
|
|
- const options = this._options;
|
101
|
|
- const size = Utils.resolution(options.resolution, options.aspectRatio);
|
|
117
|
+ const idealSize = Utils.resolution(options.resolution, options.aspectRatio);
|
|
118
|
+ const userConstraints = options.constraints;
|
|
119
|
+ const ourConstraints/*: MediaTrackConstraints*/ = {
|
|
120
|
+ width: { ideal: idealSize.width },
|
|
121
|
+ height: { ideal: idealSize.height },
|
|
122
|
+ resizeMode: 'none' // request native resolution to encourage usage of standard resolutions
|
|
123
|
+ // users can opt-in to 'crop-and-scale' if they so desire
|
|
124
|
+ };
|
102
|
125
|
const constraints: MediaStreamConstraints = {
|
103
|
126
|
audio: false,
|
104
|
|
- video: {
|
105
|
|
- width: size.width,
|
106
|
|
- height: size.height,
|
107
|
|
- aspectRatio: options.aspectRatio,
|
108
|
|
- ...options.constraints,
|
109
|
|
- }
|
|
127
|
+ video: Object.assign({}, ourConstraints, userConstraints)
|
110
|
128
|
};
|
111
|
129
|
|
112
|
130
|
// load camera stream
|
113
|
|
- return new Speedy.Promise<HTMLVideoElement>((resolve, reject) => {
|
114
|
|
- navigator.mediaDevices.getUserMedia(constraints).then(stream => {
|
|
131
|
+ return new Speedy.Promise<void>((resolve, reject) => {
|
|
132
|
+ navigator.mediaDevices.getUserMedia(constraints)
|
|
133
|
+ .then(stream => {
|
115
|
134
|
const video = this.video;
|
116
|
135
|
video.onloadedmetadata = () => {
|
117
|
136
|
const promise = video.play();
|
|
@@ -120,14 +139,14 @@ export class CameraSource extends VideoSource
|
120
|
139
|
// handle older browsers
|
121
|
140
|
if(promise === undefined) {
|
122
|
141
|
Utils.log(success);
|
123
|
|
- resolve(video);
|
|
142
|
+ resolve();
|
124
|
143
|
return;
|
125
|
144
|
}
|
126
|
145
|
|
127
|
146
|
// handle promise
|
128
|
147
|
promise.then(() => {
|
129
|
148
|
Utils.log(success);
|
130
|
|
- resolve(video);
|
|
149
|
+ resolve();
|
131
|
150
|
}).catch(error => {
|
132
|
151
|
reject(new IllegalOperationError(
|
133
|
152
|
'Webcam error!',
|
|
@@ -146,13 +165,15 @@ export class CameraSource extends VideoSource
|
146
|
165
|
video.autoplay = true;
|
147
|
166
|
|
148
|
167
|
video.srcObject = stream;
|
149
|
|
- }).catch(error => {
|
|
168
|
+ })
|
|
169
|
+ .catch(error => {
|
150
|
170
|
reject(new AccessDeniedError(
|
151
|
171
|
'Please give access to the webcam and reload the page.',
|
152
|
172
|
error
|
153
|
173
|
));
|
154
|
174
|
});
|
155
|
|
- }).then(_ => super._init()); // this will handle browser quirks
|
|
175
|
+ })
|
|
176
|
+ .then(() => super._init()); // this will handle browser quirks
|
156
|
177
|
}
|
157
|
178
|
|
158
|
179
|
/**
|
|
@@ -163,15 +184,17 @@ export class CameraSource extends VideoSource
|
163
|
184
|
_release(): SpeedyPromise<void>
|
164
|
185
|
{
|
165
|
186
|
const video = this.video;
|
|
187
|
+
|
|
188
|
+ // stop the camera feed
|
166
|
189
|
const stream = video.srcObject as MediaStream;
|
167
|
190
|
const tracks = stream.getTracks();
|
168
|
|
-
|
169
|
|
- // stop camera feed
|
170
|
191
|
tracks.forEach(track => track.stop());
|
|
192
|
+
|
|
193
|
+ // release references
|
171
|
194
|
video.onloadedmetadata = null;
|
172
|
195
|
video.srcObject = null;
|
173
|
196
|
|
174
|
197
|
// release the media
|
175
|
198
|
return super._release();
|
176
|
199
|
}
|
177
|
|
-}
|
|
200
|
+}
|