tauri-plugin-camera-kessdev 0.2.0-beta.1 → 0.2.0-beta.2
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/README.md +5 -0
- package/dist-js/index.cjs +60 -22
- package/dist-js/index.js +60 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -198,6 +198,11 @@ const withThumbnail = await stopRecording({ thumbnail: true })
|
|
|
198
198
|
await startRecording({ quality: 'fhd', fps: 30, bitrate: 2_000_000 }) // quality: 'sd' | 'hd' | 'fhd' | 'uhd'
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
+
On desktop the preview stays alive during recording: the recording reuses the
|
|
202
|
+
preview's video track (only the microphone is acquired separately), so the
|
|
203
|
+
`<video>` element bound to `getPreviewStream()` keeps playing. The recording
|
|
204
|
+
mime type is chosen from the browser's supported types (WebM or MP4).
|
|
205
|
+
|
|
201
206
|
### Video thumbnail
|
|
202
207
|
|
|
203
208
|
Pass `{ thumbnail: true }` to `stopRecording()` to include a base64-encoded JPEG
|
package/dist-js/index.cjs
CHANGED
|
@@ -18,6 +18,7 @@ function isMobile() {
|
|
|
18
18
|
let activeStream = null;
|
|
19
19
|
let activeVideo = null;
|
|
20
20
|
let mediaRecorder = null;
|
|
21
|
+
let recordingAudioStream = null;
|
|
21
22
|
let recordedChunks = [];
|
|
22
23
|
let recordedBytes = 0;
|
|
23
24
|
let recordingStartedAt = 0;
|
|
@@ -26,10 +27,28 @@ let progressTimer = null;
|
|
|
26
27
|
const RECORDING_PROGRESS_INTERVAL_MS = 250;
|
|
27
28
|
/** Ask MediaRecorder for data chunks at least this often so size is known live. */
|
|
28
29
|
const RECORDING_TIMESLICE_MS = 1000;
|
|
30
|
+
/** Picks an explicit recording mime type supported by this browser, if any. */
|
|
31
|
+
function pickDesktopRecordingMimeType() {
|
|
32
|
+
if (typeof MediaRecorder === 'undefined' || typeof MediaRecorder.isTypeSupported !== 'function') {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const candidates = [
|
|
36
|
+
'video/webm;codecs=vp9,opus',
|
|
37
|
+
'video/webm;codecs=vp8,opus',
|
|
38
|
+
'video/webm',
|
|
39
|
+
'video/mp4'
|
|
40
|
+
];
|
|
41
|
+
return candidates.find((type) => MediaRecorder.isTypeSupported(type));
|
|
42
|
+
}
|
|
29
43
|
function buildRecordingProgress(duration, bytes) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
// Omit the size-derived fields until a real chunk has arrived, matching the
|
|
45
|
+
// mobile backends (do not report a misleading 0).
|
|
46
|
+
const progress = { duration };
|
|
47
|
+
if (bytes > 0) {
|
|
48
|
+
progress.bytesRecorded = bytes;
|
|
49
|
+
if (duration > 0) {
|
|
50
|
+
progress.bitrate = Math.round((bytes * 8) / duration);
|
|
51
|
+
}
|
|
33
52
|
}
|
|
34
53
|
return progress;
|
|
35
54
|
}
|
|
@@ -72,6 +91,8 @@ function teardownDesktopStream() {
|
|
|
72
91
|
mediaRecorder.stop();
|
|
73
92
|
}
|
|
74
93
|
mediaRecorder = null;
|
|
94
|
+
recordingAudioStream?.getTracks().forEach((t) => t.stop());
|
|
95
|
+
recordingAudioStream = null;
|
|
75
96
|
recordedChunks = [];
|
|
76
97
|
recordedBytes = 0;
|
|
77
98
|
if (activeStream) {
|
|
@@ -377,30 +398,44 @@ async function startRecording(options) {
|
|
|
377
398
|
await core.invoke('plugin:camera-kessdev|start_recording', { ...options });
|
|
378
399
|
return;
|
|
379
400
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
401
|
+
// Reuse the active preview video track when possible so the preview is not
|
|
402
|
+
// invalidated (which would freeze the app's <video> element).
|
|
403
|
+
let videoTrack = activeStream?.getVideoTracks()[0];
|
|
404
|
+
if (!videoTrack) {
|
|
405
|
+
const videoConstraints = {};
|
|
406
|
+
if (options?.quality)
|
|
407
|
+
videoConstraints.height = { ideal: qualityHeight(options.quality) };
|
|
408
|
+
if (options?.fps)
|
|
409
|
+
videoConstraints.frameRate = { ideal: options.fps };
|
|
410
|
+
const videoStream = await navigator.mediaDevices.getUserMedia({
|
|
411
|
+
video: videoConstraints,
|
|
412
|
+
audio: false
|
|
413
|
+
});
|
|
414
|
+
activeStream = videoStream;
|
|
415
|
+
videoTrack = videoStream.getVideoTracks()[0];
|
|
416
|
+
}
|
|
417
|
+
if (!videoTrack) {
|
|
418
|
+
throw new Error('No video track available for recording.');
|
|
384
419
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
video.srcObject = stream;
|
|
396
|
-
await video.play();
|
|
397
|
-
activeVideo = video;
|
|
420
|
+
if (!activeVideo) {
|
|
421
|
+
const video = document.createElement('video');
|
|
422
|
+
video.muted = true;
|
|
423
|
+
video.playsInline = true;
|
|
424
|
+
video.srcObject = activeStream;
|
|
425
|
+
await video.play();
|
|
426
|
+
activeVideo = video;
|
|
427
|
+
}
|
|
428
|
+
recordingAudioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
429
|
+
const recordStream = new MediaStream([videoTrack, ...recordingAudioStream.getAudioTracks()]);
|
|
398
430
|
recordedChunks = [];
|
|
399
431
|
recordedBytes = 0;
|
|
400
432
|
const recorderOptions = {};
|
|
401
433
|
if (options?.bitrate)
|
|
402
434
|
recorderOptions.videoBitsPerSecond = options.bitrate;
|
|
403
|
-
|
|
435
|
+
const mimeType = pickDesktopRecordingMimeType();
|
|
436
|
+
if (mimeType)
|
|
437
|
+
recorderOptions.mimeType = mimeType;
|
|
438
|
+
mediaRecorder = new MediaRecorder(recordStream, recorderOptions);
|
|
404
439
|
mediaRecorder.ondataavailable = (event) => {
|
|
405
440
|
if (event.data.size > 0) {
|
|
406
441
|
recordedChunks.push(event.data);
|
|
@@ -435,9 +470,12 @@ async function stopRecording(options) {
|
|
|
435
470
|
stopProgressTimer();
|
|
436
471
|
return await new Promise((resolve, reject) => {
|
|
437
472
|
mediaRecorder.onstop = () => {
|
|
438
|
-
const
|
|
473
|
+
const mimeType = mediaRecorder.mimeType || 'video/webm';
|
|
474
|
+
const type = mimeType.split(';')[0];
|
|
439
475
|
const blob = new Blob(recordedChunks, { type });
|
|
440
476
|
const url = URL.createObjectURL(blob);
|
|
477
|
+
recordingAudioStream?.getTracks().forEach((t) => t.stop());
|
|
478
|
+
recordingAudioStream = null;
|
|
441
479
|
recordedChunks = [];
|
|
442
480
|
recordedBytes = 0;
|
|
443
481
|
mediaRecorder = null;
|
package/dist-js/index.js
CHANGED
|
@@ -17,6 +17,7 @@ function isMobile() {
|
|
|
17
17
|
let activeStream = null;
|
|
18
18
|
let activeVideo = null;
|
|
19
19
|
let mediaRecorder = null;
|
|
20
|
+
let recordingAudioStream = null;
|
|
20
21
|
let recordedChunks = [];
|
|
21
22
|
let recordedBytes = 0;
|
|
22
23
|
let recordingStartedAt = 0;
|
|
@@ -25,10 +26,28 @@ let progressTimer = null;
|
|
|
25
26
|
const RECORDING_PROGRESS_INTERVAL_MS = 250;
|
|
26
27
|
/** Ask MediaRecorder for data chunks at least this often so size is known live. */
|
|
27
28
|
const RECORDING_TIMESLICE_MS = 1000;
|
|
29
|
+
/** Picks an explicit recording mime type supported by this browser, if any. */
|
|
30
|
+
function pickDesktopRecordingMimeType() {
|
|
31
|
+
if (typeof MediaRecorder === 'undefined' || typeof MediaRecorder.isTypeSupported !== 'function') {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const candidates = [
|
|
35
|
+
'video/webm;codecs=vp9,opus',
|
|
36
|
+
'video/webm;codecs=vp8,opus',
|
|
37
|
+
'video/webm',
|
|
38
|
+
'video/mp4'
|
|
39
|
+
];
|
|
40
|
+
return candidates.find((type) => MediaRecorder.isTypeSupported(type));
|
|
41
|
+
}
|
|
28
42
|
function buildRecordingProgress(duration, bytes) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
// Omit the size-derived fields until a real chunk has arrived, matching the
|
|
44
|
+
// mobile backends (do not report a misleading 0).
|
|
45
|
+
const progress = { duration };
|
|
46
|
+
if (bytes > 0) {
|
|
47
|
+
progress.bytesRecorded = bytes;
|
|
48
|
+
if (duration > 0) {
|
|
49
|
+
progress.bitrate = Math.round((bytes * 8) / duration);
|
|
50
|
+
}
|
|
32
51
|
}
|
|
33
52
|
return progress;
|
|
34
53
|
}
|
|
@@ -71,6 +90,8 @@ function teardownDesktopStream() {
|
|
|
71
90
|
mediaRecorder.stop();
|
|
72
91
|
}
|
|
73
92
|
mediaRecorder = null;
|
|
93
|
+
recordingAudioStream?.getTracks().forEach((t) => t.stop());
|
|
94
|
+
recordingAudioStream = null;
|
|
74
95
|
recordedChunks = [];
|
|
75
96
|
recordedBytes = 0;
|
|
76
97
|
if (activeStream) {
|
|
@@ -376,30 +397,44 @@ async function startRecording(options) {
|
|
|
376
397
|
await invoke('plugin:camera-kessdev|start_recording', { ...options });
|
|
377
398
|
return;
|
|
378
399
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
400
|
+
// Reuse the active preview video track when possible so the preview is not
|
|
401
|
+
// invalidated (which would freeze the app's <video> element).
|
|
402
|
+
let videoTrack = activeStream?.getVideoTracks()[0];
|
|
403
|
+
if (!videoTrack) {
|
|
404
|
+
const videoConstraints = {};
|
|
405
|
+
if (options?.quality)
|
|
406
|
+
videoConstraints.height = { ideal: qualityHeight(options.quality) };
|
|
407
|
+
if (options?.fps)
|
|
408
|
+
videoConstraints.frameRate = { ideal: options.fps };
|
|
409
|
+
const videoStream = await navigator.mediaDevices.getUserMedia({
|
|
410
|
+
video: videoConstraints,
|
|
411
|
+
audio: false
|
|
412
|
+
});
|
|
413
|
+
activeStream = videoStream;
|
|
414
|
+
videoTrack = videoStream.getVideoTracks()[0];
|
|
415
|
+
}
|
|
416
|
+
if (!videoTrack) {
|
|
417
|
+
throw new Error('No video track available for recording.');
|
|
383
418
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
video.srcObject = stream;
|
|
395
|
-
await video.play();
|
|
396
|
-
activeVideo = video;
|
|
419
|
+
if (!activeVideo) {
|
|
420
|
+
const video = document.createElement('video');
|
|
421
|
+
video.muted = true;
|
|
422
|
+
video.playsInline = true;
|
|
423
|
+
video.srcObject = activeStream;
|
|
424
|
+
await video.play();
|
|
425
|
+
activeVideo = video;
|
|
426
|
+
}
|
|
427
|
+
recordingAudioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
428
|
+
const recordStream = new MediaStream([videoTrack, ...recordingAudioStream.getAudioTracks()]);
|
|
397
429
|
recordedChunks = [];
|
|
398
430
|
recordedBytes = 0;
|
|
399
431
|
const recorderOptions = {};
|
|
400
432
|
if (options?.bitrate)
|
|
401
433
|
recorderOptions.videoBitsPerSecond = options.bitrate;
|
|
402
|
-
|
|
434
|
+
const mimeType = pickDesktopRecordingMimeType();
|
|
435
|
+
if (mimeType)
|
|
436
|
+
recorderOptions.mimeType = mimeType;
|
|
437
|
+
mediaRecorder = new MediaRecorder(recordStream, recorderOptions);
|
|
403
438
|
mediaRecorder.ondataavailable = (event) => {
|
|
404
439
|
if (event.data.size > 0) {
|
|
405
440
|
recordedChunks.push(event.data);
|
|
@@ -434,9 +469,12 @@ async function stopRecording(options) {
|
|
|
434
469
|
stopProgressTimer();
|
|
435
470
|
return await new Promise((resolve, reject) => {
|
|
436
471
|
mediaRecorder.onstop = () => {
|
|
437
|
-
const
|
|
472
|
+
const mimeType = mediaRecorder.mimeType || 'video/webm';
|
|
473
|
+
const type = mimeType.split(';')[0];
|
|
438
474
|
const blob = new Blob(recordedChunks, { type });
|
|
439
475
|
const url = URL.createObjectURL(blob);
|
|
476
|
+
recordingAudioStream?.getTracks().forEach((t) => t.stop());
|
|
477
|
+
recordingAudioStream = null;
|
|
440
478
|
recordedChunks = [];
|
|
441
479
|
recordedBytes = 0;
|
|
442
480
|
mediaRecorder = null;
|
package/package.json
CHANGED