@es-joy/jsoe 0.6.1 → 0.7.0
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.
- package/.eslintrc.cjs +4 -0
- package/.nyc_output/out.json +20626 -44170
- package/CHANGES.md +5 -0
- package/README.md +15 -3
- package/demo/index.js +17 -2
- package/package.json +3 -2
- package/src/formats/structuredCloning.js +2 -1
- package/src/fundamentalTypes/errorType.js +1 -0
- package/src/fundamentalTypes/fileType.js +1148 -0
- package/src/jsoe.css +41 -0
- package/src/types.js +2 -3
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
import {jml} from '../vendor-imports.js';
|
|
2
|
+
import {$e} from '../utils/templateUtils.js';
|
|
3
|
+
import dialogs from '../utils/dialogs.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {File|{
|
|
7
|
+
* name: string, size: string, type: string,
|
|
8
|
+
* lastModified: string
|
|
9
|
+
* }} FileInfo
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {(file: FileInfo) => void} SetValue
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @type {AudioContext} */
|
|
17
|
+
let audioCtx;
|
|
18
|
+
/**
|
|
19
|
+
* @param {MediaStream} stream
|
|
20
|
+
* @param {HTMLCanvasElement} canvas
|
|
21
|
+
* @see Adapted from {@link https://mdn.github.io/dom-examples/media/web-dictaphone/}
|
|
22
|
+
* @returns {void}
|
|
23
|
+
*/
|
|
24
|
+
function visualize (stream, canvas) {
|
|
25
|
+
if (!audioCtx) {
|
|
26
|
+
audioCtx = new AudioContext();
|
|
27
|
+
}
|
|
28
|
+
const canvasCtx = /** @type {CanvasRenderingContext2D} */ (
|
|
29
|
+
canvas.getContext('2d')
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const source = audioCtx.createMediaStreamSource(stream);
|
|
33
|
+
|
|
34
|
+
const analyser = audioCtx.createAnalyser();
|
|
35
|
+
analyser.fftSize = 2048;
|
|
36
|
+
const bufferLength = analyser.frequencyBinCount;
|
|
37
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
38
|
+
|
|
39
|
+
source.connect(analyser);
|
|
40
|
+
// analyser.connect(audioCtx.destination);
|
|
41
|
+
|
|
42
|
+
draw();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
function draw () {
|
|
48
|
+
const WIDTH = canvas.width;
|
|
49
|
+
const HEIGHT = canvas.height;
|
|
50
|
+
|
|
51
|
+
requestAnimationFrame(draw);
|
|
52
|
+
|
|
53
|
+
analyser.getByteTimeDomainData(dataArray);
|
|
54
|
+
|
|
55
|
+
canvasCtx.fillStyle = 'rgb(200, 200, 200)';
|
|
56
|
+
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
57
|
+
|
|
58
|
+
canvasCtx.lineWidth = 2;
|
|
59
|
+
canvasCtx.strokeStyle = 'rgb(0, 0, 0)';
|
|
60
|
+
|
|
61
|
+
canvasCtx.beginPath();
|
|
62
|
+
|
|
63
|
+
const sliceWidth = WIDTH * 1 / bufferLength;
|
|
64
|
+
let x = 0;
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
67
|
+
const v = dataArray[i] / 128;
|
|
68
|
+
const y = v * HEIGHT / 2;
|
|
69
|
+
|
|
70
|
+
if (i === 0) {
|
|
71
|
+
canvasCtx.moveTo(x, y);
|
|
72
|
+
} else {
|
|
73
|
+
canvasCtx.lineTo(x, y);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
x += sliceWidth;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
canvasCtx.lineTo(canvas.width, canvas.height / 2);
|
|
80
|
+
canvasCtx.stroke();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {MediaStreamConstraints} constraints
|
|
86
|
+
* @returns {Promise<MediaStream|null>}
|
|
87
|
+
*/
|
|
88
|
+
async function getUserMedia (constraints) {
|
|
89
|
+
let stream = null;
|
|
90
|
+
try {
|
|
91
|
+
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
92
|
+
/* c8 ignore next 4 */
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error('err', err);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return stream;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {DisplayMediaStreamOptions|undefined} displayMediaOptions
|
|
102
|
+
* @returns {Promise<MediaStream|null>}
|
|
103
|
+
*/
|
|
104
|
+
async function startScreenCapture (displayMediaOptions) {
|
|
105
|
+
let captureStream = null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
captureStream =
|
|
109
|
+
await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
|
|
110
|
+
/* c8 ignore next 3 */
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`Error: ${err}`);
|
|
113
|
+
}
|
|
114
|
+
return captureStream;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {number} timestamp
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
function getDateString (timestamp) {
|
|
122
|
+
const d = new Date(timestamp);
|
|
123
|
+
const dateTimeLocalValue = (
|
|
124
|
+
new Date(
|
|
125
|
+
d.getTime() - (d.getTimezoneOffset() * 60000)
|
|
126
|
+
).toISOString()
|
|
127
|
+
).slice(0, -1);
|
|
128
|
+
return dateTimeLocalValue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {File} value
|
|
133
|
+
* @returns {import('jamilih').JamilihArray}
|
|
134
|
+
*/
|
|
135
|
+
function binaryButton (value) {
|
|
136
|
+
// @ts-expect-error It's ok
|
|
137
|
+
return ['button', /** @type {import('jamilih').JamilihAttributes} */ ({
|
|
138
|
+
class: 'viewBinary',
|
|
139
|
+
$custom: {
|
|
140
|
+
$value: value
|
|
141
|
+
},
|
|
142
|
+
$on: {
|
|
143
|
+
/**
|
|
144
|
+
* @this {HTMLButtonElement & {$value: File}}
|
|
145
|
+
* @param {Event} e
|
|
146
|
+
*/
|
|
147
|
+
click (e) {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
if (
|
|
150
|
+
!this.$value ||
|
|
151
|
+
Object.prototype.toString.call(this.$value).slice(8, -1) !== 'File'
|
|
152
|
+
) {
|
|
153
|
+
dialogs.alert(
|
|
154
|
+
'There is no file chosen with binary data'
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const reader = new FileReader();
|
|
159
|
+
reader.addEventListener('load', async function () {
|
|
160
|
+
await dialogs.alert({
|
|
161
|
+
message: ['div', [
|
|
162
|
+
'Binary source',
|
|
163
|
+
['br'],
|
|
164
|
+
['textarea', {class: 'view-binary'}, [
|
|
165
|
+
/* c8 ignore next */
|
|
166
|
+
/** @type {string|null} */ (reader.result) ?? ''
|
|
167
|
+
]]
|
|
168
|
+
]]
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
// Seems not feasible to accurately simulate
|
|
172
|
+
/* c8 ignore next 10 */
|
|
173
|
+
reader.addEventListener(
|
|
174
|
+
'error',
|
|
175
|
+
/* c8 ignore next 6 */
|
|
176
|
+
async function () {
|
|
177
|
+
console.error(reader.error);
|
|
178
|
+
await dialogs.alert(/** @type {string} */ (
|
|
179
|
+
/** @type {DOMException} */ (reader.error).message
|
|
180
|
+
));
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
reader.readAsBinaryString(this.$value);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}), ['View binary data']];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @type {import('../types.js').TypeObject}
|
|
191
|
+
*/
|
|
192
|
+
const fileType = {
|
|
193
|
+
option: ['File'],
|
|
194
|
+
stringRegex: /^File\((.*)\)$/u,
|
|
195
|
+
toValue (s) {
|
|
196
|
+
const obj = JSON.parse(s);
|
|
197
|
+
return {value: new File([obj.stringContents], obj.name, {
|
|
198
|
+
type: obj.type,
|
|
199
|
+
lastModified: obj.lastModified
|
|
200
|
+
})};
|
|
201
|
+
},
|
|
202
|
+
getInput ({root}) {
|
|
203
|
+
return /** @type {HTMLButtonElement} */ ($e(root, 'button.viewBinary'));
|
|
204
|
+
},
|
|
205
|
+
setValue ({root, value}) {
|
|
206
|
+
/** @type {HTMLFieldSetElement & {$setValue: SetValue}} */
|
|
207
|
+
($e(root, 'fieldset.fileMetaData')).$setValue(value);
|
|
208
|
+
},
|
|
209
|
+
getValue ({root}) {
|
|
210
|
+
// Get value attached to DOM element rather than input,
|
|
211
|
+
// so can be from preexisting file too
|
|
212
|
+
return /** @type {HTMLButtonElement & {$value: File}} */ (
|
|
213
|
+
this.getInput({root})
|
|
214
|
+
).$value;
|
|
215
|
+
},
|
|
216
|
+
viewUI ({value}) {
|
|
217
|
+
return ['div', {dataset: {type: 'file'}}, [
|
|
218
|
+
['b', {class: 'emphasis'}, [
|
|
219
|
+
'File'
|
|
220
|
+
]],
|
|
221
|
+
['br'],
|
|
222
|
+
['br'],
|
|
223
|
+
['b', [
|
|
224
|
+
'Name '
|
|
225
|
+
]],
|
|
226
|
+
value.name,
|
|
227
|
+
['br'],
|
|
228
|
+
['b', [
|
|
229
|
+
'Size (in bytes) '
|
|
230
|
+
]],
|
|
231
|
+
String(value.size),
|
|
232
|
+
['br'],
|
|
233
|
+
['b', [
|
|
234
|
+
'Content type '
|
|
235
|
+
]],
|
|
236
|
+
value.type,
|
|
237
|
+
['br'],
|
|
238
|
+
['b', [
|
|
239
|
+
'Last modified date '
|
|
240
|
+
]],
|
|
241
|
+
getDateString(value.lastModified),
|
|
242
|
+
['br'],
|
|
243
|
+
value.type.startsWith('text/') || value.type === 'application/json'
|
|
244
|
+
? (() => {
|
|
245
|
+
const div = /** @type {HTMLDivElement} */ (jml('div'));
|
|
246
|
+
const reader = new FileReader();
|
|
247
|
+
reader.addEventListener('load', function () {
|
|
248
|
+
div.append(jml('div', [
|
|
249
|
+
'Text source',
|
|
250
|
+
['br'],
|
|
251
|
+
['textarea', {class: 'view-text'}, [
|
|
252
|
+
/* c8 ignore next */
|
|
253
|
+
/** @type {string|null} */ (reader.result) ?? ''
|
|
254
|
+
]]
|
|
255
|
+
]));
|
|
256
|
+
});
|
|
257
|
+
// Seems not feasible to accurately simulate
|
|
258
|
+
/* c8 ignore next 10 */
|
|
259
|
+
reader.addEventListener(
|
|
260
|
+
'error',
|
|
261
|
+
/* c8 ignore next 6 */
|
|
262
|
+
function () {
|
|
263
|
+
console.error(reader.error);
|
|
264
|
+
div.append(
|
|
265
|
+
/** @type {DOMException} */ (reader.error).message
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
reader.readAsBinaryString(value);
|
|
270
|
+
return div;
|
|
271
|
+
})()
|
|
272
|
+
: binaryButton(value),
|
|
273
|
+
value.type.startsWith('video/')
|
|
274
|
+
? (() => {
|
|
275
|
+
const objURL = URL.createObjectURL(value);
|
|
276
|
+
const video = /** @type {HTMLVideoElement} */ (jml('video', {
|
|
277
|
+
src: objURL,
|
|
278
|
+
class: 'video',
|
|
279
|
+
$on: {
|
|
280
|
+
loadeddata () {
|
|
281
|
+
URL.revokeObjectURL(objURL);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
return ['div', [
|
|
287
|
+
video,
|
|
288
|
+
['button', {
|
|
289
|
+
class: 'play',
|
|
290
|
+
$on: {
|
|
291
|
+
click (e) {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
video.play();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}, [
|
|
297
|
+
'Play'
|
|
298
|
+
]],
|
|
299
|
+
['button', {
|
|
300
|
+
class: 'pause',
|
|
301
|
+
$on: {
|
|
302
|
+
click (e) {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
video.pause();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}, [
|
|
308
|
+
'Pause'
|
|
309
|
+
]],
|
|
310
|
+
['button', {
|
|
311
|
+
class: 'replay',
|
|
312
|
+
$on: {
|
|
313
|
+
click (e) {
|
|
314
|
+
e.preventDefault();
|
|
315
|
+
video.pause();
|
|
316
|
+
video.currentTime = 0;
|
|
317
|
+
video.play();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}, [
|
|
321
|
+
'Replay'
|
|
322
|
+
]]
|
|
323
|
+
]];
|
|
324
|
+
})()
|
|
325
|
+
: '',
|
|
326
|
+
value.type.startsWith('audio/')
|
|
327
|
+
? (() => {
|
|
328
|
+
const objURL = URL.createObjectURL(value);
|
|
329
|
+
const audio = /** @type {HTMLAudioElement} */ (jml('audio', {
|
|
330
|
+
src: objURL,
|
|
331
|
+
class: 'audio',
|
|
332
|
+
$on: {
|
|
333
|
+
loadeddata () {
|
|
334
|
+
URL.revokeObjectURL(objURL);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}));
|
|
338
|
+
return ['div', [
|
|
339
|
+
audio,
|
|
340
|
+
['button', {
|
|
341
|
+
class: 'play',
|
|
342
|
+
$on: {
|
|
343
|
+
click (e) {
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
audio.play();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}, [
|
|
349
|
+
'Play'
|
|
350
|
+
]],
|
|
351
|
+
['button', {
|
|
352
|
+
class: 'pause',
|
|
353
|
+
$on: {
|
|
354
|
+
click (e) {
|
|
355
|
+
e.preventDefault();
|
|
356
|
+
audio.pause();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}, [
|
|
360
|
+
'Pause'
|
|
361
|
+
]],
|
|
362
|
+
['button', {
|
|
363
|
+
class: 'replay',
|
|
364
|
+
$on: {
|
|
365
|
+
click (e) {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
audio.pause();
|
|
368
|
+
audio.currentTime = 0;
|
|
369
|
+
audio.play();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}, [
|
|
373
|
+
'Replay'
|
|
374
|
+
]]
|
|
375
|
+
]];
|
|
376
|
+
})()
|
|
377
|
+
: '',
|
|
378
|
+
value.type === 'application/pdf'
|
|
379
|
+
? (() => {
|
|
380
|
+
const objURL = URL.createObjectURL(value);
|
|
381
|
+
const iframe = jml('iframe', {
|
|
382
|
+
class: 'PDF',
|
|
383
|
+
src: objURL,
|
|
384
|
+
$on: {
|
|
385
|
+
load () {
|
|
386
|
+
URL.revokeObjectURL(objURL);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
return iframe;
|
|
391
|
+
})()
|
|
392
|
+
: '',
|
|
393
|
+
value.type.startsWith('image/')
|
|
394
|
+
// @ts-expect-error It's ok
|
|
395
|
+
? ['img', /** @type {import('jamilih').JamilihAttributes} */ ({
|
|
396
|
+
class: 'imageView',
|
|
397
|
+
src: URL.createObjectURL(value),
|
|
398
|
+
$on: {
|
|
399
|
+
/** @this {HTMLImageElement} */
|
|
400
|
+
load () {
|
|
401
|
+
URL.revokeObjectURL(this.src);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
})]
|
|
405
|
+
: ''
|
|
406
|
+
]];
|
|
407
|
+
},
|
|
408
|
+
editUI ({typeNamespace, value = {}}) {
|
|
409
|
+
// Todo: Could add way to preview file in edit mode (whether
|
|
410
|
+
// recorded or uploaded)
|
|
411
|
+
return ['div', {dataset: {type: 'file'}}, [
|
|
412
|
+
['fieldset', {
|
|
413
|
+
class: 'fileMetaData',
|
|
414
|
+
$custom: {
|
|
415
|
+
/**
|
|
416
|
+
* @this {HTMLFieldSetElement}
|
|
417
|
+
* @param {FileInfo} file
|
|
418
|
+
*/
|
|
419
|
+
$setValue (file) {
|
|
420
|
+
// eslint-disable-next-line consistent-this -- Clarity
|
|
421
|
+
const metadataFieldset = this;
|
|
422
|
+
/** @type {HTMLInputElement} */ ($e(
|
|
423
|
+
metadataFieldset,
|
|
424
|
+
'.fileName'
|
|
425
|
+
)).value = file.name;
|
|
426
|
+
|
|
427
|
+
/** @type {HTMLInputElement} */ ($e(
|
|
428
|
+
metadataFieldset,
|
|
429
|
+
'.size'
|
|
430
|
+
)).value = String(file.size);
|
|
431
|
+
|
|
432
|
+
/** @type {HTMLInputElement} */ ($e(
|
|
433
|
+
metadataFieldset,
|
|
434
|
+
'.contentType'
|
|
435
|
+
)).value = file.type;
|
|
436
|
+
|
|
437
|
+
/** @type {HTMLInputElement} */ ($e(
|
|
438
|
+
metadataFieldset,
|
|
439
|
+
'.lastModified'
|
|
440
|
+
)).value = typeof file.lastModified === 'number'
|
|
441
|
+
? getDateString(file.lastModified)
|
|
442
|
+
: '';
|
|
443
|
+
|
|
444
|
+
/** @type {HTMLButtonElement & {$value: File|undefined}} */ ($e(
|
|
445
|
+
metadataFieldset,
|
|
446
|
+
'.viewBinary'
|
|
447
|
+
)).$value = typeof file.lastModified === 'number'
|
|
448
|
+
? /** @type {File} */ (file)
|
|
449
|
+
: undefined;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}, [
|
|
453
|
+
['legend', ['Current file data']],
|
|
454
|
+
['label', [
|
|
455
|
+
'Name ',
|
|
456
|
+
['input', {
|
|
457
|
+
size: 50,
|
|
458
|
+
class: 'fileName', value: value.name ?? '',
|
|
459
|
+
$on: {
|
|
460
|
+
change () {
|
|
461
|
+
const viewBinary =
|
|
462
|
+
/**
|
|
463
|
+
* @type {HTMLButtonElement & {$value: File}}
|
|
464
|
+
*/ (
|
|
465
|
+
$e(
|
|
466
|
+
/** @type {HTMLElement} */
|
|
467
|
+
(this.parentElement?.parentElement),
|
|
468
|
+
'.viewBinary'
|
|
469
|
+
)
|
|
470
|
+
);
|
|
471
|
+
const oldFile = /** @type {File|undefined} */ (
|
|
472
|
+
viewBinary.$value
|
|
473
|
+
);
|
|
474
|
+
if (
|
|
475
|
+
!oldFile ||
|
|
476
|
+
Object.prototype.toString.call(
|
|
477
|
+
oldFile
|
|
478
|
+
).slice(8, -1) !== 'File'
|
|
479
|
+
) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const newName = /** @type {HTMLInputElement} */ (this).value;
|
|
483
|
+
const file = new File(
|
|
484
|
+
[oldFile],
|
|
485
|
+
newName,
|
|
486
|
+
{
|
|
487
|
+
type: oldFile.type,
|
|
488
|
+
lastModified: oldFile.lastModified
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
viewBinary.$value = file;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}]
|
|
495
|
+
]],
|
|
496
|
+
['br'],
|
|
497
|
+
['label', [
|
|
498
|
+
'Size (in bytes) ',
|
|
499
|
+
['input', {
|
|
500
|
+
type: 'number', disabled: true, class: 'size',
|
|
501
|
+
value: String(value.size)
|
|
502
|
+
}]
|
|
503
|
+
]],
|
|
504
|
+
['br'],
|
|
505
|
+
['label', [
|
|
506
|
+
'Content type ',
|
|
507
|
+
['input', {
|
|
508
|
+
class: 'contentType', value: value.type ?? '',
|
|
509
|
+
$on: {
|
|
510
|
+
change () {
|
|
511
|
+
const viewBinary =
|
|
512
|
+
/**
|
|
513
|
+
* @type {HTMLButtonElement & {$value: File}}
|
|
514
|
+
*/ (
|
|
515
|
+
$e(
|
|
516
|
+
/** @type {HTMLElement} */
|
|
517
|
+
(this.parentElement?.parentElement),
|
|
518
|
+
'.viewBinary'
|
|
519
|
+
)
|
|
520
|
+
);
|
|
521
|
+
const oldFile = /** @type {File|undefined} */ (
|
|
522
|
+
viewBinary.$value
|
|
523
|
+
);
|
|
524
|
+
if (
|
|
525
|
+
!oldFile ||
|
|
526
|
+
Object.prototype.toString.call(
|
|
527
|
+
oldFile
|
|
528
|
+
).slice(8, -1) !== 'File'
|
|
529
|
+
) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const newContentType =
|
|
533
|
+
/** @type {HTMLInputElement} */ (this).value;
|
|
534
|
+
const file = new File(
|
|
535
|
+
[oldFile],
|
|
536
|
+
oldFile.name,
|
|
537
|
+
{
|
|
538
|
+
type: newContentType,
|
|
539
|
+
lastModified: oldFile.lastModified
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
viewBinary.$value = file;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}]
|
|
546
|
+
]],
|
|
547
|
+
['br'],
|
|
548
|
+
['label', [
|
|
549
|
+
'Last modified date ',
|
|
550
|
+
['input', {
|
|
551
|
+
type: 'datetime-local', disabled: true, class: 'lastModified',
|
|
552
|
+
value: value.lastModified
|
|
553
|
+
? getDateString(value.lastModified)
|
|
554
|
+
: undefined
|
|
555
|
+
}]
|
|
556
|
+
]],
|
|
557
|
+
['br'],
|
|
558
|
+
binaryButton(value),
|
|
559
|
+
['button', {
|
|
560
|
+
class: 'clearData',
|
|
561
|
+
$on: {
|
|
562
|
+
click (e) {
|
|
563
|
+
e.preventDefault();
|
|
564
|
+
/**
|
|
565
|
+
* @type {HTMLFieldSetElement & {$setValue: SetValue}}
|
|
566
|
+
*/ (this.parentElement).$setValue({
|
|
567
|
+
name: '',
|
|
568
|
+
size: '',
|
|
569
|
+
type: '',
|
|
570
|
+
lastModified: ''
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}, [
|
|
575
|
+
'Clear data'
|
|
576
|
+
]]
|
|
577
|
+
]],
|
|
578
|
+
['fieldset', [
|
|
579
|
+
['legend', ['Supply file through upload']],
|
|
580
|
+
['label', [
|
|
581
|
+
'File ',
|
|
582
|
+
// @ts-expect-error It's ok
|
|
583
|
+
['input', /** @type {import('jamilih').JamilihAttributes} */ ({
|
|
584
|
+
$on: {
|
|
585
|
+
/**
|
|
586
|
+
* @this {HTMLInputElement}
|
|
587
|
+
*/
|
|
588
|
+
change () {
|
|
589
|
+
/* c8 ignore next 3 -- TS */
|
|
590
|
+
if (!this.files) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const file = this.files[0];
|
|
594
|
+
|
|
595
|
+
const metadataFieldset =
|
|
596
|
+
/**
|
|
597
|
+
* @type {HTMLFieldSetElement & {
|
|
598
|
+
* $setValue: SetValue
|
|
599
|
+
* }}
|
|
600
|
+
*/ (/** @type {HTMLElement} */ (
|
|
601
|
+
this.parentElement?.parentElement
|
|
602
|
+
)?.previousElementSibling);
|
|
603
|
+
metadataFieldset.$setValue(file);
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
name: `${typeNamespace}-file`, type: 'file'
|
|
607
|
+
})]
|
|
608
|
+
]]
|
|
609
|
+
]],
|
|
610
|
+
['fieldset', [
|
|
611
|
+
['legend', [
|
|
612
|
+
'Supply file through recording'
|
|
613
|
+
]],
|
|
614
|
+
(() => {
|
|
615
|
+
const select = /** @type {HTMLSelectElement} */ (
|
|
616
|
+
jml('select', {
|
|
617
|
+
class: 'device',
|
|
618
|
+
$on: {
|
|
619
|
+
async change () {
|
|
620
|
+
const videoContainer = /** @type {HTMLDivElement} */ (
|
|
621
|
+
this.nextElementSibling?.nextElementSibling
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
const photo =
|
|
625
|
+
/**
|
|
626
|
+
* @type {HTMLCanvasElement}
|
|
627
|
+
*/
|
|
628
|
+
($e(videoContainer, 'img.photo'));
|
|
629
|
+
photo.hidden = true;
|
|
630
|
+
|
|
631
|
+
const oldVideo =
|
|
632
|
+
/**
|
|
633
|
+
* @type {HTMLVideoElement & {
|
|
634
|
+
* $stream: MediaStream
|
|
635
|
+
* }}
|
|
636
|
+
*/
|
|
637
|
+
($e(
|
|
638
|
+
videoContainer,
|
|
639
|
+
'video.previewMedia'
|
|
640
|
+
));
|
|
641
|
+
|
|
642
|
+
// Drop preexisting listeners
|
|
643
|
+
const previewMedia =
|
|
644
|
+
/**
|
|
645
|
+
* @type {HTMLVideoElement & {
|
|
646
|
+
* $stream: MediaStream,
|
|
647
|
+
* $screenShare: boolean
|
|
648
|
+
* }}
|
|
649
|
+
*/ (
|
|
650
|
+
jml('video', {
|
|
651
|
+
class: 'previewMedia'
|
|
652
|
+
})
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
if (oldVideo.$stream) {
|
|
656
|
+
const tracks = /** @type {MediaStream} */ (
|
|
657
|
+
oldVideo.$stream
|
|
658
|
+
).getTracks();
|
|
659
|
+
tracks.forEach((track) => {
|
|
660
|
+
track.stop();
|
|
661
|
+
});
|
|
662
|
+
oldVideo.srcObject = null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
oldVideo.replaceWith(previewMedia);
|
|
666
|
+
|
|
667
|
+
const takeSnapshot = /** @type {HTMLButtonElement} */ (
|
|
668
|
+
$e(/** @type {HTMLDivElement} */ (
|
|
669
|
+
select.nextElementSibling
|
|
670
|
+
), '.takeSnapshot')
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
const recordMedia = /** @type {HTMLButtonElement} */ (
|
|
674
|
+
$e(/** @type {HTMLDivElement} */ (
|
|
675
|
+
select.nextElementSibling
|
|
676
|
+
), '.recordMedia')
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
/** @type {HTMLDivElement} */ ($e(
|
|
680
|
+
/** @type {HTMLDivElement} */
|
|
681
|
+
(select.nextElementSibling?.
|
|
682
|
+
nextElementSibling),
|
|
683
|
+
'div.recordedMedia'
|
|
684
|
+
)).hidden = true;
|
|
685
|
+
|
|
686
|
+
const visualizer = /** @type {HTMLCanvasElement} */ (
|
|
687
|
+
$e(videoContainer, 'canvas.visualizer')
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* @type {{
|
|
692
|
+
* video?: true,
|
|
693
|
+
* audio?: true
|
|
694
|
+
* }|undefined}
|
|
695
|
+
*/
|
|
696
|
+
let constraints;
|
|
697
|
+
let screenShareConstraints;
|
|
698
|
+
|
|
699
|
+
previewMedia.$screenShare = false;
|
|
700
|
+
switch (select.value) {
|
|
701
|
+
case 'audio-and-video':
|
|
702
|
+
constraints = {video: true, audio: true};
|
|
703
|
+
previewMedia.hidden = false;
|
|
704
|
+
takeSnapshot.hidden = false;
|
|
705
|
+
recordMedia.textContent = 'Record video/audio';
|
|
706
|
+
break;
|
|
707
|
+
case 'video':
|
|
708
|
+
constraints = {video: true};
|
|
709
|
+
previewMedia.hidden = false;
|
|
710
|
+
takeSnapshot.hidden = false;
|
|
711
|
+
recordMedia.textContent = 'Record video';
|
|
712
|
+
break;
|
|
713
|
+
case 'audio':
|
|
714
|
+
constraints = {audio: true};
|
|
715
|
+
previewMedia.hidden = true;
|
|
716
|
+
takeSnapshot.hidden = true;
|
|
717
|
+
recordMedia.textContent = 'Record audio';
|
|
718
|
+
break;
|
|
719
|
+
case 'screenShare':
|
|
720
|
+
screenShareConstraints = {video: true};
|
|
721
|
+
// Fallthrough
|
|
722
|
+
case 'screenShareAndVideo':
|
|
723
|
+
if (!screenShareConstraints) {
|
|
724
|
+
screenShareConstraints = {video: true, audio: true};
|
|
725
|
+
}
|
|
726
|
+
previewMedia.$screenShare = true;
|
|
727
|
+
previewMedia.hidden = true;
|
|
728
|
+
takeSnapshot.hidden = true;
|
|
729
|
+
visualizer.hidden = true;
|
|
730
|
+
break;
|
|
731
|
+
default:
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (constraints) {
|
|
736
|
+
const mediaStream = await getUserMedia(constraints);
|
|
737
|
+
/* c8 ignore next 4 */
|
|
738
|
+
if (!mediaStream) {
|
|
739
|
+
await dialogs.alert('Error getting user media');
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
videoContainer.hidden = false;
|
|
744
|
+
previewMedia.srcObject = mediaStream;
|
|
745
|
+
|
|
746
|
+
// Save as stream for later reuse as stream
|
|
747
|
+
previewMedia.$stream = mediaStream;
|
|
748
|
+
|
|
749
|
+
if (constraints.video) {
|
|
750
|
+
const canvas =
|
|
751
|
+
/**
|
|
752
|
+
* @type {HTMLCanvasElement}
|
|
753
|
+
*/
|
|
754
|
+
($e(
|
|
755
|
+
videoContainer,
|
|
756
|
+
'canvas.recordedImage'
|
|
757
|
+
));
|
|
758
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API/Taking_still_photos
|
|
759
|
+
previewMedia.addEventListener('canplay', () => {
|
|
760
|
+
canvas.setAttribute('width', String(
|
|
761
|
+
previewMedia.videoWidth
|
|
762
|
+
));
|
|
763
|
+
canvas.setAttribute(
|
|
764
|
+
'height', String(previewMedia.videoHeight)
|
|
765
|
+
);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
previewMedia.addEventListener('loadedmetadata', () => {
|
|
770
|
+
previewMedia.play();
|
|
771
|
+
if (constraints?.audio) {
|
|
772
|
+
visualizer.hidden = false;
|
|
773
|
+
visualize(
|
|
774
|
+
mediaStream,
|
|
775
|
+
visualizer
|
|
776
|
+
);
|
|
777
|
+
} else {
|
|
778
|
+
visualizer.hidden = true;
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
} else if (screenShareConstraints) {
|
|
782
|
+
const mediaStream = await startScreenCapture(
|
|
783
|
+
screenShareConstraints
|
|
784
|
+
);
|
|
785
|
+
/* c8 ignore next 4 */
|
|
786
|
+
if (!mediaStream) {
|
|
787
|
+
await dialogs.alert('Error getting user media');
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
videoContainer.hidden = false;
|
|
792
|
+
previewMedia.srcObject = mediaStream;
|
|
793
|
+
|
|
794
|
+
// Save as stream for later reuse as stream
|
|
795
|
+
previewMedia.$stream = mediaStream;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}, [
|
|
800
|
+
['option', {value: ''}, ['Please choose a device']]
|
|
801
|
+
])
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
(async () => {
|
|
805
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
806
|
+
let hasAudioInput = false;
|
|
807
|
+
let hasVideoInput = false;
|
|
808
|
+
devices.forEach((device) => {
|
|
809
|
+
switch (device.kind) {
|
|
810
|
+
case 'audioinput':
|
|
811
|
+
hasAudioInput = true;
|
|
812
|
+
break;
|
|
813
|
+
case 'videoinput':
|
|
814
|
+
hasVideoInput = true;
|
|
815
|
+
break;
|
|
816
|
+
default:
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
if (hasAudioInput) {
|
|
822
|
+
jml('option', {value: 'audio'}, ['Microphone only'], select);
|
|
823
|
+
}
|
|
824
|
+
if (hasVideoInput) {
|
|
825
|
+
jml('option', {value: 'video'}, ['Video camera only'], select);
|
|
826
|
+
}
|
|
827
|
+
if (hasAudioInput && hasVideoInput) {
|
|
828
|
+
jml(
|
|
829
|
+
'option',
|
|
830
|
+
{value: 'audio-and-video'},
|
|
831
|
+
['Video camera and microphone'],
|
|
832
|
+
select
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Can't be feature detected for privacy reasons
|
|
837
|
+
jml('option', {
|
|
838
|
+
value: 'screenShare'
|
|
839
|
+
}, ['Screen share'], select);
|
|
840
|
+
jml('option', {
|
|
841
|
+
value: 'screenShareAndVideo'
|
|
842
|
+
}, ['Screen share and microphone'], select);
|
|
843
|
+
})();
|
|
844
|
+
|
|
845
|
+
return select;
|
|
846
|
+
})(),
|
|
847
|
+
' ',
|
|
848
|
+
['div', [
|
|
849
|
+
// @ts-expect-error It's ok
|
|
850
|
+
['button', /** @type {import('jamilih').JamilihAttributes} */ ({
|
|
851
|
+
class: 'recordMedia',
|
|
852
|
+
$on: {
|
|
853
|
+
/**
|
|
854
|
+
* @param {Event} e
|
|
855
|
+
* @this {HTMLButtonElement & {
|
|
856
|
+
* $mediaRecorder: MediaRecorder
|
|
857
|
+
* }}
|
|
858
|
+
*/
|
|
859
|
+
click (e) {
|
|
860
|
+
e.preventDefault();
|
|
861
|
+
const previewMedia =
|
|
862
|
+
/**
|
|
863
|
+
* @type {HTMLVideoElement & {$stream: MediaStream}}
|
|
864
|
+
*/ ($e(
|
|
865
|
+
/** @type {HTMLDivElement} */
|
|
866
|
+
(this.parentElement?.nextElementSibling),
|
|
867
|
+
'video.previewMedia'
|
|
868
|
+
));
|
|
869
|
+
|
|
870
|
+
/* c8 ignore next 4 */
|
|
871
|
+
if (!previewMedia.srcObject) {
|
|
872
|
+
dialogs.alert('No stream found to record');
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Not interested in the photo now, so hide it
|
|
877
|
+
const videoContainer = /** @type {HTMLDivElement} */ (
|
|
878
|
+
this.parentElement?.nextElementSibling
|
|
879
|
+
);
|
|
880
|
+
const photo =
|
|
881
|
+
/**
|
|
882
|
+
* @type {HTMLCanvasElement}
|
|
883
|
+
*/
|
|
884
|
+
($e(videoContainer, 'img.photo'));
|
|
885
|
+
photo.hidden = true;
|
|
886
|
+
|
|
887
|
+
// Todo: Could check codecs for allowable values
|
|
888
|
+
// as second argument
|
|
889
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/isTypeSupported_static
|
|
890
|
+
const mimeType = 'video/webm';
|
|
891
|
+
const mediaRecorder = new MediaRecorder(previewMedia.$stream, {
|
|
892
|
+
mimeType
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
/** @type {Blob[]} */
|
|
896
|
+
let chunks = [];
|
|
897
|
+
mediaRecorder.addEventListener('dataavailable', (e) => {
|
|
898
|
+
chunks.push(e.data);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
mediaRecorder.start();
|
|
903
|
+
/* c8 ignore next 4 */
|
|
904
|
+
} catch (err) {
|
|
905
|
+
dialogs.alert('Error starting media recorder');
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
mediaRecorder.addEventListener('stop', () => {
|
|
909
|
+
const blob = new Blob(chunks, {
|
|
910
|
+
type: mimeType
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
const url = URL.createObjectURL(blob);
|
|
914
|
+
|
|
915
|
+
const recordedMedia = /** @type {HTMLVideoElement} */ ($e(
|
|
916
|
+
/** @type {HTMLDivElement} */
|
|
917
|
+
(this.parentElement?.nextElementSibling),
|
|
918
|
+
'video.recordedMedia'
|
|
919
|
+
// )).srcObject = blob;
|
|
920
|
+
));
|
|
921
|
+
/** @type {HTMLDivElement} */ (
|
|
922
|
+
recordedMedia.parentElement
|
|
923
|
+
).hidden = false;
|
|
924
|
+
|
|
925
|
+
recordedMedia.src = url;
|
|
926
|
+
|
|
927
|
+
recordedMedia.addEventListener('loadeddata', () => {
|
|
928
|
+
URL.revokeObjectURL(url);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
const file = new File(
|
|
932
|
+
[blob],
|
|
933
|
+
'placeholder.webm',
|
|
934
|
+
{type: mimeType}
|
|
935
|
+
);
|
|
936
|
+
console.log('file', file);
|
|
937
|
+
|
|
938
|
+
const root = /** @type {HTMLDivElement} */
|
|
939
|
+
(this.closest('[data-type="file"]'));
|
|
940
|
+
/**
|
|
941
|
+
* @type {HTMLFieldSetElement & {
|
|
942
|
+
* $setValue: SetValue
|
|
943
|
+
* }}
|
|
944
|
+
*/ ($e(
|
|
945
|
+
root, 'fieldset.fileMetaData'
|
|
946
|
+
)).$setValue(file);
|
|
947
|
+
|
|
948
|
+
chunks = [];
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
this.$mediaRecorder = mediaRecorder;
|
|
952
|
+
|
|
953
|
+
// Todo: Could make option for front/back
|
|
954
|
+
// camera ("user"/"environment") and bringIntoFocus
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}), [
|
|
958
|
+
'Record media'
|
|
959
|
+
]],
|
|
960
|
+
['button', {
|
|
961
|
+
class: 'stopRecording',
|
|
962
|
+
$on: {
|
|
963
|
+
click (e) {
|
|
964
|
+
e.preventDefault();
|
|
965
|
+
/**
|
|
966
|
+
* @type {HTMLButtonElement & {$mediaRecorder: MediaRecorder}}
|
|
967
|
+
*/ (
|
|
968
|
+
this.previousElementSibling
|
|
969
|
+
).$mediaRecorder.stop();
|
|
970
|
+
|
|
971
|
+
// const previewMedia =
|
|
972
|
+
// /**
|
|
973
|
+
// * @type {HTMLVideoElement & {
|
|
974
|
+
// * $stream: MediaStream,
|
|
975
|
+
// * $screenShare: boolean
|
|
976
|
+
// * }}
|
|
977
|
+
// */ (
|
|
978
|
+
// $e(
|
|
979
|
+
// /** @type {HTMLDivElement} */
|
|
980
|
+
// (this.parentElement?.nextElementSibling),
|
|
981
|
+
// '.previewMedia'
|
|
982
|
+
// )
|
|
983
|
+
// );
|
|
984
|
+
|
|
985
|
+
// Doesn't hurt to keep the normal video going as user
|
|
986
|
+
// may wish to re-record, but for screen-sharing, we
|
|
987
|
+
// may want to reenable this, at the cost that the
|
|
988
|
+
// screen sharing cannot be re-done
|
|
989
|
+
// if (previewMedia.$screenShare) {
|
|
990
|
+
// const tracks = /** @type {MediaStream} */ (
|
|
991
|
+
// previewMedia.$stream
|
|
992
|
+
// ).getTracks();
|
|
993
|
+
// tracks.forEach((track) => {
|
|
994
|
+
// track.stop();
|
|
995
|
+
// });
|
|
996
|
+
// }
|
|
997
|
+
// previewMedia.srcObject = null;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}, [
|
|
1001
|
+
'Stop recording'
|
|
1002
|
+
]],
|
|
1003
|
+
['button', {
|
|
1004
|
+
class: 'takeSnapshot',
|
|
1005
|
+
hidden: true
|
|
1006
|
+
}, {
|
|
1007
|
+
$on: {
|
|
1008
|
+
click (e) {
|
|
1009
|
+
e.preventDefault();
|
|
1010
|
+
|
|
1011
|
+
const videoContainer = /** @type {HTMLDivElement} */
|
|
1012
|
+
(this.parentElement?.nextElementSibling);
|
|
1013
|
+
|
|
1014
|
+
// Not interested in this anymore
|
|
1015
|
+
/** @type {HTMLVideoElement} */
|
|
1016
|
+
($e(videoContainer, '.recordedMedia')).hidden = true;
|
|
1017
|
+
|
|
1018
|
+
const canvas =
|
|
1019
|
+
/**
|
|
1020
|
+
* @type {HTMLCanvasElement}
|
|
1021
|
+
*/
|
|
1022
|
+
($e(videoContainer, 'canvas.recordedImage'));
|
|
1023
|
+
|
|
1024
|
+
const context = /** @type {CanvasRenderingContext2D} */ (
|
|
1025
|
+
canvas.getContext('2d')
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
const previewMedia =
|
|
1029
|
+
/**
|
|
1030
|
+
* @type {HTMLVideoElement}
|
|
1031
|
+
*/
|
|
1032
|
+
($e(videoContainer, 'video.previewMedia'));
|
|
1033
|
+
context.drawImage(
|
|
1034
|
+
previewMedia, 0, 0, canvas.width, canvas.height
|
|
1035
|
+
);
|
|
1036
|
+
const photo =
|
|
1037
|
+
/**
|
|
1038
|
+
* @type {HTMLCanvasElement}
|
|
1039
|
+
*/
|
|
1040
|
+
($e(videoContainer, 'img.photo'));
|
|
1041
|
+
|
|
1042
|
+
canvas.toBlob((blob) => {
|
|
1043
|
+
/* c8 ignore next 4 */
|
|
1044
|
+
if (!blob) {
|
|
1045
|
+
dialogs.alert('Error converting canvas to Blob');
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const newPhoto = /** @type {HTMLImageElement} */ (jml('img', {
|
|
1049
|
+
class: 'photo'
|
|
1050
|
+
}));
|
|
1051
|
+
const url = URL.createObjectURL(blob);
|
|
1052
|
+
|
|
1053
|
+
newPhoto.addEventListener('load', () => {
|
|
1054
|
+
// no longer need to read the blob so it's revoked
|
|
1055
|
+
URL.revokeObjectURL(url);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
const file = new File(
|
|
1059
|
+
[blob],
|
|
1060
|
+
'placeholder.png',
|
|
1061
|
+
{type: blob.type}
|
|
1062
|
+
);
|
|
1063
|
+
console.log('file', file);
|
|
1064
|
+
|
|
1065
|
+
const root = /** @type {HTMLDivElement} */
|
|
1066
|
+
(this.closest('[data-type="file"]'));
|
|
1067
|
+
/**
|
|
1068
|
+
* @type {HTMLFieldSetElement & {
|
|
1069
|
+
* $setValue: SetValue
|
|
1070
|
+
* }}
|
|
1071
|
+
*/ ($e(
|
|
1072
|
+
root, 'fieldset.fileMetaData'
|
|
1073
|
+
)).$setValue(file);
|
|
1074
|
+
|
|
1075
|
+
newPhoto.src = url;
|
|
1076
|
+
photo.replaceWith(newPhoto);
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}, [
|
|
1081
|
+
'Take and use snapshot as file'
|
|
1082
|
+
]]
|
|
1083
|
+
]],
|
|
1084
|
+
['div', {class: 'videoContainer', hidden: true}, [
|
|
1085
|
+
['canvas', {class: 'visualizer', hidden: true}],
|
|
1086
|
+
['br'],
|
|
1087
|
+
['video', {class: 'previewMedia'}],
|
|
1088
|
+
['div', [
|
|
1089
|
+
['canvas', {hidden: true, class: 'recordedImage'}],
|
|
1090
|
+
['img', {class: 'photo'}]
|
|
1091
|
+
]],
|
|
1092
|
+
[
|
|
1093
|
+
'div',
|
|
1094
|
+
{class: 'recordedMedia'},
|
|
1095
|
+
/** @type {import('jamilih').JamilihChildren} */ ([
|
|
1096
|
+
...(() => {
|
|
1097
|
+
const recordedMedia = /** @type {HTMLVideoElement} */ (jml(
|
|
1098
|
+
'video', {class: 'recordedMedia'}
|
|
1099
|
+
));
|
|
1100
|
+
return [
|
|
1101
|
+
recordedMedia,
|
|
1102
|
+
['br'],
|
|
1103
|
+
jml('button', {
|
|
1104
|
+
class: 'play',
|
|
1105
|
+
$on: {
|
|
1106
|
+
click (e) {
|
|
1107
|
+
e.preventDefault();
|
|
1108
|
+
recordedMedia.play();
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}, [
|
|
1112
|
+
'Play'
|
|
1113
|
+
]),
|
|
1114
|
+
jml('button', {
|
|
1115
|
+
class: 'pause',
|
|
1116
|
+
$on: {
|
|
1117
|
+
click (e) {
|
|
1118
|
+
e.preventDefault();
|
|
1119
|
+
recordedMedia.pause();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}, [
|
|
1123
|
+
'Pause'
|
|
1124
|
+
]),
|
|
1125
|
+
jml('button', {
|
|
1126
|
+
class: 'replay',
|
|
1127
|
+
$on: {
|
|
1128
|
+
click (e) {
|
|
1129
|
+
e.preventDefault();
|
|
1130
|
+
recordedMedia.pause();
|
|
1131
|
+
recordedMedia.currentTime = 0;
|
|
1132
|
+
recordedMedia.play();
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}, [
|
|
1136
|
+
'Replay'
|
|
1137
|
+
])
|
|
1138
|
+
];
|
|
1139
|
+
})()
|
|
1140
|
+
])
|
|
1141
|
+
]
|
|
1142
|
+
]]
|
|
1143
|
+
]]
|
|
1144
|
+
]];
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
export default fileType;
|