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