@uploadcare/file-uploader 1.11.0-alpha.2 → 1.11.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/abstract/UploaderPublicApi.js +1 -1
  2. package/blocks/CameraSource/CameraSource.d.ts +22 -169
  3. package/blocks/CameraSource/CameraSource.d.ts.map +1 -1
  4. package/blocks/CameraSource/CameraSource.js +126 -803
  5. package/blocks/CameraSource/camera-source.css +7 -105
  6. package/blocks/CloudImageEditorActivity/index.css +3 -3
  7. package/blocks/Config/Config.d.ts +2 -4
  8. package/blocks/Config/Config.d.ts.map +1 -1
  9. package/blocks/Config/Config.js +0 -2
  10. package/blocks/Config/initialConfig.d.ts.map +1 -1
  11. package/blocks/Config/initialConfig.js +0 -6
  12. package/blocks/Config/normalizeConfigValue.d.ts.map +1 -1
  13. package/blocks/Config/normalizeConfigValue.js +1 -17
  14. package/blocks/ExternalSource/ExternalSource.d.ts +20 -50
  15. package/blocks/ExternalSource/ExternalSource.d.ts.map +1 -1
  16. package/blocks/ExternalSource/ExternalSource.js +96 -99
  17. package/blocks/ExternalSource/MessageBridge.d.ts +20 -0
  18. package/blocks/ExternalSource/MessageBridge.d.ts.map +1 -0
  19. package/blocks/ExternalSource/MessageBridge.js +71 -0
  20. package/blocks/ExternalSource/buildThemeDefinition.d.ts +3 -0
  21. package/blocks/ExternalSource/buildThemeDefinition.d.ts.map +1 -0
  22. package/blocks/ExternalSource/buildThemeDefinition.js +47 -0
  23. package/blocks/ExternalSource/external-source.css +37 -22
  24. package/blocks/ExternalSource/types.d.ts +113 -0
  25. package/blocks/ExternalSource/types.d.ts.map +1 -0
  26. package/blocks/ExternalSource/types.js +143 -0
  27. package/blocks/Modal/Modal.d.ts.map +1 -1
  28. package/blocks/Modal/Modal.js +4 -11
  29. package/blocks/Modal/modal.css +3 -54
  30. package/blocks/StartFrom/start-from.css +2 -6
  31. package/blocks/UploadList/upload-list.css +1 -11
  32. package/blocks/UrlSource/url-source.css +0 -8
  33. package/blocks/themes/uc-basic/common.css +0 -5
  34. package/blocks/themes/uc-basic/svg-sprite.d.ts +1 -1
  35. package/blocks/themes/uc-basic/svg-sprite.js +1 -1
  36. package/blocks/themes/uc-basic/theme.css +2 -3
  37. package/index.ssr.d.ts +0 -14
  38. package/index.ssr.d.ts.map +1 -1
  39. package/index.ssr.js +8 -81
  40. package/locales/file-uploader/ar.d.ts +2 -0
  41. package/locales/file-uploader/ar.js +2 -0
  42. package/locales/file-uploader/az.d.ts +2 -0
  43. package/locales/file-uploader/az.js +2 -0
  44. package/locales/file-uploader/ca.d.ts +2 -0
  45. package/locales/file-uploader/ca.js +2 -0
  46. package/locales/file-uploader/cs.d.ts +2 -0
  47. package/locales/file-uploader/cs.js +2 -0
  48. package/locales/file-uploader/da.d.ts +2 -0
  49. package/locales/file-uploader/da.js +2 -0
  50. package/locales/file-uploader/de.d.ts +2 -0
  51. package/locales/file-uploader/de.js +2 -0
  52. package/locales/file-uploader/el.d.ts +2 -0
  53. package/locales/file-uploader/el.js +2 -0
  54. package/locales/file-uploader/en.d.ts +2 -0
  55. package/locales/file-uploader/en.js +3 -1
  56. package/locales/file-uploader/es.d.ts +2 -0
  57. package/locales/file-uploader/es.js +2 -0
  58. package/locales/file-uploader/et.d.ts +2 -0
  59. package/locales/file-uploader/et.js +2 -0
  60. package/locales/file-uploader/fr.d.ts +2 -0
  61. package/locales/file-uploader/fr.js +2 -0
  62. package/locales/file-uploader/he.d.ts +2 -0
  63. package/locales/file-uploader/he.js +2 -0
  64. package/locales/file-uploader/hy.d.ts +2 -0
  65. package/locales/file-uploader/hy.js +2 -0
  66. package/locales/file-uploader/is.d.ts +2 -0
  67. package/locales/file-uploader/is.js +2 -0
  68. package/locales/file-uploader/it.d.ts +2 -0
  69. package/locales/file-uploader/it.js +2 -0
  70. package/locales/file-uploader/ja.d.ts +2 -0
  71. package/locales/file-uploader/ja.js +2 -0
  72. package/locales/file-uploader/ka.d.ts +2 -0
  73. package/locales/file-uploader/ka.js +2 -0
  74. package/locales/file-uploader/kk.d.ts +2 -0
  75. package/locales/file-uploader/kk.js +2 -0
  76. package/locales/file-uploader/ko.d.ts +2 -0
  77. package/locales/file-uploader/ko.js +2 -0
  78. package/locales/file-uploader/lv.d.ts +2 -0
  79. package/locales/file-uploader/lv.js +2 -0
  80. package/locales/file-uploader/nb.d.ts +2 -0
  81. package/locales/file-uploader/nb.js +2 -0
  82. package/locales/file-uploader/nl.d.ts +2 -0
  83. package/locales/file-uploader/nl.js +2 -0
  84. package/locales/file-uploader/pl.d.ts +2 -0
  85. package/locales/file-uploader/pl.js +2 -0
  86. package/locales/file-uploader/pt.d.ts +2 -0
  87. package/locales/file-uploader/pt.js +2 -0
  88. package/locales/file-uploader/ro.d.ts +2 -0
  89. package/locales/file-uploader/ro.js +2 -0
  90. package/locales/file-uploader/ru.d.ts +2 -0
  91. package/locales/file-uploader/ru.js +2 -0
  92. package/locales/file-uploader/sk.d.ts +2 -0
  93. package/locales/file-uploader/sk.js +2 -0
  94. package/locales/file-uploader/sr.d.ts +2 -0
  95. package/locales/file-uploader/sr.js +2 -0
  96. package/locales/file-uploader/sv.d.ts +2 -0
  97. package/locales/file-uploader/sv.js +2 -0
  98. package/locales/file-uploader/tr.d.ts +2 -0
  99. package/locales/file-uploader/tr.js +2 -0
  100. package/locales/file-uploader/uk.d.ts +2 -0
  101. package/locales/file-uploader/uk.js +2 -0
  102. package/locales/file-uploader/vi.d.ts +2 -0
  103. package/locales/file-uploader/vi.js +2 -0
  104. package/locales/file-uploader/zh-TW.d.ts +2 -0
  105. package/locales/file-uploader/zh-TW.js +2 -0
  106. package/locales/file-uploader/zh.d.ts +2 -0
  107. package/locales/file-uploader/zh.js +2 -0
  108. package/package.json +2 -4
  109. package/types/exported.d.ts +0 -21
  110. package/web/file-uploader.iife.min.js +4 -4
  111. package/web/file-uploader.min.js +4 -4
  112. package/web/uc-basic.min.css +1 -1
  113. package/web/uc-cloud-image-editor.min.css +1 -1
  114. package/web/uc-cloud-image-editor.min.js +4 -4
  115. package/web/uc-file-uploader-inline.min.css +1 -1
  116. package/web/uc-file-uploader-inline.min.js +4 -4
  117. package/web/uc-file-uploader-minimal.min.css +1 -1
  118. package/web/uc-file-uploader-minimal.min.js +3 -3
  119. package/web/uc-file-uploader-regular.min.css +1 -1
  120. package/web/uc-file-uploader-regular.min.js +4 -4
  121. package/web/uc-img.min.js +1 -1
  122. package/blocks/ExternalSource/buildStyles.d.ts +0 -27
  123. package/blocks/ExternalSource/buildStyles.d.ts.map +0 -1
  124. package/blocks/ExternalSource/buildStyles.js +0 -133
  125. package/blocks/ExternalSource/messages.d.ts +0 -3
  126. package/blocks/ExternalSource/messages.d.ts.map +0 -1
  127. package/blocks/ExternalSource/messages.js +0 -35
  128. package/blocks/LocalEditorImage/LocalEditorImage.d.ts +0 -22
  129. package/blocks/LocalEditorImage/LocalEditorImage.d.ts.map +0 -1
  130. package/blocks/LocalEditorImage/LocalEditorImage.js +0 -139
  131. package/blocks/LocalEditorImage/localEditorImage.css +0 -19
