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 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
- const progress = { duration, bytesRecorded: bytes };
31
- if (duration > 0) {
32
- progress.bitrate = Math.round((bytes * 8) / duration);
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
- if (activeStream) {
381
- activeStream.getTracks().forEach((t) => t.stop());
382
- activeStream = null;
383
- activeVideo = null;
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
- const videoConstraints = {};
386
- if (options?.quality)
387
- videoConstraints.height = { ideal: qualityHeight(options.quality) };
388
- if (options?.fps)
389
- videoConstraints.frameRate = { ideal: options.fps };
390
- const stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: true });
391
- activeStream = stream;
392
- const video = document.createElement('video');
393
- video.muted = true;
394
- video.playsInline = true;
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
- mediaRecorder = new MediaRecorder(stream, recorderOptions);
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 type = mediaRecorder.mimeType || 'video/webm';
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
- const progress = { duration, bytesRecorded: bytes };
30
- if (duration > 0) {
31
- progress.bitrate = Math.round((bytes * 8) / duration);
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
- if (activeStream) {
380
- activeStream.getTracks().forEach((t) => t.stop());
381
- activeStream = null;
382
- activeVideo = null;
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
- const videoConstraints = {};
385
- if (options?.quality)
386
- videoConstraints.height = { ideal: qualityHeight(options.quality) };
387
- if (options?.fps)
388
- videoConstraints.frameRate = { ideal: options.fps };
389
- const stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: true });
390
- activeStream = stream;
391
- const video = document.createElement('video');
392
- video.muted = true;
393
- video.playsInline = true;
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
- mediaRecorder = new MediaRecorder(stream, recorderOptions);
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 type = mediaRecorder.mimeType || 'video/webm';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tauri-plugin-camera-kessdev",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.2.0-beta.2",
4
4
  "author": "kess",
5
5
  "description": "Camera plugin for Tauri v2: photo capture, video recording and preview on Android, iOS and desktop.",
6
6
  "license": "MIT",