@@ -1,45 +1,9 @@
1
- //@ts-nocheck
2
- import { ActivityBlock } from '../../abstract/ActivityBlock.js';
3
1
  import { UploaderBlock } from '../../abstract/UploaderBlock.js';
2
+ import { ActivityBlock } from '../../abstract/ActivityBlock.js';
4
3
  import { canUsePermissionsApi } from '../utils/abilities.js';
5
4
  import { debounce } from '../utils/debounce.js';
6
5
  import { UploadSource } from '../utils/UploadSource.js';
7
6
 
8
- const DEFAULT_VIDEO_CONFIG = {
9
- width: {
10
- ideal: 1920,
11
- },
12
- height: {
13
- ideal: 1080,
14
- },
15
- frameRate: {
16
- ideal: 30,
17
- },
18
- };
19
-
20
- const DEFAULT_PERMISSIONS = ['camera', 'microphone'];
21
-
22
- /**
23
- * @param {Number} time
24
- * @returns
25
- */
26
- function formatTime(time) {
27
- const minutes = Math.floor(time / 60)
28
- .toString()
29
- .padStart(2, '0');
30
- const seconds = Math.floor(time % 60)
31
- .toString()
32
- .padStart(2, '0');
33
- return `${minutes}:${seconds}`;
34
- }
35
-
36
- const DEFAULT_PICTURE_FORMAT = 'image/jpeg';
37
- const DEFAULT_VIDEO_FORMAT = 'video/webm';
38
-
39
- /** @typedef {'photo' | 'video'} CameraTabId */
40
-
41
- /** @typedef {'shot' | 'retake' | 'accept' | 'play' | 'stop' | 'pause' | 'resume'} CameraStatus */
42
-
43
7
  export class CameraSource extends UploaderBlock {
44
8
  couldBeCtxOwner = true;
45
9
  activityType = ActivityBlock.activities.CAMERA;
@@ -47,524 +11,54 @@ export class CameraSource extends UploaderBlock {
47
11
  /** @private */
48
12
  _unsubPermissions = null;
49
13
 
50
- /** @type {BlobPart[]} */
51
- _chunks = [];
52
-
53
- /** @type {MediaRecorder | null} */
54
- _mediaRecorder = null;
55
-
56
- /** @type {MediaStream | null} */
57
- _stream = null;
58
-
59
- /** @type {string | null} */
60
- _selectedAudioId = null;
61
-
62
- /** @type {string | null} */
63
- _selectedCameraId = null;
64
-
65
- constructor() {
66
- super();
67
-
68
- this.init$ = {
69
- ...this.init$,
70
- video: null,
71
- videoTransformCss: null,
72
-
73
- videoHidden: true,
74
- messageHidden: true,
75
- requestBtnHidden: canUsePermissionsApi(),
76
- cameraSelectOptions: null,
77
- cameraSelectHidden: true,
78
- l10nMessage: '',
79
-
80
- // This is refs
81
- switcher: null,
82
- panels: null,
83
- timer: null,
84
-
85
- timerHidden: true,
86
- cameraHidden: true,
87
- cameraActionsHidden: true,
88
-
89
- audioSelectOptions: null,
90
- audioSelectHidden: true,
91
- audioSelectDisabled: true,
92
- audioToggleMicorphoneHidden: true,
93
-
94
- tabCameraHidden: true,
95
- tabVideoHidden: true,
96
-
97
- currentIcon: 'camera-full',
98
- currentTimelineIcon: 'play',
99
- toggleMicorphoneIcon: 'microphone',
100
-
101
- /** @type {Number} */
102
- _startTime: 0,
103
- /** @type {Number} */
104
- _elapsedTime: 0,
105
- _animationFrameId: null,
106
-
107
- mutableClassButton: 'uc-shot-btn uc-camera-action',
108
-
109
- /** @param {Event} e */
110
- onCameraSelectChange: (e) => {
111
- this._selectedCameraId = e.target.value;
112
- this._capture();
113
- },
114
-
115
- /** @param {Event} e */
116
- onAudioSelectChange: (e) => {
117
- this._selectedAudioId = e.target.value;
118
- this._capture();
119
- },
120
-
121
- onCancel: () => {
122
- this.historyBack();
123
- },
124
-
125
- onShot: () => this._shot(),
126
-
127
- onRequestPermissions: () => this._capture(),
128
-
129
- /** General method for photo and video capture */
130
- onStartCamera: () => this._chooseActionWithCamera(),
131
-
132
- onStartRecording: () => this._startRecording(),
133
-
134
- onStopRecording: () => this._stopRecording(),
135
-
136
- onToggleRecording: () => this._toggleRecording(),
137
-
138
- onToggleAudio: () => this._toggleEnableAudio(),
139
-
140
- onRetake: () => this._retake(),
141
-
142
- onAccept: () => this._accept(),
143
-
144
- /** @param {MouseEvent} e */
145
- onClickTab: (e) => {
146
- const id = /** @type {HTMLElement} */ (e.currentTarget).getAttribute('data-id');
147
- if (id) this._handleActiveTab(/** @type {CameraTabId} */ (id));
148
- },
149
- };
150
- }
151
-
152
- _chooseActionWithCamera = () => {
153
- if (this._activeTab === CameraSource.types.PHOTO) {
14
+ init$ = {
15
+ ...this.init$,
16
+ video: null,
17
+ videoTransformCss: null,
18
+ shotBtnDisabled: true,
19
+ videoHidden: true,
20
+ messageHidden: true,
21
+ requestBtnHidden: canUsePermissionsApi(),
22
+ cameraSelectOptions: null,
23
+ cameraSelectHidden: true,
24
+ l10nMessage: '',
25
+
26
+ onCameraSelectChange: (e) => {
27
+ /** @type {String} */
28
+ this._selectedCameraId = e.target.value;
29
+ this._capture();
30
+ },
31
+ onCancel: () => {
32
+ this.historyBack();
33
+ },
34
+ onShot: () => {
154
35
  this._shot();
155
- }
156
-
157
- if (this._activeTab === CameraSource.types.VIDEO) {
158
- if (this._mediaRecorder?.state === 'recording') {
159
- this._stopRecording();
160
- return;
161
- }
162
-
163
- this._startRecording();
164
- }
165
- };
166
-
167
- _updateTimer = () => {
168
- const currentTime = Math.floor((performance.now() - this.$._startTime + this.$._elapsedTime) / 1000);
169
-
170
- if (typeof this.cfg.maxDurationVideoRecord === 'number' && this.cfg.maxDurationVideoRecord > 0) {
171
- const remainingTime = this.cfg.maxDurationVideoRecord - currentTime;
172
-
173
- if (remainingTime <= 0) {
174
- this.ref.timer.textContent = formatTime(remainingTime);
175
- this._stopRecording();
176
- return;
177
- }
178
-
179
- this.ref.timer.textContent = formatTime(remainingTime);
180
- } else {
181
- this.ref.timer.textContent = formatTime(currentTime);
182
- }
183
-
184
- this._animationFrameId = requestAnimationFrame(this._updateTimer);
185
- };
186
-
187
- _startTimer = () => {
188
- this.$._startTime = performance.now();
189
- this.$._elapsedTime = 0;
190
-
191
- this._updateTimer();
192
- };
193
-
194
- _stopTimer = () => {
195
- if (this._animationFrameId) cancelAnimationFrame(this._animationFrameId);
196
- };
197
-
198
- _startTimeline = () => {
199
- const currentTime = this.ref.video.currentTime;
200
- const duration = this.ref.video.duration;
201
-
202
- this.ref.line.style.transform = `scaleX(${currentTime / duration})`;
203
- this.ref.timer.textContent = formatTime(currentTime);
204
- this._animationFrameId = requestAnimationFrame(this._startTimeline);
205
- };
206
-
207
- _stopTimeline = () => {
208
- if (this._animationFrameId) cancelAnimationFrame(this._animationFrameId);
209
- };
210
-
211
- _startRecording = () => {
212
- try {
213
- this._chunks = [];
214
- this._options = {
215
- ...this.cfg.optionsMediaRecorder,
216
- };
217
-
218
- const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
219
-
220
- if (
221
- this.cfg.optionsMediaRecorder?.mimeType &&
222
- MediaRecorder.isTypeSupported(this.cfg.optionsMediaRecorder.mimeType)
223
- ) {
224
- this._options.mimeType = this.cfg.optionsMediaRecorder.mimeType;
225
- } else if (
226
- isFirefox &&
227
- this.cfg.optionsMediaRecorder?.mimeType &&
228
- MediaRecorder.isTypeSupported(this.cfg.optionsMediaRecorder.mimeType)
229
- ) {
230
- const mimeType = this.cfg.optionsMediaRecorder?.mimeType;
231
-
232
- if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) {
233
- throw new Error(`MIME type ${mimeType} is not supported`);
234
- }
235
-
236
- this._options.mimeType = DEFAULT_VIDEO_FORMAT;
237
- }
238
-
239
- if (this._stream) {
240
- this._mediaRecorder = new MediaRecorder(this._stream, this._options);
241
- this._mediaRecorder.start();
242
-
243
- this._mediaRecorder.addEventListener('dataavailable', (e) => {
244
- this._chunks.push(e.data);
245
- });
246
-
247
- this._startTimer();
248
-
249
- this.classList.add('uc-recording');
250
- this._setCameraState(CameraSource.events.PLAY);
251
- }
252
- } catch (error) {
253
- console.error('Failed to start recording', error);
254
- }
36
+ },
37
+ onRequestPermissions: () => {
38
+ this._capture();
39
+ },
255
40
  };
256
41
 
257
42
  /** @private */
258
- _stopRecording = () => {
259
- this._mediaRecorder?.addEventListener('stop', () => {
260
- this._previewVideo();
261
-
262
- this._stopTimer();
263
-
264
- this._setCameraState(CameraSource.events.STOP);
265
- });
266
-
267
- this._mediaRecorder?.stop();
268
- this.classList.remove('uc-recording');
269
- };
270
-
271
- /** This method is used to toggle recording pause/resume */
272
- _toggleRecording = () => {
273
- if (this._mediaRecorder?.state === 'recording') return;
274
-
275
- if (!this.ref.video.paused && !this.ref.video.ended && this.ref.video.readyState > 2) {
276
- this.ref.video.pause();
277
- } else if (this.ref.video.paused) {
278
- this.ref.video.play();
279
- }
280
- };
281
-
282
- _toggleEnableAudio = () => {
283
- this._stream?.getAudioTracks().forEach((track) => {
284
- track.enabled = !track.enabled;
285
-
286
- this.$.toggleMicorphoneIcon = !track.enabled ? 'microphone-mute' : 'microphone';
287
- this.$.audioSelectDisabled = !track.enabled;
288
- });
289
- };
290
-
291
- /**
292
- * Previewing the video that was recorded on the camera
293
- *
294
- * @private
295
- */
296
- _previewVideo = () => {
297
- try {
298
- const blob = new Blob(this._chunks, {
299
- type: this._mediaRecorder?.mimeType,
300
- });
301
-
302
- const videoURL = URL.createObjectURL(blob);
303
-
304
- this.ref.video.muted = false;
305
- this.ref.video.volume = 1;
306
- this.$.video = null;
307
- this.ref.video.src = videoURL;
308
-
309
- this.ref.video.addEventListener('play', () => {
310
- this._startTimeline();
311
- this.set$({
312
- currentTimelineIcon: 'pause',
313
- });
314
- });
315
-
316
- this.ref.video.addEventListener('pause', () => {
317
- this.set$({
318
- currentTimelineIcon: 'play',
319
- });
320
- this._stopTimeline();
321
- });
322
- } catch (error) {
323
- console.error('Failed to preview video', error);
324
- }
325
- };
326
-
327
- _retake = () => {
328
- this._setCameraState(CameraSource.events.RETAKE);
329
-
330
- /** Reset video */
331
- if (this._activeTab === CameraSource.types.VIDEO) {
332
- this.$.video = this._stream;
333
- this.ref.video.muted = true;
334
- }
335
-
336
- this.ref.video.play();
337
- };
338
-
339
- _accept = () => {
340
- this._setCameraState(CameraSource.events.ACCEPT);
341
-
342
- if (this._activeTab === CameraSource.types.PHOTO) {
343
- this._canvas?.toBlob((blob) => {
344
- const file = this._createFile('camera', 'jpeg', DEFAULT_PICTURE_FORMAT, blob);
345
- this._toSend(file);
346
- }, DEFAULT_PICTURE_FORMAT);
347
- return;
348
- }
349
-
350
- const blob = new Blob(this._chunks, {
351
- type: this._mediaRecorder?.mimeType,
352
- });
353
-
354
- const ext = this._guessExtensionByMime(this._mediaRecorder?.mimeType);
355
- const file = this._createFile('video', ext, `video/${ext}`, blob);
356
-
357
- this._toSend(file);
358
- this._chunks = [];
359
- };
360
-
361
- /** @param {CameraStatus} status */
362
- _handlePhoto = (status) => {
363
- if (status === CameraSource.events.SHOT) {
364
- this.set$({
365
- tabVideoHidden: true,
366
- cameraHidden: true,
367
- tabCameraHidden: true,
368
- cameraActionsHidden: false,
369
- cameraSelectHidden: true,
370
- });
371
- }
372
-
373
- if (status === CameraSource.events.RETAKE || status === CameraSource.events.ACCEPT) {
374
- this.set$({
375
- tabVideoHidden: !this.cfg.enableVideoRecording,
376
- cameraHidden: false,
377
- tabCameraHidden: false,
378
- cameraActionsHidden: true,
379
- cameraSelectHidden: this.cameraDevices.length <= 1,
380
- });
381
- }
382
- };
383
-
384
- /** @param {CameraStatus} status */
385
- _handleVideo = (status) => {
386
- if (status === CameraSource.events.PLAY) {
387
- this.set$({
388
- timerHidden: false,
389
- tabCameraHidden: true,
390
-
391
- cameraSelectHidden: true,
392
- audioSelectHidden: true,
393
-
394
- currentTimelineIcon: 'pause',
395
- currentIcon: 'square',
396
- mutableClassButton: 'uc-shot-btn uc-camera-action uc-stop-record',
397
- });
398
- }
399
-
400
- if (status === CameraSource.events.STOP) {
401
- this.set$({
402
- timerHidden: false,
403
- cameraHidden: true,
404
- audioToggleMicorphoneHidden: true,
405
- cameraActionsHidden: false,
406
- });
407
- }
408
-
409
- if (status === CameraSource.events.RETAKE || status === CameraSource.events.ACCEPT) {
410
- this.set$({
411
- timerHidden: true,
412
- tabCameraHidden: false,
413
- cameraHidden: false,
414
- cameraActionsHidden: true,
415
- audioToggleMicorphoneHidden: !this.cfg.enableAudioRecording,
416
- currentIcon: 'video-camera-full',
417
- mutableClassButton: 'uc-shot-btn uc-camera-action',
418
-
419
- audioSelectHidden: !this.cfg.enableAudioRecording,
420
- cameraSelectHidden: this.cameraDevices.length <= 1,
421
- });
422
- }
423
- };
424
-
425
- /**
426
- * @private
427
- * @param {CameraStatus} status
428
- */
429
- _setCameraState = (status) => {
430
- if (
431
- this._activeTab === CameraSource.types.PHOTO &&
432
- (status === 'shot' || status === 'retake' || status === 'accept')
433
- ) {
434
- this._handlePhoto(status);
435
- }
436
-
437
- if (
438
- this._activeTab === CameraSource.types.VIDEO &&
439
- (status === 'play' ||
440
- status === 'stop' ||
441
- status === 'retake' ||
442
- status === 'accept' ||
443
- status === 'pause' ||
444
- status === 'resume')
445
- ) {
446
- this._handleVideo(status);
43
+ _onActivate = () => {
44
+ if (canUsePermissionsApi()) {
45
+ this._subscribePermissions();
447
46
  }
47
+ this._capture();
448
48
  };
449
49
 
450
50
  /** @private */
451
- _shot() {
452
- this._setCameraState('shot');
453
-
454
- this._canvas = document.createElement('canvas');
455
- this._ctx = this._canvas.getContext('2d');
456
-
457
- if (!this._ctx) {
458
- throw new Error('Failed to get canvas context');
459
- }
460
-
461
- this._canvas.height = this.ref.video['videoHeight'];
462
- this._canvas.width = this.ref.video['videoWidth'];
463
-
464
- if (this.cfg.cameraMirror) {
465
- this._ctx.translate(this._canvas.width, 0);
466
- this._ctx.scale(-1, 1);
467
- }
468
-
469
- this._ctx.drawImage(this.ref.video, 0, 0);
470
- this.ref.video.pause();
471
- }
472
-
473
- /**
474
- * @private
475
- * @param {CameraTabId} tabId
476
- */
477
- _handleActiveTab = (tabId) => {
478
- this.ref.switcher.querySelectorAll('button').forEach((/** @type {HTMLElement} */ btn) => {
479
- btn.classList.toggle('uc-active', btn.getAttribute('data-id') === tabId);
480
- });
481
-
482
- if (tabId === CameraSource.types.PHOTO) {
483
- this.set$({
484
- currentIcon: 'camera-full',
485
- audioSelectHidden: true,
486
- audioToggleMicorphoneHidden: true,
487
- });
488
- }
489
-
490
- if (tabId === CameraSource.types.VIDEO) {
491
- this.set$({
492
- currentTimelineIcon: 'play',
493
- currentIcon: 'video-camera-full',
494
-
495
- audioSelectHidden: !this.cfg.enableAudioRecording,
496
- audioToggleMicorphoneHidden: !this.cfg.enableAudioRecording,
497
- });
51
+ _onDeactivate = () => {
52
+ if (this._unsubPermissions) {
53
+ this._unsubPermissions();
498
54
  }
499
55
 
500
- this._activeTab = tabId;
501
- };
502
-
503
- /**
504
- * @param {'camera' | 'video'} type
505
- * @param {'jpeg' | 'webm'} ext
506
- * @param {String} format
507
- * @param {Blob} blob
508
- */
509
- _createFile = (type, ext, format, blob) => {
510
- const date = Date.now();
511
- const name = `${type}-${date}.${ext}`;
512
-
513
- const file = new File([blob], name, {
514
- lastModified: date,
515
- type: format,
516
- });
517
-
518
- return file;
56
+ this._stopCapture();
519
57
  };
520
58
 
521
- /** @param {String | undefined} mime */
522
- _guessExtensionByMime(mime) {
523
- const knownContainers = {
524
- mp4: 'mp4',
525
- ogg: 'ogg',
526
- webm: 'webm',
527
- quicktime: 'mov',
528
- 'x-matroska': 'mkv',
529
- };
530
-
531
- // MediaRecorder.mimeType returns empty string in Firefox.
532
- // Firefox record video as WebM now by default.
533
- // @link https://bugzilla.mozilla.org/show_bug.cgi?id=1512175
534
- if (mime === '') {
535
- return 'webm';
536
- }
537
-
538
- // e.g. "video/x-matroska;codecs=avc1,opus"
539
- if (mime) {
540
- // e.g. ["video", "x-matroska;codecs=avc1,opus"]
541
- /** @type {string | string[]} */ (mime) = mime.split('/');
542
- if (mime?.[0] === 'video') {
543
- // e.g. "x-matroska;codecs=avc1,opus"
544
- mime = mime.slice(1).join('/');
545
- // e.g. "x-matroska"
546
- const container = mime?.split(';')[0];
547
- // e.g. "mkv"
548
- if (knownContainers[container]) {
549
- return knownContainers[container];
550
- }
551
- }
552
- }
553
-
554
- // In all other cases just return the base extension for all times
555
- return 'avi';
556
- }
557
-
558
- /**
559
- * The send file to the server
560
- *
561
- * @param {File} file
562
- */
563
- _toSend = (file) => {
564
- this.api.addFileFromObject(file, { source: UploadSource.CAMERA });
565
- this.set$({
566
- '*currentActivity': ActivityBlock.activities.UPLOAD_LIST,
567
- });
59
+ /** @private */
60
+ _handlePermissionsChange = () => {
61
+ this._capture();
568
62
  };
569
63
 
570
64
  /**
@@ -572,210 +66,117 @@ export class CameraSource extends UploaderBlock {
572
66
  * @param {'granted' | 'denied' | 'prompt'} state
573
67
  */
574
68
  _setPermissionsState = debounce((state) => {
575
- this.classList.toggle('uc-initialized', state === 'granted');
576
-
577
- const visibleAudio = this._activeTab === CameraSource.types.VIDEO && this.cfg.enableAudioRecording;
578
- const currentIcon = this._activeTab === CameraSource.types.PHOTO ? 'camera-full' : 'video-camera-full';
579
-
580
69
  if (state === 'granted') {
581
70
  this.set$({
582
71
  videoHidden: false,
583
- cameraHidden: false,
584
- tabCameraHidden: false,
72
+ shotBtnDisabled: false,
585
73
  messageHidden: true,
586
- timerHidden: true,
587
-
588
- currentIcon,
589
- audioToggleMicorphoneHidden: !visibleAudio,
590
- audioSelectHidden: !visibleAudio,
591
74
  });
592
75
  } else if (state === 'prompt') {
593
76
  this.$.l10nMessage = 'camera-permissions-prompt';
594
-
595
77
  this.set$({
596
78
  videoHidden: true,
597
- cameraHidden: true,
598
- tabCameraHidden: true,
79
+ shotBtnDisabled: true,
599
80
  messageHidden: false,
600
81
  });
601
-
602
82
  this._stopCapture();
603
83
  } else {
604
84
  this.$.l10nMessage = 'camera-permissions-denied';
605
85
 
606
86
  this.set$({
607
87
  videoHidden: true,
88
+ shotBtnDisabled: true,
608
89
  messageHidden: false,
609
-
610
- tabCameraHidden: false,
611
- tabVideoHidden: !this.cfg.enableVideoRecording,
612
-
613
- cameraActionsHidden: true,
614
-
615
- mutableClassButton: 'uc-shot-btn uc-camera-action',
616
90
  });
617
-
618
91
  this._stopCapture();
619
92
  }
620
93
  }, 300);
621
94
 
622
- _makeStreamInactive = () => {
623
- if (!this._stream) return false;
624
-
625
- const audioTracks = this._stream?.getAudioTracks();
626
- const videoTracks = this._stream?.getVideoTracks();
627
-
628
- /** @type {MediaStreamTrack[]} */ (audioTracks).forEach((track) => track.stop());
629
- /** @type {MediaStreamTrack[]} */ (videoTracks).forEach((track) => track.stop());
630
- };
631
-
632
- _stopCapture = () => {
633
- if (this._capturing) {
634
- this.ref.video.volume = 0;
635
- this.$.video?.getTracks()[0].stop();
636
- this.$.video = null;
637
-
638
- this._makeStreamInactive();
639
-
640
- this._capturing = false;
95
+ /** @private */
96
+ async _subscribePermissions() {
97
+ try {
98
+ // @ts-ignore
99
+ let permissionsResponse = await navigator.permissions.query({ name: 'camera' });
100
+ permissionsResponse.addEventListener('change', this._handlePermissionsChange);
101
+ } catch (err) {
102
+ console.log('Failed to use permissions API. Fallback to manual request mode.', err);
103
+ this._capture();
641
104
  }
642
- };
643
-
644
- _capture = async () => {
645
- const constraints = {
646
- video: DEFAULT_VIDEO_CONFIG,
647
- audio: this.cfg.enableAudioRecording ? {} : false,
648
- };
105
+ }
649
106
 
650
- if (this._selectedCameraId) {
651
- constraints.video = {
652
- deviceId: {
653
- exact: this._selectedCameraId,
107
+ /** @private */
108
+ async _capture() {
109
+ let constr = {
110
+ video: {
111
+ width: {
112
+ ideal: 1920,
654
113
  },
655
- };
656
- }
657
-
658
- if (this._selectedAudioId && this.cfg.enableAudioRecording) {
659
- constraints.audio = {
660
- deviceId: {
661
- exact: this._selectedAudioId,
114
+ height: {
115
+ ideal: 1080,
116
+ },
117
+ frameRate: {
118
+ ideal: 30,
662
119
  },
120
+ },
121
+ audio: false,
122
+ };
123
+ if (this._selectedCameraId) {
124
+ constr.video.deviceId = {
125
+ exact: this._selectedCameraId,
663
126
  };
664
127
  }
665
-
666
- // Mute the video to prevent feedback for Firefox
667
- this.ref.video.volume = 0;
128
+ /** @private */
129
+ this._canvas = document.createElement('canvas');
130
+ /** @private */
131
+ this._ctx = this._canvas.getContext('2d');
668
132
 
669
133
  try {
670
134
  this._setPermissionsState('prompt');
671
- this._stream = await navigator.mediaDevices.getUserMedia(constraints);
672
-
673
- this._stream.addEventListener('inactive', () => {
135
+ let stream = await navigator.mediaDevices.getUserMedia(constr);
136
+ stream.addEventListener('inactive', () => {
674
137
  this._setPermissionsState('denied');
675
138
  });
676
-
677
- this.$.video = this._stream;
139
+ this.$.video = stream;
678
140
  /** @private */
679
141
  this._capturing = true;
680
142
  this._setPermissionsState('granted');
681
- } catch (error) {
143
+ } catch (err) {
682
144
  this._setPermissionsState('denied');
683
- console.log('Failed to capture camera', error);
684
- }
685
- };
686
-
687
- _handlePermissionsChange = () => {
688
- this._capture();
689
- };
690
-
691
- _permissionAccess = async () => {
692
- try {
693
- for (const permission of DEFAULT_PERMISSIONS) {
694
- // @ts-ignore https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API
695
- this[`${permission}Response`] = await navigator.permissions.query({ name: permission });
696
-
697
- this[`${permission}Response`].addEventListener('change', this._handlePermissionsChange);
698
- }
699
- } catch (error) {
700
- console.log('Failed to use permissions API. Fallback to manual request mode.', error);
701
- this._capture();
702
- }
703
- };
704
-
705
- _getPermission = () => {};
706
-
707
- _requestDeviceAccess = async () => {
708
- try {
709
- await navigator.mediaDevices.getUserMedia({ video: true, audio: this.cfg.enableAudioRecording });
710
- await this._getDevices();
711
-
712
- navigator.mediaDevices.addEventListener('devicechange', this._getDevices);
713
- } catch (error) {
714
- console.log('Failed to get user media', error);
715
- }
716
- };
717
-
718
- _getDevices = async () => {
719
- try {
720
- const devices = await navigator.mediaDevices.enumerateDevices();
721
-
722
- this.cameraDevices = devices
723
- .filter((device) => device.kind === 'videoinput')
724
- .map((device, index) => ({
725
- text: device.label.trim() || `${this.l10n('caption-camera')} ${index + 1}`,
726
- value: device.deviceId,
727
- }));
728
-
729
- this.audioDevices =
730
- this.cfg.enableAudioRecording &&
731
- devices
732
- .filter((device) => device.kind === 'audioinput')
733
- .map((device) => ({
734
- text: device.label.trim(),
735
- value: device.deviceId,
736
- }));
737
-
738
- if (this.cameraDevices.length > 1) {
739
- this.set$({
740
- cameraSelectOptions: this.cameraDevices,
741
- cameraSelectHidden: false,
742
- });
743
- }
744
- this._selectedCameraId = this.cameraDevices[0]?.value;
745
-
746
- if (this.audioDevices.length > 1) {
747
- this.set$({
748
- audioSelectOptions: this.audioDevices,
749
- audioSelectHidden: false,
750
- });
751
- }
752
- this._selectedAudioId = this.audioDevices[0]?.value;
753
- } catch (error) {
754
- console.log('Failed to get devices', error);
755
- }
756
- };
757
-
758
- _onActivate = async () => {
759
- await this._permissionAccess();
760
- await this._requestDeviceAccess();
761
- await this._capture();
762
- };
763
-
764
- _onDeactivate = async () => {
765
- if (this._unsubPermissions) {
766
- this._unsubPermissions();
145
+ console.error('Failed to capture camera', err);
767
146
  }
147
+ }
768
148
 
769
- /** Calling this method here because safari and firefox don't support the inactive event yet */
770
- const isChromium = !!window.chrome;
771
- if (!isChromium) {
772
- this._setPermissionsState('denied');
149
+ /** @private */
150
+ _stopCapture() {
151
+ if (this._capturing) {
152
+ this.$.video?.getTracks()[0].stop();
153
+ this.$.video = null;
154
+ this._capturing = false;
773
155
  }
156
+ }
774
157
 
775
- this._stopCapture();
776
- };
158
+ /** @private */
159
+ _shot() {
160
+ this._canvas.height = this.ref.video['videoHeight'];
161
+ this._canvas.width = this.ref.video['videoWidth'];
162
+ // @ts-ignore
163
+ this._ctx.drawImage(this.ref.video, 0, 0);
164
+ const date = Date.now();
165
+ const name = `camera-${date}.jpeg`;
166
+ const format = 'image/jpeg';
167
+ this._canvas.toBlob((blob) => {
168
+ let file = new File([blob], name, {
169
+ lastModified: date,
170
+ type: format,
171
+ });
172
+ this.api.addFileFromObject(file, { source: UploadSource.CAMERA });
173
+ this.set$({
174
+ '*currentActivity': ActivityBlock.activities.UPLOAD_LIST,
175
+ });
176
+ }, format);
177
+ }
777
178
 
778
- initCallback() {
179
+ async initCallback() {
779
180
  super.initCallback();
780
181
  this.registerActivity(this.activityType, {
781
182
  onActivate: this._onActivate,
@@ -786,58 +187,30 @@ export class CameraSource extends UploaderBlock {
786
187
  this.$.videoTransformCss = val ? 'scaleX(-1)' : null;
787
188
  });
788
189
 
789
- this.subConfigValue('enableAudioRecording', (val) => {
790
- this.$.audioToggleMicorphoneHidden = !val;
791
- this.$.audioSelectDisabled = !val;
792
- });
793
-
794
- this.subConfigValue('enableVideoRecording', (val) => {
795
- this.$.tabVideoHidden = !val;
796
- });
797
-
798
- this.subConfigValue('defaultCameraTab', (val) => {
799
- if (this.cfg.enableVideoRecording) {
800
- this._handleActiveTab(val);
801
- return;
190
+ try {
191
+ let deviceList = await navigator.mediaDevices.enumerateDevices();
192
+ let cameraSelectOptions = deviceList
193
+ .filter((info) => {
194
+ return info.kind === 'videoinput';
195
+ })
196
+ .map((info, idx) => {
197
+ return {
198
+ text: info.label.trim() || `${this.l10n('caption-camera')} ${idx + 1}`,
199
+ value: info.deviceId,
200
+ };
201
+ });
202
+ if (cameraSelectOptions.length > 1) {
203
+ this.$.cameraSelectOptions = cameraSelectOptions;
204
+ this.$.cameraSelectHidden = false;
802
205
  }
803
-
804
- this._handleActiveTab('photo');
805
- });
806
- }
807
-
808
- _destroy() {
809
- for (const permission of DEFAULT_PERMISSIONS) {
810
- this[`${permission}Response`].removeEventListener('change', this._handlePermissionsChange);
206
+ this._selectedCameraId = cameraSelectOptions[0]?.value;
207
+ } catch (err) {
208
+ // mediaDevices isn't available for HTTP
209
+ // TODO: handle this case
811
210
  }
812
-
813
- navigator.mediaDevices.removeEventListener('devicechange', this._getDevices);
814
- }
815
-
816
- async destroyCallback() {
817
- super.destroyCallback();
818
-
819
- this._destroy();
820
211
  }
821
212
  }
822
213
 
823
- CameraSource.types = Object.freeze({
824
- PHOTO: 'photo',
825
- VIDEO: 'video',
826
- });
827
-
828
- CameraSource.events = Object.freeze({
829
- IDLE: 'idle',
830
- SHOT: 'shot',
831
-
832
- PLAY: 'play',
833
- PAUSE: 'pause',
834
- RESUME: 'resume',
835
- STOP: 'stop',
836
-
837
- RETAKE: 'retake',
838
- ACCEPT: 'accept',
839
- });
840
-
841
214
  CameraSource.template = /* HTML */ `
842
215
  <uc-activity-header>
843
216
  <button type="button" class="uc-mini-btn" set="onclick: *historyBack" l10n="@title:back">
@@ -863,7 +236,6 @@ CameraSource.template = /* HTML */ `
863
236
  </uc-activity-header>
864
237
  <div class="uc-content">
865
238
  <video
866
- muted
867
239
  autoplay
868
240
  playsinline
869
241
  set="srcObject: video; style.transform: videoTransformCss; @hidden: videoHidden"
@@ -877,57 +249,8 @@ CameraSource.template = /* HTML */ `
877
249
  l10n="camera-permissions-request"
878
250
  ></button>
879
251
  </div>
880
- </div>
881
-
882
- <div class="uc-controls">
883
- <div ref="switcher" class="uc-switcher" set="@hidden:!timerHidden">
884
- <button
885
- data-id="photo"
886
- type="button"
887
- class="uc-switch uc-mini-btn"
888
- set="onclick: onClickTab; @hidden: tabCameraHidden"
889
- >
890
- <uc-icon name="camera"></uc-icon>
891
- </button>
892
- <button
893
- data-id="video"
894
- type="button"
895
- class="uc-switch uc-mini-btn"
896
- set="onclick: onClickTab; @hidden: tabVideoHidden"
897
- >
898
- <uc-icon name="video-camera"></uc-icon>
899
- </button>
900
- </div>
901
-
902
- <button class="uc-secondary-btn uc-recording-timer" set="@hidden:timerHidden; onclick: onToggleRecording">
903
- <uc-icon set="@name: currentTimelineIcon"></uc-icon>
904
- <span ref="timer"> 00:00 </span>
905
- <span ref="line" class="uc-line"></span>
906
- </button>
907
-
908
- <div class="uc-camera-actions uc-camera-action" set="@hidden: cameraActionsHidden">
909
- <button type="button" class="uc-secondary-btn" set="onclick: onRetake">Retake</button>
910
- <button type="button" class="uc-primary-btn" set="onclick: onAccept">Accept</button>
911
- </div>
912
-
913
- <button
914
- type="button"
915
- class="uc-shot-btn uc-camera-action"
916
- set="onclick: onStartCamera; @class: mutableClassButton; @hidden: cameraHidden;"
917
- >
918
- <uc-icon set="@name: currentIcon"></uc-icon>
252
+ <button type="button" class="uc-shot-btn" set="onclick: onShot; @disabled: shotBtnDisabled">
253
+ <uc-icon name="camera"></uc-icon>
919
254
  </button>
920
-
921
- <div class="uc-select">
922
- <button class="uc-mini-btn uc-btn-microphone" set="onclick: onToggleAudio; @hidden: audioToggleMicorphoneHidden;">
923
- <uc-icon set="@name:toggleMicorphoneIcon"></uc-icon>
924
- </button>
925
-
926
- <uc-select
927
- class="uc-audio-select"
928
- set="$.options: audioSelectOptions; onchange: onAudioSelectChange; @hidden: audioSelectHidden; @disabled: audioSelectDisabled"
929
- >
930
- </uc-select>
931
- </div>
932
255
  </div>
933
256
  `;